/* Farb-Tokens liegen in theme-dark.css (Default) und theme-light.css
   (Override via html.light-theme) — beide werden in index.html VOR
   dieser Datei eingebunden. Hier ausschließlich nicht-farbliche
   Design-Tokens.

   Design/Typewriter: --radius wurde von 12px auf 0 gesetzt, damit Karten,
   Buttons, Modals brutalistisch-scharfe Kanten bekommen. Elemente mit
   hardcodiertem border-radius (z.B. Hex-Kreise via border-radius: 50%)
   bleiben davon unberührt. */
:root {
  --radius: 0;
}

/* Light-Theme: Buchstaben-Kreise erben das Basis-Fill (var(--surface)) —
   gleiche Cremefläche wie die Eck-Tasten (.hex-corner verwendet ebenfalls
   var(--surface)). Vorher saß hier ein Override auf var(--surface2)
   (dunkleres Papiergrau), das die Letter-Kreise dunkler erscheinen ließ
   als die Eck-Tasten — kein Override mehr nötig. .hex-center bleibt per
   eigener Light-Theme-Regel auf var(--accent) als Pflichtbuchstaben-
   Marker. */
.light-theme .sol-word.missed { color: var(--muted); }

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Safe-Area-Insets als Custom Properties: alle Layout-Regeln nutzen
   var(--safe-*) statt env() direkt. Im Normalfall spiegeln die Variablen
   env() 1:1. Das Cutout-Gedächtnis in index.html (updateNotch) überschreibt
   sie inline auf <html>, wenn die Seite edge-to-edge zeichnet (Fullscreen/
   Standalone), env() aber fälschlich 0 meldet (z.B. Firefox Android im
   API-Fullscreen) und die Insets aus einer früheren Messung bekannt sind.
   Neue Safe-Area-Regeln müssen var(--safe-*) verwenden, nie env() direkt —
   sonst greift das Gedächtnis dort nicht. */
:root {
  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-right:  env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left, 0px);
}
/* Deterministischer Fullscreen-Fallback: zeichnet die Seite edge-to-edge
   (html.edge-to-edge, gesetzt von updateNotch bei API-Fullscreen/Standalone),
   kann ein Top-Cutout im Seiteninhalt liegen — aber Android-Browser melden
   env() im JS-Fullscreen oft nicht. Darum außerhalb des Spiels ein Mindest-
   Band von 32px (deckt übliche Statusleisten/Punch-Holes ab); meldet env()
   doch echte Werte, gewinnt der größere. Im Spiel (body.game-mode) bewusst
   NICHT: dort würde das Band auf Cutout-losen Geräten nur Hex-Höhe kosten,
   und der Spiel-Header hält seine Inhalte ohnehin an den Rändern, neben dem
   mittigen Cutout. Das Cutout-Gedächtnis (updateNotch) überschreibt die
   Variable inline mit exakten Messwerten, sobald welche existieren. */
html.edge-to-edge:has(body:not(.game-mode)) {
  --safe-top: max(32px, env(safe-area-inset-top, 0px));
}

/* Horizontale Scroll-/Overscroll-Gesten sind auf KEINER Seite erwünscht —
   keinen Screen scrollt man je horizontal, und Firefox Androids seitliche
   Wisch-Geste löst sonst ungewollt eine Browser-Zurück-Navigation aus.
   `overscroll-behavior-x: none` auf html+body unterbindet beides global,
   ohne die vertikale Pull-to-Refresh-Geste außerhalb des Spielbildschirms
   anzutasten. */
html { overflow-x: hidden; max-width: 100%; overscroll-behavior-x: none; }

/* Eigener Hintergrund auf html: html ist die Canvas — diese Farbe füllt den
   Overscroll-/Rubberband-Bereich. Dort gehört die ruhige Grundfarbe hin,
   bewusst ohne Papier: eine an der Kante abgeschnittene Textur sähe dort
   falsch aus. Der Body deckt mit min-height:100dvh ohnehin das ganze
   Sichtfeld ab.
   Historie: hier stand früher zusätzlich der Verdacht, iOS-Safari verliere
   beim Hochpropagieren des Body-Hintergrunds dessen
   `background-blend-mode: multiply`. Der Fehler (im Dunkel-Theme helle Seite
   + cremefarbene, unsichtbare Schrift) trat danach auf einem iPhone 13 Pro
   erneut auf, also lag es nicht an der Propagation, sondern am Blend-Modus
   selbst. Konsequenz: es wird gar nicht mehr geblendet — die Textur ist je
   Theme fertig mit --bg multipliziert (--paper, siehe theme-*.css und
   scripts/make-paper-textures.js). Diese Regel bleibt aus dem Grund oben. */
html { background-color: var(--bg); }

/* ── Force-Dark: „ich mache beide Schemata selbst" ──────────────────────────
   Samsung Internet (und Chromes „Auto Dark Mode") kennen einen Modus, der eine
   Seite NACH dem Rendern umrechnet: die Autorenfarben werden algorithmisch
   invertiert. Aus dem cremefarbenen Papier wurde so ein Graubraun (gemeldet aus
   Samsung Internet). Besonders bitter hier: `prefers-color-scheme` meldet dabei
   `light`, die Seite kann den Zustand also nicht erkennen — wir liefern brav
   das Hell-Theme aus und der Browser malt es um.

   `color-scheme: light dark` ist die Zusicherung an den Browser, dass die Seite
   BEIDE Schemata selbst beherrscht. Wer sie liest, schaltet seine
   Umrechnung ab und lässt unser Theme (samt Umschalter) entscheiden. Dieselbe
   Angabe steht als <meta name="color-scheme"> im <head> von index.html — das
   Meta greift schon vor dem ersten Stylesheet, und es ist die Form, die
   Samsungs Adaptive-Force-Dark ab Version 25 auswertet.

   Bewusst NUR hier auf der Wurzel und NICHT je Theme verengt (kein
   `color-scheme: light` unter html.light-theme): sobald die Wurzel nur noch ein
   Schema nennt, gilt die Seite wieder als „kann kein Dunkel" — und genau dann
   schaltet die Umrechnung sich WIEDER ein. Der Preis ist gering: native Teile,
   die wir nicht selbst einfärben (Bildlaufleisten, Auswahl-Popups von <select>,
   der Textcursor), folgen dem Gerät statt dem App-Theme. Wer im Hell-Theme auf
   einem dunkel gestellten Gerät sitzt, bekommt dort dunkle Beigaben.

   Ebenfalls bewusst NICHT `only light`: das verbietet dem Browser jede
   Umrechnung, nimmt damit aber auch dem Nutzer, dessen Browser auf Dunkel
   steht, die Dunkelheit — statt eines umgerechneten Hell-Themes bekäme er ein
   unangetastetes. Diese App hat ein echtes Dunkel-Theme; die Zusicherung
   „light dark" führt zum gewünschten Ergebnis, `only light` nur zum
   erträglichen. */
:root { color-scheme: light dark; }

/* Im Spielbildschirm dürfen weder Scroll- noch Reload-Gesten in irgendeine
   Richtung laufen — beim schnellen Tippen (Buchstabe oben → Submit unten)
   interpretiert Firefox Android sonst eine leichte Abwärts-Wischbewegung
   als Reload; vertikale Overscroll-Sperre nur hier, weil Pull-to-Refresh
   auf den Nicht-Spiel-Screens erhalten bleiben muss. `touch-action: none`
   auf #screen-game blockiert zusätzlich die Browser-Default-Gesten
   (Pan/Pinch/Swipe-Nav), während Taps per pointerdown weiter feuern.
   Overlays (Wikt-Popup u.a.) sind body-level Geschwister von #screen-game
   und bleiben scrollbar. */
html:has(body.game-mode) { overscroll-behavior: none; }

/* ── JetBrains Mono — selbst gehostet (SIL OFL 1.1) ─────────────────
   Variable-Font-woff2 (wght-Achse) aus @fontsource-variable/jetbrains-mono,
   abgelegt unter public/fonts/. Ersetzt den früheren Google-Fonts-CDN-Link
   → keine Nutzer-IP mehr an Google. Lizenz: public/fonts/OFL.txt.
   Subsets latin + latin-ext werden per unicode-range nur bei Bedarf geladen;
   je Subset eine Normal- und eine Italic-Datei. CSS wird nicht getemplatet,
   daher kein ?v=-Query — die Dateinamen sind stabil (neue Schrift ⇒ keine
   Stale-Cache-Gefahr; künftige Updates ggf. über neuen Dateinamen). */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('../fonts/jetbrains-mono-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: italic;
  font-weight: 100 800;
  font-display: swap;
  src: url('../fonts/jetbrains-mono-latin-wght-italic.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('../fonts/jetbrains-mono-latin-ext-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: italic;
  font-weight: 100 800;
  font-display: swap;
  src: url('../fonts/jetbrains-mono-latin-ext-wght-italic.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

body {
  /* Design/Typewriter: Monospace (JetBrains Mono) ist der Haupt-Stack.
     Fallback bleibt System-Mono (ui-monospace deckt SF Mono auf iOS,
     Roboto Mono auf Android, Consolas auf Windows ab). Die Schrift wird
     selbst gehostet (@font-face oben), nicht mehr von Google geladen. */
  font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, 'Courier New', monospace;
  /* --paper ist die getilte Papier-Textur des aktiven Themes (public/
     paper-light.png bzw. paper-dark.png), OFFLINE mit --bg multipliziert:
     im Hell-Theme = Papier mit warmer Tönung, im Dunkel-Theme = Walnuss mit
     Holzkorn-Andeutung. Kein `background-blend-mode` — iOS Safari wandte den
     nicht an und malte die helle Rohtextur deckend über das Dunkel-Theme.
     --bg bleibt als Farbe darunter: die Fläche ist damit schon vor dem
     Laden des Bildes richtig getönt. */
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
  color: var(--text);
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* safe center: Firefox unterstützt 'safe' nicht → Fallback auf flex-start via margin auto */
  justify-content: flex-start;
  /* Notch-Safe (außerhalb des Spiels): seit viewport-fit=cover zeichnet die
     Seite edge-to-edge. Der Body ist der Scroll-Container der Nicht-Spiel-
     Screens (Start, Lobby, Statistik, Spielende), daher spart sein Padding
     den Cutout/Home-Indicator aus. max(16px, env(...)) = normaler 16px-Rand
     ohne Notch, sonst der größere Geräte-Inset. Im Spiel ist body padding:0
     (body.game-mode) — dort übernimmt der .game-wrapper die Safe-Area. Der
     Papier-Hintergrund bleibt full-bleed (Background ignoriert Padding). */
  padding:
    max(16px, var(--safe-top))
    max(16px, var(--safe-right))
    max(16px, var(--safe-bottom))
    max(16px, var(--safe-left));
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-x: none;
}

/* Links. `a:visited` MUSS mit rein: die UA-Regel dafür ist spezifischer als
   ein blankes `a` und färbte besuchte Links sonst violett — im Dunkel-Theme
   noch schlechter lesbar als das Standardblau. Regeln, die eine eigene Farbe
   setzen (`#modal-credits a`, `.wikt-license a`, `.wikt-rules-link`), sind
   spezifischer und bleiben unberührt. */
a, a:visited { color: var(--link); }

/* Form-Controls (Dropdowns, Inputs, Buttons, Textareas) erben den
   Body-Font NICHT automatisch — Browser-Default ist System-Schrift.
   Globaler `font-family: inherit` zieht JetBrains Mono auch auf die
   Bedien-Elemente, ohne dass jede einzelne Regel das explizit setzen
   muss. */
button, input, select, textarea, optgroup, option {
  font-family: inherit;
}


/* ── Screens ─────────────────────────────────────────────────── */

.screen { display: none; width: 100%; max-width: 960px; min-width: 0; }
/* margin: auto zentriert den Screen vertikal in Firefox (safe center nicht unterstützt) */
.screen.active { display: flex; flex-direction: column; align-items: center; margin-top: auto; margin-bottom: auto; }

/* ── Cards ───────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px;
  width: 100%;
  max-width: 420px;
  min-width: 0;
}

/* Transparente, randlose Start-Karte. (z-index:1 stammt aus der Zeit der
   Tannenwald-Deko auf dem Start-Screen — inzwischen ins Impressum-Modal
   verlegt — und bleibt als harmlose Absicherung stehen.) */
#screen-start .card { position: relative; z-index: 1; border: 0; background: transparent; }
/* Sprach-Umschalter: ein normaler, niedriger Button (Klassen `btn btn-sm`)
   mit dem inline eingebetteten world.svg-Globus statt des 🌐-Emojis. Der
   Globus (currentColor) wird dunkelgrün (--violet); die übrigen Button-
   Eigenschaften (Fläche, erhabene Optik, Padding) kommen aus .btn/.btn-sm.
   margin-left rückt den Button etwas nach rechts, weg vom Theme-Umschalter.
   Nur nicht-konfligierende Eigenschaften hier — Padding/Fläche/Farbe der
   Fläche bleiben bei .btn/.btn-sm. */
.locale-btn {
  /* Echter, niedriger Button (Klassen `btn btn-sm btn-ghost`): die themed
     Surface-Fläche + Rahmen + Textfarbe kommen aus .btn-ghost — dadurch in
     beiden Themes konsistent (kein grauer Browser-Default, kein
     Schwarz-auf-Weiß im Dark-Theme). Hier nur: etwas nach rechts rücken und
     den grauen Mobile-Tap-Highlight abschalten. Der Globus bleibt
     dunkelgrün (--violet, s.u.). */
  margin-left: 16px;
  -webkit-tap-highlight-color: transparent;
}
.locale-btn .locale-icon {
  width: 1.05em; height: 1.05em;
  color: var(--violet);   /* Globus dunkelgrün (world.svg nutzt currentColor) */
  flex: 0 0 auto;
}
.locale-btn .locale-code { line-height: 1; font-weight: 700; letter-spacing: 0.5px; }

h1 {
  font-size: 2rem;
  font-weight: 800;
  text-align: center;
  letter-spacing: -0.5px;
}
.subtitle {
  color: var(--muted);
  text-align: center;
  margin-top: 4px;
  margin-bottom: 28px;
  font-size: 1.1rem;
  font-weight: 700;
}
/* Dunkel-Theme: Untertitel so hell wie der Spielername (--text). --muted
   wirkt auf Walnuss zu dunkel für diesen prominenten Text. */
html:not(.light-theme) .subtitle { color: var(--text); }

/* ── Forms ───────────────────────────────────────────────────── */

.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 0.8rem; color: var(--muted); margin-bottom: 6px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; }
input[type=text] {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 1rem;
  padding: 10px 14px;
  outline: none;
  transition: border-color 0.15s;
}
input[type=text]:focus { border-color: var(--accent); }
input[type=text]::placeholder { color: var(--muted); }

.code-input { text-transform: uppercase; letter-spacing: 4px; font-size: 1.2rem; font-weight: 700; text-align: center; }

/* ── Buttons ─────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  border: none; border-radius: 8px; font-size: 0.95rem; font-weight: 600;
  cursor: pointer; padding: 6px 12px; transition: opacity 0.15s, transform 0.1s;
  white-space: nowrap;
}
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Ausgegraute Starttaste: der Klick darauf soll erklären, WARUM sie aus ist
   (siehe #lobby-start-wrap-Handler in main.js). Eine deaktivierte Taste feuert
   selbst kein `click` und lässt es auch nicht zu den Vorfahren durch —
   pointer-events: none nimmt sie aus dem Treffer-Test, sodass der umgebende
   Wrapper das Ziel wird und den Klick sieht. */
#lobby-start-wrap #btn-start:disabled { pointer-events: none; }
/* …und weil die Taste damit aus dem Treffer-Test fällt, trägt der Wrapper ihren
   Zeiger: sonst zeigte er den Standard-Pfeil statt „geht nicht". */
#lobby-start-wrap:has(#btn-start:disabled) { cursor: not-allowed; }
/* Primary & Secondary = Marken-Grün (Neuen Raum erstellen / Spiel starten /
   Beitreten …), Ghost = neutrale Papierfläche (Statistiken, Spielregeln,
   Zurück …) — wie im Logo-/Start-Screen-Design. */
.btn-primary  { background: var(--violet);   color: #fff; }
.btn-secondary { background: var(--violet);  color: #fff; }
.btn-danger   { background: var(--red);      color: #fff; }
.btn-ghost    { background: var(--surface);  color: var(--text); border: 1px solid var(--border); }
.btn-full     { width: 100%; }
.btn-sm       { padding: 4.2px 8.4px; font-size: 0.85rem; }
.btn-lg       { padding: 6.6px 13.2px; font-size: 1.3rem; }

/* „Zurück"-Buttons: der Pfeil ist ein dekoratives ::before-Flex-Item statt
   Teil des Textknotens. Dadurch zentriert ihn `align-items: center` der .btn
   vertikal als eigene Box (als Teil des Wortes saß die ←-Glyphe je nach Font
   leicht zu tief) und sein Abstand zum Wort lässt sich über `gap` direkt
   steuern, statt über ein breites Monospace-Leerzeichen. Das Wort trägt die
   Bedeutung (a11y); der Pfeil bleibt rein schmückend. */
.btn-back { gap: 6px; }
.btn-back::before {
  content: "";
  width: 0.78em;
  height: 0.78em;
  flex: 0 0 auto;
  background-color: currentColor;
  /* Pfeil als currentColor-Maske (gleiches Masken-Muster wie die Krabbenzangen): die
     SVG-Form ist symmetrisch zur vertikalen viewBox-Mitte, daher zentriert
     `align-items: center` der .btn sie deterministisch — unabhängig von
     Font-Fallback und Glyph-Grundlinie (das war die Ursache des Tiefstands). */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M2 12 L10 4 L10 10 L20 10 L20 14 L10 14 L10 20 Z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M2 12 L10 4 L10 10 L20 10 L20 14 L10 14 L10 20 Z'/%3E%3C/svg%3E") center / contain no-repeat;
}

.btn-group { display: flex; flex-direction: column; gap: 10px; }
.join-row  { display: flex; gap: 8px; }
.join-row input { flex: 1; }

.divider {
  display: flex; align-items: center; gap: 10px;
  color: var(--muted); font-size: 0.8rem;
}
.divider::before, .divider::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

/* ── Start-Screen: Welcome + Name-Anzeige ────────────────────── */
/* Fließtext-Absätze linksbündig — mehrzeiliger Satz liest sich an einer
   festen Kante besser; nur die Begrüßungszeile (.welcome-greeting, einzeilig)
   bleibt zentriert unter dem Logo. */
.welcome-text {
  text-align: left; font-size: 0.92rem; line-height: 1.55;
  margin: 0 0 14px 0; color: var(--text);
}
.welcome-greeting { text-align: center; }
/* Besonderheiten-Liste unter dem Willkommenstext: ebenfalls linksbündig, mit
   eigenem Spiegelstrich statt list-style, damit die Einrückung unabhängig
   vom Browser-Default sitzt. */
.welcome-list {
  list-style: none; margin: 0 0 22px 0; padding: 0;
  display: flex; flex-direction: column; gap: 9px;
  font-size: 0.92rem; line-height: 1.45; color: var(--text);
}
.welcome-list > li { position: relative; padding-left: 1.1em; }
.welcome-list > li::before {
  content: '–'; position: absolute; left: 0; color: var(--muted);
}
/* Gleich große Gaps über/unter dem Namen: Label-Margin-bottom (oben) ==
   Wrap-Margin-bottom (unten, zum „Neuen Raum erstellen"-Button); der Name
   selbst trägt keinen Eigen-Margin (siehe #name-display), damit er mittig
   zwischen „Du spielst als" und dem Button sitzt. */
#name-display-wrap { text-align: center; margin-bottom: 18px; }
.name-display-label {
  font-size: 0.72rem; color: var(--muted); font-weight: 600;
  letter-spacing: 0.5px; text-transform: uppercase;
  margin-bottom: 18px;
}
.name-display {
  display: inline-flex; align-items: center;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 10px; color: var(--text);
  font-size: 1.5rem; font-weight: 700;
  padding: 10px 22px; margin: 6px 0;
  font-family: inherit;
}
/* Hauptseite: der Spielername steht ohne Box (kein Hintergrund/Rahmen) und
   etwas größer. Nur die Start-Screen-Instanz (#name-display) — die Box im
   Namensbestätigungs-Popup (#name-confirm-value) bleibt erhalten. */
#name-display {
  background: transparent; border: 0;
  padding: 0; margin: 0;
  font-size: 1.8rem;
}

/* Namensbestätigungs-Popup (Enter in der Namenseingabe): der Name nutzt die
   .name-display-Box, braucht aber Luft zu den Modal-Buttons darunter. */
#name-confirm-value { margin-bottom: 18px; }

/* Namensfeld: Fokus-Rahmen in Textfarbe (schwarz im hellen Theme, creme im
   dunklen) statt Akzent-Rostrot (input[type=text]:focus weiter oben) — das
   Feld steht im Erstbesuch-Intro allein im Mittelpunkt, und der rote Rahmen
   las sich dort wie eine Fehlermeldung. */
#input-name:focus { border-color: var(--text); }

/* „Namen interessieren mich nicht"-Button unterm Namensfeld (Teil der
   #name-input-group, damit nur für Neu-Spieler sichtbar). Zweizeilig — Satz
   oben, Zufallsname fett in der zweiten Zeile — daher das nowrap der
   .btn-Basisregel aufheben. */
.btn-random-name {
  display: block; width: 100%; margin-top: 8px;
  white-space: normal; line-height: 1.4;
}

/* ── Start-Screen: Erstbesuch-Intro ──────────────────────────────
   Beim allerersten Besuch (kein gespeicherter Name) baut sich der Start-
   Screen gestaffelt auf: Logo → Untertitel → Welcome-Text → Namenseingabe →
   Sprachwahl-Button oben rechts (2 s Abstand, 0.6 s Fade — Muster wie der
   Lobby-Onboarding-Hinweis). Alles Übrige bleibt per visibility unsichtbar — nicht
   display:none, damit das Layout stabil bleibt und beim Einblenden am Ende
   nichts springt. visibility ist bewusst nicht in der Transition: sie
   flippt beim Zeigen sofort, nur die Deckkraft fadet.
   html.intro-pending wird vom Früh-Script im index.html-Head gesetzt (vor
   dem ersten Paint, main.js läuft erst danach) — der Screen startet damit
   garantiert leer (nur Hintergrund) und der Logo-Fade beginnt echt bei 0. */
html.intro-pending #screen-start .card > :where(:not(.start-prefs-row)),
html.intro-pending #screen-start :where(.start-prefs-row) > *,
#screen-start.intro-mode .card > :where(:not(.start-prefs-row)),
#screen-start.intro-mode :where(.start-prefs-row) > * {
  opacity: 0; visibility: hidden;
  transition: opacity 0.6s ease;
}
/* Willkommens-Absätze und Besonderheiten-Liste blenden im Intro Stück für
   Stück ein, deshalb sind sie zusätzlich einzeln versteckt (die Regel oben
   greift nur auf den direkten Karten-Kindern). :where() hält die Spezifität
   unter der .intro-show-Regel darunter, damit die das Zeigen gewinnt. */
html.intro-pending #screen-start :where(#welcome-block) > p,
#screen-start.intro-mode :where(#welcome-block) > p,
html.intro-pending #screen-start :where(#welcome-list) > li,
#screen-start.intro-mode :where(#welcome-list) > li {
  opacity: 0; visibility: hidden;
  transition: opacity 0.6s ease;
}
#screen-start.intro-mode .intro-show {
  opacity: 1; visibility: visible;
}
/* Während der laufenden Einführung ist der Willkommenstext eine Blätter-Fläche
   (Tipp = nächster Absatz), kein markierbarer Text — analog .onboard-hint /
   .tutorial-pop. Bewusst nur der Textblock: Namensfeld und Bedienelemente
   daneben bleiben normal bedienbar, und nach dem Intro ist der Text wieder
   markierbar (die Regel hängt an .intro-mode). */
html.intro-pending #screen-start #welcome-block,
#screen-start.intro-mode #welcome-block {
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* Die .start-prefs-row (Theme + Sprache) wird nicht als Ganzes versteckt,
   sondern ihre Kinder einzeln (Regeln oben, per :where niedrig-spezifisch
   gehalten, damit .intro-show gewinnt): so kann der Sprachwahl-Button allein
   einblenden. Er sitzt währenddessen oben rechts fixiert — fremdsprachige
   Neu-Spieler sollen die Sprache wechseln können, ohne erst den deutschen
   Text weglesen zu müssen. Nach dem Intro-Cleanup sitzt er wieder normal in
   seiner Zeile. */
html.intro-pending #screen-start #btn-locale,
#screen-start.intro-mode #btn-locale {
  position: fixed;
  top: calc(10px + var(--safe-top));
  right: calc(12px + var(--safe-right));
  z-index: 5;
}

/* Erstbesuch ohne gewählten Namen: keine Raum-Aktionen. „Neuen Raum erstellen"
   und die Beitreten-Zeile erscheinen erst, wenn ein Name feststeht (JS setzt
   .needs-name, siehe setNameGate) — sonst stünden sie nach einem Reload
   mitten in der Einführung schon da, obwohl beide ohne Namen nicht
   funktionieren. */
#screen-start.needs-name .btn-group { display: none; }

/* Überspringen-Angebot über der Namenseingabe: sichtbar erst, wenn während
   der Einführung irgendwo hin getippt wurde (main.js setzt display) — hier
   steht nur die Optik. Es liegt als Karten-Kind unter denselben Intro-Regeln
   wie alles andere, blendet also mit demselben Fade ein. */
#intro-skip-wrap { text-align: center; margin-bottom: 14px; }

/* ── Start-Screen: Krabbenzangen-Akzente ─────────────────────────
   Kleine Zangen-Glyphen am Primär-Button und den Namens-Labels. Rein
   schmückend: aria-hidden, pointer-events:none, gedämpfte Deckkraft.
   Farbe = Marken-Grün (--violet), funktioniert in beiden Themes.
   (z-index:1 auf den Karten-Kindern ist vestigial — die frühere
   Tannenwald-Deko ist ins Impressum-Modal verlegt.) */
#screen-start .card > h1,
#screen-start .card > .subtitle,
#screen-start .card > #welcome-block,
#screen-start .card > #name-display-wrap,
#screen-start .card > .form-group,
#screen-start .card > .btn-group { position: relative; z-index: 1; }

/* Start-CTAs: einheitliche Button-Höhen.
   „Neuen Raum erstellen" soll exakt so hoch sein wie die Beitreten-Zeile.
   Deren Höhe kam bisher implizit aus dem Code-Input (1.2rem × ~1.32
   Zeilenhöhe + 2×10px Padding + 2×1px Rahmen ≈ 47px) — wir pinnen Input
   UND Button auf dieselbe explizite Höhe, damit die Gleichheit nicht an
   Font-Metriken hängt (#btn-join streckt sich über das Flex-Stretch der
   .join-row automatisch mit). „Ergebnisse"/„Spielregeln" bekommen die
   bisherige Höhe des Erstellen-Buttons (.btn: 0.95rem + 2×6px ≈ 32px). */
.btn-group { --start-cta-h: 47px; }
.btn-group .code-input {
  height: var(--start-cta-h);
  /* Höhe kommt aus `height`; den Text zentriert der Browser im Input. */
  padding-top: 0;
  padding-bottom: 0;
}
#btn-solo,
#btn-create { min-height: var(--start-cta-h); }
#btn-stats,
#btn-rules { min-height: 32px; }

/* Krabbenzangen-Glyph vor den beiden Spiel-startenden Tasten („Solospiel",
   „Neuen Raum erstellen"): die aus claw-left.png getracte Zangen-Silhouette
   (--claw-left, Öffnung nach rechts → zeigt zum Text) als Maske, gefüllt mit
   currentColor (Buttontext-Farbe). Das Glyph ist hochkant (192×214); `contain`
   im quadratischen Kästchen erhält das Seitenverhältnis. Beide Tasten tragen
   es, weil beide dasselbe tun (ein Spiel aufsetzen) — nur mit oder ohne
   Mitspieler; die Trennlinie dazwischen hält sie ohnehin auseinander. */
#btn-solo::before,
#btn-create::before {
  content: "";
  display: inline-block;
  width: 1.05em;
  height: 1.05em;
  margin-right: 0.45em;
  background-color: currentColor;
  vertical-align: -0.16em;
  -webkit-mask: var(--claw-left) center / contain no-repeat;
          mask: var(--claw-left) center / contain no-repeat;
  flex: 0 0 auto;
}
:root {
  /* Krabbenzangen-Silhouetten: aus den vom Nutzer gemalten PNGs
     (image-sources/claw-left/right.png) mit potrace zu inline-SVG getract und
     als Masken eingebettet (nehmen currentColor / --violet an; kein
     Extra-Request, gestochen scharf). Zwei gespiegelte Varianten:
     claw-left öffnet nach rechts, claw-right nach links — beim
     Namens-Label zeigen so beide zur Mitte (zum Namen). Die PNG-Quellen liegen
     nicht mehr in public/ (Docroot), sondern unter image-sources/. Neu tracen
     bei Bildänderung: potrace <(convert image-sources/claw-X.png -alpha extract
     -threshold 50% -negate pbm:-) --svg --flat. */
  --claw-left:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192.000000 214.000000'%3E%3Cg transform='translate(0.000000,214.000000) scale(0.100000,-0.100000)' fill='black'%3E%3Cpath d='M973 2096 c-358 -68 -700 -356 -852 -715 -118 -279 -110 -608 20 -864 85 -168 223 -309 384 -390 151 -76 327 -108 482 -88 446 59 802 446 863 939 14 108 5 166 -31 206 -23 26 -34 31 -71 30 -55 -1 -143 -47 -218 -112 -48 -42 -60 -47 -116 -53 -54 -5 -70 -12 -104 -43 -34 -31 -43 -34 -65 -26 -57 22 -115 10 -162 -33 -12 -11 -29 -17 -37 -14 -14 5 -14 11 -1 48 21 57 19 77 -11 138 -29 57 -29 53 21 136 33 54 39 102 21 156 -22 63 -21 71 24 125 28 34 43 65 51 103 16 71 21 82 87 164 129 163 134 235 20 282 -57 23 -211 29 -305 11z'/%3E%3C/g%3E%3C/svg%3E");
  --claw-right: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192.000000 214.000000'%3E%3Cg transform='translate(0.000000,214.000000) scale(0.100000,-0.100000)' fill='black'%3E%3Cpath d='M687 2099 c-119 -28 -155 -94 -103 -186 13 -23 49 -72 78 -110 66 -82 71 -93 87 -164 8 -38 23 -69 51 -103 45 -54 46 -62 24 -125 -18 -54 -12 -102 21 -156 50 -83 50 -79 21 -136 -30 -61 -32 -81 -11 -138 13 -37 13 -43 -1 -48 -8 -3 -25 3 -37 14 -47 43 -105 55 -162 33 -22 -8 -31 -5 -65 26 -34 31 -50 38 -104 43 -56 6 -68 11 -116 53 -75 65 -163 111 -218 112 -37 1 -48 -4 -71 -30 -36 -40 -45 -98 -31 -206 61 -493 417 -880 863 -939 296 -38 613 104 792 356 255 357 235 846 -48 1234 -140 191 -363 359 -572 431 -126 43 -305 61 -398 39z'/%3E%3C/g%3E%3C/svg%3E");
}

/* Namens-Label („Du spielst als"): kleine Krabbenzangen links/rechts, beide
   zur Mitte (zum Namen) geöffnet — links claw-left (öffnet rechts), rechts
   claw-right (öffnet links). Gefüllt mit Marken-Grün. */
.name-display-label { display: flex; align-items: center; justify-content: center; gap: 8px; }
.name-display-label::before,
.name-display-label::after {
  content: "";
  width: 0.95em; height: 0.95em;
  background: var(--violet);
  opacity: 0.55;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain;    mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.name-display-label::before { -webkit-mask-image: var(--claw-left);  mask-image: var(--claw-left); }
.name-display-label::after  { -webkit-mask-image: var(--claw-right); mask-image: var(--claw-right); }

/* Aquarell-Tannenwald — vom Start-Screen ins Impressum-Modal verlegt: ein
   In-Flow-Block am Ende der Modal-Karte. EINE Hintergrund-Ebene:
     trees-day (AVIF/WebP) — rechts unten verankert (no-repeat); im Dark-Mode
     durch trees-night ersetzt (siehe Override weiter unten).
   Das Bild blendet zur Mitte hin aus; links daneben bleibt der Block leer
   (transparent). Ebene `auto 100%`. pointer-events:none (nur der „Made"-Wurm
   der Signatur nimmt Events an). position:relative ist der Anker für die
   absolut positionierte .made-in-bf-Signatur.
   CSS-`url()` ohne `?v=`-Query (CSS wird nicht getemplatet); Invalidierung
   läuft über ETag (express.static, max-age=0). */
.impressum-forest {
  position: relative;
  /* Höhe kommt als Variable von der Modal-Karte (dort hängt der Breiten-Deckel
     der Karte an derselben Zahl); der Fallback hält die Regel eigenständig. */
  height: var(--impressum-forest-h, clamp(110px, 20vh, 170px));
  margin: 4px 0 14px;
  pointer-events: none;
  opacity: 0.92;
  /* AVIF (kleinste Größe) mit WebP-Fallback via image-set(); die schlichte
     url()-Zeile darüber fängt Engines ohne image-set ab. */
  background-image: url("../trees-day.webp");
  background-image: image-set(
    url("../trees-day.avif") type("image/avif"),
    url("../trees-day.webp") type("image/webp"));
  background-position: right bottom;
  background-repeat: no-repeat;
  background-size: auto 100%;
}
/* Dark mode: eigenes Nacht-Waldbild (trees-night, AVIF/WebP) statt der
   Aquarell-Tannen; höhere Deckkraft, da eigens dunkles Artwork. */
html:not(.light-theme) .impressum-forest {
  opacity: 0.9;
  background-image: url("../trees-night.webp");
  background-image: image-set(
    url("../trees-night.avif") type("image/avif"),
    url("../trees-night.webp") type("image/webp"));
}

/* Schwarzwald-Signatur unten rechts im .impressum-forest-Block (absoluter
   Anker dort). Zweizeilig und rechtsbündig: „🪱" (= Made) über
   „im Schwarzwald" (EN: „Made" über „in Black Forest"), der Umbruch kommt
   per <br> aus der i18n-Quelle. Der Container ist pointer-events:none (blockt
   keine Taps); nur der Wurm nimmt Events an, damit sein title-Tooltip („Made")
   auf Hover erscheint. */
.made-in-bf {
  position: absolute;
  bottom: 8px;
  right: 10px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.15;
  text-align: right;
  color: var(--muted);
  opacity: 0.85;
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
}
/* Light-Mode: Die Signatur liegt rechts unten über dem dunkelgrünen Laub/der
   Hütte des Tagbilds — die dunkle --muted-Tinte verschwindet dort. Daher
   cremeweißer Text mit weichem dunklem Schatten, der über hellem Nebel wie
   über dunklem Laub lesbar bleibt. (Dark-Mode bleibt bei --muted, das über dem
   Nachtbild bereits hell genug ist.) */
html.light-theme .made-in-bf {
  color: #F2EFE8;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}
.made-in-bf .made-worm {
  pointer-events: auto;
  cursor: help;
  font-size: 1.5em;
  vertical-align: -0.18em;
}
/* DE-Variante: kleines „Made"-Bild (data/made-contrast.png, verkleinert) statt
   des 🪱-Zeichens; klein dargestellt, an der Grundlinie von „im" ausgerichtet. */
.made-in-bf .made-worm img {
  display: inline-block; height: 1rem; width: auto;
  vertical-align: -0.25rem;
}

/* Impressum-Karte. Breite und Textausrichtung liegen hier (nicht inline im
   Markup), damit die Desktop-Media-Query unten die Breite überhaupt deckeln
   kann — ein Inline-`style` würde jede Stylesheet-Regel überstimmen. */
#modal-impressum .modal-card {
  text-align: left;
  max-width: 480px;
  --impressum-forest-h: clamp(110px, 20vh, 170px);
}
/* Auf Desktop nur so breit, dass das Waldbild die Karte noch ganz ausfüllt.
   Der Wald ist ein Hintergrundbild mit `background-size: auto 100%`, wird also
   über die HÖHE skaliert: seine gezeichnete Breite ist Höhe × Seitenverhältnis
   (trees-day/night sind 1120×630). Ist die Karte breiter, bleibt links ein
   leerer Streifen. Deshalb hängt der Deckel an derselben Höhen-Variablen wie
   der Wald selbst und wandert mit ihr (20vh) mit; +2×32px Karten-Padding, da
   `box-sizing: border-box` das Padding in max-width einschließt.
   Nur ab 700px (= Desktop-Schwelle dieser Datei): auf Telefonen — besonders im
   Querformat, wo 20vh klein ist — würde der Deckel die Karte einschnüren,
   dort begrenzt `width: 90%` die Breite ohnehin. */
@media (min-width: 700px) {
  #modal-impressum .modal-card {
    max-width: calc(var(--impressum-forest-h) * 1120 / 630 + 64px);
  }
}

/* „Black Forest"-Look auf dem Start-Screen: alle Bedienelemente — Buttons
   (.btn, .name-display, .locale-btn), die Eingabe sowie der Theme-Umschalter
   (Pille + Knopf) — sind scharfkantig und folgen damit ausnahmslos der
   globalen „Brutalist sharp corners"-Regel am Dateiende
   (`*,*::before,*::after { border-radius:0 !important }`). Es gibt hier
   bewusst KEINE border-radius-Overrides mehr. */

/* ── Lobby ───────────────────────────────────────────────────── */

/* Lobby-Card: scrollender Container, oben sticky Bar (analog zum Gameover-
   Screen). Rahmenlos und transparent — der Papier-Hintergrund des Body
   trägt den Look, kein eigener Surface-Block. Füllt das Viewport vertikal
   (siehe `#screen-lobby.active`-Override unten), damit Raum-Code/Header
   oben am Bildschirmrand sitzen und Spielerliste/Buttons unten — statt
   die ganze Card in der Mitte des Bildschirms zu zentrieren. */
#screen-lobby .card,
#lobby-card {
  max-width: 540px;
  width: 100%;
  flex: 1 1 auto;        /* zusammen mit screen-lobby.active → volle Höhe */
  min-height: 0;         /* erlaubt Schrumpfen unter Content-Höhe (Scroll) */
  /* Deckel auf Viewport-Höhe (16px Body-Padding oben+unten), damit bei zu
     hohem Inhalt der overflow-y:auto-Scroll INNERHALB der Card greift, statt
     die Card über den Bildschirmrand hinauswachsen zu lassen. */
  max-height: calc(100dvh - max(16px, var(--safe-top)) - max(16px, var(--safe-bottom)));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: 0;
  border: 0;
  background: transparent;
}
/* Dev-Bar (fixed, 50px, am unteren Rand) setzt das Body-Padding-Bottom auf
   50px → Deckel entsprechend kleiner, sonst rutscht die Card-Unterkante
   (inkl. sticky Button-Bar) unter die Bar. */
body.has-top-bar #screen-lobby .card,
body.has-top-bar #lobby-card {
  max-height: calc(100dvh - max(16px, var(--safe-top)) - 50px - var(--safe-bottom));
}
/* Standard-`.screen.active` zentriert den Inhalt vertikal per `margin: auto`.
   In der Lobby wollen wir das Gegenteil — die Card streckt sich zwischen
   Body-Top-Padding und Body-Bottom-Padding, damit Header (Raum-Code) am
   oberen und Bottom-Bar (Spieler + Buttons) am unteren Viewport-Rand
   verankert sind. Höhere Spezifität (ID) überstimmt die Klassen-Regel. */
#screen-lobby.active {
  margin-top: 0;
  margin-bottom: 0;
  flex: 1 1 auto;
  min-height: 0;
  justify-content: flex-start;
}

/* Sticky Bottom-Bar mit Spielerliste und Start/Zurück-Buttons. Bekommt
   dieselbe Papier-Textur wie der Body, damit die Bar visuell nahtlos in
   den gescrollten Lobby-Inhalt oberhalb übergeht (statt einer opaken
   Cremefläche, die als sichtbare Kante über dem Papier säße). */
#lobby-bar {
  position: sticky;
  bottom: 0;
  z-index: 10;
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
  border-radius: 0 0 14px 14px;
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex-shrink: 0;
}
/* Drei-Spalten-Raster wie in der Spielende-Bar: „Zurück" links, „Spiel starten"
   (bzw. die Warte-Nachricht für Nicht-Hosts) exakt in der Bar-Mitte, der ☰
   rechts. Gleich breite 1fr-Außenspalten zentrieren die auto-breite
   Mittelspalte; feste Spaltenzuordnung hält den ☰ rechts, auch wenn die
   Mittelspalte mal leer ist. */
#lobby-bar-buttons {
  display: grid;
  /* 1fr = minmax(auto,1fr): jede Außenspalte ist mindestens so breit wie ihr
     Inhalt → die Buttons können sich NIE überlappen. Da „Zurück" breiter ist
     als „☰", sitzt „Spiel starten" dadurch etwas rechts der exakten Bar-Mitte
     — bewusst in Kauf genommen (gegen Überlappung). Echte Zentrierung ginge nur
     mit minmax(0,1fr), das aber auf schmalen Handys überlappt. */
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
}
/* Kompakte seitliche Polsterung wie in der Spielende-Bar — die Standard-12px
   ließen die Reihe (Zurück + „Spiel starten" + ☰) auf schmalen Handys über die
   Card-Breite laufen und den ☰ am rechten Rand abschneiden. */
#lobby-bar-buttons .btn { padding-left: 7.2px; padding-right: 7.2px; }
#lobby-bar-buttons #btn-back-lobby   { grid-column: 1; justify-self: start; }
#lobby-bar-buttons .lobby-bar-right  { grid-column: 2; justify-self: center; }
#lobby-bar-buttons .btn-menu         { grid-column: 3; justify-self: end; }
/* Einheitliche Höhe für alle drei Bar-Buttons, damit sie optisch auf einer
   Linie sitzen. box-sizing:border-box → 36px ist die Gesamthöhe. min/max-height
   zusätzlich, weil ein Flex-Strecken (align-self:stretch) zwar eine definite
   `height`, aber niemals `max-height` überschreibt → harte Obergrenze. */
#lobby-bar-buttons #btn-back-lobby,
#lobby-bar-buttons #btn-start,
#lobby-bar-buttons .btn-menu {
  height: 36px; min-height: 36px; max-height: 36px;
  align-self: center;
}
/* „Spiel starten" steckt zwei Flex-Wrapper tief (.lobby-bar-right →
   #lobby-start-wrap). Damit die Wrapper den Button nicht über 36px strecken:
   die Wrapper schrumpfen auf Inhaltshöhe (align-items:center) und der Button
   selbst zentriert sich und wird hart auf 36px begrenzt. Der transparente
   1px-Rahmen gleicht zudem das Box-Modell an die umrandeten Ghost-Buttons an
   (sonst wirkt der gefüllte Button minimal höher). */
#lobby-bar-buttons .lobby-bar-right,
#lobby-bar-buttons #lobby-start-wrap { align-items: center; align-self: center; }
#lobby-bar-buttons #btn-start {
  border: 1px solid transparent;
  align-self: center;
  min-height: 36px;
  max-height: 36px;
  flex: 0 0 auto;
}
.lobby-bar-right {
  display: flex; gap: 8px; align-items: center; justify-content: center;
  flex-wrap: wrap;
  min-width: 0;
}
#lobby-bar-status {
  display: flex; flex-direction: column; gap: 6px;
}

/* Restlicher Lobby-Inhalt scrollt unter der Bar. */
#lobby-content {
  padding: 12px 16px;
  flex: 1;
}
/* Sticky Kopfzeile mit Raum-Code-Box links und Menü-Button rechts.
   `position: sticky; top: 0` hält sie beim Scrollen oben sichtbar.
   Eigene Zeile oberhalb von #lobby-content — der Fließtext beginnt
   darunter am linken Card-Rand. Papier-Hintergrund wie die Bottom-Bar,
   damit die Kante beim Drüberscrollen visuell verschmilzt. */
#lobby-header {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Linkes Padding = linkes Padding von #lobby-content (16px), damit die
     linke Kante der Raum-Code-Box exakt mit der ersten Zeile des Fließtextes
     darunter fluchtet. */
  padding: 8px 16px 4px;
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
  border-radius: 14px 14px 0 0;
}
#lobby-menu-btn {
  padding: 2px 10px;
  font-size: 1.1rem;
  line-height: 1;
}
/* Marken-Logo rechts oben in der Lobby-Kopfzeile (Titel links, Code mittig). */
#lobby-logo {
  height: 30px;
  width: auto;
  flex-shrink: 0;
  margin-left: auto;             /* rechtsbündig in die obere rechte Ecke */
  pointer-events: none;          /* rein dekorativ, fängt nie einen Tap ab */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
}
/* Raum-Code-Gruppe rechtsbündig in der oberen rechten Ecke */
/* Raum-Code mittig im Header (über die ganze Headerbreite zentriert,
   unabhängig von Titel links und Logo rechts). */
.lobby-code-group {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
}
/* Scroll-Schatten an den sticky Kanten — sichtbar nur, wenn der Inhalt der
   Card vertikal über die sichtbare Höhe hinausgeht. Die Flags werden in
   main.js (setupLobbyScrollShadows) auf #lobby-card gesetzt. Der Schatten
   sitzt jeweils auf der Innenkante der sticky Bar zum scrollenden Inhalt
   hin, damit Spieler*innen sehen, dass es oberhalb/unterhalb weitergeht. */
#lobby-card[data-overflow-top="true"] #lobby-header {
  box-shadow: 0 6px 8px -4px rgba(0,0,0,0.28);
}
#lobby-card[data-overflow-bot="true"] #lobby-bar {
  box-shadow: 0 -6px 8px -4px rgba(0,0,0,0.28);
}
/* Raum-Code-Box oben links: dezente graue Markierung zum schnellen Erkennen
   des Raumes, klickbar zum Kopieren. Bewusst optisch zurückgenommen — die
   prominente, fettgedruckte Erwähnung des Codes lebt im ersten Satz des
   Fließtextes (siehe .prose-room-code). */
.lobby-code-box {
  background: var(--violet);
  border: 1px solid var(--violet-d);
  border-radius: 6px;
  padding: 2px 10px;
  color: #fff;
  font: inherit;
  font-weight: 600;
  letter-spacing: 0.15em;
  cursor: pointer;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.lobby-code-box:hover,
.lobby-code-box:focus-visible {
  background: var(--violet-d);
  border-color: var(--violet-d);
  color: #fff;
  outline: none;
}
.lobby-code-box:active { transform: scale(0.99); }
/* "Raum"-Label + Code-Box als Gruppe links nebeneinander, damit das Label
   beim space-between des Headers nicht von der Box weggeschoben wird. */
.lobby-code-group {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}
/* Subtitel über Sub-Abschnitten in der Sticky-Bar (z.B. "Spieler:"). */
.lobby-section-label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 2px;
}

/* Spielvarianten-Popup — vom Menü-Button oben rechts geöffnet. Backdrop
   blockiert Klicks hinter dem Popup; das Popup selbst sitzt mittig im
   Viewport. Optik analog zu .settings-popup, aber breiter, weil die
   Variantenzeilen lang werden können. */
.lobby-variants-backdrop {
  position: fixed; inset: 0;
  z-index: 1100;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(3px);
}
.lobby-variants-backdrop.hidden { display: none; }
.lobby-variants-popup {
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 1101;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 14px 40px rgba(0,0,0,0.55);
  width: min(420px, 92vw);
  max-height: 80dvh;
  display: flex;
  flex-direction: column;
  touch-action: pan-y;
  overscroll-behavior-x: none;
  animation: fadeIn 0.12s;
}
.lobby-variants-popup.hidden { display: none; }
.lobby-variants-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  padding: 10px 12px 6px;
}
.lobby-variants-head h3 {
  margin: 0; font-size: 0.95rem; font-weight: 800;
}
.lobby-variants-list {
  flex: 1;
  overflow-y: auto;
  padding: 4px 6px 8px;
  display: flex; flex-direction: column; gap: 4px;
}
.lobby-variant-row {
  display: flex; align-items: stretch; gap: 4px;
}
.lobby-variant-item {
  flex: 1; min-width: 0;
  text-align: left;
  background: none; border: none;
  border-radius: var(--radius);
  padding: 8px 10px;
  cursor: pointer;
  font: inherit;
  color: var(--text);
  white-space: normal;
  transition: background 0.12s;
}
.lobby-variant-item:hover,
.lobby-variant-item:focus-visible { background: var(--surface2); outline: none; }
.lobby-variant-item.is-current {
  background: var(--surface2);
  box-shadow: inset 3px 0 0 var(--accent);
}
.lobby-variant-remove {
  flex: 0 0 auto;
  background: none; border: none;
  border-radius: var(--radius);
  padding: 0 10px;
  cursor: pointer; color: var(--muted);
  font: inherit;
  transition: background 0.12s, color 0.12s;
}
.lobby-variant-remove:hover { background: var(--surface2); color: var(--red); }
.lobby-variants-empty {
  color: var(--muted);
  text-align: center;
  padding: 18px 12px;
  font-size: 0.88rem;
}
/* Raum-Code im Fließtext: kleine umrandete Box mit dem Code zum schnellen
   Erkennen, klickbar zum Kopieren. Sitzt als Inline-Button im ersten Satz
   ("Wir spielen … in Raum CODE."). */
.prose-room-code {
  border: 1px solid var(--text);
  border-radius: 6px;
  background: var(--bg);
  padding: 1px 8px;
  letter-spacing: 0.2em;
  transition: border-color 0.12s, background 0.12s;
}
@media (hover: hover) {
  .prose-room-code:hover { border-color: var(--accent); background: var(--surface2); }
}
.prose-room-code:active { transform: scale(0.99); }
/* Lobby-Spielerliste: inline-flow mit Kommas zwischen den Kacheln. Die Liste
   sitzt in der sticky Bottom-Bar und wächst nach oben — die darunter liegenden
   Buttons bleiben fix verankert, also kein Reservieren leerer Reihen für
   spätere Beitritte (war zu Zeiten nötig, als die Liste oberhalb der Buttons
   im Fluss lag). max-height begrenzt auf drei Reihen, alles darüber scrollt
   innerhalb des Containers. touch-action:pan-y / overscroll-behavior-x:none
   verhindern, dass FF Android leichte horizontale Wischbewegungen als
   Zurück-Geste interpretiert. */
.player-list {
  --player-row-h: 30px;
  --player-row-gap: 6px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: flex-start;
  gap: var(--player-row-gap);
  max-height: calc(var(--player-row-h) * 3 + var(--player-row-gap) * 2);
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior-x: none;
  margin-bottom: 12px;
}
.player-item {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--bg); border-radius: 8px; padding: 0 10px;
  font-size: 0.85rem;
  height: var(--player-row-h);
  box-sizing: border-box;
}
.player-item .pname { white-space: nowrap; }
.player-item .me    { color: var(--muted); font-size: 0.7rem; flex-shrink: 0; }
/* Kommas zwischen den Kacheln — kleine, vertikal zentrierte Text-Items. */
.player-sep {
  align-self: center;
  color: var(--muted);
  font-weight: 700;
}

.settings-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  margin-bottom: 20px;
}
.settings-grid label { font-size: 0.78rem; color: var(--muted); font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; display: block; margin-bottom: 4px; }
.settings-grid select, .settings-grid input[type=number] {
  width: 100%; background: var(--bg); border: 1px solid var(--border);
  border-radius: 6px; color: var(--text); padding: 7px 10px; font-size: 0.9rem; outline: none;
}
.settings-grid select:focus, .settings-grid input[type=number]:focus { border-color: var(--accent); }
.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; }
.toggle-row label { font-size: 0.85rem; }
input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }

/* ── Game screen ─────────────────────────────────────────────── */

.game-wrapper {
  width: 100%;
  max-width: 960px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.game-body {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 20px;
}
.game-left {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  min-width: 300px;
}
.game-right {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

/* header bar */
.game-header {
  display: flex; align-items: center; gap: 8px;
  width: 100%; background: var(--surface); border-radius: var(--radius);
  padding: 8px 12px; border: 1px solid var(--border);
  overflow: hidden; /* verhindert horizontales Überlaufen in Firefox */
  flex-shrink: 0;
}
.game-header #game-logo { height: 22px; width: auto; flex-shrink: 0; margin-left: 28px; pointer-events: none; }
.game-header .spacer { flex: 1; min-width: 4px; }
/* Icon-Buttons in der Header-Leiste kompakter */
.game-header .btn-sm { padding: 3px 5.4px; flex-shrink: 0; }
.timer-display { font-size: 1rem; font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.timer-display.urgent { color: var(--red); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }

/* word display */
.word-display {
  font-size: 2rem; font-weight: 800; letter-spacing: 4px; min-height: 52px;
  text-align: center; color: var(--text);
  /* KEIN text-transform: uppercase — der Inhalt kommt bereits großgeschrieben
     aus updateDisplay() (upperWord, ß-erhaltend). CSS-uppercase würde `ß` wieder
     zu `SS` machen (CSS bildet ß auf SS ab), also das getippte Eszett kaputt-
     rendern, obwohl im DOM korrekt `ß` steht. */
  position: relative;
}
/* Pflichtbuchstabe im Tipp-Bereich bekommt keine Sonderfarbe — der
   Akzent sitzt einzig am Hex-Eingabe-Center-Knopf; ein doppeltes
   Highlight im Tipp-Bereich macht den Block nur unruhiger. */
.word-display .word-display-inner {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: break-word;
  padding: 4px 14px;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: #fff;
}

/* Abgeschicktes Wort, dessen Server-Antwort noch aussteht: ausgegraut
   stehen lassen statt sofort zu verschwinden — bei langsamer Verbindung
   sieht der Spieler so, dass das Wort noch "unterwegs" ist. Opacity statt
   Farbwechsel, damit es in beiden Themes (dunkle Box / helle Buchstaben-
   Kästen) gleichermaßen als "inaktiv" liest. Neues Tippen ersetzt die
   graue Anzeige sofort — Eingabe ist nie blockiert. */
.word-display .word-display-inner.wd-pending { opacity: 0.45; }

/* Light-Theme Tipp-Bereich: KEIN umrahmender Karten-Kasten mehr — der
   word-display-inner ist transparent (kein Hintergrund, kein Rahmen,
   kein eigener Shadow). Stattdessen bekommt JEDER Buchstaben-Kasten
   einen eigenen weichen Drop-Shadow, sodass die Einzelboxen wie kleine
   Karten auf dem Papier schweben. */
.light-theme .word-display { letter-spacing: 1px; }
.light-theme .word-display .word-display-inner {
  background: transparent;
  border: none;
  box-shadow: none;
  /* Etwas vertikales Padding, damit die Drop-Shadows der Letter nicht
     vom .word-display-Container abgeschnitten werden. */
  padding: 6px 4px;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  color: var(--text);
}
.light-theme .word-display .wd-letter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width:  1.25em;
  height: 1.45em;
  margin: 0 2px;
  vertical-align: middle;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--text);
  border-radius: 4px;
  font-weight: 800;
  line-height: 1;
  /* Drop-Shadow pro Buchstabenkasten — hebt jeden einzelnen Letter vom
     Papier ab, ohne dass eine umschließende Karte nötig ist. */
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.28);
}
/* Pflichtbuchstabe im Tipp-Bereich: gleiche Cremebox + Drop-Shadow wie die
   anderen Buchstaben (geerbt), zusätzlich ein Akzent-Streifen INNEN am
   unteren Boxrand. Höhe 6 px → sitzt im unteren Padding-Bereich der Box
   (Letter ist via display: inline-flex; align-items: center vertikal in
   ~1.45em Höhe zentriert; bei 1.5rem Schrift bleiben ~9 px Leerraum
   unterhalb des Glyphs frei). Der Buchstabe behält Größe und Position;
   der Streifen darf den Glyph leicht berühren, schluckt ihn aber nicht. */
.light-theme .word-display .wd-letter.center-char {
  box-shadow:
    inset 0 -6px 0 var(--accent),
    0 3px 6px rgba(0, 0, 0, 0.28);
}
/* Dark-Theme Tipp-Bereich: keine Buchstaben-Boxen (Buchstaben sind schlichter
   Text), daher kein Inset-Streifen wie im Light-Theme. Den Pflichtbuchstaben
   markieren wir hier stattdessen mit einer Terrakotta-Unterstreichung (--gold,
   in beiden Themes Terrakotta) — gleiche Aussage wie der Akzent-Streifen oben. */
html:not(.light-theme) .word-display .wd-letter.center-char {
  text-decoration: underline;
  text-decoration-color: var(--gold);
  text-decoration-thickness: 3px;
  text-underline-offset: 3px;
}
.word-cursor {
  display: inline-block; width: 3px; height: 1.1em;
  /* Terrakotta in beiden Themes (--gold). Eigene Variable statt --accent,
     damit der Cursor unabhängig vom Akzent retuschiert werden kann. */
  background: var(--gold); margin-left: 3px;
  vertical-align: middle; border-radius: 2px;
  animation: blink 1s step-end infinite;
}
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
.word-placeholder { color: var(--muted); font-size: 1rem; font-weight: 400; letter-spacing: 1px; }

/* hex grid — kein transition, kein tap-highlight → kein Flackern in Chrome */
/* Design/Typewriter: alle Letter-Kreise gleich gefärbt wie die hellen
   Eck-Tasten (Löschen/Mischen). Vorher war .hex-center in Akzent-
   Vermilion gerendert — wir nehmen das raus, weil die Mitte hier
   keine spezielle Spielbedeutung hat (kein Required-Letter wie bei
   Spelling Bee). */
/* Pflichtbuchstabe (Mittel-Kreis) + Außen-Buchstaben (Annulus-Sektoren auf
   einem Ring). Die Sektoren sind SVG-Pfade, der Mittel-Knopf ist ein Kreis.
   Der Schatten liegt NICHT mehr pro Taste, sondern als EINE gemeinsame
   Ebene (.hex-key-shadow, Filter #key-shadow-only in index.html) unter
   allen Tasten-Körpern — so heben sich alle Buchstaben-Tasten auf
   derselben „z-Höhe" vom Papier ab (kein Körper verdeckt den Schatten
   des Nachbarn). Stroke: none — die Tasten werden allein über Fill +
   Schatten abgehoben. */

/* Pflichtbuchstabe (Center-Kreis): warmer Terrakotta-Akzent als Hintergrund —
   Sichtanker analog zum mittleren Knopf bei Spelling Bee. Hover → --accent-d.
   Hell-Theme nutzt --accent, Dunkel-Theme --gold (beide Terrakotta). Da
   Terrakotta in BEIDEN Themes dunkel ist, trägt das Center-Label helle
   (cremefarbene) Schrift; das !important übersteuert den Inline-fill aus
   renderHex() (var(--text), im Hell-Theme dunkel). */
/* Brücke zum Eingabe-Modul (public/hex-input/): das Modul kennt keine
   App-Variablen, es liest nur eigene --hx-*-Tokens mit Defaults. Hier werden
   sie mit den Theme-Farben belegt. Wer eine Farbe ändern will, ändert sie
   HIER — im Modul stehen nur die Rückfallwerte.

   Auf --hx-center steht in beiden Themes ein dunkler Terrakotta-Ton, das
   Label darauf trägt deshalb immer Creme (Theme-Regel: auf --accent steht
   IMMER cremefarbener Text). */
.hex-region, .word-display {
  --hx-key:          var(--hex-key);
  --hx-key-hover:    var(--hex-key-hover);
  --hx-center:       var(--gold);
  --hx-center-hover: var(--gold-d);
  --hx-center-label: #FBF3E3;
  --hx-submit:       var(--violet);
  --hx-submit-hover: var(--violet-d);
  --hx-submit-icon:  #fff;
  --hx-countdown:    var(--gold);
  --hx-text:         var(--text);
}
.light-theme .hex-region, .light-theme .word-display {
  --hx-center:       var(--accent);
  --hx-center-hover: var(--accent-d);
}

/* Eck-Tasten als Annulus-Sektoren AUSSERHALB des Letter-Rings (NW=Löschen,
   NE=Eingeben, SE=Mischen). Statisch in index.html als SVG-Gruppen mit IDs
   `btn-delete` / `btn-submit` / `btn-shuffle`; die Pointer-Handler weiter
   unten in main.js binden weiterhin per getElementById. */

/* Hex-Region: voller-Breite-Container, der den quadratischen .hex-frame
   unten-mittig hält. Die Region nimmt im .game-left-Flex den restlichen
   Vertikalplatz ein; der Frame sitzt am unteren Rand (align-items: flex-end),
   damit das Eingabefeld immer am unteren Bildschirmrand klebt. */
.hex-region {
  position: relative;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

/* Quadratischer Inner-Frame: hält das SVG zentriert. Die Eck-Tasten sind
   NICHT mehr am Frame, sondern an der .hex-region pinned (= linker/
   rechter Bildschirmrand) — Frame braucht daher kein position: relative
   mehr, sonst würden die Buttons ihn als nächsten positionierten Vorfahren
   verwenden und wieder an den SVG-Ecken kleben. flex:0 0 auto verhindert
   Quer-Stretch im hex-region-Flex. */

/* (Vorige HTML-Button-Eck-Tasten entfernt — Eck-Tasten sind jetzt SVG-
   Sektoren innerhalb von #hex-svg, gestylt oben unter .hex-corner-svg.) */

/* input row */
.input-row { display: flex; gap: 8px; width: 100%; }

/* ── Scores panel ────────────────────────────────────────────── */

.panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 16px;
}
/* Section-Break-Anpassung: Panel hat 16px Innenabstand statt 20px. */
.panel .panel-title { margin: 8px -16px 8px; padding: 6px 16px 0; }
/* Section-Break: Titel in dezentem Hellgrau, Linie OBERHALB — reicht bis zum
   Card-Rand (negative seitliche Margin gleicht das 20px-Padding des
   Eltern-Containers #lobby-content / #lobby-bar / #gameover-content aus). */
.panel-title {
  font-size: 0.85rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--muted);
  margin: 12px -20px 8px;
  padding: 6px 20px 0;
  border-top: 1px solid var(--border);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
/* Erste Section-Break-Linie weglassen, damit über dem ersten Titel kein
   leerer Abschnitt entsteht. */
.panel-title:first-child { border-top: none; margin-top: 0; padding-top: 0; }
.panel-title.panel-title--with-code {
  display: flex !important;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: nowrap;
  text-decoration: none;
}
.panel-title.panel-title--with-code > span:first-child {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  flex: 1 1 auto;
  min-width: 0;
}
.settings-room-code {
  display: inline-flex; align-items: baseline; gap: 6px;
  text-decoration: none;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
}
.settings-room-code .src-label {
  color: var(--muted);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.settings-room-code .src-code {
  color: var(--accent-text);
  font-weight: 800;
  font-size: 0.95rem;
  letter-spacing: 2px;
}


.score-list { display: flex; flex-direction: column; gap: 4px; }
.score-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 6px; font-size: 0.9rem;
}
.score-item.me-item { background: rgba(181,90,46,0.1); }
.score-item .rank { font-weight: 700; color: var(--muted); width: 18px; text-align: center; font-size: 0.8rem; }
.score-item .s-name { flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.score-item .s-score { font-weight: 800; font-variant-numeric: tabular-nums; }
.score-item.rank-1 .s-score { color: var(--accent-text); }

/* word feed */
.word-feed { display: flex; flex-direction: column; gap: 3px; max-height: 240px; overflow-y: auto; }

/* Alle-haben-Bereich im Wörter-Drawer */
.all-found-section { margin-top: 10px; }
.all-found-title { font-size: 0.72rem; font-weight: 700; color: var(--muted); letter-spacing: 0.5px; text-transform: uppercase; margin-bottom: 6px; }
.all-found-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.all-found-chip {
  padding: 3px 10px; border-radius: 20px; font-size: 0.8rem; font-weight: 800;
  background: #d4d4d8; color: #18181b; letter-spacing: 0.5px;
  border: 1.5px solid #d4d4d8;
}
.all-found-chip.pangram { letter-spacing: 0.15em; }
.word-feed::-webkit-scrollbar { width: 4px; }
.word-feed::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 4px; }
.feed-item {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px; border-radius: 6px; font-size: 0.85rem;
  background: var(--bg); animation: fadeIn 0.3s;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; } }
.feed-item .f-word { font-weight: 700; letter-spacing: 1px; flex: 1; }
.feed-item .f-pts  { font-weight: 700; color: var(--green); font-size: 0.8rem; }
.feed-item .f-player { color: var(--muted); font-size: 0.78rem; }
.feed-item.pangram { background: rgba(181,90,46,0.1); }
.feed-item.pangram .f-word { font-weight: 900; }
.feed-item.first-finder { background: rgba(181,90,46,0.1); }
.feed-item.first-finder .f-word { color: var(--accent-text); }

/* ── Score-Chips über den Waben (vertikale Liste, eine Zeile je Spieler) ─── */
.score-chips {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  margin-bottom: 8px;
}
/* Im Desktop-Layout ist .game-left flex:0 0 auto — eine width:100% auf
   .score-chips erzeugt zirkuläre Größenberechnung in Firefox und kollabiert
   die ganze Spalte (inkl. SVG). Daher Volle-Breite-Box nur im game-mode. */
body.game-mode .score-chips {
  width: 100%;
  max-width: 500px;
  align-self: center;
}
.score-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px;
  border-radius: 10px;
  border: 1.5px solid var(--border);
  background: var(--surface);
  font-size: 0.85rem;
  position: relative;
  will-change: background, box-shadow;
}
body.game-mode .score-chip { width: 100%; }
.score-chip.me-chip   { border-color: var(--accent); }
.score-chip.bot-chip  { border-color: #8b5cf6; opacity: 0.85; }
.score-chip .c-rank     { font-size: 0.8rem; flex-shrink: 0; }
.score-chip .c-name-col { display: flex; flex-direction: column; min-width: 0; flex-shrink: 1; }
.score-chip .c-name     { font-weight: 600; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.score-chip .c-pts      { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--accent-text); flex-shrink: 0; order: 99; }
.score-chip .c-activity {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: row;
  justify-content: flex-end; align-items: center;
  gap: 6px; overflow: hidden;
}
.score-chip .c-count {
  font-size: 0.78rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.score-chip .c-count.warn {
  color: var(--red);
  animation: pulse 1s infinite;
}
.score-chip .c-delta  { font-size: 0.72rem; font-weight: 700; opacity: 0; position: absolute; }
.score-chip .c-word   {
  font-size: 0.75rem; font-weight: 800; letter-spacing: 0.5px;
  max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  animation: fadeIn 0.15s;
}
.score-chip .c-word.ok   { color: var(--green); }

/* Aufleuchten: normales Wort */
@keyframes chip-gain {
  0%   { background: rgba(34,197,94,0.55);  box-shadow: 0 0 0 3px rgba(34,197,94,0.35);  border-color: var(--green); }
  70%  { background: rgba(34,197,94,0.25);  box-shadow: 0 0 0 1px rgba(34,197,94,0.15);  border-color: var(--green); }
  100% { background: var(--surface);        box-shadow: none;                             border-color: var(--border); }
}
/* Aufleuchten: Punktverlust (Stadt-Land-Fluss-Bonus-Korrektur) */
@keyframes chip-lost {
  0%   { background: rgba(239,68,68,0.55);  box-shadow: 0 0 0 4px rgba(239,68,68,0.4);   border-color: var(--red); }
  60%  { background: rgba(239,68,68,0.25);  box-shadow: 0 0 0 2px rgba(239,68,68,0.15);  border-color: var(--red); }
  100% { background: var(--surface);        box-shadow: none;                             border-color: var(--border); }
}
.score-chip.flash-gain  { animation: chip-gain  0.9s ease-out forwards; }
.score-chip.flash-lost  { animation: chip-lost  1.3s ease-out forwards; }

/* me-chip bekommt bei gain/lost die eigene Hintergrundfarbe als Ausgangspunkt */
.score-chip.me-chip.flash-gain  { animation: chip-gain  0.9s ease-out forwards; }
.score-chip.me-chip.flash-lost  { animation: chip-lost  1.3s ease-out forwards; }

/* Offline-Plakette: zeigt Mitspieler, deren Browser/Verbindung weg ist und
   die nach DISCONNECT_GRACE_MS noch nicht zurück sind. Das Icon-Span ist als
   Kind des Namen-Spans eingebettet (sitzt damit direkt hinter dem Namen,
   ohne Flex-Gap dazwischen) und bekommt eine ruhige Blink-Animation.
   Kein Dimming des Eintrags — der Spieler steht schließlich noch im Spiel. */
.score-chip .c-offline,
.grid-score-table .gst-box .gst-offline-icon {
  display: none;
}
.score-chip.chip-offline .c-offline,
.grid-score-table .gst-box.gst-offline .gst-offline-icon {
  display: inline;
  margin-left: 4px;
  font-size: 0.9em;
  animation: blink-offline 1.4s ease-in-out infinite;
}
@keyframes blink-offline {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

/* progress bar (info panel) */
.progress-wrap { margin-bottom: 4px; }
.progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--muted); margin-bottom: 4px; }
.progress-bar { height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--green); border-radius: 3px; transition: width 0.4s; }

/* ── Game over ───────────────────────────────────────────────── */

/* Der Bildschirm reicht hier von Rand zu Rand, und das ist eine Scroll-Frage,
   keine Geschmacksfrage: der Scroll-Container ist die KARTE, also ist alles
   außerhalb von ihr tote Fläche — dort greift man zu und die Seite rührt sich
   nicht. Das seitliche Body-Padding wird darum wieder herausgenommen (senkrecht
   bleibt es stehen, Cutout und Home-Indicator bleiben ausgespart), die Karte
   nimmt die volle Breite, und die LESESPALTE wird eine Ebene weiter innen
   gekappt (#gameover-content, #rematch-bar > *). Sichtbar ändert das nichts:
   die Karte ist rahmen- und flächenlos, sie hat gar keine Kante, die breiter
   werden könnte. Gewonnen ist der Griffstreifen — und die Breite, die das
   Krebsworträtsel-Gitter daraus zieht (siehe .solution-grid--crab).

   `align-self: stretch` + `width: auto`: der Screen ist ein Flex-Kind des
   Body (align-items: center), das sich sonst auf seine Inhaltsbreite zöge und
   die negativen Margins nur verschöben statt zu dehnen. */
#screen-gameover {
  max-width: none;
  width: auto;
  align-self: stretch;
  margin-inline:
    calc(-1 * max(16px, var(--safe-left)))
    calc(-1 * max(16px, var(--safe-right)));
}

/* Card als Scroll-Container mit Viewport-Höhe — der rahmenlose Look
   passt zum Papier-Hintergrund: kein Rahmen, kein Surface-Block. */
#screen-gameover .card,
#gameover-card {
  /* Die Lesespalte, wie sie früher die Kartenbreite war, und die Polsterung,
     die sie einrückt. EINE Quelle für beides: die Kappung sitzt jetzt an
     mehreren Stellen (Inhalt, Kopfzeile), und wer sie ändert, soll das an
     einer Stelle tun. Das Herausziehen an den Rand rechnet ebenfalls damit. */
  --go-col: 500px;
  --go-pad: 20px;
  max-width: none; /* die Kappung sitzt eine Ebene tiefer */
  width: 100%;
  max-height: calc(100dvh - max(16px, var(--safe-top)) - max(16px, var(--safe-bottom))); /* = Viewport minus Body-Padding (16px bzw. Safe-Area-Inset) oben+unten */
  overflow-y: auto;
  overflow-x: hidden; /* nichts darf seitlich schiebbar werden (CLAUDE.md) */
  display: flex;
  flex-direction: column;
  padding: 0; /* Padding wird von #gameover-content übernommen */
  border: 0;
  background: transparent;
}

/* Kopfzeile (Titel + Logo + Buttons) immer OBEN am Bildschirm halten — auch
   wenn die Auswertung in weniger als eine Bildschirmhöhe passt. `.screen.active`
   zentriert sonst per `margin: auto 0` die kurze Card vertikal, sodass die
   Spielende-Kopfzeile in der Bildschirmmitte schwebt. margin-top:0 heftet den
   Screen oben an (margin-bottom:auto schluckt den Rest). */
#screen-gameover.active { margin-top: 0; }

/* Sticky Rematch-Bar — haftet am oberen Rand der scrollenden Card und bildet
   einen festen Bereich: „← Zurück" links, „Revanche" rechts, Logo mittig.
   Trägt dieselbe Papier-Textur wie der Body (wie die Lobby-Bars), damit der
   gescrollte Inhalt NICHT durch die Bar hindurchscheint, sondern darunter
   verschwindet — und die Kante trotzdem nahtlos ins Papier übergeht. */
#rematch-bar {
  position: sticky;
  top: 0;
  z-index: 10;
  /* Die Bar selbst geht über die volle Breite (ihre Papier-Fläche soll die
     ganze Kante decken), ihr INHALT bleibt in der Lesespalte — siehe
     `#rematch-bar > *` weiter unten. */
  padding:
    8px
    max(var(--go-pad), var(--safe-right))
    10px
    max(var(--go-pad), var(--safe-left));
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 8px;
  flex-shrink: 0;
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
}
/* Drei-Spalten-Raster: „Zurück" links, „Revanche" exakt in der Bar-Mitte,
   der ☰ rechts. minmax(0,1fr) (nicht 1fr = minmax(auto,1fr)) erzwingt exakt
   gleich breite Außenspalten unabhängig von den unterschiedlich breiten
   Seiten-Buttons und zentriert die Mittelspalte korrekt. */
#rematch-bar-buttons {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: 8px;
  align-items: center;
}
/* Feste Spaltenzuordnung: sonst rutscht der ☰ in die Mitte, sobald „Revanche"
   per display:none entfällt (Einzelspieler gegen Bot) und die Auto-Platzierung
   die verbleibenden Items nach links nachrückt. */
#rematch-bar-buttons #btn-back-lobby-top { grid-column: 1; justify-self: start; }
#rematch-bar-buttons #btn-play-again     { grid-column: 2; }
#rematch-bar-buttons .btn-menu           { grid-column: 3; justify-self: end; }
/* Schmalere seitliche Polsterung für die beiden Bar-Buttons (← Zurück /
   Revanche): die Standard-20px ließen die Reihe (Button + Logo + Button) auf
   schmalen Handys über die Card-Breite laufen → horizontales Scrollen. 12px
   reichen für ausreichend große Tap-Ziele und vermeiden den Überlauf. */
#rematch-bar-buttons .btn {
  padding-left: 7.2px;
  padding-right: 7.2px;
}
/* Logo mittig zwischen den beiden Buttons */
/* Obere Zeile im Spielende-Header: Titel links, Logo in der oberen rechten Ecke. */
.rematch-bar-top {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%;
}
#gameover-logo {
  height: 30px;
  width: auto;
  flex: 0 0 auto;
  pointer-events: none;          /* rein dekorativ, fängt nie einen Tap ab */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
}
/* Button-Zeile (Zurück links, Revanche rechts) über die volle Breite. */
#rematch-bar-buttons { width: 100%; }

/* Der Inhalt der Kopfzeile steht in derselben Spalte wie der Inhalt darunter:
   Lesespalte minus deren Polsterung — sonst stünde der „Zurück"-Knopf auf
   einem breiten Schirm links außen, während die Rangliste in der Mitte
   beginnt. Die Fläche der Bar bleibt davon unberührt und deckt weiter die
   ganze Kante. */
#rematch-bar > * {
  max-width: calc(var(--go-col) - 2 * var(--go-pad));
  margin-inline: auto;
}

/* ── Revanche-Einladung: feste Leiste am oberen Bildschirmrand ──
   position:fixed (NICHT sticky), genau wie #top-bar. Grund: der Spielende-
   Bildschirm scrollt seinen Inhalt in einem EIGENEN Container (#gameover-card
   mit overflow-y:auto + max-height), der im scrollenden <body> verschachtelt
   ist. Eine sticky-Leiste haftet aber am body-Scroll — beim Scrollen INNERHALB
   der Card bewegt sich der body nicht, also löste sich die Leiste und scrollte
   aus dem Blick („man muss wieder hochscrollen"). fixed klebt am Viewport,
   egal welcher verschachtelte Container scrollt. Der Platz wird wie bei
   #top-bar per body-Padding reserviert (body.has-rematch-invite, Höhe in JS
   gemessen, da die Leiste auf schmalen Handys umbricht). Sichtbarkeit +
   Höhenmessung steuert updateRematchInviteBar() in main.js (nur Start /
   Statistik / Spielende). */
#rematch-invite-bar {
  position: fixed;
  top: var(--safe-top);   /* unter eine evtl. Top-Notch */
  left: 0;
  right: 0;
  margin-inline: auto; /* zentriert innerhalb left/right:0, gedeckelt per max-width */
  /* Über Screen-Inhalt UND über zentrierten Overlays/Modals inkl. dem
     Wiktionary-Popup (#wikt-overlay, 7000): Die Einladung ist zeitkritisch
     (5-Min-Fenster) und muss auch beim Wörter-Kuratieren auf dem Spielende-
     Bildschirm sichtbar bleiben — sonst läuft sie unbemerkt ab und der
     Spieler erscheint dem Vorschlagenden als ✗ (wie eine Ablehnung).
     Unter dem Sprachregeln-Popup (8000) und den Toasts (9999). */
  z-index: 7500;
  width: 100%;
  max-width: 960px; /* wie .screen */
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 16px rgba(0,0,0,0.18);
  flex-shrink: 0;
}
.rematch-invite-icon { font-size: 1.3rem; line-height: 1; }
.rematch-invite-main { flex: 1; min-width: 180px; display: flex; flex-direction: column; gap: 4px; }
.rematch-invite-msg  { font-size: 0.88rem; line-height: 1.4; }
/* Teilnehmer-Stand in der Leiste (nur andere Spieler, die bereits geantwortet
   haben): ✓ (angenommen, .player-agreed) / ✗ (abgelehnt, .player-declined-x)
   wiederverwendet aus der Lobby-Spielerliste. */
.rematch-invite-players { display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 0.82rem; }
.rematch-invite-player { white-space: nowrap; }
.rematch-invite-player.is-declined { text-decoration: line-through; color: var(--muted); }
.rematch-invite-buttons { display: flex; gap: 8px; margin-left: auto; }
/* Platz für die fixierte Leiste reservieren (wie body.has-top-bar für #top-bar),
   damit sie keinen Screen-Inhalt überdeckt. --rematch-invite-h = gemessene
   Leistenhöhe (JS, da Umbruch auf schmalen Handys); +16px = Abstand darunter.
   (Die Dev-Bar sitzt unten und braucht hier keine Sonderbehandlung mehr.) */
body.has-rematch-invite { padding-top: calc(var(--rematch-invite-h, 56px) + 16px + var(--safe-top)); }

/* Restlicher Inhalt bekommt das normale Card-Padding — und hier, nicht mehr an
   der Karte, wird die Lesespalte gekappt. Seitlich mindestens die Polsterung,
   bei einem Cutout dessen Inset: die Karte reicht seit dem Randumbau bis unter
   die Aussparung, also muss der Text sie selbst aussparen. */
#gameover-content {
  max-width: var(--go-col);
  margin-inline: auto;
  padding:
    var(--go-pad)
    max(var(--go-pad), var(--safe-right))
    var(--go-pad)
    max(var(--go-pad), var(--safe-left));
  position: relative;
  flex: 1;
}
.winner-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: 14px;
}
.winner-banner .wb-emoji { font-size: 1.8rem; line-height: 1; }
.winner-banner .wb-title { font-size: 1.5rem; font-weight: 800; }
/* „Ohne Wertung" statt Rangliste, wenn am Spielende nur noch ein Teilnehmer
   übrig ist (alle Mitspieler haben aufgegeben, solo weitergespielt). Gedämpft,
   damit es nicht wie ein Sieg/Niederlage-Banner wirkt. */
.winner-banner--unscored .wb-title { color: var(--muted); }

.final-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
.final-item {
  display: flex; align-items: center; gap: 12px;
  background: var(--bg); border-radius: 8px; padding: 10px 14px;
}
.final-item .fi-rank { font-size: 1.2rem; font-weight: 800; width: 30px; }
.final-item .fi-rank.fi-rank-first { font-size: 1.5rem; }
.final-item.final-item-first .fi-name { font-size: 1.15rem; }
.final-item.final-item-first .fi-score { font-size: 1.3rem; }
.final-item .fi-name { flex: 1; font-weight: 600; }
.final-item .fi-score { font-weight: 800; font-size: 1.1rem; color: var(--accent-text); }
.fi-words { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
.final-item.clickable { cursor: pointer; transition: background 0.15s; }
.final-item.clickable:hover { background: var(--surface2); }
/* Eigene Zeile dezent mit hellerem Hintergrund hervorheben — wie die
   „self"-Zeile (.opp-self) in den Ergebnissen. */
.final-item.final-item-self { background: var(--surface); }
.final-item.final-item-self .fi-name { font-weight: 800; }
.fi-tap-hint { font-size: 0.7rem; color: var(--muted); margin-top: 2px; opacity: 0.7; }

/* ── Gameover-Spieler-Wortliste ──────────────────────────────── */
.gop-word-list { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; }
.gop-word-item {
  display: flex; justify-content: space-between; align-items: center;
  padding: 6px 10px; border-radius: 8px; background: var(--bg); font-size: 0.88rem;
}
.gop-word-item .gop-word        { font-weight: 600; letter-spacing: 0.5px; }
.gop-word-item .gop-word.pangram { font-weight: 900; letter-spacing: 0.15em; }
.gop-pts-wrap         { display: flex; flex-direction: column; align-items: flex-end; margin-left: 10px; white-space: nowrap; gap: 1px; }
.gop-word-item .gop-total       { font-weight: 700; color: #fff;           font-size: 0.88rem; }
.gop-word-item .gop-breakdown   { font-size: 0.74rem; color: var(--muted); }
.gop-word-item .gop-base-pts    { color: #fff; font-weight: 600; }
.gop-word-item .gop-bonus       { color: var(--accent-text); font-weight: 600; }

.stats-row { display: flex; gap: 12px; margin-bottom: 20px; }
.stat-box { flex: 1; background: var(--bg); border-radius: 8px; padding: 10px; text-align: center; }
.stat-box .sv { font-size: 1.4rem; font-weight: 800; color: var(--accent-text); }
.stat-box .sl { font-size: 0.75rem; color: var(--muted); }

/* ── Toasts ──────────────────────────────────────────────────── */

#toasts {
  position: fixed; top: calc(16px + var(--safe-top));
  left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 8px; z-index: 9999;
  pointer-events: none; min-width: 240px;
}
body.game-mode #toasts {
  /* In-Game-Nachrichten sitzen mittig auf dem Bildschirm — wichtige
     Aufmerksamkeitsbox (z.B. „Unbekanntes Wort", „Wort geklaut",
     „Pangram!"), kein peripherer Toast-Streifen oben rechts. Die
     einzelnen Toasts erben dieselbe Box-Optik wie das Resign-Popup
     (siehe Regel weiter unten in style.css). */
  top: 50%;
  left: 50%;
  right: auto;
  transform: translate(-50%, -50%);
  align-items: center;
  min-width: 0;
}
/* In-Game-Toasts: opake Box-Optik wie #resign-popup (Surface-Hintergrund
   pro Toast-Typ, Tinten-Rahmen, Schatten). Position übernimmt der
   #toasts-Container (zentriert via translate); keine eigene Fixed-
   Positionierung pro Toast — sonst stapeln sich mehrere Toasts nicht
   sauber. */
body.game-mode .toast {
  min-width: 240px;
  max-width: min(360px, 85vw);
  padding: 18px 24px;
  border: 1px solid var(--text);
  font-size: 1rem;
  font-weight: 700;
  text-align: center;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.toast {
  padding: 10px 18px; border-radius: 8px; font-weight: 600; font-size: 0.9rem;
  text-align: center; animation: toastIn 0.25s ease, toastOut 0.3s ease 2.7s forwards;
  pointer-events: none; box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
/* In-Game-Nachrichten: halbe Anzeigedauer, kein Fade-In. Snap-in vermeidet
   einen sichtbaren Dip, wenn schnelles Tippen den Toast ersetzt — ein neues
   Element mit opacity-0-Fade-In würde die noch sichtbare Box kurzzeitig auf
   0 schicken und wieder einblenden („Blinken"). Stapel verhindert toast()
   in main.js per innerHTML=''. */
body.game-mode .toast {
  animation: toastOut 0.2s ease 1.15s forwards;
}
/* Persistente Toasts: kein automatisches Ausblenden — bleiben so lange
   stehen, bis JS sie entfernt (z. B. nach einem Klick). */
.toast.toast-persistent,
body.game-mode .toast.toast-persistent {
  animation: toastIn 0.25s ease;
}
@keyframes toastIn  { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; transform: translateY(-8px); } }
/* „Erstfund"-/OK-Toast: helleres Grün mit weißer Schrift — der dunkle
   --green ist auf dem Papier-Hintergrund mit schwarzer Schrift zu wuchtig
   für eine positive Bestätigung. */
.toast-ok      { background: #6E9A4A; color: #fff; }
/* Fehler-/Hinweis-Toasts (z.B. „Unbekanntes Wort", „Wort schon vergeben",
   „Wort zu kurz") nutzen die warme Marken-Fläche (--gold = Terrakotta) statt
   reinem Rot. Heller (weißer) Text liegt in beiden Themes klar über WCAG AA. */
.toast-err     { background: var(--gold); color: #fff; }
.toast-info    { background: var(--surface2); color: var(--text); }
.toast-pangram { background: var(--purple); color: #fff; }


/* ── Connection indicator (top-right) ─────────────────────────── */
#conn-indicator {
  position: fixed;
  top: calc(12px + var(--safe-top));
  right: calc(14px + var(--safe-right));
  z-index: 10000;
  display: none;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--red); color: #fff;
  align-items: center; justify-content: center;
  font-size: 16px; font-weight: 700; line-height: 1;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  animation: conn-pulse 1.4s ease-in-out infinite;
  pointer-events: none;
}
#conn-indicator.show { display: flex; }

/* ── Globales Menü (☰) oben links ───────────────────────────────────────────
   Fixes, body-Level-Element auf jeder Seite außer der Startseite. z-index
   unter den Modals (1000), über den Screens. */
/* Schwebender ☰ NUR im Spiel: dort gibt es keine „Zurück"-Zeile, in die der
   Box-Auslöser passen würde, also bleibt der Hamburger oben links (kein Umbau
   des Spiel-Headers). Auf allen anderen Screens sitzt der ☰ als .btn-menu am
   rechten Rand der Button-Zeile (Lobby/Ergebnisse/Spielende) und dieser hier
   ist ausgeblendet. */
#app-menu-btn {
  /* In-Game-☰. Sitzt im Spiel-Header-Fluss (rechts neben der Uhr) — die
     konkrete Positionierung/Größe kommt aus der game-mode-Regel; hier nur
     das Aussehen (3 Balken) und der versteckte Default außerhalb des Spiels. */
  display: none;
  flex-direction: column; justify-content: center; gap: 3.5px;
  width: 22px; height: 22px; padding: 0;
  background: none; border: 0; box-shadow: none; cursor: pointer;
}
body.game-mode #app-menu-btn { display: flex; }
#app-menu-btn span {
  display: block; height: 2px; width: 100%; border-radius: 2px;
  background: var(--text);
}
#app-menu-btn:hover { opacity: 0.7; }
/* Box-Auslöser in der Button-Zeile. .btn .btn-ghost liefert Box, Hintergrund
   und Rahmen wie beim Zurück-Button; .btn-menu zentriert nur das ☰-Zeichen.
   Das Dropdown wird in JS am angeklickten Auslöser verankert (positionAppMenu). */
.btn-menu {
  font-size: 1.15rem; line-height: 1;
  padding-left: 12px; padding-right: 12px;
}
/* Der ☰ steht in der Ergebnisse-Bar links neben dem Zahnrad (rechts von
   Zurück); align-self:stretch lässt ihn exakt so hoch werden wie die übrigen
   Bar-Buttons. Die Lobby-Bar ist dagegen ein Raster mit align-items:center —
   dort werden alle Buttons vertikal zentriert, kein Stretch (sonst würde nur
   der ☰ auf die volle Zeilenhöhe gezogen). */
#stats-bar .btn-menu { margin-left: 8px; align-self: stretch; }

.app-menu {
  position: fixed; top: 60px; left: 16px; right: auto; z-index: 60;
  min-width: 168px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 11px;
  box-shadow: 0 6px 22px rgba(0,0,0,0.28);
  animation: fadeIn 0.15s;
}
.app-menu-item {
  display: block; width: 100%; text-align: left;
  padding: 10px 12px; border: 0; border-radius: 7px;
  background: transparent; color: var(--text);
  font-family: inherit; font-size: 0.9rem; font-weight: 600; cursor: pointer;
}
.app-menu-item:hover { background: var(--bg); }

/* Seitentitel oben links in den Card-Headern (Lobby, Ergebnisse, Spielende). */
.header-title {
  font-weight: 800; font-size: 1.1rem; color: var(--text);
  white-space: nowrap; flex: 0 0 auto;
}
@keyframes conn-pulse {
  0%, 100% { opacity: 0.85; transform: scale(1); }
  50%      { opacity: 1;    transform: scale(1.08); }
}

@keyframes chip-bonus-delta {
  0%   { opacity: 1; transform: translateY(0);     }
  70%  { opacity: 1; transform: translateY(-8px);  }
  100% { opacity: 0; transform: translateY(-14px); }
}
.c-bonus-delta {
  font-size: 0.7rem; font-weight: 800;
  color: var(--red);
  line-height: 1.2;
  white-space: nowrap;
  animation: chip-bonus-delta 2.2s ease-out forwards;
  pointer-events: none;
}

/* ── Floating score pop ──────────────────────────────────────── */
.score-pop {
  position: fixed;
  transform: translateX(-50%);
  /* Größer und in derselben hellen Grün-Note wie der „Erstfund"-Toast
     (.toast-ok-Hintergrund) — die fliegende Punkt-Zahl ist die positive
     Quittung beim Einreichen, soll sich also visuell mit der OK-
     Bestätigung verzahnen. Vorher: var(--green) (dunkles Tannengrün),
     2.6rem → optisch hinter dem Toast verstummt. */
  font-size: 3.6rem;
  font-weight: 900;
  letter-spacing: -1px;
  color: #6E9A4A;
  text-shadow: 0 2px 14px rgba(0,0,0,0.45);
  pointer-events: none;
  z-index: 9999;
  white-space: nowrap;
  animation: score-fly 1.25s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
.score-pop.pangram { color: var(--purple); filter: drop-shadow(0 0 8px rgba(62,158,148,0.7)); }
.score-pop.first   { color: var(--accent-text); }
/* Abzug (Definitionsjagd: Wegwischen kostet Punkte) — gleiche Bewegung, nur in
   der Gegenfarbe. --red ist die Status-Farbe beider Themes, bewusst nicht der
   Akzent. */
.score-pop.penalty { color: var(--red); }
@keyframes score-fly {
  0%   { opacity: 0;   transform: translateX(-50%) translateY(0)    scale(0.6); }
  12%  { opacity: 1;   transform: translateX(-50%) translateY(-10px) scale(1.25); }
  45%  { opacity: 1;   transform: translateX(-50%) translateY(-50px) scale(1.05); }
  100% { opacity: 0;   transform: translateX(-50%) translateY(-110px) scale(0.85); }
}

/* ── Interrupted modal ───────────────────────────────────────── */

.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.7);
  display: flex; align-items: center; justify-content: center;
  z-index: 1000; backdrop-filter: blur(4px);
  /* Safe-Area: das zentrierte .modal-card innerhalb des Cutout-/Home-
     Indicator-freien Bereichs halten (greift erst bei sehr hohen Modals,
     die sonst oben/unten unter die Notch reichen würden). */
  padding:
    var(--safe-top)
    var(--safe-right)
    var(--safe-bottom)
    var(--safe-left);
}
.modal-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 36px 32px;
  max-width: 400px; width: 90%; text-align: center;
  animation: fadeIn 0.2s;
}
.modal-icon  { font-size: 2.5rem; margin-bottom: 12px; }
.modal-title { font-size: 1.3rem; font-weight: 800; margin-bottom: 10px; }
/* `touch-action: pan-y` — Modal-Bodies sind teils vertikale Scroll-Container
   (z.B. Spielregeln, Was-ist-neu); horizontales Wischen ist auf KEINEM Modal
   erwünscht. Ohne pan-y interpretiert Firefox Android leichte diagonale
   Bewegungen als Browser-Zurück-Navigation, auch wenn `overscroll-behavior-x`
   auf html/body bereits gesetzt ist. */
.modal-body  { color: var(--muted); font-size: 0.82rem; margin-bottom: 24px; line-height: 1.35; touch-action: pan-y; overscroll-behavior-x: none; }
.modal-buttons { display: flex; flex-direction: column; gap: 10px; }

/* ── „Was ist neu"-Modal ─────────────────────────────────────────────────────
   Pro Version ein Abschnitt (Versionszeile + Inhalt). Ein Feature ist entweder
   eine schlichte Zeile in der Aufzählung (.wn-list) oder — wenn es in
   RELEASE_NOTES.json eine Überschrift trägt — ein eigener Block mit großer
   fetter Zeile samt Icon (.wn-feature). Die .modal-body-Grundschrift ist klein
   und gedämpft; die Überschrift bricht das bewusst auf. */
/* Vorschau-Hinweis im „Was ist neu"-Fenster (?rl=…). Abgesetzt und gedämpft:
   er gehört nicht zum Inhalt, sondern erklärt den Zustand des Fensters. Kein
   Akzent als Textfarbe (Theme-Regel). */
.wn-preview-note {
  margin: 0 0 12px; padding: 7px 10px;
  border: 1px dashed var(--border); border-radius: 8px;
  font-size: 0.78rem; line-height: 1.4; color: var(--muted);
}
.wn-section { margin-bottom: 14px; }
.wn-section-head { font-weight: 600; margin-bottom: 4px; }
.wn-list { margin: 0; padding-left: 20px; }
.wn-list li { margin-bottom: 4px; }
.wn-feature { margin: 10px 0 12px; }
.wn-feature-title {
  font-size: 1.02rem; font-weight: 800; color: var(--text);
  line-height: 1.25; margin-bottom: 3px;
  display: flex; align-items: baseline; gap: 6px;
}
.wn-feature-icon { font-size: 1.15em; flex: 0 0 auto; }
.wn-feature-text { margin: 0; }

/* ── Scroll-Ausblendung für scrollbare Popups ────────────────────────────────
   Wenn ein scrollbarer Container unten noch Inhalt verbirgt, blenden wir die
   letzten ~1.5 Zeilen sanft aus (mask-image) und zeigen einen kleinen Pfeil
   rechts unten — beides nur, solange etwas folgt (JS toggelt data-more-below,
   siehe attachScrollFade). Der Pfeil sitzt im positionierten Eltern-Element
   außerhalb des maskierten Containers, sonst würde er mit ausgeblendet. */
/* Eine Maske für beide Richtungen, gesteuert über zwei Custom Properties —
   mit je einer Regel für unten/oben/beides liefe man Gefahr, eine Kombination
   zu vergessen (dieselbe Bauart wie bei den Bedeutungs-Kästchen der
   Sinn-Suche, siehe .dh-slot.dh-more-*). Ohne Klasse bleibt der Container
   ganz ohne Maske. */
.scroll-fade[data-more-below="true"],
.scroll-fade[data-more-above="true"] {
  --scroll-fade-top: 0em;
  --scroll-fade-bottom: 0em;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--scroll-fade-top),
    #000 calc(100% - var(--scroll-fade-bottom)), transparent 100%);
          mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--scroll-fade-top),
    #000 calc(100% - var(--scroll-fade-bottom)), transparent 100%);
}
.scroll-fade[data-more-below="true"] { --scroll-fade-bottom: 2.2em; }
.scroll-fade[data-more-above="true"] { --scroll-fade-top:    2.2em; }
.scroll-more-arrow {
  position: absolute; z-index: 2;
  width: 24px; height: 24px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface2); border: 1px solid var(--border); color: var(--text);
  opacity: 0; transform: translateY(-3px);
  transition: opacity 0.2s ease, transform 0.2s ease;
  pointer-events: none;   /* rein hinweisend; Scrollen/Tippen geht durch */
}
/* CSS-Chevron statt Glyph — sitzt sauber mittig und folgt der Textfarbe.
   Nach oben gedreht, wenn der Pfeil auf verborgenen Inhalt ÜBER dem
   Ausschnitt zeigt. */
.scroll-more-arrow::before {
  content: ''; width: 7px; height: 7px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.scroll-more-arrow.up::before { transform: translateY(2px) rotate(-135deg); }
.scroll-more-arrow.visible { opacity: 0.9; transform: none; }

/* ── Lobby-Einstellungen als Fließtext ──────────────────────── */

.settings-prose {
  font-size: 0.855rem;
  line-height: 1.5;
  color: var(--text);
  margin: 8px 0 4px;
}
/* Dunkel-Theme: der normale (nicht-fette) Fließtext der Lobby-Einstellungen
   gedämpft (--muted), damit die fetten Werte (.prose-val, eigene --text-Farbe)
   stärker hervortreten. */
html:not(.light-theme) .settings-prose { color: var(--muted); }

/* Ein hervorgehobener Wert im Fließtext — schlicht fett & in Textfarbe,
   keine Unterstreichung. Die Klickbarkeit (Host) zeigt sich nur beim Hover. */
.prose-val {
  display: inline;
  font: inherit;
  font-weight: 800;
  color: var(--text);
  background: none;
  border: none;
  border-radius: var(--radius);
  /* Kein horizontales Padding: in der Monospace-Prosa würde es die fetten
     Werte um 2px vom Buchstabenraster wegschieben, sodass sie gegenüber dem
     normalen Text leicht „eingerückt" wirken. 0 hält sie exakt auf dem Raster.
     (Explizit, nicht weggelassen — sonst greift das UA-<button>-Default-Padding.) */
  padding: 0;
  margin: 0;
  cursor: pointer;
  white-space: normal;
  /* Browser-Default (`text-align: center` auf <button>) überstimmen — bei
     mehrwortigen Werten („gibt mindestens 1 Pangramm") würde die zweite Zeile
     sonst innerhalb des Buttons mittig statt linksbündig zum Fließtext sitzen. */
  text-align: inherit;
  transition: background 0.12s;
}
/* `:hover` nur auf Geräten, die echtes Hover unterstützen — sonst klebt die
   graue Markierung nach einem Tap auf dem Button (Popup-Anker), bis der
   Spieler woanders hin tippt. `:focus-visible` bleibt unbeschränkt aktiv,
   damit Tastatur-Navigation weiterhin sichtbar ist. */
@media (hover: hover) {
  .prose-val:hover { background: var(--surface2); outline: none; }
}
.prose-val:focus-visible { background: var(--surface2); outline: none; }
/* Eine halbtransparente helle Box PRO Einstellung (nicht pro Wort): der
   Hintergrund sitzt auf der Gruppe .prose-val-box, die alle Wort-Chunks einer
   editierbaren Einstellung umschließt. Ihr Inline-Hintergrund deckt auch die
   Leerzeichen zwischen den Chunks ab → durchgehende Box. Die Fläche kommt aus
   --prose-val-bg, weil die beiden Themes hier auseinandergehen: auf Walnuss
   genügt eine halbdeckende Stufe --surface, auf Papier hebt die sich kaum ab
   und braucht ein Fast-Weiß. Kleiner box-shadow-Spread (1px) statt Padding
   gibt etwas Luft, ohne das Monospace-Raster zu verschieben, und hält
   benachbarte Textzeilen frei (keine vertikale Überlappung).
   box-decoration-break:clone rundet jede Zeile, wenn eine Einstellung umbricht. */
.prose-val-box {
  display: inline;
  background: var(--prose-val-bg);
  box-shadow: 0 0 0 1px var(--prose-val-bg);
  border-radius: 5px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background 0.12s, box-shadow 0.12s;
}
@media (hover: hover) {
  .prose-val-box:hover { background: var(--surface2); box-shadow: 0 0 0 1px var(--surface2); }
}
/* Innerhalb der Gruppe tragen die einzelnen Wort-Buttons keinen eigenen
   Hintergrund mehr — die Box liefert ihn (sonst entstünde wieder ein Kästchen
   pro Wort). Tastatur-Fokus (:focus-visible) bleibt absichtlich am Button. */
.prose-val-box .prose-val,
.prose-val-box .prose-val:hover { background: none; }
/* Nicht editierbare Werte (Raumcode, Spielernamen, Nicht-Host-Ansicht):
   gleiche Optik, aber kein Hover-Affordance. */
.prose-val--readonly { cursor: default; }
/* Im aktuellen Modus wirkungsloses Fragment (z.B. keep-Klausel im Wörterdieb-
   Modus): durchgestrichen + gedämpft. Der innere Wert-Button erbt line-through
   nicht automatisch → explizit setzen. */
.prose-struck { text-decoration: line-through; opacity: 0.6; cursor: help; }
.prose-struck .prose-val { text-decoration: line-through; cursor: help; }

/* ── Onboarding-Hinweis beim allerersten Raum ────────────────────────────────
   Abgedunkelter Hintergrund + Hinweiskarte unten mittig. Das Fett-Beispiel im
   Text trägt die normale .prose-val-Optik (Wiedererkennung); die echten
   Fettwerte im Fließtext pulsieren kurz (prose-val-pulse). */
.onboard-backdrop {
  position: fixed; inset: 0; z-index: 9998;
  background: rgba(0, 0, 0, 0.28);
  animation: fadeIn 0.2s;
}
.onboard-hint {
  position: fixed; z-index: 9999;
  /* Nicht markierbar: ein Tipp auf die Box blättert zum nächsten Satz, ein
     etwas längerer Druck würde sonst Text markieren (und auf Mobilgeräten die
     Auswahl-Leiste aufziehen) statt weiterzublättern. Gilt für alle
     Einführungsboxen — siehe .tutorial-pop. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  left: 50%; bottom: calc(24px + var(--safe-bottom));
  transform: translateX(-50%);
  width: min(440px, 92vw);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px;
  padding: 16px 18px; box-shadow: 0 10px 34px rgba(0, 0, 0, 0.4);
  font-size: 0.95rem; line-height: 1.45; text-align: center;
  animation: fadeIn 0.2s;
}
/* Gestaffelt einblendende Sätze im Onboarding-Hinweis. */
/* Sätze linksbündig (wie .tutorial-pop-body und der Willkommenstext) — die
   Karte selbst bleibt zentriert, der „Verstanden!"-Button darunter ebenfalls. */
.onboard-hint .onboard-line { margin: 0 0 9px; opacity: 0; transition: opacity 0.6s ease; text-align: left; }
.onboard-hint .onboard-line:last-child { margin-bottom: 0; }
.onboard-hint .onboard-line.show { opacity: 1; }
/* „Verstanden!"-Button — erscheint erst mit dem letzten Satz; vorher nicht
   klickbar (pointer-events: none), damit der unsichtbare Bereich nicht schließt. */
.onboard-hint .onboard-ok {
  display: block; margin: 12px auto 0;
  opacity: 0; pointer-events: none; transition: opacity 0.5s ease;
}
.onboard-hint .onboard-ok.show { opacity: 1; pointer-events: auto; }
/* Auswahl-Knöpfe im Onboarding-Hinweis (z.B. „allein oder mit Mitspieler?").
   Untereinander statt nebeneinander: die Beschriftungen sind ganze Sätze und
   müssen umbrechen dürfen (siehe CLAUDE.md, kein Querscrollen). Erscheinen wie
   der „Verstanden!"-Button erst mit dem letzten Satz. */
.onboard-hint .onboard-choices {
  display: flex; flex-direction: column; gap: 8px; margin: 14px 0 0;
  opacity: 0; pointer-events: none; transition: opacity 0.5s ease;
}
.onboard-hint .onboard-choices.show { opacity: 1; pointer-events: auto; }
.onboard-hint .onboard-choices .btn {
  white-space: normal; text-align: center; line-height: 1.25;
}

/* Dauerhinweis während des Tutorial-Halts: ein Mitspieler bekommt gerade die
   Bedienelemente erklärt, das Spielfeld steht. Bewusst ohne Backdrop — es ist
   eine Zustandsanzeige, kein Dialog (Einreichungen lehnt der Server bis zur
   Freigabe ohnehin ab). Oben, damit es die Eingabe unten nicht verdeckt. */
.tutorial-wait {
  position: fixed; z-index: 9998;
  left: 50%; top: calc(8px + var(--safe-top));
  transform: translateX(-50%);
  max-width: 92vw;
  padding: 7px 14px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 999px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  font-size: 0.85rem; text-align: center;
  user-select: none; -webkit-user-select: none;
  animation: fadeIn 0.2s;
}

/* Raumcode blinkt, während der Hinweis darauf zeigt: sanftes Ein-/Ausblenden
   des Texts (Opacity, kein Glow) — layout-neutral. Endlos; JS entfernt die
   Klasse. */
@keyframes roomcode-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.2; }
}
.roomcode-blink { animation: roomcode-blink 1.1s ease-in-out infinite; }
/* Gleiches Blinken für Spieler-Kacheln in der Lobby (siehe
   blinkLobbyPlayersBriefly): der Hinweis „… hat noch nicht zugestimmt" zeigt
   damit auch, WESSEN Kachel gemeint ist. Eigene Klasse statt .roomcode-blink,
   weil der Name sonst lügt; die Keyframes teilen sie sich. */
.lobby-blink { animation: roomcode-blink 1.1s ease-in-out infinite; }
/* Puls der tippbaren Fettwerte: ebenfalls sanftes Ein-/Ausblenden (Opacity),
   layout-neutral (kein transform/scale, damit inline-Werte nicht umbrechen).
   Endlich (3 Zyklen ≈ 2,7 s). */
@keyframes prose-val-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.prose-val-pulse { animation: prose-val-pulse 0.9s ease-in-out 3; }
@media (hover: hover) {
  .prose-val--readonly:hover { background: none; }
}

/* ── Tutorial: In-Game-Element-Tour ──────────────────────────────────────────
   Transparenter, klick-schluckender Backdrop (kein Abdunkeln, damit das
   erklärte Element klar sichtbar bleibt) + frei positioniertes Erklär-Popup
   daneben. Das aktuelle Element blinkt (layout-neutral, nur Opacity). Muster
   analog zu .roomcode-blink / .onboard-*. */
.tutorial-backdrop {
  position: fixed; inset: 0; z-index: 9998;
  background: transparent;
  animation: fadeIn 0.2s;
  /* blockiert Eingaben während der Tour (Spiel ist serverseitig angehalten) */
  touch-action: none;
}
.tutorial-pop {
  position: fixed; z-index: 9999;
  /* Nicht markierbar — gleiche Begründung wie bei .onboard-hint: die Box ist
     eine Blätter-Fläche, keine Textquelle. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  width: min(320px, 88vw);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px;
  padding: 14px 16px; box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45);
  font-size: 0.95rem; line-height: 1.45; text-align: center;
  animation: fadeIn 0.2s;
}
.tutorial-pop-title { font-weight: 700; margin-bottom: 6px; }
/* Erklärtext linksbündig (die Karte selbst zentriert Titel und Button): die
   Absätze laufen über mehrere Zeilen, und Fließtext liest sich an einer festen
   Kante besser — wie der Willkommenstext auf der Startseite. */
.tutorial-pop-body  { margin-bottom: 12px; text-align: left; }
/* Body als Absätze (Array-Body je Satz ein <p>): dezenter Abstand, letzter ohne. */
.tutorial-pop-body p { margin: 0 0 8px; }
.tutorial-pop-body p:last-child { margin-bottom: 0; }
/* Gestaffeltes Einblenden der Absätze (wie beim Start-Intro): opacity 0 → 1,
   Layout bleibt reserviert, daher springt die Popup-Position nicht. Fade-Dauer
   identisch zu den beiden anderen Einführungen (.intro-show, .onboard-line) —
   der Takt selbst kommt aus INTRO_PAUSE_MS in main.js. */
.tutorial-pop-body p.tut-line { opacity: 0; transition: opacity 0.6s ease; }
.tutorial-pop-body p.tut-line.show { opacity: 1; }
/* „Weiter"-Button erscheint erst mit dem letzten Absatz; vorher unsichtbar und
   nicht klickbar, Layout aber reserviert (kein Höhensprung). */
.tutorial-pop-ok { transition: opacity 0.4s ease; }
.tutorial-pop-ok.tut-hidden { opacity: 0; pointer-events: none; }
.tutorial-pop-ok    { display: block; margin: 0 auto; }
/* „hier"-Link im Eingabefeld-Schritt der Tour: als Inline-Link im Erklärtext
   (öffnet die Sprachregeln). UA-Button-Defaults neutralisieren, nur die
   Unterstreichung markiert die Klickbarkeit. Unterstreichung (Stroke), nicht
   Textfarbe — --accent bleibt reine Kontur/Fläche (siehe CLAUDE.md). */
.tut-rules-link {
  font: inherit; color: inherit;
  background: none; border: none; padding: 0; margin: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
@media (hover: hover) {
  .tut-rules-link:hover { text-decoration-color: var(--accent); }
}
.tut-rules-link:focus-visible { outline: none; text-decoration-color: var(--accent); }
/* Blinken des gerade erklärten Elements — endlos, JS entfernt die Klasse beim
   Weiterschalten. Nur Opacity → layout-neutral, kein Reflow der Spielfläche. */
@keyframes tutorial-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.tutorial-blink { animation: tutorial-blink 1.0s ease-in-out infinite; }

/* Settings ohne echte Auswahl (nur ein Wert übrig nach Frontend-Options-
   Filter): keine fette Box, keine Click-Affordance — der Wert liest sich wie
   eine normale Aussage im Fließtext. Der span-Wrapper bleibt nur deshalb, damit
   der Punctuation-Glue-Regex (renderSettingsProse) trailing-Satzzeichen
   weiter einfangen kann. */
.prose-val--plain {
  font-weight: inherit;
  display: inline;
}
/* Einzige spielbare Sprache: kein editierbarer Wert, aber unterstrichen und
   anklickbar (öffnet die Sprachregeln). Als <button> gerendert, daher die
   UA-Button-Defaults (Fläche, Padding, Zentrierung) neutralisieren, damit der
   Text nahtlos im Fließtext sitzt — nur die Unterstreichung + der Zeiger
   heben ihn hervor. */
button.prose-lang-rules {
  font: inherit; color: inherit;
  background: none; border: none; padding: 0; margin: 0;
  text-align: inherit; white-space: normal;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
@media (hover: hover) {
  button.prose-lang-rules:hover { text-decoration-color: var(--accent); }
}
button.prose-lang-rules:focus-visible { outline: none; text-decoration-color: var(--accent); }

/* Trailing-Punctuation, die per renderSettingsProse() vom Fließtext
   in den Wert-Button verschoben wird, damit das Satzzeichen nie eine
   neue Zeile beginnt. Im fett gesetzten Button auf normale Schrift-
   stärke zurückdrehen — sonst wäre auch der Punkt fett. */
.prose-val .prose-punct { font-weight: 400; }

/* Setting-Flash: blinkt für Mitspieler die vom Host frisch geänderten
   Werte, bis sie „Einverstanden" klicken. Wird in main.js (valBtn) per
   `.prose-val--flash` an alle Chunks (Wörter) eines geänderten Keys
   verteilt; ein Setting kann aus mehreren Chunks bestehen — sie blinken
   synchron, weil sie dieselbe Animation mit derselben Dauer haben. */
.prose-val--flash {
  animation: prose-val-flash 1.2s ease-in-out infinite;
}
@keyframes prose-val-flash {
  0%, 100% { background: transparent; }
  50%      { background: var(--accent); }
}

/* Zustimmungs-Zeile (nur Nicht-Host) — lebt in der fixen Bottom-Bar
   (#lobby-bar) zwischen Spielerliste und Buttons. Der vertikale Abstand
   kommt aus dem `gap: 10px` der Bar; der border-top trennt die Zeile
   sichtbar von der Spielerliste darüber. */
#lobby-consent {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
#lobby-consent-text {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--muted);
}
/* Bestätigtes „Ich bin dabei!" — Grün als Erfolgs-Signalfarbe, nicht `--accent`:
   die Akzentfarbe darf im Hell-Theme nicht als Vordergrund-Text auftauchen
   (Gold auf Creme = unlesbar; siehe CLAUDE.md). */
#lobby-consent-text.is-agreed { color: var(--green); }

/* ✓ neben einem Mitspieler, der den Einstellungen zugestimmt hat. Grün
   konsistent zum „Ich bin dabei!"-Text — beide signalisieren denselben
   Zustand „hat zugestimmt". In der Revanche-Lobby markiert dasselbe ✓ die
   Teilnehmer, die die Revanche angenommen haben. */
.player-agreed {
  color: var(--green);
  font-weight: 800;
  font-size: 0.9rem;
  margin-left: 2px;
}

/* ── Revanche ───────────────────────────────────────────────── */

/* Plakette im Lobby-Header: kennzeichnet eine Revanche-Lobby (Einstellungen
   gesperrt, Teilnehmer des Vorspiels gelistet). Akzentfarbe nur als Rahmen —
   Text bleibt var(--text); --accent ist nie Textfarbe (siehe CLAUDE.md). */
.lobby-rematch-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1.5px solid var(--accent);
  border-radius: 6px;
  padding: 2px 10px;
  font-weight: 800;
  font-size: 0.9rem;
  color: var(--text);
  white-space: nowrap;
}

/* Revanche-Lobby: Teilnehmer, die nicht (mehr) teilnehmen können — Revanche
   abgelehnt, anderes Spiel begonnen oder Einladungsfrist abgelaufen. */
.player-item.player-declined .pname {
  text-decoration: line-through;
  color: var(--muted);
}
.player-declined-x {
  color: var(--red);
  font-weight: 800;
  font-size: 0.9rem;
  margin-left: 2px;
}
/* Teilnehmer, die noch nicht auf die Einladung geantwortet haben. */
.player-item.player-pending { opacity: 0.65; }

/* ── Einstellungs-Auswahl-Popup ─────────────────────────────── */

.settings-popup-backdrop {
  position: fixed; inset: 0;
  z-index: 1100;
  background: transparent;
}
/* Mobil: abdunkeln + leicht weichzeichnen (wie .modal-backdrop). */
.settings-popup-backdrop.mobile {
  background: rgba(0,0,0,0.7);
  backdrop-filter: blur(4px);
}
.settings-popup-backdrop.hidden { display: none; }

.settings-popup {
  position: fixed;
  z-index: 1101;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0,0,0,0.55);
  padding: 6px;
  width: min(320px, 92vw);
  max-height: 70dvh;
  overflow-y: auto;
  /* Vertikales Scrollen erlaubt, horizontales Wischen geblockt — analog
     .modal-body (siehe CLAUDE.md, Pull-to-refresh / scroll lock). */
  touch-action: pan-y;
  overscroll-behavior-x: none;
  animation: fadeIn 0.12s;
}
.settings-popup.hidden { display: none; }
/* Mobil mittig statt am Element verankert. */
.settings-popup.mobile {
  left: 50% !important;
  top: 50% !important;
  transform: translate(-50%, -50%);
  width: min(360px, 92vw);
  max-height: 80dvh;
}

.settings-popup-opt {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: var(--radius);
  padding: 9px 11px;
  cursor: pointer;
  color: var(--text);
  font: inherit;
  transition: background 0.12s;
}
.settings-popup-opt:hover,
.settings-popup-opt:focus-visible { background: var(--surface2); outline: none; }
.settings-popup-opt.is-selected {
  background: var(--surface2);
  box-shadow: inset 3px 0 0 var(--accent);
}
.settings-popup-opt-label {
  display: block;
  font-weight: 700;
  font-size: 0.92rem;
}
.settings-popup-opt-desc {
  display: block;
  margin-top: 3px;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--muted);
}
/* Allgemeiner Info-Kopf über den Optionen (z.B. Grundwertung im Spielmodus-
   Popup) — durch eine Trennlinie von der Optionsliste abgesetzt. */
.settings-popup-intro {
  padding: 7px 11px 9px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--muted);
  /* '\n' im Intro-Text (z.B. Pangramm-Hinweis) erzeugt einen Zeilenumbruch;
     normale einzeilige Intros sind unbeeinflusst. */
  white-space: pre-line;
}

/* Sprachauswahl: zweigeteiltes Popup — oben die Sprachen, darunter die
   Wortregeln der gewählten Sprache bzw. die beiden Spielart-Prinzipien,
   getrennt durch eine waagerechte Linie.
   Das Popup selbst scrollt dabei NICHT (overflow: hidden überschreibt die
   Basisregel), stattdessen scrollt allein der Regel-Bereich: die Auswahl
   bleibt immer sichtbar. Die Höhe kommt weiter aus max-height der Basisregel
   (70dvh bzw. 80dvh mobil); min-height: 0 erlaubt dem Flex-Kind zu
   schrumpfen, sonst würde es seinen Inhalt herausdrücken. */
.settings-popup.is-split {
  display: flex; flex-direction: column;
  overflow: hidden;
  /* Etwas breiter als die reine Optionsliste — darunter steht Fließtext. */
  width: min(420px, 92vw);
}
.settings-popup.is-split .settings-popup-opts { flex: 0 0 auto; }
.settings-popup.is-split .settings-popup-rules {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  margin-top: 6px; padding-top: 8px;
  border-top: 1px solid var(--border);
}
/* Spielart-Popup: die beiden Prinzipien untereinander. Erster Absatz ohne
   oberen Abstand, damit er direkt unter der Trennlinie sitzt. */
.settings-popup-principle { margin: 8px 0 0; }
.settings-popup-principle:first-child { margin-top: 0; }
.settings-popup-rules-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto;
  padding: 0 11px 4px;
  font-size: 0.82rem; line-height: 1.45;
  color: var(--text);
  /* Wie .modal-body: vertikal scrollen ja, seitliches Wischen nein
     (siehe CLAUDE.md, Pull-to-refresh / scroll lock). */
  touch-action: pan-y;
  overscroll-behavior-x: none;
}

/* Dev: Texteingabe für feste Buchstaben. */
.settings-popup-form { padding: 6px; display: flex; flex-direction: column; gap: 10px; }
.settings-popup-form input {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 7px 9px;
  font: inherit;
  /* Kein CSS-uppercase — das würde ein getipptes `ß` als `SS` rendern. Die
     Großschreibung macht JS live per upperWord (ß-erhaltend), siehe
     openFixedLettersPopup. */
  letter-spacing: 2px;
  outline: none;
}
.settings-popup-form input.invalid { border-color: var(--err, #c44); }
.settings-popup-form-row { display: flex; justify-content: space-between; gap: 8px; }

/* ── Solution word list (gameover) ──────────────────────────── */

.solution-section { margin-top: 20px; }
.solution-section .panel-title { margin-bottom: 8px; }
/* Section-Divider: dünne Linie von Card-Rand zu Card-Rand (negative
   seitliche Margin gleicht das 20px-Padding von #gameover-content aus).
   Optisches Pendant zur Linie OBERHALB des Auflösung-Titels. */
.section-divider {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 16px -20px 0;
}
.solution-grid {
  display: flex; flex-wrap: wrap; gap: 5px;
  padding-right: 4px;
}
/* Verfügbare Buchstaben oberhalb der Wortliste — Optik wie die
   In-Game-Buchstabentasten (.hex-center / .hex-outer in style.css):
   var(--surface) als Fläche, kein Rand, gleicher Drop-Shadow wie
   der #key-raised-Filter im Hex-SVG. Pflichtbuchstabe übernimmt im
   Light-Theme die Akzentfarbe (analog .light-theme .hex-center). */
.sol-letter {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.2rem; height: 2.2rem; border-radius: 6px;
  font-size: 1.1rem; font-weight: 800;
  background: var(--surface); color: var(--text);
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.55);
}
.light-theme .sol-letter.sol-letter-center {
  background: var(--accent); color: #fff;
}
.sol-word {
  padding: 4px 10px; border-radius: 20px; font-size: 0.82rem; font-weight: 600;
  letter-spacing: 0.5px; cursor: default; border: 1.5px solid transparent;
}
/* Fallback wenn kein colorMap vorhanden (Solo-Spiel ohne Farben).
   Textfarbe bleibt regulär — Spielerinformation steckt allein in
   Background/Border (siehe inline-Style aus renderSolutionGrid). */
.sol-word.found        { background: rgba(34,197,94,0.18); }
.sol-word.found.mine   { background: rgba(34,197,94,0.42); border-color: var(--green); }
.sol-word.pangram        { font-weight: 900; letter-spacing: 0.15em; }
.sol-word.pangram.missed { opacity: 1; }
/* Nicht gefundene Wörter: klar grau */
.sol-word.missed { background: rgba(255,255,255,0.05); color: var(--muted); border-color: transparent; }
.sol-word[title]       { cursor: pointer; }

/* ── Sinn-Suche: Auflösung als Tabelle ───────────────────────────
   Ein Wort je Zeile in der Reihenfolge, in der es beim betrachtenden
   Spieler erschien; rechts die Wisch-Zählung (leer bei 0). Die
   Ungelösten unterscheiden sich danach, ob der Spieler sie überhaupt
   zu sehen bekam: gesehen → nur Rahmen, nie gesehen → gar nichts. */
/* Volle Breite, damit die Linie unter der Überschrift bis zum rechten Rand
   läuft. Die Wortspalte bleibt trotzdem inhaltsbreit (width:1% = schrumpfe auf
   den Inhalt), sodass der Rest an die Wisch-Spalte geht und die Trennlinie
   dicht an den Wörtern steht. */
.solution-grid > .sol-table { flex: 1 1 100%; }
.sol-table { border-collapse: collapse; width: 100%; }
.sol-table td { padding: 2px 0; vertical-align: middle; }
.sol-table--skips th.sol-word-cell,
.sol-table--skips td.sol-word-cell { width: 1%; white-space: nowrap; }
/* In der Pillen-Wolke ist .sol-word ein Flex-Kind; in der Tabelle steht es
   inline und bräuchte sonst Platz für seine senkrechte Polsterung. */
.sol-table .sol-word { display: inline-block; }
.sol-table .sol-skips { padding-left: 10px; text-align: left; }
/* Umbruch erlaubt: bei vielen Spielern passen die Zahlen sonst nicht in die
   Spalte, und die Tabelle würde die Karte seitlich aufschieben (CLAUDE.md:
   lieber umbrechen als waagerecht scrollen). Die einzelne Zahl bleibt
   zusammen — umbrochen wird nur ZWISCHEN den Spielern. */
.sol-table td.sol-skips {
  white-space: normal; font-size: 0.78rem; font-weight: 600; color: var(--muted);
}
/* Je Spieler ein PLATZ fester Breite, in seiner Spielerfarbe (Inline-Style aus
   huntSolutionTableHTML) — der eigene ganz links. Die feste Breite ist der
   ganze Trick: dadurch stehen die Zahlen eines Spielers in allen Zeilen
   untereinander, auch wenn er ein Wort ausgelassen hat (dann ist sein Platz
   leer). Echte Tabellenspalten wären dafür der schwerere Weg — ihre Zahl
   hinge an der Spielerzahl. */
.sol-table .sol-skip {
  display: inline-block; white-space: nowrap; font-weight: 700;
  min-width: 2.6em; margin-right: 6px;
}
.sol-table .sol-skip:last-child { margin-right: 0; }
/* Überschriften: zurückhaltend, mit EINER Linie darunter. */
.sol-table th {
  padding: 0 0 4px; text-align: left; vertical-align: bottom;
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
  color: var(--muted); border-bottom: 1px solid var(--border);
}
/* Und EINE Linie zwischen den beiden Spalten (nur wenn es zwei gibt) —
   außen bleibt die Tabelle randlos. */
.sol-table--skips th.sol-word-cell,
.sol-table--skips td.sol-word-cell { padding-right: 10px; border-right: 1px solid var(--border); }
.sol-word.missed.hunt-seen   { background: none; border-color: var(--muted); }
.sol-word.missed.hunt-unseen { background: none; border-color: transparent; }

/* ── Compact inline settings ─────────────────────────────────── */
.settings-compact {
  display: flex; flex-direction: column; gap: 0;
  margin-bottom: 14px;
}
.settings-compact-cell {
  display: flex; align-items: center; justify-content: space-between;
  padding: 5px 2px; gap: 8px;
}
.settings-compact-cell label {
  font-size: 0.78rem; color: var(--muted); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.4px; white-space: nowrap;
}
.settings-compact-cell select,
.settings-compact-cell input[type=number],
.settings-compact-cell input[type=text] {
  width: 54px; background: var(--bg); border: 1px solid var(--border);
  border-radius: 5px; color: var(--text); padding: 3px 6px; font-size: 0.88rem;
  outline: none; text-align: right; -moz-appearance: textfield;
}
.settings-compact-cell select { width: auto; text-align: left; }
.settings-compact-cell input[type=text] { width: 110px; text-align: left; letter-spacing: 1px; }
.settings-compact-cell input[type=text].invalid { border-color: var(--err, #c44); }
.settings-compact-cell select:focus,
.settings-compact-cell input[type=number]:focus,
.settings-compact-cell input[type=text]:focus { border-color: var(--accent); }
.settings-compact-cell select:disabled,
.settings-compact-cell input[type=number]:disabled,
.settings-compact-cell input[type=text]:disabled {
  color: var(--muted); opacity: 1; -webkit-text-fill-color: var(--muted);
}

/* Dev-only Settings-Zeilen: ohne body.dev-mode komplett ausblenden. */
.settings-compact-cell--dev-only { display: none; }
body.dev-mode .settings-compact-cell--dev-only { display: flex; }

/* Gruppierte Sub-Einstellungen (z.B. Tipps): umrahmter Block mit Überschrift,
   visuelles Ausgrauen wenn die ganze Gruppe aus ist (`is-off`) oder nur die
   Sub-Controls (`is-partial`). Disabled-Inputs werden vom Browser bereits
   visuell zurückgenommen — die Klassen geben dem ganzen Block zusätzliche
   Trauerränder. */
.settings-group {
  margin: 6px 0;
  padding: 6px 10px 4px;
  border: 1px solid var(--border);
  border-radius: 8px;
  transition: opacity 0.2s, border-color 0.2s;
}
.settings-group-head {
  font-size: 0.72rem; font-weight: 700; color: var(--muted);
  letter-spacing: 0.5px; text-transform: uppercase;
  padding: 2px 0 4px;
}
.settings-group-disabled-msg {
  font-size: 0.78rem; line-height: 1.35; color: var(--muted);
  padding: 6px 8px; margin-bottom: 4px;
  background: var(--bg); border: 1px dashed var(--border); border-radius: 6px;
}
.settings-group-disabled-msg.flash {
  animation: hint-msg-flash 0.6s ease-out;
}
@keyframes hint-msg-flash {
  0%   { background: rgba(181,90,46,0.35); border-color: var(--accent, #b55a2e); }
  100% { background: var(--bg); border-color: var(--border); }
}
.settings-group.is-off { opacity: 0.55; cursor: pointer; }
.settings-group.is-off .settings-group-body { pointer-events: none; }
.settings-group.is-partial .settings-group-body .settings-compact-cell:not(:first-child) {
  opacity: 0.55;
}
/* Toggle-Zeilen innerhalb der Settings-Gruppe — Schriftbild identisch zu den
   Dropdown-Zellen (uppercase, gemuted, 0.78rem). `white-space: normal` erlaubt
   längeren Labels (z.B. mit Klammer-Zusatz) den Umbruch. */
.settings-compact-cell--toggle label {
  font-size: 0.78rem; color: var(--muted); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.4px;
  white-space: normal;
}

/* Kleine ℹ-Schaltfläche neben Dropdowns mit Erklärung dahinter. */
.info-btn {
  flex-shrink: 0;
  width: 22px; height: 22px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--border); border-radius: 50%;
  color: var(--muted); font-size: 0.78rem; cursor: pointer;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.info-btn:hover  { border-color: var(--accent); color: var(--accent-text); }
.info-btn[aria-expanded="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff;
}

/* ── Player-words popup (in game) ────────────────────────────── */
#player-words-popup {
  position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 18px;
  z-index: 6000; min-width: 220px; max-width: 340px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  display: none; animation: fadeIn 0.18s;
}
#player-words-popup.visible { display: block; }
.pw-head {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 10px; gap: 8px;
}
.pw-title { font-size: 0.9rem; font-weight: 700; }
.pw-words {
  display: flex; flex-wrap: wrap; gap: 5px;
  max-height: 180px; overflow-y: auto;
}
.pw-word {
  padding: 3px 9px; border-radius: 20px; font-size: 0.8rem; font-weight: 600;
  background: rgba(255,255,255,0.08); color: var(--text); letter-spacing: 0.5px;
}
.pw-subtitle {
  font-size: 0.78rem; color: var(--muted);
  margin: -4px 0 10px;
  letter-spacing: 0.2px;
}
.pw-subtitle .pw-total { color: var(--text); font-weight: 700; }
.pw-subtitle .pw-bonus-pos { color: #fbbf24; font-weight: 600; }
.pw-word.pw-hidden {
  display: inline-flex; gap: 3px; align-items: center;
  padding: 4px 8px; background: rgba(255,255,255,0.05);
}
.pw-word.pw-hidden .pw-box {
  display: inline-block; width: 0.55rem; height: 0.85rem;
  border: 1px solid var(--muted); border-radius: 2px;
  background: rgba(255,255,255,0.04);
}

/* ── Resign popup (timer click) ────────────────────────────────
   Vertikal/horizontal mittig auf dem Screen — gleiche Position wie
   die zentralen Steal/Lost-Toast-Boxen, damit „Nachricht in der Mitte"
   überall einheitlich wirkt. */
#resign-popup {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  background: var(--surface); border: 1px solid var(--text);
  border-radius: var(--radius); padding: 18px 24px;
  z-index: 6000; min-width: 240px; text-align: center;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  display: none; animation: fadeIn 0.18s;
}
#resign-popup.visible { display: block; }
#resign-popup p { font-size: 0.88rem; color: var(--muted); margin-bottom: 12px; }
#resign-popup .resign-note { font-size: 0.78rem; opacity: 0.85; }
.resign-btns { display: flex; gap: 8px; justify-content: center; }
.timer-display { cursor: pointer; }
.timer-display:hover { opacity: 0.8; }

/* ── Wiktionary-Popup (Spielende, Wort-Klick) ────────────────── */
#wikt-overlay {
  position: fixed; inset: 0; z-index: 7000;
  background: rgba(0,0,0,0.55); backdrop-filter: blur(2px);
  display: flex; align-items: center; justify-content: center;
  animation: fadeIn 0.15s;
  /* Safe-Area: zentriertes #wikt-card im Cutout-freien Bereich halten. */
  padding:
    var(--safe-top)
    var(--safe-right)
    var(--safe-bottom)
    var(--safe-left);
}
#wikt-overlay.hidden { display: none; }
#wikt-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 20px 22px;
  width: min(480px, 92vw); max-height: 80dvh;
  display: flex; flex-direction: column;
  box-shadow: 0 12px 40px rgba(0,0,0,0.6);
}
#wikt-card-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-bottom: 14px;
}
/* Knöpfe rechts in der Kopfzeile. `margin-left: auto` schiebt sie samt
   Schließen ans Ende, das Wort bleibt links — mit space-between allein stünden
   sie sonst zu dritt auseinandergezogen. */
#wikt-head-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; }
#wikt-head-actions:empty { display: none; }
/* Die Knöpfe rechts stehen bewusst NICHT auf der Grundlinie des Worts: ein
   Knopf mit Symbol statt Text hat keine eigene Grundlinie, sie wird aus der
   Unterkante des Symbols gefälscht — dadurch säße der Daumen höher als das
   Schließen. Beide deshalb mittig ausrichten, dann sitzen sie gleich hoch. */
#wikt-head-actions, #btn-close-wikt { align-self: center; }
/* Und gleich GROSS: dieselbe Polsterung (aus .btn-sm) und eine feste
   Zeilenhöhe, die für Text wie Symbol dieselbe Kastenhöhe ergibt. */
#wikt-card-head .btn-sm { line-height: 1.2; }
/* Daumen runter in der Kopfzeile (Sinn-Suche-Auflösung): gleiche Geste wie an
   den Bedeutungen, deshalb dasselbe Symbol und dieselbe Farbe im gesetzten
   Zustand. Kein Akzent als Textfarbe (Theme-Regel). Das Symbol misst 1.2em —
   genau die Zeilenhöhe des ✕ daneben, also derselbe Inhaltskasten. */
.wikt-head-thumb { display: inline-flex; align-items: center; }
.wikt-head-thumb .dh-thumb { display: block; width: 1.2em; height: 1.2em; fill: currentColor; }
.wikt-head-thumb.is-on { color: var(--red); }
#wikt-word {
  font-size: 1.4rem; font-weight: 800; letter-spacing: -0.3px;
}
#wikt-card-body { overflow-y: auto; flex: 1; }

/* ── Definitions-Tipp-Leiste (Feature definitionsHint) ───────────────────────
   Im Fluss zwischen Titelzeile und Punkteanzeige (erstes Kind der .game-left).
   Nimmt Höhe vom Grid-Bereich (flex:0 0 auto), max 1/3; der Body scrollt. */
#defhint-overlay {
  flex: 0 0 auto;
  width: 100%;
  margin-bottom: 4px;
}
#defhint-overlay.hidden { display: none; }
#defhint-card {
  position: relative;
  width: 100%; max-height: 33vh;
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  padding: 8px 12px 10px;
  animation: fadeIn 0.15s;
}
/* Fund des Worts: Hintergrund zweimal kurz grün, dann verschwindet die Leiste
   (JS entfernt sie nach animationend). Nur Hintergrund, kein Rand. */
@keyframes defhint-found-blink {
  0%   { background: var(--surface); }
  20%  { background: var(--green); }
  40%  { background: var(--surface); }
  60%  { background: var(--green); }
  80%  { background: var(--surface); }
  100% { background: var(--surface); }
}
#defhint-card.defhint-found { animation: defhint-found-blink 0.9s ease; }
/* Kein Titel mehr — nur ein kleiner Schließen-Button oben rechts, aus dem
   Textfluss genommen; der Body reserviert rechts Platz dafür. */
#btn-close-defhint {
  position: absolute; top: 4px; right: 6px; z-index: 1;
  padding: 2px 6px; line-height: 1;
}
#defhint-body {
  overflow-y: auto; flex: 1;
  padding-right: 24px;   /* Platz für den ✕ oben rechts */
  /* Scroll-Lock-Regel (siehe CLAUDE.md): vertikal ja, seitliches Wischen nein. */
  touch-action: pan-y; overscroll-behavior-x: none;
  font-size: 0.86rem; line-height: 1.4; color: var(--text);
}
/* Eine Bedeutung pro Block, KEIN Aufzählungspunkt/-nummer; mehrere durch eine
   dünne Linie getrennt. */
.defhint-sense { margin: 0; }
.defhint-sep {
  border: 0; border-top: 1px solid var(--border);
  margin: 6px 0;
}
.defhint-note { color: var(--muted); font-style: italic; margin: 2px 0; }
.defhint-license { color: var(--muted); font-size: 0.72rem; margin: 6px 0 0; }

/* ── Definitionsjagd (Feature senseSnatch) ────────────────────────────────
   Tritt an die Stelle des Wörter-Gitters: drei gleich hohe Slots zwischen
   Punkteanzeige und Eingabe. Die Slot-HÖHE ist fix (1fr je Slot im Grid) —
   längere Bedeutungen scrollen im Slot, statt die Nachbarn zu stauchen. So
   springt beim Nachrücken eines Wortes nichts im Layout. */
.defhunt-slots {
  display: none;
  /* Schriftgröße der Bedeutungen — per Zoom-Geste umschaltbar (JS setzt die
     Variable auf DEFHUNT_FONT_SIZES[stufe]). Zentral hier, damit die Geste
     immer ALLE Kästchen gleichzeitig trifft: ein Wert, drei Slots. */
  --dh-font: 0.86rem;
}
body.game-mode .defhunt-slots:not([hidden]) {
  display: flex;
  flex-direction: column;
  width: 100%;
  flex: 1 1 0;
  min-height: 0;
  /* Anker für die Quellenzeile (siehe .dh-license) — sie soll direkt an der
     Unterkante des letzten Kästchens hängen, nicht am Ende der Spielfläche. */
  position: relative;
}
/* Während zwei Finger zoomen, darf der Browser die Bewegung nicht als
   senkrechtes Scrollen beanspruchen — sonst bricht er unsere Pointer-Ereignisse
   mit pointercancel ab. Nur für die Dauer der Geste (JS setzt/entfernt die
   Klasse), damit das Scrollen im Slot sonst unverändert funktioniert. */
.dh-grid.dh-pinching, .dh-grid.dh-pinching .dh-slot, .dh-grid.dh-pinching .dh-body {
  touch-action: none;
}
/* Column-Flex statt CSS-Grid: die naheliegende Grid-Variante bräuchte die
   Slot-Anzahl als Custom-Property in der Zeilen-Wiederholung, und das ist
   fragil. `flex: 1 1 0` auf den Slots leistet dasselbe — alle Slots gleich
   hoch, unabhängig vom Inhalt. */
.dh-grid {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 0 6px;
  /* Ein weggewischter Slot fährt nach rechts aus dem Bild — hier wird er
     abgeschnitten, statt über den Rand der Spielfläche zu wandern. */
  overflow: hidden;
}
.dh-slot {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  /* Wisch-Geste (Feature „Bedeutung wegwischen"): senkrecht scrollt der Slot
     wie gehabt, waagerecht beansprucht der Browser nichts — genau deshalb
     erreichen uns die pointermove-Ereignisse (siehe CLAUDE.md, Scroll-Sperre). */
  touch-action: pan-y;
  transition: transform 0.22s ease, opacity 0.22s ease;
}
/* Während der Finger zieht, folgt der Slot ohne Nachlauf. */
.dh-slot.dh-dragging { transition: none; }
/* Raus in Wischrichtung: `--dh-skip-dir` ist +1 (rechts) oder -1 (links) und
   wird beim Wischen von skipDefHuntSlot gesetzt. Links und rechts sind
   dieselbe Handlung — die Richtung lebt nur in dieser Animation. Ohne die
   Variable (etwa wenn der Wisch nicht von der Geste kam) gilt rechts. */
.dh-slot.dh-skip-out { transform: translateX(calc(var(--dh-skip-dir, 1) * 115%)); opacity: 0; }
/* … und der Nachrücker kommt von der GEGENSEITE herein. Die Klasse setzt nur
   den Startzustand (ohne Übergang); ihr Entfernen fährt ihn dann an. */
.dh-slot.dh-skip-in  { transform: translateX(calc(var(--dh-skip-dir, 1) * -25%)); opacity: 0; transition: none; }
/* Fund: der RAHMEN leuchtet einmal grün auf, danach tauscht JS den Inhalt aus.
   Bewusst nicht der Hintergrund und bewusst nur einmal — der Text bleibt so
   durchgehend lesbar, und die fliegende Punktzahl (.score-pop) ist ohnehin das
   auffällige Signal. Zweimaliges Hintergrund-Blinken übertönte sie.

   Der Ring liegt INSET. Nach außen (`0 0 0 8px`) schnitt ihn `overflow: hidden`
   auf .dh-grid ab — dem obersten Kasten fehlte die obere Kante, dem untersten
   die untere. Das Klipping ist dort nötig (ein weggewischter Kasten soll nicht
   über den Rand der Spielfläche wandern), also muss der Ring nach innen.

   Die Dicke steckt weiterhin im Schatten, nicht in border-width: eine geänderte
   Rahmenbreite verschöbe das Layout im Kasten und ließe den Text zucken. */
/* Fund-Blitz am Kastenrahmen. Die Farbe steckt in `--dh-found`: beim EIGENEN
   Fund bleibt es beim Grün (Erfolg), schnappt ein Mitspieler das Wort weg,
   setzt onDefHuntResolved dort dessen Spielerfarbe — so ist ohne Lesen zu
   sehen, wer zugegriffen hat. */
@keyframes defhunt-found-flash {
  0%   { border-color: var(--border); box-shadow: none; }
  25%  { border-color: var(--dh-found, var(--green));
         box-shadow: inset 0 0 0 8px var(--dh-found, var(--green)); }
  100% { border-color: var(--border); box-shadow: none; }
}
.dh-slot.dh-found { animation: defhunt-found-flash 0.9s ease; }
/* Eine Bedeutung ist nachgerückt, während der Spieler selbst gescrollt hatte:
   dann wird NICHT gesprungen (das riss ihm die gelesene Zeile weg), sondern der
   Kasten blinkt zweimal sanft. --surface2 ist der „eine Stufe daneben"-Ton
   beider Themes (im hellen ein weiches Grau), also kein Farbsignal — Grün und
   Rot sind hier bereits mit Fund und Wisch-Abzug belegt. */
@keyframes defhunt-new-sense-pulse {
  0%, 100% { background: var(--surface); }
  50%      { background: var(--surface2); }
}
.dh-slot.dh-new-sense { animation: defhunt-new-sense-pulse 0.65s ease 2; }

/* Es steht noch Inhalt außerhalb des sichtbaren Bereichs — unten, oben oder
   beides (JS setzt die Klassen, siehe updateDefHuntOverflow). Je Richtung zwei
   Signale:
   (a) die äußersten ~1,5 Zeilen laufen sanft aus — ein angeschnittenes Wort
       sagt „hier geht es weiter" deutlicher als eine harte Kante;
   (b) ein Pfeil in der jeweiligen Ecke rechts.
   Die Maske liegt auf .dh-body, die Pfeile bewusst NICHT: sie säßen sonst
   mitten im ausgeblendeten Bereich und verschwänden gerade dann, wenn sie
   gebraucht werden.

   Eine Maske für beide Richtungen, gesteuert über zwei Custom Properties: mit
   vier Einzelregeln (keine/oben/unten/beide) liefe man Gefahr, eine Kombination
   zu vergessen. 2.1em = 1,5 Zeilen bei line-height 1.4; `em` bezieht sich hier
   auf --dh-font, die Maske wandert also mit der Zoom-Stufe mit. Die Regel greift
   nur, wenn wirklich etwas auszublenden ist — ohne Klasse bleibt .dh-body ganz
   ohne Maske. */
.dh-slot.dh-more-below .dh-body,
.dh-slot.dh-more-above .dh-body {
  --dh-fade-top: 0em;
  --dh-fade-bottom: 0em;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--dh-fade-top),
    #000 calc(100% - var(--dh-fade-bottom)), transparent 100%);
          mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--dh-fade-top),
    #000 calc(100% - var(--dh-fade-bottom)), transparent 100%);
}
.dh-slot.dh-more-below .dh-body { --dh-fade-bottom: 2.1em; }
.dh-slot.dh-more-above .dh-body { --dh-fade-top:    2.1em; }

.dh-slot.dh-more-below::after,
.dh-slot.dh-more-above::before {
  position: absolute; left: 7px;
  color: var(--muted);
  font-size: 0.95rem; line-height: 1;
  pointer-events: none;
}
/* LINKS, nicht rechts: rechts unten sitzt jetzt die Bewertungszeile der
   letzten Bedeutung (.dh-rate), und der Pfeil läge sonst darüber. */
.dh-slot.dh-more-below::after  { content: '▾'; bottom: 1px; }
.dh-slot.dh-more-above::before { content: '▴'; top: 1px; }
.dh-body {
  /* Positionierungs-Anker: nur so misst `offsetTop` einer .dh-sense relativ zum
     scrollenden Kasten — genau der Wert, den das Auto-Scrollen zur neu
     nachgerückten Bedeutung als scrollTop braucht. */
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  overflow-y: auto;
  padding: 6px 10px;
  /* Scroll-Lock-Regel (siehe CLAUDE.md): vertikal ja, seitliches Wischen nein. */
  touch-action: pan-y; overscroll-behavior-x: none;
  font-size: var(--dh-font); line-height: 1.4; color: var(--text);
}
/* Bedeutungen wie im Definitions-Tipp: ein Block je Bedeutung, ohne
   Aufzählungszeichen, getrennt durch eine dünne Linie. */
.dh-sense { margin: 0; }
.dh-sep { border: 0; border-top: 1px solid var(--border); margin: 6px 0; }
/* Bewertung EINER Bedeutung: eigene, rechtsbündige Zeile unter dem Text statt
   seitlich daneben. So kann sie mit keinem noch so langen Text kollidieren und
   die Knöpfe bleiben antippbar; sie kostet dafür etwas Höhe.
   Dieselbe Zeile dient im Wiktionary-Popup der Auflösung (dort in einem <li>). */
.dh-rate {
  display: flex; justify-content: flex-end; align-items: center;
  gap: 2px; margin-top: 1px;
}
.dh-rate-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 26px; padding: 0;
  border: 0; border-radius: 6px; background: none;
  cursor: pointer; color: var(--muted);
  -webkit-tap-highlight-color: transparent;
  /* Kein 300-ms-Doppeltipp-Verzug (siehe CLAUDE.md „Input responsiveness"). */
  touch-action: manipulation;
}
/* Ist bewertet, bleibt nur der gewählte Daumen stehen — `display: inline-flex`
   oben würde das eingebaute [hidden] sonst überstimmen. */
.dh-rate-btn[hidden] { display: none; }
.dh-rate-btn:hover { color: var(--text); }
.dh-rate-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* `fill: currentColor` macht die Farbe zur Sache des Themes — deshalb eine
   gezeichnete Hand statt eines Emojis, das seine Farben mitbrächte. */
.dh-rate-btn .dh-thumb { display: block; width: 15px; height: 15px; fill: currentColor; }
.dh-rate-up.is-on   { color: var(--green-soft); }
.dh-rate-down.is-on { color: var(--red); }
.dh-note { color: var(--muted); font-style: italic; margin: 2px 0; }
/* Vorrat erschöpft, während die Runde weiterläuft: ein einzelner Kasten statt
   mehrerer leerer. Ruhig gehalten — das ist eine Auskunft, keine Niederlage. */
.dh-slot--done .dh-body {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; text-align: center;
}
.dh-done-title { margin: 0; font-weight: 600; }
/* Auflösung eines verlorenen Worts: kurz mittig im Kasten (Dauer steht in
   DEFHUNT_REVEAL_MS). Bewusst ruhig (kein Akzent-Hintergrund, kein
   Blinken) — es ist eine Auskunft, keine Belohnung. `--accent-text` statt
   `--accent`, siehe CLAUDE.md: Akzent als Textfarbe wäre im hellen Thema
   nicht lesbar. */
.dh-reveal {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; height: 100%; min-height: 3em; text-align: center;
}
.dh-reveal-label { margin: 0; color: var(--muted); font-size: 0.85em; }
/* Der Name des Wegschnappers steht fett und in SEINER Farbe (inline gesetzt),
   der Rest des Satzes bleibt zurückhaltend. */
.dh-reveal-taker { font-weight: 700; }
.dh-reveal-word  {
  margin: 0; font-weight: 700; font-size: 1.35em; color: var(--accent-text);
  /* Lange Komposita sprengen den Kasten sonst waagerecht (siehe CLAUDE.md:
     lieber umbrechen als seitwärts scrollen). */
  overflow-wrap: anywhere;
}
/* Quellenangabe (Lizenz-Pflicht): rechtsbündig direkt UNTER dem letzten
   Kästchen — auf der Papierfläche, aber bündig an dessen Unterkante.

   `top: 100%` am .defhunt-slots-Anker statt `bottom: 0`: aus dem Fluss heraus
   an die Unterkante der Kästchen gehängt. Mit `bottom: 0` an der ganzen
   Spielfläche säße sie eine Zeile tiefer, mit `bottom: 0` an den Kästchen
   dagegen INNEN im letzten Kasten — beides schon dagewesen.

   Sie verbraucht keine eigene Höhe (sie ragt in die Zeile der Wort-Eingabe,
   die meistens leer ist) und darf vom getippten Wort überdeckt werden
   (.word-display-wrap trägt z-index 3): die Eingabe ist für den Moment
   wichtiger, die Angabe steht danach wieder da.

   pointer-events: die Fläche bleibt durchlässig, damit sie keinen unsichtbaren
   Streifen über der Eingabezeile bildet — nur die Links selbst fangen Tippen
   ab. Anklickbar müssen sie bleiben: sie sind die geforderte Attribution. */
.dh-license {
  position: absolute; right: 0; top: 100%;
  color: var(--muted); font-size: 0.72rem;
  margin: 0; padding: 0 6px; text-align: right;
  pointer-events: none;
}
.dh-license a { pointer-events: auto; }
.dh-license:empty { display: none; }
.wikt-entry { margin-bottom: 14px; }
.wikt-entry:last-child { margin-bottom: 0; }
.wikt-subentry + .wikt-subentry {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);
}
.wikt-entry-title {
  font-size: 1rem; font-weight: 700; color: var(--text);
  margin: 0 0 8px;
}
.wikt-pos {
  display: inline-block; font-size: 0.72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--accent-text); background: rgba(181,90,46,0.12);
  border-radius: 4px; padding: 2px 7px; margin-bottom: 7px;
}
/* Bedeutungen wie in der Sinn-Suche (.dh-sense/.dh-sep): ein Block je
   Bedeutung, KEIN Aufzählungszeichen, getrennt durch eine dünne Linie. Der
   Punkt davor war doppelt gemoppelt — die Linie trennt schon, und mit Punkt
   sah dasselbe Wort im Popup anders aus als im Spiel. Abstände wie dort
   (6px), damit der Wechsel zwischen beiden Ansichten nicht auffällt. */
.wikt-glosses { list-style: none; padding: 0; margin: 0; }
.wikt-glosses li {
  font-size: 0.88rem; color: var(--muted); line-height: 1.5;
  padding: 6px 0; border-bottom: 1px solid var(--border);
}
.wikt-glosses li:first-child { padding-top: 0; }
.wikt-glosses li:last-child  { padding-bottom: 0; border-bottom: none; }
.wikt-cats { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 7px; }
.wikt-cat {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.3px;
  padding: 2px 8px; border-radius: 20px;
  background: rgba(255,255,255,0.07); color: var(--muted);
  border: 1px solid var(--border);
}
/* Ladezustand des Wiktionary-Popups. Nach 3 s ergänzt showWiktPopup eine
   animierte Sanduhr (zähe Verbindung; die Fetches timeouten erst später). */
.wikt-loading { color: var(--muted); font-size: 0.9rem; text-align: center; padding: 20px 0; }
.wikt-hourglass {
  display: block;
  font-size: 1.7rem;
  margin-bottom: 6px;
  animation: wikt-hourglass-turn 2.4s ease-in-out infinite;
}
/* Drehen–Pause–Drehen–Pause; 360° am Zyklusende = nahtloser Loop. */
@keyframes wikt-hourglass-turn {
  0%   { transform: rotate(0deg); }
  25%  { transform: rotate(180deg); }
  50%  { transform: rotate(180deg); }
  75%  { transform: rotate(360deg); }
  100% { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .wikt-hourglass { animation: none; }
}
.wikt-empty   { color: var(--muted); font-size: 0.88rem; text-align: center; padding: 16px 0; }
.wikt-ext-link {
  font-size: 0.72rem; color: var(--muted); text-decoration: none;
  border: 1px solid var(--border); border-radius: 4px; padding: 2px 7px;
  margin-left: auto;
}
.wikt-ext-link:hover { color: var(--text); border-color: var(--text); }
/* Inline-Link im „Wort existiert, aber ausgeschlossen"-Hinweis (Vorschlag-
   Flow): öffnet das Sprachregeln-Popup. Textfarbe = --text (Theme-Regel:
   --accent nie als Schriftfarbe), Unterstreichung signalisiert Klickbarkeit. */
.wikt-rules-link {
  color: var(--text); text-decoration: underline; cursor: pointer;
  font-weight: 700;
}
.wikt-rules-link:hover { text-decoration: none; }
/* Sprachregeln-Popup muss ÜBER dem Wiktionary-Popup (#wikt-overlay z-index
   7000) liegen, wenn es aus diesem heraus geöffnet wird (Inline-Regel-Link),
   und ebenso über der In-Game-Element-Tour (.tutorial-backdrop/-pop z-index
   9998/9999), aus deren Eingabefeld-Schritt ein „hier"-Link die Regeln öffnet.
   Schließen des Regel-Popups führt zurück auf die darunterliegende Ebene. */
#modal-lang-rules { z-index: 10000; }
/* Inline-„Warum?"-Link in den Wortregeln — als Link, nicht als Button gestylt. */
.rules-why-link {
  background: none; border: none; padding: 0; margin: 0;
  font: inherit; color: var(--text); text-decoration: underline;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.rules-why-link:hover { opacity: 0.75; }
/* Gestapelte Buttons (Vorschlagen/Schließen, Beanstandungs-Gründe) im
   Hinweis-Popup. */
.dev-actions--stack { flex-direction: column; align-items: stretch; }
/* Popup-Aktions-Buttons tragen lange Labels (das Wort im Vorschlag-Label, die
   Beanstandungs-Gründe) und würden mit dem `.btn`-Default `white-space: nowrap`
   breiter als das Popup → horizontales Scrollen. Generelle Regel (CLAUDE.md
   „Kein horizontales Scrollen"): Labels brechen stattdessen um. `text-align:
   center` zentriert die mehrzeiligen Textzeilen (justify-content zentriert nur
   die einzelne Flex-Zeilenbox). */
.dev-actions .btn { white-space: normal; text-align: center; line-height: 1.25; }
/* Verborgene Bedeutung — nur in der Dev-/Whitelist-Ansicht sichtbar.
   Text bleibt voll lesbar (Operator inspiziert ihn); markiert per linkem
   Akzent-Rand + vorangestelltem Badge. --accent nur als Rand/Border, nie als
   Textfarbe (Theme-Regel). */
.wikt-glosses li.wikt-gloss-filtered {
  border-left: 3px solid var(--accent);
  padding-left: 8px;
}
.wikt-glosses li.wikt-gloss-filtered .wikt-gloss-flag {
  display: inline-block; margin-right: 6px; vertical-align: baseline;
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.4px;
  text-transform: uppercase; white-space: nowrap;
  padding: 1px 6px; border-radius: 10px;
  border: 1px solid var(--accent); color: var(--text);
}
/* Kürzungshinweis in der Spieler-Ansicht (CC BY-SA: Änderung kenntlich). */
.wikt-filtered-note {
  margin-top: 10px; font-size: 0.7rem; color: var(--muted);
  font-style: italic; line-height: 1.4;
}
/* Kleingedrucktes am Ende des Eintrags. OHNE Trennlinie darüber: sie schnitt
   den Nachweis von dem ab, was er belegt, und in einem Popup, das ohnehin nur
   aus wenigen Blöcken besteht, war sie eine Linie zu viel. Der Abstand und die
   kleinere, gedämpfte Schrift setzen die Zeile deutlich genug ab. */
.wikt-license {
  margin-top: 14px;
  font-size: 0.68rem; color: var(--muted); line-height: 1.4;
}
.wikt-license a { color: inherit; }

/* Bild + Bildnachweis der Bilder-Runde als eigener Block, abgeschlossen durch
   eine Trennlinie: darüber steht, woher die ABBILDUNG kommt, darunter fängt das
   Wort an (Einstufung, Bedeutungen). Die Linie gehört deshalb unter den Block
   und nicht über ihn — oben schnitte sie das Bild von seinem Nachweis ab. */
.wikt-image-block {
  margin-bottom: 12px;
  padding-bottom: 8px; border-bottom: 1px solid var(--border);
}

/* Bild aus dem Wiktionary (Krebsworträtsel-Bilderrunde). Die Höhe ist in `em`
   der umgebenden Schrift gedeckelt, damit das Bild einer Schriftgrößen-Änderung
   folgt und in der Eingabe-Karte nie die Tastatur wegdrückt. `max-width: 100%`
   plus `height: auto` hält das Seitenverhältnis und verhindert waagerechtes
   Überlaufen — ein Querbalken in einem Dialog wäre ein Fehler, keine Option. */
.wikt-image { margin: 0; }
.wikt-image img {
  display: block; margin: 0 auto;
  max-width: 100%; max-height: 16em; height: auto; width: auto;
  border-radius: 8px;
}
/* Der Nachweis der DATEI — eigene Pflicht neben dem Wiktionary-Nachweis für den
   Text, deshalb steht er unmittelbar am Bild und nicht unten bei der Quelle.
   Rechtsbündig, damit er als Bildunterschrift gelesen wird und nicht als
   Fließtext. */
.wikt-image-credit {
  margin-top: 4px; text-align: right;
  font-size: 0.62rem; color: var(--muted); line-height: 1.35;
}
.wikt-image-credit a { color: inherit; }

/* Die Einstufung steht UNTER der Quellenangabe und bekommt ihre eigene
   Trennlinie: sonst klebte sie am Lizenztext, als gehörte sie dazu. Sie ist
   aber die Frage NACH der Bedeutung („wie selten ist das?"), kein Kleingedrucktes. */
/* Der (i) neben der Einstufung: klein, rund, zurückhaltend — er erklärt eine
   Nebenfrage und darf den gerahmten Wert daneben nicht überstrahlen. */
.wikt-rank-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.35em;
  height: 1.35em;
  margin-left: 4px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: none;
  color: var(--muted);
  font: 600 0.78em/1 system-ui, sans-serif;
  cursor: pointer;
  vertical-align: middle;
}
.wikt-rank-info:hover { border-color: var(--accent); color: var(--text); }

/* Eine Erklärung, die AUS dem Wiktionary-Popup heraus geöffnet wird, muss
   darüber liegen: `.modal-backdrop` steht auf 1000, das Popup auf 7000 — ohne
   eigene Ebene öffnete sie sich dahinter und wäre unsichtbar. Wie die
   Grund-Auswahl: über 7000, aber unter den Sprachregeln (8000). */
.modal-backdrop.modal-over-wikt { z-index: 7500; }

.wikt-license + .wikt-rank-line {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}

/* Die Zoomknöpfe liegen ÜBER dem Brett. Ihr Container spannt dabei mehr
   Fläche auf als die beiden Knöpfe selbst (Abstand dazwischen, Ausrichtung) —
   und schluckte damit Berührungen, die dem Brett oder den Knöpfen darunter
   galten, in fingerkuppenbreitem Umkreis. Nur die Knöpfe selbst nehmen
   Zeiger an; dasselbe Muster wie beim Hex-SVG (siehe CLAUDE.md). */
.crab-zoom { pointer-events: none; }
.crab-zoom-btn { pointer-events: auto; }
/* Dev-Controls (Rang + Kategorisieren/Sperren) ganz unten, klar abgesetzt — die
   Linie schützt hier vor dem versehentlichen Griff zu einer Kuratierung. */
.wikt-dev-controls {
  margin-top: 14px; padding-top: 12px; border-top: 2px solid var(--border);
}
/* Am Spielende steht an derselben Stelle ein einzelner, dezenter Knopf
   („… beanstanden" / „… vorschlagen"). Ohne die Linie: sie machte aus einem
   Knopf einen eigenen Abschnitt und gewichtete ihn schwerer, als er gemeint
   ist. Der Abstand bleibt. */
.wikt-dev-controls--plain {
  padding-top: 0; border-top: 0;
}
/* Beanstandungs-Auswahl als eigenes Popup (Sinn-Suche: der Auslöser ist der
   Daumen in der Kopfzeile, unten gibt es keinen Kasten zum Aufklappen).
   Über dem Wiktionary-Popup (7000), aber unter den Sprachregeln (8000) — die
   öffnet der „Regeln"-Link aus dieser Auswahl heraus. Linksbündig statt
   `.modal-card`-mittig: das ist Fließtext plus eine Liste von Gründen. */
#report-popup { z-index: 7600; }
#report-popup .modal-card { padding: 18px; text-align: left; max-width: 420px; }
#report-popup .dev-actions { margin-top: 0; }
/* Quellen-&-Lizenzen-Modal: Links nicht einfärben (kein UA-Blau), nur
   unterstreichen — sie erben die Textfarbe der Umgebung. */
#modal-credits a { color: inherit; text-decoration: underline; }
.wikt-warning {
  margin: 6px 0 4px; padding: 8px 11px;
  background: rgba(220,150,40,0.12); border: 1px solid rgba(220,150,40,0.45);
  border-radius: 7px;
  font-size: 0.86rem; line-height: 1.4; color: var(--text);
}
.wikt-rank-info {
  margin: 4px 0 8px; padding: 6px 10px;
  background: var(--surface2); border-radius: 6px;
  font-size: 0.82rem; color: var(--muted); font-weight: 600;
}
/* Einstufung des Wortes auf der ERSTEN Popup-Seite (Spieler-Ansicht, über den
   Bedeutungen). Gerahmt ist NUR der Wert, nicht die Beschriftung davor: der
   Rahmen ist der Knopf (er öffnet die Beanstandung), also umfasst er genau das
   Klickbare. Der Wert trägt die Textfarbe (Prominenz über Fettung; --accent
   wäre als Textfarbe im Hell-Theme unlesbar, siehe CLAUDE.md „Theme colors"). */
.wikt-rank-line {
  margin: 0 0 10px;
  font-size: 0.82rem; color: var(--muted); font-weight: 600;
}
.wikt-rank-val {
  font: inherit; font-weight: 700; color: var(--text);
  padding: 4px 9px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface);
  cursor: pointer;
}
.wikt-rank-val:hover { border-color: var(--accent); }
/* Quellen-Link — steht nur noch dort, wo es unten keine Lizenzzeile gibt
   (kein Wiktionary-Eintrag gefunden). Über Bedeutungen wäre er deren Dopplung.
   `.wikt-ext-link` hat für die alte Flex-Zeile ein `margin-left: auto`; im
   Absatz hier ist es wirkungslos, der Link steht links. */
.wikt-source-line { margin: 8px 0 0; }
.wikt-en-fallback-bar {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 10px;
  font-size: 0.76rem; color: var(--muted);
}
.wikt-info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border); border-radius: 50%;
  font-size: 0.72rem; font-weight: 700; font-style: italic;
  cursor: pointer; padding: 0; line-height: 1;
}
.wikt-info-btn:hover { color: var(--text); border-color: var(--text); }
.wikt-en-fallback-info {
  margin: 0 0 10px; padding: 8px 11px;
  background: rgba(80,140,220,0.10); border: 1px solid rgba(80,140,220,0.35);
  border-radius: 7px;
  font-size: 0.82rem; line-height: 1.45; color: var(--text);
}
.wikt-en-fallback-info.hidden { display: none; }
/* Dev-Modus: Buttons zum Sperren/Whitelisten von Wörtern */
.dev-actions {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin-top: 10px;
}
.dev-btn {
  display: inline-flex; align-items: center; gap: 5px;
  background: rgba(220,80,60,0.12); color: var(--text);
  border: 1px solid rgba(220,80,60,0.4); border-radius: 6px;
  padding: 5px 11px; font-size: 0.82rem; font-weight: 600;
  cursor: pointer;
}
.dev-btn:hover { background: rgba(220,80,60,0.22); }
.dev-btn.dev-btn-ok { background: rgba(60,160,90,0.12); border-color: rgba(60,160,90,0.5); }
.dev-btn.dev-btn-ok:hover { background: rgba(60,160,90,0.22); }
/* Wrapper als Positionsanker: Prompt liegt absolut über dem Wort, schiebt nichts. */
.word-display-wrap {
  position: relative;
  width: 100%;
}

/* Wrapper für Wörter-Grid + überlagerte Tipp-Zeile.
   Im Grid-Modus liegt die Tipp-Zeile absolut zentriert über dem Grid (sie
   beansprucht KEINE eigene Layout-Zeile, ihr Buchstaben-Cursor blinkt direkt
   im Grid). Außerhalb des Grid-Modus hat das Wörter-Grid display:none und die
   Tipp-Zeile sitzt in normalem Fluss als kompakter Block. */
.word-grid-area {
  position: relative;
  width: 100%;
  flex: 0 0 auto;
  min-height: 0;
}
/* Im Grid-Modus halbiert dieses Wrapping-Element die .game-left-Höhe fest:
   obere Hälfte = Score-Tabelle + Grid + überlagerter Cursor; untere Hälfte =
   Hex-Buttons + Hex-Controls. Das Hex-SVG bekommt dadurch garantiert
   (50% − Controls-Höhe). Inhalt als column flex, damit die Score-Tabelle
   oben sitzt und das Grid den Rest füllt. */
body.game-mode .word-grid-area {
  /* flex: 1 1 auto — der Grid-Bereich frisst den gesamten freien
     Vertikalplatz oberhalb der Eingabe. Vorher (0 0 50%) klebte er
     starr an der Bildschirmmitte, sodass auf Portrait-Phones bis zu
     ~30 px Leerraum zwischen Grid-Unterkante und Hex-Frame-Oberkante
     übrig blieben (Hex-Region war 50% hoch, das quadratische SVG aber
     nur so groß wie die Region-Breite). Mit `flex: 1 1 auto` zieht sich
     das Grid bis direkt an die Hex-Region heran; die Hex-Region wird
     dafür unten auf Inhaltsgröße geschrumpft (flex: 0 0 auto). */
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
body.game-mode .hex-region {
  /* Region schrumpft auf SVG-Inhaltsgröße — kein Leerraum mehr über
     dem Hex-Frame, weil die Region selbst exakt so hoch wird wie das
     quadratische SVG. JS sizet das SVG (HexSvg.relayout) im Grid-Modus
     auf min(game-left-W, game-left-H * 0.5, 500), damit die zirkuläre
     Abhängigkeit zur eigenen Region-Höhe wegfällt. */
  flex: 0 0 auto;
}

/* ── In-Game Score-Tabelle (kompakt, 2 Spalten) ─────────────── */
.grid-score-table {
  display: none;
  width: 100%;
  flex: 0 0 auto;
}
body.game-mode .grid-score-table {
  display: grid;
  /* Zwei Spalten — bei ungerader Spielerzahl wandert der letzte Eintrag in
     Spalte 1, Spalte 2 bleibt leer. */
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 4px 6px 6px;
  font-size: 0.85rem;
  line-height: 1.25;
}
.grid-score-table .gst-box {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 8px;
  border: 1px solid var(--score-color, var(--border));
  border-radius: 4px;
  background: var(--score-bg, rgba(255,255,255,0.04));
  min-width: 0;
}
.grid-score-table .gst-box.me {
  border-width: 2px;
  padding: 2px 7px; /* gleicht Border-Verbreiterung aus → Box bleibt gleich groß */
}
.grid-score-table .gst-name {
  color: var(--score-color, var(--text));
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 1 1 auto;
}
.grid-score-table .gst-box.me .gst-name {
  font-weight: 800;
}
.grid-score-table .gst-score {
  color: var(--score-color, var(--text));
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}
body.game-mode .word-grid-area .word-display-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Klicks fallen durch zur Wörter-Grid-Zelle darunter (die ohnehin nicht
     interaktiv ist) — wichtiger ist, dass die Tipp-Zeile keinen Hex-Button
     blockiert. */
  pointer-events: none;
  width: 100%;
  z-index: 2;
}
body.game-mode .word-grid-area .word-display {
  pointer-events: none;
}
/* Definitionsjagd: es gibt kein Wörter-Gitter, über dem die Tipp-Zeile
   schweben könnte — absolut zentriert läge sie mitten im mittleren
   Bedeutungs-Slot. Sie sitzt deshalb unten, direkt über dem Eingabe-Bereich.
   Im Fluss reserviert sie GENAU EINE Zeile; das eigentliche Wort ist darin
   absolut an der Unterkante verankert und wächst nach OBEN über die
   Bedeutungen. So ändert ein langes getipptes Wort die Größe des
   Definitionsbereichs nicht — es überdeckt ihn kurzzeitig. */
body.game-mode.defhunt-mode .word-grid-area .word-display-wrap {
  position: relative;
  /* top/left aus der Regel darüber MÜSSEN mit zurückgesetzt werden: bei
     position:relative wirken sie als Verschiebung gegenüber der Flussposition
     und schöben die Zeile um ein halbes Elternteil nach rechts unten (mitten
     ins Hex-Rad). transform:none allein reicht dafür nicht. */
  top: auto;
  left: auto;
  transform: none;
  flex: 0 0 auto;
  /* Eine Zeile in der Schriftgröße von .word-display (clamp weiter oben). */
  height: clamp(0.95rem, 4.2vh, 2rem);
  margin-top: 4px;
  overflow: visible;
  z-index: 3;
}
body.game-mode.defhunt-mode .word-grid-area .word-display {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* KEIN eigener Hintergrund: die Zeile ist die meiste Zeit leer, und ein
     Untergrund läge dann als grauer Kasten auf der Quellenzeile der
     Bedeutungen (real aufgetreten). Nur Text auf freier Fläche. */
  background: none;
  /* Mehrzeilig statt seitlich überlaufen — waagerechtes Scrollen ist überall
     unerwünscht (siehe CLAUDE.md). */
  white-space: normal;
  overflow-wrap: anywhere;
}
/* ── Countdown Overlay ───────────────────────────────────────── */

#countdown-overlay {
  /* `inset: 0` würde gegen die Layout-Viewport-Höhe gespannt (auf Firefox
     Klar inkl. unsichtbarem Bereich hinter der Bottom-Toolbar) und die
     vertikale Mitte säße unter der Bildschirmkante. Stattdessen explizit
     mit --vh-basierter Höhe spannen — siehe head-script. */
  position: fixed; top: 0; left: 0; right: 0;
  height: var(--vh, 100dvh);
  z-index: 9000;
  /* Design/Typewriter: Papier-Hintergrund wie auf body — sonst stand
     der Countdown auf einer flachen Surface-Farbe. */
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  pointer-events: none; opacity: 0;
  transition: opacity 0.35s;
}
#countdown-overlay.visible { opacity: 1; pointer-events: all; }
#countdown-number {
  /* Basisgröße × --cd-scale (JS setzt den Faktor pro Ziffer: 5 klein …
     1 groß — siehe styleCountdownDigit). Farbe kommt ebenfalls per JS
     (Button-Grün → Krabben-Orange); der --violet-Wert hier ist nur
     Fallback vor dem ersten Tick. */
  --cd-base: clamp(6rem, 22vw, 18rem);
  font-size: calc(var(--cd-base) * var(--cd-scale, 1));
  font-weight: 900;
  color: var(--violet);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  /* Box-Höhe FIX = größte Ziffer (max --cd-scale = 1.38), Ziffer darin
     zentriert. So bleibt der Hinweistext darunter von Anfang an auf seiner
     (tiefen) Position und verrückt nicht, während die Zahlen wachsen. */
  height: calc(var(--cd-base) * 1.38);
  display: flex; align-items: center; justify-content: center;
  animation: cdpop 0.7s ease-out forwards;
}
#countdown-number.go { color: var(--green); font-size: clamp(3rem, 14vw, 10rem); }
@keyframes cdpop {
  0%   { transform: scale(1.6); opacity: 0; }
  45%  { transform: scale(1.0); opacity: 1; }
  100% { transform: scale(0.93); opacity: 1; }
}
/* Schluss-Bild des Countdowns: das Logo ersetzt das frühere „LOS!". Wird per
   .show eingeblendet (Fade + Scale-Overshoot + Entschärfen des Blurs). */
#countdown-logo {
  display: none;
  width: min(74vw, 460px);
  max-width: 92%;
  height: auto;
}
#countdown-logo.show {
  display: block;
  animation: cdlogo 0.9s cubic-bezier(0.18, 0.7, 0.2, 1) forwards;
}
@keyframes cdlogo {
  0%   { opacity: 0; transform: scale(0.6);  filter: blur(12px); }
  55%  { opacity: 1; transform: scale(1.05); filter: blur(0); }
  100% { opacity: 1; transform: scale(1); }
}
#countdown-hint {
  margin-top: 28px; font-size: 1.1rem; color: var(--muted); letter-spacing: 1px;
}

/* ── Fullscreen game mode ────────────────────────────────────── */

body.game-mode {
  /* Firefox Klar / Android: 100svh und 100dvh werden gegen die LAYOUT-
     Viewport-Höhe berechnet, die auch den Bereich HINTER der unteren
     Toolbar einschließt → bottom-anchored Inhalt (Hex-Eingabe) landet
     unter dem sichtbaren Rand und der Pre-Game-Countdown kippt nach
     unten. Wir nutzen daher die per JS gesetzte --vh (= visualViewport
     .height, der einzig zuverlässige Wert für die wirklich sichtbare
     Pixelhöhe) und fallen auf 100dvh zurück, falls JS noch nicht lief.
     Gleichzeitig override-en wir base-body min-height (100dvh) auf den
     gleichen Wert — sonst würde der min-height-Wert die Höhe wieder
     über die sichtbare Fläche heben. */
  height: var(--vh, 100dvh);
  min-height: var(--vh, 100dvh);
  overflow: hidden; padding: 0; justify-content: flex-start;
  overscroll-behavior: none;
}
body.game-mode #screen-game.active {
  height: var(--vh, 100dvh); max-width: 100%; padding: 0; overflow: hidden;
  align-items: stretch;
  margin-top: 0; margin-bottom: 0; /* auto-margin-Zentrierung im game-mode deaktivieren */
  /* Browser-Default-Gesten (Scroll/Pinch/Swipe-Nav) im Spielbildschirm
     komplett unterdrücken. Scoped auf #screen-game — Overlays/Popups sind
     body-level Geschwister und bleiben scrollbar. Taps feuern weiter über
     pointerdown. */
  touch-action: none;
}
body.game-mode .game-wrapper {
  /* padding-bottom: 7px schafft die kleine Lücke zwischen Eingabe-Bereich
     (SVG-Unterkante) und unterer Bildschirmkante — passend zur seitlichen
     ~7 px Lücke, die die SVG-ViewBox-Innenmarge zwischen Tasten und Bild-
     schirmrand erzeugt. Seitlich und oben weiterhin 0, damit das Hex-SVG
     seine bestehende Breite behält.

     Safe-Area (Cutout/Home-Indicator): seit viewport-fit=cover im
     Meta-Tag zeichnet die Seite edge-to-edge; wir sparen den vom Gerät
     beanspruchten Rand selbst aus, indem wir env(safe-area-inset-*) auf
     die Paddings addieren. Auf Geräten ohne Cutout sind alle Insets 0px
     → identisch zum alten `padding: 0 0 7px 0`. box-sizing:border-box
     (global) + height:var(--vh) heißt: das Padding schiebt den Inhalt
     nach innen, ohne die Gesamthöhe zu verändern.

     TOP ist hier bewusst 0 (nicht inset-top): das Top-Band der Notch wird
     NICHT vom Wrapper-Padding ausgespart, sondern vom Spiel-Header selbst
     gefüllt (min-height = inset-top unter html.has-top-inset) — dort
     flankieren Logo links und Uhr+☰ rechts die mittige Notch. Würden wir
     hier zusätzlich inset-top aufschlagen, käme das Band doppelt. */
  height: var(--vh, 100dvh); max-width: 100%; gap: 0;
  padding:
    0
    var(--safe-right)
    calc(7px + var(--safe-bottom))
    var(--safe-left);
}
/* Im In-Game-Modus enge Vertikal-Spacings, damit der Hex-Bereich (und
   damit die Buchstaben-Tasten) mehr Höhe bekommt — Ziel: jede Taste
   mindestens fingerkuppen-groß. game-left-gap = 0, damit das Wörter-
   Grid bündig an den Eingabe-Bereich (hex-region) anschließt; die
   einzelnen Sub-Regionen setzen ihre Innen-Padding/Margin selbst. */
body.game-mode .game-left {
  gap: 0;
}
body.game-mode .game-body {
  flex: 1; min-height: 0; overflow: hidden;
  /* Cross-axis: alle direkten Kinder (.game-left) sollen auf die volle Game-
     Body-Höhe gestreckt werden — sonst kollabieren Prozent-flex-basis-Werte
     auf den Kindern (z.B. .word-grid-area: flex 0 0 50%) auf 0, weil die
     Höhe des Eltern-flex-Items nicht definit ist. Das war die Ursache dafür,
     dass die hex-controls auf Vivaldi/Desktop nicht sichtbar waren: game-left
     war nur so hoch wie sein Content, und absolute Controls saßen mitten im
     Layout statt am unteren Rand. */
  align-items: stretch;
}
body.game-mode .game-left {
  flex: 1; min-width: 0; min-height: 0;
  /* height:100% defensiv für Browser, die align-items:stretch nicht für
     flex-Items mit indefiniter Cross-Axis-Höhe respektieren. */
  height: 100%;
  align-self: stretch;
  justify-content: flex-start; align-items: center;
  overflow: hidden;
  position: relative;
  box-sizing: border-box;
}
body.game-mode .game-left .hex-svg {
  /* Konkrete Pixelgröße kommt von JS (sizeHexSvg() in index.html). CSS hier
     stellt nur sicher, dass die SVG sich nie automatisch auf 100% aufbläht
     (das hatte sie auf breiten Viewports unter Chromium/Vivaldi über den
     flex-Slot hinausgeschoben → bottom-controls verdeckt). flex:0 0 auto
     deaktiviert auto-grow; max-width/max-height fangen den Fall ab, dass JS
     noch nicht gelaufen ist. */
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  flex: 0 0 auto;
  min-height: 0;
  min-width: 0;
  margin-top: auto;
}
body.game-mode .game-left .hex-svg {
  margin-top: 0;
}
body.game-mode .game-left > .score-chips {
  flex-shrink: 0;
}
/* Word-Display schrumpft als erstes wenn der Platz knapp wird — die SVG-Buchstaben
   werden erst als letzte Reserve kleiner. min-height:0 erlaubt den Kollaps,
   die clamp()-Schriftgröße passt sich an die Viewport-Höhe an. */
body.game-mode .word-display {
  min-height: 0;
  font-size: clamp(0.95rem, 4.2vh, 2rem);
  letter-spacing: 3px;
  flex-shrink: 1;
}
body.game-mode .word-display .word-cursor {
  height: 1em;
}
body.game-mode .game-right { display: none !important; }

/* ── In-Game-Wörter-Grid (steal) ─────────────────────────────── */
/* Eine Tabelle leerer „Tasten", je eine pro Lösungswort. Zellen werden
   in der Spielerfarbe enthüllt, sobald jemand das Wort findet. */
#word-grid[hidden] { display: none; }
body.game-mode #score-chips { display: none !important; }
/* Top-Bar im Spiel: nur Raumcode, Modus-Badge und Timer — der
   X/Y-Fortschritt ist redundant, weil das Grid den Stand visuell zeigt. */
body.game-mode #game-progress { display: none !important; }
body.game-mode .game-left #word-grid {
  width: 100%;
  /* Innerhalb der .word-grid-area (column-flex) füllen wir den Rest unterhalb
     der Score-Tabelle. flex:1 1 0 + min-height:0 erlaubt Schrumpfen, max-
     height:100% verhindert dass das Grid die Score-Tabelle aus der area
     drückt, falls JS-Berechnung knapp übersteht. */
  height: auto;
  max-height: 100%;
  flex: 1 1 0;
  min-height: 0;
  /* flex-wrap statt CSS-Grid: feste Pixelbreite pro Zelle (per JS gesetzt),
     gap=0 (jede subpixel-Breite würde sonst Firefox Android über den Rand
     drücken — bestätigt). Cells fließen links-nach-rechts, brechen am
     Zeilenende um. */
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 0;
  max-width: 100%;
  box-sizing: border-box;
  overflow: hidden;
}
/* `hidden` muss auch IM SPIEL gewinnen. Die Regel darüber ist spezifischer als
   das globale `#word-grid[hidden] { display: none }` und setzt `display: flex`
   — ein per `hidden` ausgeblendetes Gitter liefe damit weiter als leere
   `flex: 1 1 0`-Box mit und nähme die halbe Fläche der .word-grid-area ein.
   Aufgefallen ist das in der Definitionsjagd, wo es gar kein Gitter gibt und
   die Bedeutungs-Slots deshalb nur die Hälfte des Platzes bekamen. */
body.game-mode .game-left #word-grid[hidden] { display: none; }
/* No-Wrap-Variante: Wörter werden in flex-nowrap-Wrapper gepackt, der ganze
   Wrapper umbricht beim Zeilenwechsel — so können mehrere Wörter eine Zeile
   teilen, aber kein Wort wird mitten gespalten. Die JS-Berechnung wählt die
   Zellgröße so, dass das längste Wort horizontal in die Breite passt und alle
   per Greedy-Packing prognostizierten Zeilen vertikal in die Region passen. */
/* Scroll-Modus für sehr lange Wortlisten — Zellen behalten ihre lesbare
   Größe, statt sie zu schrumpfen schaltet der Packer hier overflow-y: auto
   ein. touch-action: pan-y hebt das game-mode-globale touch-action: none
   für genau diesen Scroll-Container auf, damit auf Mobile vertikal
   gescrollt werden kann; horizontal-overscroll bleibt blockiert (matched
   CLAUDE.md-Regel zu inneren Scroll-Containern). */
body.game-mode .game-left #word-grid.wg-scrollable {
  overflow-y: auto;
  overscroll-behavior-x: none;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
/* Scroll-Fade-Hinweise: oben/unten am Grid wird der Inhalt sanft
   ausgeblendet, sobald in dieser Richtung weiterer (unsichtbarer)
   Inhalt existiert. JS toggelt die Klassen on/off pro Scroll-Position
   (siehe _updateScrollFades in main.js). mask-image arbeitet auf
   visueller Ebene — kein Layout-Einfluss, kein Interaktions-Block.
   --fade-h = Höhe des Verlaufs (ein Cell ≈ 18 px wirkt natürlich). */
body.game-mode .game-left #word-grid.wg-scrollable {
  --fade-h: 22px;
}
body.game-mode .game-left #word-grid.wg-scrollable.wg-fade-top {
  mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-h), #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-h), #000 100%);
  /* Zusätzlich zur Mask: ein dezenter dunkler Schatten am Rand →
     Inhalt scheint nicht nur durchsichtig zu werden, sondern unter
     eine Kante zu rutschen. inset box-shadow malt am visiblen Rand
     der Scroll-Box und bewegt sich nicht mit dem Inhalt. */
  box-shadow: inset 0 14px 16px -10px rgba(0, 0, 0, 0.55);
}
body.game-mode .game-left #word-grid.wg-scrollable.wg-fade-bottom {
  mask-image: linear-gradient(to top, transparent 0, #000 var(--fade-h), #000 100%);
  -webkit-mask-image: linear-gradient(to top, transparent 0, #000 var(--fade-h), #000 100%);
  box-shadow: inset 0 -14px 16px -10px rgba(0, 0, 0, 0.55);
}
body.game-mode .game-left #word-grid.wg-scrollable.wg-fade-top.wg-fade-bottom {
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--fade-h),
    #000 calc(100% - var(--fade-h)), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--fade-h),
    #000 calc(100% - var(--fade-h)), transparent 100%);
  box-shadow:
    inset 0 14px 16px -10px rgba(0, 0, 0, 0.55),
    inset 0 -14px 16px -10px rgba(0, 0, 0, 0.55);
}
body.game-mode .game-left #word-grid.wg-nowrap {
  /* Design/Typewriter: links ausrichten statt zentrieren, damit alle Zellen
     über die Zeilen hinweg in einem klaren Raster fluchten. Beim Zentrieren
     verschob jede Zeile um ihren restlichen Leerraum / 2 → Buchstaben
     stehen pro Zeile unterschiedlich weit links. flex-start klebt die
     Wörter alle an der linken Kante der Region.

     Zwischen-Wort-Lücke wird per echter <div class="wg-spacer"></div>
     im DOM gezeichnet (siehe relayout/_build in index.html). Das ist
     pixel-genau eine Zellbreite groß und bleibt am Zeilenende durch
     ihre Position INNERHALB des vorigen .wg-word automatisch unsichtbar
     hängen. column-gap wird daher nicht mehr benötigt. */
  justify-content: flex-start;
}
/* Unsichtbarer Spacer zwischen Wörtern — gleiche Größe wie eine .wg-cell,
   wird in JS gesetzt. Kein Hintergrund, kein Inner-Kästchen. */
.word-grid .wg-spacer {
  /* Sizing rein über --wg-cell-w (in JS auf #word-grid gesetzt; CSS-Var
     erbt zum Spacer). Kein JS-Sizing-Loop nötig — eine fehlende
     Inline-Width war der Grund warum der Spacer vorher 0×0 blieb. */
  flex: 0 0 var(--wg-cell-w, 18px);
  width:  var(--wg-cell-w, 18px);
  height: var(--wg-cell-w, 18px);
  box-sizing: border-box;
  background: transparent;
  pointer-events: none;
  /* Keine ::before-Pseudo, kein Rahmen — reine Leerfläche; die Papier-
     Textur vom Body scheint einfach durch. */
}
.word-grid .wg-word {
  display: flex;
  flex-wrap: nowrap;
  flex: 0 0 auto;
}
.word-grid .wg-cell {
  display: flex; align-items: center; justify-content: center;
  font-weight: 700;
  /* Kein CSS-uppercase — die Zell-Buchstaben kommen bereits großgeschrieben
     (upperWord) aus dem JS; CSS-uppercase würde ein Lösungs-`ß` als `SS`
     rendern (zwei Zeichen in einer Ein-Buchstaben-Zelle). */
  color: transparent;
  user-select: none;
  overflow: hidden;
  white-space: nowrap;
  /* box-sizing + flex-shrink:0 garantieren, dass Zellen ihre per JS gesetzte
     Pixelbreite behalten und nicht durch Padding/Border oder Flex-Shrink
     überlaufen oder fließen. */
  box-sizing: border-box;
  flex: 0 0 auto;
  min-width: 0;
  padding: 0;
  line-height: 1;
  /* Raster: waagerechte 1 px Linien (Zeilen-Trennung) + senkrechte 1 px
     Linien INNERHALB eines Worts (Buchstaben zählen). An Wort-Grenzen
     wird die rechte Border per Klasse transparent geschaltet, damit dort
     der Fade-Effekt sauber alleinsteht (Wort sichtbar als Block). Border
     wird nicht entfernt, sondern nur transparent — so bleibt die Zell-
     breite unter `box-sizing:border-box` einheitlich. */
  /* >>> Farben des Wörter-Grids — anpassen, wenn der Look nicht passt:
       Separator-Linien (Border)  = var(--bg)
       Zell-Hintergrund (--cell-bg) = #27272a (= var(--surface))
     Light-Theme-Override unten setzt --cell-bg ebenfalls auf var(--surface). */
  /* Design/Typewriter: Zellrand transparent statt solid var(--bg) — sonst
     entstand ein 1-px-Streifen ohne Papier-Grain um jedes Innen-Kästchen.
     transparent lässt die Body-Textur durchscheinen → optisch fließt das
     Papier um die Kästchen herum. */
  border-top:    1px solid transparent;
  border-bottom: 1px solid transparent;
  border-left:   0;
  border-right:  1px solid transparent;
  --cell-bg: #27272a;
  /* Design/Typewriter: Zelle selbst ist transparent (Papier scheint im
     1-2 px Außenrand durch); das eigentliche Buchstaben-Fach wird per
     ::before-Pseudo gezeichnet — leicht eingerückt, mit dunklem Rand.
     Layout-Größe bleibt identisch (Pseudo ist absolut positioniert,
     beeinflusst die Zellbreite nicht). */
  background: transparent;
  position: relative;
  isolation: isolate;
}
.word-grid .wg-cell::before {
  content: '';
  position: absolute;
  /* inset: 0 → das Innen-Kästchen füllt die volle Padding-Box, nur die
     1-px-Zellborder (var(--bg)) bleibt als Papier-Außenrand. Vorher 1px
     ließ einen 2-px-Rand entstehen, der dem Nutzer zu klein wirkte. */
  inset: 0;
  background: var(--cell-bg);
  border: 1px solid var(--text);
  box-sizing: border-box;
  z-index: -1;
  pointer-events: none;
}
/* Sobald eine Zelle als gefunden markiert ist (egal in welchem Found-State),
   verschwindet die dunkle Rahmenlinie des Innen-Kästchens — Border-Color
   wird auf den eigenen Hintergrund gesetzt, damit der gefärbte Block
   visuell als gefüllte Fläche statt als umrahmtes Fach erscheint.
   Box-sizing bleibt border-box, also keine Layoutverschiebung. */
.word-grid .wg-cell.revealed::before,
.word-grid .wg-cell.partial-found::before,
.word-grid .wg-cell.all-found::before,
.word-grid .wg-cell.hinted::before {
  border-color: var(--cell-bg);
}
.light-theme .word-grid .wg-cell {
  --cell-bg: var(--surface);
}
/* Design/Typewriter: keine vertikale Markierungs-Linie mehr am linken
   Rand der ersten Zelle eines Worts. Die Wort-Grenzen ergeben sich aus
   der unsichtbaren .wg-spacer-Zelle zwischen den Wörtern. Die alte Linie
   war im hellen Theme eine schwarze Strich-Linie und brach den
   Raster-Look. */
.word-grid .wg-cell.wg-first {
  box-shadow: none;
}
/* Wort-Ende: keine senkrechte Trennlinie zum nächsten Wort (die Akzent-Linie
   am Anfang des Folge-Worts grenzt ab). Border bleibt 1 px breit, Farbe
   transparent → Zellbreite unverändert. */
/*.word-grid .wg-cell.wg-last {*/
/*  border-right-color: transparent;*/
/*}*/
/* Design/Typewriter: in der „single finder"-Zelle ist der Hintergrund die
   voll gesättigte Spielerfarbe (siehe _paintCell/setCellFinders in JS) —
   Buchstaben darüber durchgehend in Weiß, das liefert genug Kontrast auf
   jeder Spielerfarbe und passt zum Setzkasten-Look der Mockups. Beide
   Themes verwenden denselben Weiß-Wert. */
.word-grid .wg-cell.revealed,
.light-theme .word-grid .wg-cell.revealed {
  color: #fff;
}
.light-theme .word-grid .wg-cell.partial-found.mine-finder {
  color: #fff;
  text-shadow: none;
}
/* Hidden-Modi: Partial-Found = ein oder mehrere (aber nicht alle) Spieler
   haben das Wort. Single-Finder geht über --cell-bg → Word-Edge-Fade greift.
   Multi-Finder (.multi-finder) bekommt einen neutralen Hellgrau-Ton — bewusst
   etwas dunkler als die all-found-Zellen, damit beide Zustände unterscheidbar
   bleiben (bei all-found sind außerdem die Buchstaben für alle sichtbar).
   Buchstabe bleibt verborgen außer .mine-finder. */
.word-grid .wg-cell.partial-found {
  color: transparent;
}
.word-grid .wg-cell.partial-found.mine-finder {
  color: #fff;
  font-weight: 800;
}
.word-grid .wg-cell.partial-found.multi-finder {
  --cell-bg: var(--grid-found-bg-multi);
  --cell-bg-fade: rgba(161,161,170,0);  /* transparente Variante von --grid-found-bg-multi für Gradient-Fades */
  color: transparent;
}
.word-grid .wg-cell.partial-found.multi-finder.mine-finder {
  color: var(--grid-found-text);
  text-shadow: none;
  font-weight: 800;
}
/* Alle Spieler haben das Wort gefunden → hellgraue Zelle, dunkler Text.
   Reines Weiß war auf dunklem Theme zu kontraststark; --grid-found-bg-all
   hebt die Zelle weiterhin klar vom Standard-Hintergrund ab, ohne zu
   blenden. Token-Werte siehe theme-dark.css / theme-light.css. */
.word-grid .wg-cell.all-found {
  --cell-bg: var(--grid-found-bg-all);
  --cell-bg-fade: rgba(212,212,216,0);  /* transparente Variante von --grid-found-bg-all für Gradient-Fades */
  color: var(--grid-found-text);
  font-weight: 800;
}
/* Tipp-Buchstabe: helles Cream-Kästchen mit schwarzer Schrift — bewusst
   theme-unabhängig, damit Tipps in beiden Themes sofort als „aufgedeckter
   Buchstabe, aber noch ungelöst" lesbar sind. --cell-bg wird gesetzt,
   damit das ::before-Pseudo des Zell-Renders ihn übernimmt (Cell selbst
   bleibt transparent). Border-color auf cell-bg ist bereits durch die
   `.hinted::before { border-color: var(--cell-bg) }`-Regel weiter oben
   gehandhabt. `hinted-pop` triggert die Erst-Erscheinen-Animation. */
.word-grid .wg-cell.hinted {
  --cell-bg: #F2EBDA;
  color: #28241E;
  font-weight: 800;
}
.word-grid .wg-cell.hinted-pop {
  animation: hint-pop 0.6s ease-out;
}
@keyframes hint-pop {
  /* Text auf der Akzent-Fläche → schwarz (cream-Variante mit dem hellen
     Gelb-Gold hatte zu wenig Kontrast). */
  0%   { transform: scale(0.4); background-color: var(--accent); color: #000; }
  35%  { transform: scale(1.25); background-color: var(--accent); }
  60%  { transform: scale(0.95); }
  100% { transform: scale(1); }
}
/* Design/Typewriter: KEIN sichtbarer Trenner mehr zwischen Wörtern —
   Wortgrenzen werden visuell durch die einzeln gerahmten Innen-Kästchen
   der Zellen (siehe .wg-cell::before) markiert. Vorher gesetztes
   `border-right: 18px` auf .wg-word addierte pro Wort eine Zellbreite
   horizontalen Layoutraums, von dem der JS-Packer nichts wusste → er
   pickte einen Side-Wert, der im Render die letzte Zeile unter den
   verfügbaren Bereich schob. */

/* Eigene Wörter: dezent dickere Hintergrund-Sättigung statt Outline (Outlines
   um jede Buchstabenzelle wären visuell zu laut). */
.word-grid .wg-cell.mine {
  font-weight: 800;
}

/* Wörter-Grid-Cursor: gefundene Zellen sind klickbar (öffnet Finder-Popover). */
.word-grid .wg-cell.revealed,
.word-grid .wg-cell.partial-found,
.word-grid .wg-cell.all-found {
  cursor: pointer;
}

/* ── Finder-Chip-Popover (Klick auf gefundene Zelle im Wörter-Grid) ──────
   Kleines fixed-positioniertes Floater-Element, das direkt an der angeklick-
   ten Zelle ankert. Zeigt einen oder mehrere Spieler-Chips (Spielerfarbe) und
   den Punktwert des Worts, sofern dem Client bekannt. */
.wf-popover {
  position: fixed;
  z-index: 6000;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 6px 10px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.55);
  display: flex;
  gap: 8px;
  align-items: center;
  max-width: min(90vw, 360px);
  flex-wrap: wrap;
  font-size: 0.85rem;
}
.wf-popover.hidden { display: none; }
.wf-pop-word {
  font-weight: 800;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  color: var(--text);
  white-space: nowrap;
}
.wf-pop-word:empty { display: none; }
.wf-pop-finders {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}
.wf-pop-finder-chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--chip-bg, rgba(255,255,255,0.1));
  color: var(--chip-color, var(--text));
  font-weight: 600;
  font-size: 0.85rem;
  line-height: 1.2;
  white-space: nowrap;
}
.wf-pop-points {
  font-weight: 700;
  color: var(--accent-text);
  white-space: nowrap;
}
.wf-pop-points:empty { display: none; }

/* ── Info-Drawer (slide-up popup) ────────────────────────────── */

#info-popup, #words-popup, #gameover-player-popup {
  position: fixed; inset: 0; z-index: 5000;
  background: rgba(0,0,0,0.65);
  display: flex; align-items: flex-end; justify-content: center;
  backdrop-filter: blur(4px);
  opacity: 0; pointer-events: none;
  transition: opacity 0.25s;
}
#info-popup.visible, #words-popup.visible, #gameover-player-popup.visible { opacity: 1; pointer-events: all; }
.info-drawer {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  /* Bottom-Sheet: bündig an der Unterkante, Inhalt aber über dem Home-
     Indicator (env(safe-area-inset-bottom) aufs untere Padding addiert). */
  padding: 20px 20px calc(20px + var(--safe-bottom));
  width: 100%; max-width: 520px;
  max-height: 82dvh; overflow-y: auto;
  transform: translateY(100%); transition: transform 0.3s ease;
  display: flex; flex-direction: column; gap: 10px;
}
#info-popup.visible .info-drawer, #words-popup.visible .info-drawer,
#gameover-player-popup.visible .info-drawer { transform: translateY(0); }
.info-drawer-head {
  display: flex; align-items: center; justify-content: space-between;
}
.info-drawer-head h3 { font-size: 1rem; font-weight: 700; }

/* ── Client log (dev-mode only UI) ──────────────────────────── */
.log-toggle-row { display: none; }
body.dev-mode .log-toggle-row { display: block; }

/* Dev-only: Whitelist-Button (in der Top-Bar) + zugehöriges Eingabe-Modal */
#btn-dev-add-whitelist { display: none !important; }
body.dev-mode #btn-dev-add-whitelist { display: inline-flex !important; }
#dev-add-whitelist-input {
  width: 100%;
  padding: 10px 12px;
  font-size: 1rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: inherit;
  text-align: center;
  letter-spacing: 0.04em;
}
#dev-add-whitelist-input:focus {
  outline: none;
  border-color: var(--accent);
}

/* Dev-only: Endbildschirm-Sektion „Abgelehnte Versuche" (rejected-section) */
.rejected-section { margin-top: 22px; }
.rejected-section .panel-title { margin-bottom: 8px; }
.rejected-add-row {
  display: flex; gap: 6px; margin-bottom: 10px; align-items: center;
}
.rejected-add-row input {
  flex: 1; min-width: 0;
  padding: 6px 10px; font-size: 0.9rem;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  font-family: inherit;
}
.rejected-add-row input:focus { outline: none; border-color: var(--accent); }
.rejected-filters {
  display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px;
}
.rejected-filter-chip {
  padding: 3px 10px; border-radius: 12px;
  font-size: 0.78rem; font-weight: 600;
  background: var(--surface2); color: var(--text);
  border: 1.5px solid transparent; cursor: pointer;
}
.rejected-filter-chip.active {
  border-color: var(--accent); color: var(--accent-text);
}
/* Wörter-Pills im rejected-grid: optisch wie sol-word.missed, aber klickbar */
#rejected-grid .sol-word.rejected {
  background: rgba(248,113,113,0.14);
  color: var(--text);
  border-color: rgba(248,113,113,0.35);
  cursor: pointer;
}

/* Dev-Bar am UNTEREN Rand (historisch oben, daher die Namen top-bar/
   has-top-bar): unten kollidiert sie weder mit dem Notch/Cutout noch mit
   dem Safe-Area-Layout der Screens — oben hätte sie z.B. beim Notch-Lineal
   die Messkante verdeckt. Home-Indicator-Inset wird aufaddiert. */
#top-bar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 9998;
  display: none; align-items: center; justify-content: flex-end;
  gap: 10px; padding: 8px 14px;
  height: calc(50px + var(--safe-bottom));
  padding-bottom: calc(8px + var(--safe-bottom));
  box-sizing: border-box;
  background: var(--bg);
  border-top: 1px solid var(--border);
}
#top-bar.show { display: flex; }
/* Bar reserviert ihren eigenen Platz unten — Inhalte enden oberhalb. */
body.has-top-bar { padding-bottom: calc(50px + var(--safe-bottom)); }

.top-bar-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4.8px 8.4px; border-radius: 22px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); cursor: pointer;
  font-size: 0.78rem; font-weight: 600;
  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
  font-family: inherit;
}
.top-bar-btn:hover { background: var(--surface2); }

#log-fab { display: none; }
#log-fab .log-fab-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--green);
}
#log-fab.has-warn  .log-fab-dot { background: var(--accent); }
#log-fab.has-error .log-fab-dot { background: var(--red); }
#log-fab .log-fab-count {
  font-variant-numeric: tabular-nums; color: var(--muted); font-weight: 700;
}

#modal-log .modal-card {
  max-width: 760px; width: 96%; max-height: 90dvh;
  display: flex; flex-direction: column; padding: 16px;
  text-align: left;
}
#modal-log .log-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px;
}
#modal-log .log-head h2 { font-size: 1rem; font-weight: 700; margin: 0; }
#modal-log .log-toolbar {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  margin-bottom: 8px;
}
#modal-log .log-toolbar select,
#modal-log .log-toolbar input[type=text] {
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 5px 8px; font-size: 0.82rem; min-width: 0;
  font-family: inherit;
}
#modal-log .log-toolbar input[type=text] { flex: 1; min-width: 120px; }
#modal-log .log-toolbar .log-count {
  font-size: 0.72rem; color: var(--muted);
  margin-left: auto; white-space: nowrap;
}
#log-list {
  flex: 1; overflow-y: auto; padding-right: 2px;
  display: flex; flex-direction: column; gap: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.74rem;
}
.log-entry {
  border-left: 3px solid var(--surface2);
  padding: 4px 8px; background: var(--bg);
  border-radius: 4px;
  cursor: pointer;
  display: flex; gap: 6px;
}
.log-entry.log-debug { border-left-color: var(--muted); opacity: 0.85; }
.log-entry.log-info  { border-left-color: var(--blue); }
.log-entry.log-warn  { border-left-color: var(--accent); background: rgba(181,90,46,0.08); }
.log-entry.log-error { border-left-color: var(--red); background: rgba(239,68,68,0.10); }
.log-entry.selected  { box-shadow: inset 0 0 0 1px var(--accent); }
.log-select { flex: 0 0 auto; margin: 3px 0 0; cursor: pointer; }
.log-content { flex: 1; min-width: 0; }
.log-meta {
  display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap;
  font-size: 0.68rem; color: var(--muted);
}
.log-meta .log-level { text-transform: uppercase; font-weight: 700; }
.log-meta .log-cat   { color: var(--text); opacity: 0.85; }
.log-msg {
  color: var(--text); margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.log-entry.expanded .log-msg {
  white-space: pre-wrap; word-break: break-word; overflow: visible;
}
.log-data {
  display: none;
  font-size: 0.7rem; color: var(--muted); margin-top: 2px;
  white-space: pre-wrap; word-break: break-word;
  max-height: 240px; overflow: auto;
}
.log-entry.expanded .log-data { display: block; }
.log-empty { color: var(--muted); text-align: center; padding: 24px; }

/* ── Responsive ──────────────────────────────────────────────── */

@media (max-width: 640px) {
  /* ACHTUNG: dieser Block steht NACH der Basis-body-Regel und überschreibt
     deren Padding auf Telefonen komplett — ein nacktes `padding: 12px` hat
     hier monatelang den gesamten Safe-Area-Schutz ausgehebelt (Raumcode
     unterm Punch-Hole im Fullscreen). Der schmalere 12px-Rand bleibt, aber
     jede Seite respektiert weiterhin die Insets (var(--safe-*)). */
  body {
    padding:
      max(12px, var(--safe-top))
      max(12px, var(--safe-right))
      max(12px, var(--safe-bottom))
      max(12px, var(--safe-left));
  }
  .game-body { flex-direction: column; }
  .game-left { min-width: unset; width: 100%; }
  .settings-grid { grid-template-columns: 1fr; }
  .stats-row { flex-direction: column; }
  /* Ensure cards never overflow on small screens */
  .card { padding: 16px; }
  /* Buttons can wrap on narrow phones */
  .stats-row { gap: 8px; }
}

/* ── Statistik-Bildschirm ─────────────────────────────── */
/* Aufbau wie die Lobby: borderlose, transparente Card über die volle Höhe,
   sticky Kopfzeile (kleines Logo links, Titel mittig) und sticky Bottom-Bar
   mit dem Zurück-Button; dazwischen scrollt der Inhalt. */
#stats-card {
  max-width: 540px; width: 100%;
  flex: 1 1 auto; min-height: 0;
  max-height: calc(100dvh - max(16px, var(--safe-top)) - max(16px, var(--safe-bottom)));
  overflow-y: auto;
  display: flex; flex-direction: column;
  padding: 0; border: 0; background: transparent;
}
body.has-top-bar #stats-card { max-height: calc(100dvh - max(16px, var(--safe-top)) - 50px - var(--safe-bottom)); }
/* Card über die volle Höhe strecken (Header oben, Bar unten verankert),
   statt sie wie sonst vertikal zu zentrieren. */
#screen-statistics.active {
  margin-top: 0; margin-bottom: 0;
  flex: 1 1 auto; min-height: 0; justify-content: flex-start;
}

#stats-header {
  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center;
  padding: 8px 16px 6px;
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
  border-radius: 14px 14px 0 0;
}
/* Kleines Marken-Logo in der oberen rechten Ecke. */
#stats-logo {
  height: 30px; width: auto; flex-shrink: 0;
  margin-left: 10px;             /* Abstand zu den Werkzeug-Buttons links davon */
  pointer-events: none;          /* rechtsbündig in die obere rechte Ecke */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
}
/* Titel oben links; nimmt den freien Platz ein und schiebt Werkzeug-Buttons +
   Logo nach rechts. */
#stats-header #stats-title {
  flex: 1 1 auto; margin: 0; min-width: 0;
  display: flex; align-items: center; justify-content: flex-start;
  font-size: 1.1rem; font-weight: 800; line-height: 1.1;
  pointer-events: none;
}
/* Nur die eigentliche Titel-Beschriftung ist klickbar (öffnet das Info-Popup),
   nicht die ganze Header-Breite. Liegt unter den Buttons (z-index:1), daher
   fangen Filter/Kalender ihre Klicks weiterhin selbst ab. */
#stats-header #stats-title .stats-title-text {
  pointer-events: auto; cursor: pointer;
  /* Neben dem Titel stehen jetzt Umschalter und Logo — auf schmalen Geräten
     wird der Titel gekürzt, statt über sie hinauszulaufen. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Filter- und Kalender-Umschalter ganz rechts in der Bottom-Bar. Der
   Filter-Knopf steht links neben dem Kalender-Knopf; margin-left:auto schiebt
   das Paar an den rechten Rand. (In der Detailansicht blendet JS den Filter
   aus und gibt dem Kalender das auto-Margin.) */
#btn-stats-filter {
  margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1;
}
#btn-stats-filter .filter-icon { display: block; }
#btn-stats-calendar {
  margin-left: 6px;
  font-size: 1rem; line-height: 1;
}
#btn-stats-filter.active,
#btn-stats-calendar.active { border-color: var(--accent); background: var(--surface2); }
/* Aktiver Filter: Trichter gefüllt (Fill, kein Text — Theme-konform). */
#btn-stats-filter.active .filter-icon { fill: var(--accent); stroke: var(--accent); }

/* Dauerhaftes Info-Icon unten rechts in der Bilanz-Leiste. */
/* Info-Knopf rechts in der Bilanz-Leiste: gleiche Höhe wie der Zurück-Button —
   identische Box wie .btn (0.95rem Schrift, 6px vertikale Polsterung; hier 5px
   + 1px Rahmen = 6px) ergibt dieselbe Höhe; aspect-ratio hält ihn kreisrund,
   align-items:center der Leiste zentriert beide vertikal. */
/* Bottom-Bar: Zurück, ☰, Zahnrad links — Filter/Kalender rechts (s.o.).
   In der Detailansicht sind Zahnrad und Filter ausgeblendet. */
#btn-stats-data { margin-left: 8px; }
#btn-stats-info {
  font-size: 0.95rem; padding: 5px;
  width: auto; height: auto; aspect-ratio: 1 / 1;
}

/* Dezenter Link-Button im Datensicherungs-Menü („Wie funktioniert das?"). */
.btn.btn-link {
  background: none; border: none; padding: 2px;
  color: var(--text); font-weight: 500; text-decoration: underline;
}
.btn.btn-link:hover { color: var(--text); opacity: 0.8; }

/* Import-Dialog: Herkunfts-Kopfzeile der geladenen Datei. */
.stats-import-meta {
  font-size: 0.82rem; color: var(--muted);
  margin: 0 0 12px; padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

/* ── Bilanz: Erstnutzer-Hinweis oben (einmalig) ───────────────────────────── */
.stats-info-banner {
  display: flex; align-items: center; gap: 10px; justify-content: space-between;
  width: 100%; text-align: left;
  margin: 0 0 12px; padding: 10px 12px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);   /* Akzent als Border — Theme-konform */
  border-radius: 10px;
  color: var(--text); font-size: 0.88rem; line-height: 1.35;
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}
.stats-info-banner:hover { border-color: var(--accent); }
.stats-info-banner-icon {
  flex-shrink: 0;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); border-radius: 50%;
  color: #fff; font-weight: 700; font-size: 0.8rem;
}

/* ── Bilanz: Filter-/Lösch-Popup ──────────────────────────────────────────── */
.stats-filter-card {
  max-width: 460px; text-align: left;
  display: flex; flex-direction: column;
  max-height: min(80vh, 640px); padding: 24px;
}
.stats-filter-intro {
  color: var(--muted); font-size: 0.85rem; line-height: 1.45;
  margin: 0 0 14px;
}
/* Scrollender Listenbereich; Trichter-Layout-Regeln (touch-action) wie .modal-body. */
.stats-filter-list-wrap {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 10px;
  touch-action: pan-y; overscroll-behavior-x: none;
}
.stats-filter-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.stats-filter-table th, .stats-filter-table td {
  padding: 8px 10px; text-align: left; vertical-align: middle;
}
.stats-filter-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface2); color: var(--muted);
  font-weight: 600; font-size: 0.74rem; text-transform: uppercase;
  letter-spacing: 0.02em;
  border-bottom: 1px solid var(--border);
}
.stats-filter-table tbody tr { border-bottom: 1px solid var(--border); }
.stats-filter-table tbody tr:last-child { border-bottom: 0; }
.stats-filter-table .sf-name { font-weight: 600; width: 100%; }
.stats-filter-table .sf-date { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; line-height: 1.3; }
.stats-filter-table .sf-date .sf-games { font-size: 0.78rem; opacity: 0.85; }
/* Checkbox-Spalte ganz links, schmal, zentriert. */
.stats-filter-table .sf-check { width: 1%; text-align: center; padding-right: 4px; }
.stats-filter-table thead th.sf-check .filter-icon { display: inline-block; vertical-align: middle; }
.stats-filter-table .sf-check input { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }
.stats-filter-empty { padding: 24px 12px; text-align: center; color: var(--muted); font-size: 0.85rem; }

/* Fixe (immer sichtbare) Fußzeile mit Lösch-Auslöser. */
.stats-filter-footer {
  flex-shrink: 0; margin-top: 14px;
  border-top: 1px solid var(--border); padding-top: 14px;
}
.stats-filter-delete-bar { display: flex; gap: 10px; justify-content: space-between; align-items: stretch; }
.sf-danger { color: var(--red); border-color: var(--red); }
/* Lösch-Auslöser darf mehrzeilig umbrechen (langer Text auf schmalen Geräten). */
#btn-stats-filter-delete { white-space: normal; line-height: 1.25; text-align: center; }
.stats-filter-warn {
  color: var(--red); font-size: 0.82rem; line-height: 1.4; margin: 0 0 10px;
}
.stats-filter-select {
  width: 100%; margin-bottom: 12px; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); color: var(--text); font-size: 0.9rem;
}
/* Eigener Lösch-Dialog: linksbündig (Warnung + Auswahl lesen sich so besser). */
.stats-delete-card { text-align: left; }
.stats-delete-card .modal-title { margin-bottom: 12px; }
.stats-filter-delete-actions { display: flex; gap: 10px; justify-content: flex-end; }
.sf-danger-btn { background: var(--red); border-color: var(--red); }

#stats-content { padding: 12px 16px; flex: 1; }

#stats-bar {
  position: sticky; bottom: 0; z-index: 10;
  background-color: var(--bg);
  background-image: var(--paper);
  background-repeat: repeat;
  border-radius: 0 0 14px 14px;
  padding: 12px 16px;
  display: flex; flex-shrink: 0; align-items: center;
}

/* Auswahl über den Kennzahlen: Spieltyp + Sprache nebeneinander, je mit einer
   kleinen Beschriftung darüber (der Vorgabewert „alle …" sagt allein nicht,
   worauf sich das Feld bezieht, sobald ein konkreter Wert gewählt ist).
   Bewusst `stats-scope-*` und NICHT `stats-filter-*`: letzteres gehört zum
   Ausblenden von Gegnern (Popup + Lösch-Dialog, eigenes `.stats-filter-select`). */
#stats-scope {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  padding: 4px 2px 8px;
}
.stats-scope-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stats-scope-label {
  font-size: 0.7rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stats-scope-select {
  width: 100%; min-width: 0;
  font-family: inherit; font-size: 0.9rem; color: var(--text);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; padding: 6px 8px; cursor: pointer;
}
.stats-scope-select:focus { outline: none; border-color: var(--accent); }

/* ── Ergebnisse: Umschalter „Rangliste" ↔ „Wissen" ──────────────────────────
   Sieht aus wie ein Auswahlfeld — dieselbe Fläche, derselbe Rahmen, derselbe
   Pfeil wie .stats-scope-select darunter —, klappt aber nichts auf, sondern
   schaltet beim Klick sofort um. Bei GENAU ZWEI Seiten wäre eine Liste ein
   Umweg: man tippte zweimal für das, was ohnehin das einzige andere ist. Der
   Wert sagt, wo man steht; der Pfeil, dass es noch etwas gibt.

   Neben dem Titel ist wenig Platz: der Knopf schrumpft nicht (flex: 0 0 auto),
   der Titel daneben wird gekürzt. */
.stats-view-pick {
  flex: 0 0 auto;
  margin-left: 8px;
  display: inline-flex; align-items: center; gap: 5px;
  font-family: inherit; font-size: 0.85rem; font-weight: 700; line-height: 1;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 8px;
  cursor: pointer; white-space: nowrap;
}
.stats-view-pick:hover { border-color: var(--accent); }
.stats-view-pick:focus-visible { outline: none; border-color: var(--accent); }
.stats-view-caret { color: var(--muted); font-size: 0.8em; }

/* ── Wissen: Auswahlfelder ─────────────────────────────────────────────────
   Anders als #stats-scope: die Beschriftung steht LINKS neben ihrem Feld, die
   Felder sind kleiner (dieselbe Größe wie der „Wissen/Rangliste"-Umschalter
   oben — .stats-view-pick: 0.85rem, 5px 8px) und die Zeile bricht bei Bedarf
   um. Die Feldzahl wechselt ohnehin (das Sprachfeld erscheint nur bei mehreren
   Spielsprachen) — ein Flex-Umbruch braucht dafür keine Spaltenrechnung. */
#stats-knowledge-scope {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 4px 2px 8px;
}
#stats-knowledge-scope .stats-scope-field {
  flex-direction: row; align-items: center; gap: 6px;
  flex: 0 1 auto;
}
#stats-knowledge-scope .stats-scope-select {
  width: auto;
  font-size: 0.85rem;
  padding: 5px 8px;
}

/* Kopfzeile der Wissens-Seite: dieselbe Summe wie die Wurzel des Baums. */
#stats-knowledge-total {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 8px 4px 10px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 6px;
}
.know-total-label { font-weight: 800; }

/* Der Themenbaum der Wissens-Seite nutzt die Klassen der Lobby-Themenwahl
   (.topic-tree/.topic-row/.topic-name) — es IST derselbe Baum, nur ohne
   Ankreuzfelder und immer offen. Ein Oberthema schaltet nichts, also kein
   Zeigefinger; die BLATT-Zeilen öffnen das Wörter-Popup und bekommen ihn
   zurück (.topic-row--drill, weiter unten). */
#stats-knowledge-tree .topic-row { cursor: default; }
#stats-knowledge-tree .topic-row:hover { background: none; }
/* Passt der Chip nicht mehr neben einen langen Namen, rutscht er in die
   zweite Zeile — und muss dort trotzdem RECHTS stehen, wie in jeder anderen
   Zeile. Der wachsende Name schiebt ihn nur in der ERSTEN Flex-Zeile an den
   Rand; der auto-Rand tut es in jeder (dasselbe Mittel wie .topic-nums in
   der Lobby-Themenwahl). */
#stats-knowledge-tree .topic-know,
#gameover-knowledge-tree .topic-know { margin-left: auto; }
/* Die Einrückung je Ebene — knapper als die 26px der Lobby-Themenwahl: hier
   stehen rechts drei Zahlenspalten, und jede gesparte Breite kommt dem Namen
   zugute. Steht VOR den padding-left:0-Regeln der Wurzeln (gleiche
   Spezifität — die Reihenfolge entscheidet). */
#stats-knowledge-tree .topic-tree,
#gameover-knowledge-tree .topic-tree { padding-left: 18px; }
/* Die oberste Ebene steht am Rand — eingerückt wird erst ab dem Unterthema.
   Lange Themennamen kürzen statt umzubrechen: die Kennzahlen rechts sollen in
   jeder Zeile an derselben Stelle stehen. */
#stats-knowledge-tree > .topic-tree { padding-left: 0; }
#stats-knowledge-tree .topic-name {
  cursor: default;
  /* Kleiner als sonst: hier steht der Name neben drei Zahlenspalten, und die
     Zeile soll auf einem Handy nicht am Namen scheitern. Die Lobby-Themenwahl
     behält ihre Größe — dort ist der Name die Schaltfläche. */
  font-size: 0.82rem;
  display: block; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Schmaler Rand: #stats-content polstert für die Rangliste mit 16 px, hier
   holen wir zwölf davon zurück. Der Baum braucht die Breite — links rückt jede
   Ebene ein, rechts stehen die Zahlenspalten. */
#stats-knowledge { margin-left: -12px; margin-right: -12px; }
/* Halber vertikaler Abstand gegenüber der Lobby-Themenwahl (6px): hier wird
   nichts angetippt außer den Blatt-Zeilen, und der Baum soll mehr Themen auf
   einen Schirm bekommen. */
#stats-knowledge .topic-row {
  padding: 3px 2px;
}

/* Die Kennzahlen neben einem Thema: gelöst-Anteil, gesehen-Anteil, Vorrat.
   Nebeninformation wie die Wortzahl daneben — sie darf den Namen nicht
   überstrahlen. Klein gesetzt, damit auch drei Werte plus Name in eine
   Handy-Zeile passen. */
.topic-know {
  flex: 0 0 auto;
  display: inline-flex; align-items: baseline; gap: 6px;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}
/* Zahl und Zeichen sind zwei Kästen — der Abstand kommt aus dem Zwischenraum,
   nicht aus einem Rand: welcher von beiden vorne steht, wechselt (der Vorrat
   trägt sein „/" voran), und ein einseitiger Rand säße dann falsch. */
.topic-know .know-solved,
.topic-know .know-seen,
.topic-know .know-pool { display: inline-flex; align-items: baseline; gap: 0.25em; }
.topic-know .know-solved { color: var(--text); font-weight: 700; }
.topic-know .know-pool { opacity: 0.8; }

/* Feste Zahlenbreiten — der Grund, warum Zeichen und Zahl getrennte Kästen
   sind. `tabular-nums` allein macht nur die Ziffern gleich breit; dass „7%",
   „35%" und „100%" auf DERSELBEN Kante enden, kommt erst aus der reservierten
   Breite plus rechtsbündigem Satz. Sonst wanderte in jeder Zeile alles um eine
   Stelle, und die Spalte wäre keine.

   Drei Ziffern für jeden Anteil (100 % ist das Maximum), fünf für den Vorrat.
   `ch` ist bei tabular-nums exakt eine Ziffernbreite; das Prozentzeichen kommt
   als knapper Zuschlag dazu. */
.topic-know .know-num {
  display: inline-block;
  min-width: 3.9ch;              /* 3 Ziffern + „%" */
  text-align: right;
}
.topic-know .know-num--pool { min-width: 5ch; }
/* ── Der Runden-Baum im Spielergebnis ─────────────────────────────────────
   Dieselben Klassen wie die Wissens-Seite — es IST derselbe Baum, nur auf die
   Themen dieser Runde verkürzt. Hier schaltet allerdings nichts: kein Zeiger,
   kein Hover. Und die oberste Ebene steht am Rand, weil links kein Platz
   verschenkt werden muss. */
#gameover-knowledge { margin-top: 20px; }   /* derselbe Abstand wie .solution-section */
/* Statt eines Baums: die eine Zeile, warum es nichts zu zeigen gibt. Sie ist
   Auskunft, kein Fehler — also gemutet wie jede Nebeninformation. */
.gameover-knowledge-none { margin: 0; color: var(--muted); font-size: 0.85rem; }
#gameover-knowledge-tree > .topic-tree { padding-left: 0; }
#gameover-knowledge-tree .topic-row { cursor: default; padding-left: 2px; padding-right: 2px; }
#gameover-knowledge-tree .topic-row:hover { background: none; }

/* Blatt-Zeilen öffnen die Wörter dieses Themas — auf der Wissens-Seite wie im
   Spielergebnis. Der Zeigefinger und die leichte Aufhellung sind das einzige
   Signal: ein Pfeil o. ä. bekäme in einer Zeile, die schon Name und drei
   Zahlenspalten trägt, keinen Platz mehr. Muss NACH den `cursor: default`-Regeln
   beider Bäume stehen — gleiche Spezifität, also entscheidet die Reihenfolge. */
#stats-knowledge-tree .topic-row--drill,
#gameover-knowledge-tree .topic-row--drill { cursor: pointer; }
#stats-knowledge-tree .topic-row--drill:hover,
#gameover-knowledge-tree .topic-row--drill:hover { background: var(--bg); }
.topic-row--drill:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ── Das Wörter-Popup einer Blatt-Zeile ─────────────────────────────────────
   Eine Liste, keine mittige Meldung: die Karte kippt darum auf linksbündig und
   bekommt ein knapperes Polster (die Zahl steht in `twBoxScale` in main.js —
   der Maßstab der Kästchen wird daraus gerechnet, nicht gemessen). Die Höhe ist
   gedeckelt, gescrollt wird im Body: eine Karte, die über den Schirm
   hinauswächst, nähme dem Schließen-Knopf den Platz. */
.modal-card--words {
  text-align: left; padding: 20px 18px 18px;
  max-height: 82vh; display: flex; flex-direction: column;
}
.modal-card--words .modal-title { text-align: left; margin-bottom: 2px; }
/* Unter welchen Filtern die Liste steht. Im Spielergebnis gibt es keine
   Auswahlfelder, dort ist diese Zeile die einzige Auskunft darüber. */
.tw-scope {
  color: var(--muted); font-size: 0.74rem; margin: 0 0 10px;
  padding-bottom: 8px; border-bottom: 1px solid var(--border);
}
/* Der Kopf steht ÜBER dem Scrollbereich (eine eigene Tabelle, siehe
   twHeadTableHTML in main.js): die Zeilen verschwinden beim Scrollen an seiner
   Unterkante, und die Linie dort macht die Kante sichtbar. Ohne Zeilen bleibt
   der Kopf weg — dann soll auch die Linie nicht alleine dastehen.
   `flex: none` ist Pflicht: die Karte ist eine höhen-gedeckelte Flex-Spalte,
   und das `overflow: hidden` unten nimmt dem Kopf seine automatische
   Mindesthöhe — ohne den Riegel wird er GENAU DANN auf null gestaucht, wenn
   die Liste scrollen muss, also immer dann, wenn man ihn braucht. */
.tw-head { flex: none; border-bottom: 1px solid var(--border); }
.tw-head:empty { display: none; }
.tw-body { flex: 1 1 auto; overflow-y: auto; margin-bottom: 14px; min-height: 0; }
/* Eine klassische Scrollleiste (Desktop) macht den Körper schmaler als den
   Kopf, und die Spaltenkanten der beiden Tabellen laufen auseinander. Die
   Rinne wird deshalb in BEIDEN Containern fest reserviert — der Kopf ist über
   `overflow: hidden` ebenfalls ein Scroll-Container, sonst griffe die Regel
   dort nicht. Bei Overlay-Scrollleisten (mobil) reserviert `stable` nichts,
   dann sind die beiden ohnehin gleich breit. */
.tw-head { overflow: hidden; }
.tw-head, .tw-body { scrollbar-gutter: stable; }
.tw-empty { margin: 0; }
/* Eine echte Tabelle: drei Spalten mit Kopf, sortierbar. `fixed` legt die
   Breiten fest, bevor der Inhalt sie auseinanderzieht — ein langes Wort
   („Schmalnasenaffe") schöbe die Zahlenspalten sonst aus der Karte. Kopf- und
   Zeilen-Tabelle teilen sich die Spaltenbreiten über die Klassen unten; nur so
   stehen die Spalten der beiden Tabellen untereinander. */
.tw-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.tw-th { text-align: left; padding: 0 0 6px; font-weight: 600; }
/* Die Spaltenbreiten in rem, NICHT in em: Kopf und Zeilen sind zwei Tabellen
   mit verschiedenen Schriftgrößen am `<th>` bzw. `<td>` — dieselbe em-Breite
   ergäbe zwei verschiedene Pixelbreiten, und die Titel stünden nicht mehr
   bündig über ihren Zahlen. */
.tw-th--streak, .tw-streak { text-align: right; width: 3.8rem; }
.tw-th--pct, .tw-pct { text-align: right; width: 4.5rem; }
.tw-sort {
  background: none; border: 0; padding: 2px 0; margin: 0; cursor: pointer;
  font: inherit; font-size: 0.72rem; font-weight: 700; color: var(--muted);
  letter-spacing: 0.02em; text-transform: uppercase;
  display: inline-flex; align-items: baseline; gap: 0.3em;
}
.tw-sort--on { color: var(--text); }
.tw-sort:hover { color: var(--text); }
.tw-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Das Dreieck existiert nur an der aktiven Spalte (twHeadHTML rendert sonst
   keins) und reserviert KEINEN Platz: die Titel der Zahlenspalten stehen
   bündig über ihren Zahlen, und dass der aktive Titel dafür ein Stück nach
   links rückt, ist der gewollte Preis. */
.tw-arrow { display: inline-block; font-size: 0.85em; }
.tw-row td { padding: 0; vertical-align: baseline; }
/* Die GANZE Zeile führt in den Eintrag des Worts (Delegation in
   openTopicWordsPopup) — das Wort zu treffen ist auf dem Telefon zu viel
   verlangt. Die Fläche sagt es beim Überfahren: Hintergrund auf dem <tbody>
   färbt beide <tr> eines Worts zugleich. */
.tw-row { cursor: pointer; }
.tw-row:hover { background: var(--bg); }
.tw-row:hover .tw-word-btn { text-decoration: underline; }
.tw-word {
  padding-top: 7px; font-weight: 600; color: var(--text); font-size: 0.92rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Das Wort führt in seinen Eintrag — dasselbe Popup wie am Spielende. Ein
   Knopf, der aussieht wie der Text: die Zeile soll eine Liste bleiben, kein
   Knopfbrett. Er füllt die Spalte, damit der Griff auf dem Telefon trifft. */
.tw-word-btn {
  background: none; border: 0; padding: 0; margin: 0; font: inherit;
  color: inherit; cursor: pointer; width: 100%; text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tw-word-btn:hover { text-decoration: underline; }
.tw-word-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Der Streak: wie viele Lösungen seit dem letzten Fehlschlag. Eine
   Nebenauskunft zur Prozentzahl daneben, deshalb gedämpft und nicht fett. */
.tw-streak {
  padding-top: 7px;
  font-size: 0.82rem; color: var(--muted);
}
/* Derselbe gelöst-Anteil wie im Baum und dieselbe Zifferbreite — sonst tanzen
   die Prozente von Zeile zu Zeile. Das ✔ steht hier nicht mehr daneben: die
   Spalte sagt einmal im Kopf, was sie zählt. */
.tw-pct {
  padding-top: 7px; text-align: right;
  font-weight: 700; font-size: 0.82rem; color: var(--text);
}
.tw-streak .tw-num,
.tw-pct .tw-num { display: inline-block; font-variant-numeric: tabular-nums; }
/* Die Trennlinie sitzt unter der Kästchenreihe, also am Ende EINES Worts —
   und nicht zwischen dessen beiden Zeilen. */
.tw-boxes-cell { padding: 5px 0 7px; border-bottom: 1px solid var(--border); }
.tw-row:last-child .tw-boxes-cell { border-bottom: 0; }
/* Die Versuche: dieselben Kästchen wie in der Bilanz gegen andere Spieler
   (.outcome-box), neuester links und nach hinten kleiner. Die Kantenlängen
   setzt JS je Kästchen — hier steht nur, dass die Reihe nicht umbricht und
   unten bündig sitzt, damit die schrumpfende Kette auf einer Linie läuft. */
.tw-boxes { display: flex; align-items: flex-end; flex-wrap: nowrap; }
#gameover-knowledge-tree .topic-name {
  cursor: default; font-size: 0.82rem;
  display: block; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Ein Vorzeichen und eine Nachkommastelle mehr als auf der Wissens-Seite. */
.topic-know--diff .know-num { min-width: 5.6ch; }

/* Das Richtungs-Dreieck VOR der Zahl. Eigener Kasten mit fester Breite, aus
   demselben Grund, aus dem Zahl und Zeichen getrennt sind: nur so stehen die
   Ziffern untereinander. Die Breite bleibt auch reserviert, wenn nichts drin
   steht — sonst rückte eine Zeile ohne Richtung um ein Zeichen aus.

   Schriftart und -gewicht stehen AUSDRÜCKLICH hier, obwohl beides schon von
   oben käme: das Häkchen-Feld ist fett (`.know-solved`), das Augen-Feld nicht,
   und JetBrains Mono bringt das Dreieck nicht in jedem Schnitt mit — die beiden
   landeten so in verschiedenen Rückfall-Schriften und standen sichtbar
   verschieden groß nebeneinander. Feste rem-Größe aus demselben Grund: `em`
   erbte die Größe der Zelle.

   Gefärbt wird das DREIECK, nicht die Zahl: es ist das Zeichen für die
   Richtung, die Zahl bleibt die Tinte der Zeile. Grün kommt als --green-soft,
   nicht als --green: auf einer Fläche von wenigen Pixeln wirkt das dunkle
   Tinten-Grün des Hell-Themes schwarz (siehe theme-light.css, genau dafür gibt
   es die weichere Stufe). */
.topic-know--diff .know-trend {
  display: inline-block; width: 1em;
  font-family: system-ui, 'Segoe UI Symbol', sans-serif;
  font-size: 0.62rem; font-weight: 400; line-height: 1; text-align: center;
}
.topic-know--diff .know-trend.pos { color: var(--green-soft); }
.topic-know--diff .know-trend.neg { color: var(--red); }

/* Meldung auf der Gegnerseite, wenn die Auswahl dort kein Spiel übrig lässt.
   Steht in #stats-detail-summary (das fett/größer ist) — hier wieder normal. */
.stats-detail-none {
  font-weight: 400; font-size: 0.92rem; color: var(--muted);
  text-align: center; padding: 20px 8px;
}

/* Kennzahlen-Leiste der aktuellen Auswahl über der Gegnerliste: drei Werte
   nebeneinander (Wert oben, Beschriftung darunter), getrennt durch einen
   dezenten Trennstrich zur Liste hin. */
#stats-overall {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px 8px;
  padding: 4px 2px 14px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.ov-tile { text-align: center; min-width: 0; }
.ov-val {
  font-weight: 800; font-size: 1.15rem; line-height: 1.1;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.ov-val.pos { color: #15803d; }
.ov-val.neg { color: var(--red); }
.ov-val.zero { color: var(--text); }
/* Kompakte, eingefärbte Punktedifferenz (Σ ±N) in den Bilanz-Zeilen und der
   Detail-Zusammenfassung — gleiche Farben wie .ov-val / .hist-diff. */
.stat-diff { font-variant-numeric: tabular-nums; font-weight: 700; }
.stat-diff.pos  { color: #15803d; }
.stat-diff.neg  { color: var(--red); }
.stat-diff.zero { color: var(--muted); }
/* Prozentzahl eingefärbt: > 50 % grün, < 50 % rot, 50 % neutral. Fett. */
.stat-pct { font-variant-numeric: tabular-nums; font-weight: 700; }
.stat-pct.pos  { color: #15803d; }
.stat-pct.neg  { color: var(--red); }
.stat-pct.zero { color: var(--muted); }
.ov-label {
  margin-top: 2px; font-size: 0.7rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.opp-row {
  display: grid;
  grid-template-columns: 1.5em 1fr;
  grid-template-areas:
    "rank head"
    "rank boxes";
  align-items: center; column-gap: 10px; row-gap: 6px;
  /* Linker Akzentbalken: auf allen Zeilen transparent reserviert, damit die
     Rang-Spalte und die waagerechten Trennlinien überall am selben x starten
     — die „self"-Zeile färbt ihn nur ein, ohne den Inhalt zu verschieben. */
  border-left: 3px solid transparent;
  padding: 10px 4px 10px 2px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.12s;
}
.opp-row:last-child { border-bottom: 0; }
.opp-row .opp-rank {
  grid-area: rank; align-self: start;   /* bündig nach oben — auf Höhe des Namens */
  font-weight: 700; font-size: 1.05rem; color: var(--muted);
  font-variant-numeric: tabular-nums; text-align: left; line-height: 1.25;
}
.opp-row .opp-head {
  grid-area: head; display: flex; align-items: flex-start; gap: 8px; min-width: 0;
}
/* Name auf max. 3 Zeilen: Hauptname per -webkit-line-clamp begrenzt (letzte
   sichtbare Zeile endet automatisch mit …). Folgt ein Spitzname, darf der
   Hauptname nur 2 Zeilen belegen, damit der (immer sichtbare) Spitzname als
   3. Zeile passt. */
.opp-row .opp-name  {
  font-weight: 600; min-width: 0; flex: 1 1 auto; line-height: 1.25;
}
/* Name-Klassen ungescopt — auch auf der Detailseite (#stats-detail-name). */
.opp-name-main {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden; overflow-wrap: anywhere;
}
.opp-name:has(.opp-name-sub) .opp-name-main,
#stats-detail-name:has(.opp-name-sub) .opp-name-main { -webkit-line-clamp: 2; }
.opp-name-sub {
  display: block; font-weight: 400; color: var(--muted); font-size: 0.85em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
/* Detailseiten-Name darf (wie auf der Hauptseite) mehrzeilig umbrechen. */
#stats-detail-name { min-width: 0; line-height: 1.25; }
.opp-row .opp-meta  { color: var(--muted); font-size: 0.86rem; white-space: nowrap; margin-left: auto; font-variant-numeric: tabular-nums; }

/* Eigene Zeile: kein Klick, hellgrau hinterlegt (wie der Zurück-Button),
   keine Kästchen. Die Hinterlegung ragt per negativem Rand etwas über die
   Tabellenbreite hinaus (in das Padding von #stats-content); das zusätzliche
   Innenpadding hält den Inhalt mit den übrigen Zeilen bündig. */
.opp-row.opp-self {
  cursor: default;
  background: var(--surface);
  margin-left: -10px; margin-right: -10px;
  padding-left: 12px; padding-right: 14px;
  border-radius: 8px;
}
.opp-row.opp-self .opp-name { font-weight: 800; }
.opp-row.opp-self .opp-rank { color: var(--text); }
.opp-row .opp-self-tag {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.4px;
  text-transform: uppercase; color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: 1px 6px;
}

/* Ergebnis-Kästchen (übernehmen den Setzkasten-Look des Wörter-Grids). Jede
   volle Zeile füllt die ganze Breite; von oben nach unten werden die Kästchen
   kleiner. Größe (--outcome-box-size) und Zeilen-Abstand (gap) setzt JS pro
   Zeile passend zueinander; die Zeilen sind mittig ausgerichtet. */
.opp-boxes { grid-area: boxes; display: flex; flex-direction: column; gap: 2px; }
.opp-box-row { display: flex; gap: 3px; align-items: center; }
.outcome-box {
  width: var(--outcome-box-size, 16px); height: var(--outcome-box-size, 16px);
  flex: 0 0 auto; border-radius: 3px;
  border: 1px solid rgba(0, 0, 0, 0.18);
  background: var(--sb-n);
}
/* „Mehr Spiele"-Indikator am Ende der letzten Zeile: drei kleine schwarze
   Punkte (Breite/Höhe und Punktgröße setzt JS passend zur Zeile). */
.outcome-more {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; gap: 2px;
}
.outcome-more i { flex: 0 0 auto; border-radius: 50%; background: var(--text); }
.outcome-box.o-r3 { background: var(--sb-r3); }
.outcome-box.o-r2 { background: var(--sb-r2); }
.outcome-box.o-r1 { background: var(--sb-r1); }
.outcome-box.o-n  { background: var(--sb-n); }
.outcome-box.o-g1 { background: var(--sb-g1); }
.outcome-box.o-g2 { background: var(--sb-g2); }
.outcome-box.o-g3 { background: var(--sb-g3); }
/* Divergierende Rot→Grau→Grün-Palette (7 Stufen). Zentral hier definiert,
   damit der gesamte Verlauf an einer Stelle abstimmbar ist; lesbar auf
   beiden Themes (Creme bzw. Walnuss). */
:root {
  --sb-r3: #b02a23; --sb-r2: #d05a4e; --sb-r1: #e89a90;
  --sb-n:  #9aa0a6;
  --sb-g1: #8fc98a; --sb-g2: #4f9e57; --sb-g3: #2f7d34;
}

/* ── Kalenderblätter (letzte 3 Monate) ──────────────────────────────────────
   Monatsnamen-Kopfzeile einmal oben; sie nutzt dasselbe 2-Spalten-Raster wie
   .opp-row (Rang-Spalte + Inhalt) und den gleichen linken Ursprung (3px Balken
   + 2px Padding), damit die drei Monatslabels über den Monatsspalten der
   Gegnerzeilen fluchten. */
.opp-cal-head {
  display: grid; grid-template-columns: 1.5em 1fr; column-gap: 10px;
  border-left: 3px solid transparent;
  padding: 2px 4px 6px 2px;
}
.cal-months { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.cal-months span {
  text-align: center; font-size: 0.72rem; color: var(--muted);
  text-transform: capitalize;
}
/* Drei Monatsblätter nebeneinander (im boxes-Bereich der Zeile). */
.opp-cal { grid-area: boxes; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.cal-month { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cal-cell {
  aspect-ratio: 1 / 1; border-radius: 2px;
  background: var(--sb-n);
  border: 0;
}
.cal-cell.cal-pad   { background: transparent; border: 0; }      /* Vorlauf-Leerzellen (vor Monatsbeginn) → unsichtbar */
.cal-cell.cal-empty { background: rgba(128, 128, 128, 0.18); border: 0; }  /* kein Spiel → blasses Quadrat */
.cal-cell.o-r3 { background: var(--sb-r3); }
.cal-cell.o-r2 { background: var(--sb-r2); }
.cal-cell.o-r1 { background: var(--sb-r1); }
.cal-cell.o-n  { background: var(--sb-n); }
.cal-cell.o-g1 { background: var(--sb-g1); }
.cal-cell.o-g2 { background: var(--sb-g2); }
.cal-cell.o-g3 { background: var(--sb-g3); }
/* Aktueller Tag hervorgehoben — Ring, auch auf leeren Zellen sichtbar. */
.cal-cell.cal-today { outline: 2px solid var(--text); outline-offset: -2px; }

/* ── Detail-Kalender: vertikal gestapelte Monatsblätter eines Spielers ──────
   Eigenständiges Blatt pro Monat (neuestes oben), zentriert und in der Breite
   begrenzt, damit die Tageszellen nicht zu groß werden. */
.cal-sheet { max-width: 300px; margin: 0 auto 22px; }
.cal-sheet-label {
  text-align: center; font-size: 0.9rem; font-weight: 600;
  color: var(--text); text-transform: capitalize; margin-bottom: 6px;
}
.cal-dow {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 4px;
}
.cal-dow span { text-align: center; font-size: 0.66rem; color: var(--muted); }

/* Detail-Kalender: Tageszelle mit Tagesdifferenz auf dem Farbband. Das Band
   (statBoxBand-Klasse) trägt die Tendenz als Fläche; die Zahl darüber ist nach
   Vorzeichen eingefärbt (grün/rot/grau, wie .stat-diff) und bekommt einen
   hellen Heiligenschein-Schatten, damit sie auch auf dunklen Bändern lesbar
   bleibt. Spiel-Tage sind anklickbar (→ Tages-Popup). */
.cal-cell.cal-num {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border-radius: 3px; padding: 1px; line-height: 1.05;
  font-variant-numeric: tabular-nums; cursor: pointer; user-select: none;
}
/* Ein einziger, in der Zelle zentrierter weißer Heiligenschein hinter beiden
   Zahlen (statt eines Schattens je Zeichen) — verbessert die Lesbarkeit auf
   dem Farbband. Nur auf Spiel-Tagen (cal-empty hat keinen Text). */
.cal-cell.cal-num:not(.cal-empty)::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(circle at center,
    rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 30%,
    rgba(255, 255, 255, 0) 65%);
}
.cal-cell.cal-num .cal-pct,
.cal-cell.cal-num .cal-diff {
  position: relative; z-index: 1;   /* über dem Heiligenschein */
  font-size: 0.56rem; font-weight: 700;
}
/* Trennlinie zwischen Prozent (oben) und Differenz (unten): mittig, schwarz,
   nur halbe Zellenbreite. */
.cal-cell.cal-num .cal-pct,
.cal-cell.cal-num .cal-diff { width: 100%; text-align: center; }
.cal-cell.cal-num .cal-diff {
  margin-top: 1px; padding-top: 1px;
  border-top: 0.5px solid #000;
  width: 50%;
}
.cal-cell.cal-num .pos  { color: #15803d; }
.cal-cell.cal-num .neg  { color: var(--red); }
.cal-cell.cal-num .zero { color: var(--muted); }
.cal-cell.cal-num.cal-empty { cursor: default; }
.cal-cell.cal-num[data-day]:hover { filter: brightness(1.05); }

/* Vier Spalten: schmale Typ-Marke (W/S), Datum, Ergebnis, Diff. Die gleich
   breiten 1fr-Spalten um das Ergebnis halten es in der Mitte des RESTLICHEN
   Platzes — bei jeder Zeile gleich, unabhängig von der Diff-Breite, sodass die
   Doppelpunkte aller Zeilen fluchten. Die Typ-Spalte hat feste Breite, damit
   dieses Gleichgewicht von ihr unberührt bleibt. */
.hist-row, .hist-head {
  display: grid; grid-template-columns: 2.2em 1fr auto 1fr;
  gap: 8px; align-items: center;
}
/* Kein horizontales Padding: erste Spalte beginnt bündig mit dem linken Ende
   der Trennlinie, letzte Spalte schließt bündig mit dem rechten Ende ab. */
.hist-row {
  padding: 4px 0; border-bottom: 1px solid var(--border); font-size: 0.92rem;
  line-height: 1.2;
}
.hist-row:last-child { border-bottom: none; }
/* Typ-Marke: ein Buchstabe (W = Wort-Fang, S = Sinn-Suche), zurückhaltend wie
   das Datum daneben. Der volle Name steht im title der Zelle. */
.hist-row .hist-kind {
  color: var(--muted); font-size: 0.8rem; font-weight: 700;
  text-align: center; white-space: nowrap;
}
.hist-row .hist-date  {
  color: var(--muted); font-size: 0.78rem; opacity: 0.85;
  white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* Ergebnis um den Doppelpunkt zentriert: „meine"/„ihre" Punkte in gleich
   breiten Spalten (--score-side, in JS aus der größten Punktzahl gesetzt), der
   Doppelpunkt als auto-Spur dazwischen liegt damit exakt mittig — und weil die
   ganze Box in der (symmetrisch zentrierten) Zeilenmitte sitzt, fluchten die
   Doppelpunkte aller Zeilen untereinander. */
.hist-row .hist-score {
  display: grid;
  grid-template-columns: var(--score-side, 3ch) auto var(--score-side, 3ch);
  align-items: baseline; justify-self: center;
  column-gap: 4px; font-variant-numeric: tabular-nums;
  font-size: 0.8rem;   /* kleiner, damit das Datum daneben komplett passt */
}
.hist-row .hist-mine  { text-align: right; }
.hist-row .hist-their { text-align: left; }
.hist-row .hist-colon { font-weight: 600; }
.hist-row .hist-diff  { font-weight: 700; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.hist-row .hist-diff.pos { color: #15803d; }
.hist-row .hist-diff.neg { color: var(--red); }
.hist-row .hist-diff.zero { color: var(--muted); }
/* Prozent feste Breite + rechtsbündig, damit die davorstehende Punkte-
   differenz bei allen Zeilen an derselben (rechtsbündigen) x-Position endet.
   Farbe wird von der (nach Sieg/Niederlage eingefärbten) Diff-Zelle geerbt. */
.hist-row .hist-pct   {
  font-size: 0.78rem; font-weight: 700;
  display: inline-block; width: 3.4em; text-align: right;
}

.hist-head {
  padding: 4px 0; font-size: 0.74rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border);
  font-weight: 700;   /* Spaltenüberschriften fett */
}
.hist-head .hist-num { text-align: right; }
/* Spaltentitel „Typ" über den W/S-Marken — mittig wie sie. Das Kürzen greift
   nur als Notbremse (2.2em reichen für „TYP" in dieser Schriftgröße). */
.hist-head .hist-kind { text-align: center; overflow: hidden; }
/* „Punkte"-Titel über dem (um den Doppelpunkt) zentrierten Ergebnis. */
.hist-head .hist-points { text-align: center; }

/* Anzahl Spiele direkt über der Summenzeile (gleiche, etwas größere Schrift). */
/* Detail-Zusammenfassung als Kachelreihe wie die Gesamtbilanz-Leiste. */
#stats-detail-summary .ov-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 8px;
}
#stats-detail-summary .stats-summary-hint {
  margin-top: 8px; text-align: center; font-size: 0.8rem;
  font-weight: 400; color: var(--muted);
}

/* ════════════════════════════════════════════════════════════════════
   Design/Typewriter — Paper visibility (overrides at end of file)

   Papier-Textur (von <body>) soll auf jeder nicht-Button-Fläche sichtbar
   sein. Statt die Textur auf jedes Element zu mappen, machen wir die
   Surface-Flächen TRANSPARENT, sodass die Body-Textur durchscheint.
   Definition der Flächen geschieht dann allein durch Border.

   Diese Regel muss AM ENDE der Datei stehen, damit sie spätere
   `background: var(--surface)` Shorthand-Regeln überschreibt (sonst
   würde der gleich-spezifische Selektor weiter unten gewinnen).

   Buttons (.btn-*, .hex-corner) bleiben absichtlich opak — das sind
   die Knöpfe auf dem Papier. ════════════════════════════════════════ */
.card,
.panel, .panel-soft,
.score-chip,
.final-item, .final-card,
input[type="text"], input[type="number"], input[type="search"],
textarea, select {
  background-color: transparent;
  background-image: none;
}
/* Modale Dialoge (z.B. Offline-Angebot, Was-ist-neu) bleiben OPAK —
   sie schweben über einem dunkel gedimmten Backdrop, und mit
   transparenter Karte würde der dunkle Backdrop durchscheinen und der
   Dialog wirkt im Light-Theme schwarz. background: var(--surface) aus
   der Basis-Regel gilt damit weiter. */
/* Toasts sind Nachrichten-Boxen — sie sollen NICHT transparent sein,
   sondern als opake Felder über dem Spiel/Papier liegen. .toast aus
   der „Papier-sichtbar"-Whitelist oben entfernt; die typenspezifischen
   Backgrounds (toast-err: --gold, toast-info: --surface2, …) gelten
   damit wieder. */

/* ════════════════════════════════════════════════════════════════════
   Design/Typewriter — Brutalist sharp corners (overrides at end)

   Schaltet ALLE hardcodierten border-radius-Werte auf 0 (Karten, Buttons,
   Inputs, Modals, Toasts, Chips, Bars). Elemente, die echte Kreise sein
   sollen (Avatare, Indicator-Dots, FAB-Punkte etc.), werden danach
   explizit auf 50% zurückgesetzt — wer nicht in der Whitelist steht,
   wird zum Rechteck.

   Steht am ENDE der Datei, damit die universelle Regel über alle
   spezifischeren `border-radius: …px`-Deklarationen weiter oben gewinnt.
   ════════════════════════════════════════════════════════════════════ */
*, *::before, *::after {
  border-radius: 0 !important;
}
/* Echte Kreise — nicht antasten. */
.log-fab-dot,
.player-pip,
.profile-pic,
.avatar,
.avatar-circle,
.profile-circle,
.player-circle,
[class*="-circle"] {
  border-radius: 50% !important;
}

/* ════════════════════════════════════════════════════════════════════
   Design/Typewriter — In-Game upper region (header + score chips)

   Übernimmt den Setzkasten-Look aus mockups/game-typewriter-mono.html:
   eine flache horizontale Header-Leiste mit Segment-Trennlinien (Raum
   | Mode | Progress | Timer), darunter eine ebenso flache Reihe von
   Score-Chips mit 3-px-Top-Stripe in Spielerfarbe.

   Alle Overrides scoped auf body.game-mode, damit Lobby & Startscreen
   davon unberührt bleiben. !important nur dort, wo die alten Inline-
   Styles in index.html (display:none, font-size, padding, …) im Weg
   stehen — andernfalls würde der Server-Restart verlangt, um die
   Inline-Attribute aus dem templated HTML zu räumen.
   ════════════════════════════════════════════════════════════════════ */

body.game-mode .game-header {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--text);
  padding: 0;
  gap: 0;
  min-height: 38px;
  align-items: stretch;
  overflow: visible;
}
/* Header-Segmente vertikal zentrieren. */
body.game-mode .game-header > * {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
/* Mode-Badge + Fortschritt sind ausgeblendet (Stash), bleiben aber im DOM,
   weil JS sie weiter befüllt (kein Null-Zugriff). */
body.game-mode .game-header .gh-stash { display: none; }
body.game-mode .game-header #game-logo {
  padding: 0 8px 0 4px;
  height: 30px;
  align-self: center;   /* vertikal mittig statt am oberen Rand */
  /* Logo sitzt jetzt am linken Header-Rand (der ☰ ist nach rechts neben die
     Uhr gewandert), daher kein 42px-Freiraum mehr nötig. */
  margin-left: 4px;
}
/* In-Game-☰: in den Header-Fluss verschoben (war position:fixed). Sitzt als
   letztes Flex-Element rechts neben der Uhr, in der oberen rechten Ecke. */
body.game-mode .game-header #app-menu-btn {
  position: static;
  display: flex;
  width: 22px; height: 22px;
  /* rechts 8px = etwas mehr Abstand zum Rand; links 10px Abstand zur Uhr. */
  margin: 0 8px 0 10px;
  align-self: center;
}
body.game-mode .game-header #game-mode-badge {
  padding: 0 12px !important;
  background: var(--text) !important;
  color: var(--bg) !important;
  font-size: 12px !important;
  font-weight: 700 !important;
  letter-spacing: 0.16em !important;
  text-transform: uppercase;
  border-left: 1px solid var(--text);
  /* Bei knappem Header-Raum darf die Mode-Badge zusammenschrumpfen
     (alle anderen Header-Segmente haben flex-shrink: 0). overflow
     hidden auf dem Badge selbst, die ellipsis-Logik sitzt am inneren
     .game-mode-badge-text-Span (Flex-Container respektieren ellipsis
     auf direkten Text-Nodes nicht zuverlässig in allen Browsern). */
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
}
body.game-mode .game-header #game-mode-badge .game-mode-badge-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  display: block;
}
/* Mode-Badge bleibt versteckt, solange JS noch nichts gesetzt hat */
body.game-mode .game-header #game-mode-badge:empty {
  display: none !important;
}
/* Dasselbe für die Uhr: ohne Zeitlimit schreibt onGameStarted eine leere
   Zeichenkette hinein (es gibt ja nichts herunterzuzählen). Ohne diese Regel
   bliebe die goldene Fläche mit ihrer min-width von 76px als leerer Block
   stehen — was wie ein Fehler aussieht und auch als solcher gemeldet wurde. */
body.game-mode .game-header .timer-display:empty {
  display: none !important;
}
body.game-mode .game-header .spacer {
  flex: 1 1 auto;
  min-width: 4px;
  /* Spacer selbst trägt keine Trennlinie — das nächste Segment hat
     border-left, und die spacers sind reine Layout-Lücken. */
}
body.game-mode .game-header #game-progress {
  padding: 0 12px !important;
  font-size: 12px !important;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--text) !important;
  text-transform: uppercase;
  border-left: 1px solid var(--text);
}
body.game-mode .game-header .timer-display {
  padding: 0 12px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.05em;
  /* Warme Marken-Fläche (--gold = Terrakotta in beiden Themes). Heller
     (weißer) Text liegt sauber auf dem dunklen Terrakotta. */
  background: var(--gold);
  color: #fff;
  min-width: 76px;
  justify-content: center;
  /* Box füllt die volle Header-Höhe (kein vertikales Padding, kein
     Außenabstand oben/unten). align-self:stretch überschreibt das
     align-items:center des Headers im Notch-Modus. */
  align-self: stretch;
}
body.game-mode .game-header .timer-display.urgent {
  /* Rote Pulsanimation aus dem alten Stil passt nicht zum Block-Look —
     wir lassen den Timer einfach in pulsierender Opazität, Farben
     bleiben Akzent. */
  animation: pulse 1s infinite;
}

/* ── In-Game-Header bei Top-Notch ────────────────────────────────────
   html.has-top-inset wird per JS gesetzt, sobald env(safe-area-inset-top)
   > 0 ist (echte Notch / Dynamic Island / Punch-Hole im Hochformat bzw.
   Fullscreen/Standalone). Der Header bleibt EINE Zeile — er füllt nun aber
   das Notch-Band (min-height = inset-top), sodass Logo (links) und Uhr+☰
   (rechts) vertikal mittig NEBEN dem mittigen Cutout sitzen. Ohne Notch ist
   inset-top 0px → min-height 0 → exakt die normale Header-Höhe. */
html.has-top-inset body.game-mode .game-header {
  /* max(): nie unter die normale Header-Höhe (38px) fallen, auch wenn der
     Inset kleiner ist (kurze Android-Punch-Hole-Bänder). */
  min-height: max(38px, var(--safe-top));
  align-items: center;
  /* nowrap, damit zu enge Inhalte ÜBERLAUFEN statt umzubrechen — nur so
     erkennt JS (scrollWidth > clientWidth) ein echtes Platzproblem. */
  flex-wrap: nowrap;
}
/* Optionale Uhr nur ausblenden, wenn die Zeile WIRKLICH überläuft (Logo +
   Uhr + ☰ passen nicht nebeneinander) — JS setzt html.drop-clock, siehe
   updateNotch in index.html. Auf normalen Telefonen passt alles → die Uhr
   bleibt links neben dem ☰. Logo + ☰ bleiben immer. */
html.drop-clock body.game-mode .game-header .timer-display {
  display: none;
}

/* ── Grid-Score-Table — typewriter-mono Match ──────────────────────
   Horizontale Reihe von Spalten, Tinten-Trennlinien dazwischen,
   3-px-Top-Stripe in Spielerfarbe (--score-color, vom JS gesetzt).
   Die große In-Lobby-Score-Chip-Liste ist im Spiel via
   `body.game-mode #score-chips { display: none }` ausgeblendet. */
body.game-mode .grid-score-table {
  display: flex !important;     /* override des CSS-Grid-Default */
  flex-direction: row;
  flex-wrap: nowrap;
  gap: 0;
  padding: 0;
  margin: 0 0 4px;
  border: 1px solid var(--text);
  background: transparent;
  font-size: 0.85rem;
  line-height: 1.25;
  grid-template-columns: none;
}
body.game-mode .grid-score-table .gst-box {
  flex: 1 1 0;
  min-width: 0;
  padding: 6px 8px 5px;
  background: transparent;
  border: 0;
  border-left: 1px solid var(--text);
  border-radius: 0 !important;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 2px;
  position: relative;
  overflow: hidden;
}
body.game-mode .grid-score-table .gst-box:first-child {
  border-left: 0;
}
body.game-mode .grid-score-table .gst-box::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: var(--score-color, var(--text));
}
body.game-mode .grid-score-table .gst-box.me {
  /* me-Markierung kam vorher per dickerem Rahmen — der 3-px-Stripe
     in der Akzent-Spielerfarbe übernimmt das jetzt. Padding zurück
     auf Default, damit die Höhe gleich zu den anderen Boxen bleibt. */
  padding: 6px 8px 5px;
}
body.game-mode .grid-score-table .gst-name {
  color: var(--text);
  font-size: 11px;
  letter-spacing: 0.12em;
  font-weight: 700;
  text-transform: uppercase;
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
body.game-mode .grid-score-table .gst-box.me .gst-name {
  color: var(--text);
}
body.game-mode .grid-score-table .gst-score {
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
  letter-spacing: -0.01em;
  margin-top: 2px;
}

/* Light-Theme: die Chip-Reihen (Score-Chips außerhalb des Grid-Modus,
   Grid-Score-Table im Grid-Modus) standen mit transparentem Hintergrund
   direkt auf der Papier-Textur — der gewollte mono-Block-Look wird so
   visuell vom Papier-Maserung-Muster aufgeweicht. In Light-Theme: solide
   Papier-lose Cremefläche als Hintergrund auf dem Außen-Container; die
   inneren Chip-Boxen bleiben transparent und zeigen diesen Untergrund
   durch — Tinten-Trennlinien bleiben somit über der ganzen Reihe
   sichtbar. Dark-Theme unverändert (dort gibt es keine Papier-Textur). */
.light-theme body.game-mode .grid-score-table {
  background: var(--surface);
}

/* ════════════════════════════════════════════════════════════════════
   Portrait-Lock auf Mobile

   Auf einem Handy-Querformat-Display passt das Spielmodell (Hex unten,
   Wörter-Grid oben, ergonomische Buchstaben-Tasten) schlicht nicht —
   die Buttons werden fingerkuppen-zu-klein, der Header rutscht über
   die System-UI. Wir blockieren Querformat unterhalb einer Handy-
   typischen Höhe (max-height: 500px) durch ein Vollbild-Overlay mit
   Bitte-drehen-Hinweis. Tablets bleiben unberührt (höhere Viewports
   gelten als ausreichend Platz); Desktop ebenfalls — dort schließt
   `pointer: coarse` das Overlay aus, denn ein flach gezogenes Browser-
   fenster ist unter 500px hoch und hat trotzdem keinen Drehsensor: der
   Hinweis wäre eine Sackgasse. Der Hinweis-Text steht im HTML
   (#portrait-lock), damit i18n ihn übersetzen kann. */
#portrait-lock {
  display: none;
}
@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
  #portrait-lock {
    display: flex;
    position: fixed; inset: 0; z-index: 99999;
    background-color: var(--bg);
    background-image: var(--paper);
    background-repeat: repeat;
    color: var(--text);
    align-items: center; justify-content: center;
    text-align: center;
    /* Querformat → eine Top-Notch wandert an die Seite; Text per
       max(24px, seitliche Insets) aus dem Cutout halten. */
    padding:
      max(24px, var(--safe-top))
      max(24px, var(--safe-right))
      max(24px, var(--safe-bottom))
      max(24px, var(--safe-left));
    font-size: 1.1rem; font-weight: 700;
    letter-spacing: 0.04em;
  }
}

/* ── Marken-Logo (WordCrab) ────────────────────────────────────────────
   Start-Bildschirm: großes, zentriertes Wortmarken-Logo anstelle des
   früheren H1-Texts, in der oberen Hälfte der Card.
   Statistik: kleines Logo oben mittig zentriert (#top-logo).
   Lobby: Logo links in der Kopfzeile (#lobby-logo). Spielende: Logo zwischen
   den Buttons (#gameover-logo). Im Spiel: Logo links im Header / SW-Ecke. */
.brand-title {
  margin: 2px 0 20px;
  text-align: center;
  line-height: 0;
}
.brand-logo {
  display: block;
  width: auto;
  max-width: min(300px, 72%);
  height: auto;
  margin: 0 auto;
}

#top-logo {
  display: none;                 /* per Screen via :has() eingeblendet */
  position: fixed;
  /* +safe-area-inset-top: das Logo sitzt oben mittig — genau dort, wo eine
     Top-Notch liegt. Ohne Notch ist der Inset 0 → unverändert 10px. */
  top: calc(10px + var(--safe-top));
  left: 50%;
  transform: translateX(-50%);   /* horizontal zentriert */
  height: 33px;
  width: auto;
  z-index: 40;
  pointer-events: none;          /* rein dekorativ, fängt nie einen Tap ab */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
}
@media (min-width: 700px) {
  #top-logo { top: calc(14px + var(--safe-top)); height: 42px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CrabsWords — Spielbrett-Bereich und Brücke zum Modul.

   Das Brett liegt an derselben Stelle wie das Wörter-Gitter und bekommt
   denselben Platz. Wie GROSS der ist, rechnet die App aus und sagt es dem
   Modul (crossword-grid misst bewusst nicht selbst nach) — hier steht nur,
   welchen Bereich es füllen darf.
   ═══════════════════════════════════════════════════════════════════════════ */

.crab-region {
  position: relative;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* `display: flex` oben schlägt sonst das eingebaute `[hidden] {display:none}`.
   Dieselbe Falle wie beim Wörter-Gitter (siehe `#word-grid[hidden]`) und beim
   `.crab-entry`: außerhalb des Krebsworträtsels lief die ausgeblendete Region
   als leere `flex: 1 1 auto`-Box weiter, nahm dem Wort-Fang-Gitter die halbe
   Fläche (kleinere Kästchen) und legte obendrein ihre Zoomknöpfe über die
   Spielfläche — Knöpfe ohne Brett darunter. */
.crab-region[hidden] { display: none; }

/* Im Krebsworträtsel gibt es kein Wörter-Gitter — an seiner Stelle steht das
   Brett. Dieselbe Umschaltung wie bei der Sinn-Suche. */
body.game-mode.crab-mode .word-grid { display: none; }

/* Brücke zum Brett-Modul (public/crossword-grid/): das Modul kennt keine
   App-Variablen, es liest nur eigene --cw-*-Tokens mit Rückfallwerten. Hier
   werden sie mit den Theme-Farben belegt. Wer eine Farbe ändern will, ändert
   sie HIER.

   --cw-found bleibt ungesetzt: die Farbe eines gefundenen Wortes ist die des
   Finders und kommt je Wort aus der colorMap, nicht aus dem Theme. */
.crab-region {
  --cw-cell-bg:       var(--hex-key);
  --cw-cell-border:   var(--border);
  --cw-text:          var(--text);
  /* --cw-selected/-inner bleiben ABSICHTLICH ungesetzt: wie das gerade
     bearbeitete Wort markiert wird, gehört dem Brett-Modul und nicht dem Theme
     (siehe dessen README). Eine App-Farbe hier stand schon einmal zu nah an den
     Spielerfarben — auf einem Feld, das ein Mitspieler gefunden hatte, war der
     Rahmen nicht mehr auszumachen. */
  --cw-found-default: var(--violet);
  --cw-found-text:    #FBF3E3;
  --cw-multi:         var(--muted);
  /* Von allen gefunden. `--text` ist die dunkelste Farbe des Themas und damit
     im hellen wie im dunklen Theme die richtige Antwort auf „erledigt". */
  --cw-all:           var(--text);
}

/* Im dunklen Theme trägt die Tastatur cremefarbene Schrift — das Grau der
   Rücktaste muss entsprechend dunkel sein. */
:root:not(.light-theme) .crab-entry-keyboard {
  --kb-action:       #57504A;
  --kb-action-hover: #6B635C;
}

/* ── CrabsWords: Themenwahl ────────────────────────────────────────────────
   Aufklappbare Baum-Tabelle mit Ankreuzfeldern. Sie kann lang werden (55
   Themen), also scrollt sie — und zwar SIE, nicht die Seite. `touch-action:
   pan-y` ist Pflicht: ohne sie deutet Firefox Android eine leicht schräge
   Wischbewegung in einem überlagernden Scrollbereich als Zurück-Geste. */
.topic-card {
  display: flex;
  flex-direction: column;
  max-height: min(80vh, 560px);
  width: min(92vw, 420px);
}
.topic-title { margin: 0 0 8px; }
.topic-body {
  flex: 1 1 auto;
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior-x: none;
}
/* Deutliche Einrückung je Stufe: die Zugehörigkeit ist die einzige Information,
   die der Baum über die Liste hinaus trägt — sie muss man auf einen Blick
   sehen, nicht suchen. */
.topic-tree { list-style: none; margin: 0; padding: 0 0 0 26px; }
.topic-body > .topic-tree { padding-left: 0; }
/* Die Themenwahl setzt kleiner als der übrige Bildschirm: 55 Themen mit
   Einrückung, Zahlen und Wissens-Anteilen sind viel Zeile, und je mehr davon
   nebeneinander passt, desto weniger bricht um. In `rem`, nicht `em` — der Baum
   steckt in sich selbst, und relative Größen schrumpften mit jeder Ebene. */
.topic-body .topic-tree { font-size: 0.88rem; }
/* Die Mindestbreite, an der der Umbruch hängt (siehe .topic-row): darunter
   schrumpft der Name nicht weiter, also müssen die Zahlen weichen. */
.topic-body .topic-name { min-width: 8em; }

/* Zugeklappt: die Unterthemen sind da, aber nicht im Weg. */
.topic-tree > li > .topic-tree { display: none; }
.topic-tree > li.is-open > .topic-tree { display: block; }

.topic-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 4px;
  border-radius: 6px;
  /* Passt der Name nicht neben die Zahlen, rutschen DIE ZAHLEN in die zweite
     Zeile — der Name behält die erste. Deshalb bekommt er unten eine
     Mindestbreite: ohne sie schrumpfte er stattdessen, sein Text bräche um, und
     die Zahlen blieben neben einem zweizeiligen Namen stehen. */
  flex-wrap: wrap;
}
/* Die Zahlen als EIN Kasten (Wissen + Wortzahl): sie brechen gemeinsam um und
   stehen dann rechts, wo sie auch in der ersten Zeile stünden. */
.topic-nums {
  flex: 0 0 auto;
  margin-left: auto;
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}
.topic-row:hover { background: var(--surface); }
/* Die ganze Zeile schaltet — auch Polsterung, Lücken und die Zahl rechts.
   Der Zeiger sagt das an jeder Stelle, sonst sähe die Zahl nach Beschriftung
   aus und der Rand nach nichts. */
.topic-row { cursor: pointer; }
.topic-count { cursor: inherit; }
.topic-box { accent-color: var(--accent); flex: 0 0 auto; }

/* Der Name ist ein Knopf: bei einem Oberthema klappt er auf und zu, bei einem
   Blatt schaltet er die Wahl. Er sieht trotzdem wie Text aus — ein
   Knopf-Rahmen mitten in einer Liste wäre nur Lärm. */
.topic-name {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.topic-twisty {
  flex: 0 0 auto;
  width: 0.9em;
  color: var(--muted);
  transition: transform 120ms ease;
}
.topic-twisty::before { content: '▸'; }
.is-open > .topic-row .topic-twisty { transform: rotate(90deg); }

/* Teilweise gewählt — EINZELNE Unterthemen, nicht der ganze Ast. Das kennt ein
   Ankreuzfeld nicht von sich aus: es bekommt darum ein Häkchen UND einen
   grauen Grund, statt der vollen Akzentfarbe. „Hier drin steckt etwas, aber
   nicht alles." */
.topic-row--partial .topic-box { accent-color: var(--muted); }
/* Die Zahl ist Nebeninformation — sie darf den Namen nicht überstrahlen. */
/* Die Wortzahl schließt die Zeile ab. Vier Ziffern sind reserviert (das größte
   Thema hat gut 1 900 Wörter, die Wurzel darüber mehr) und rechtsbündig
   gesetzt: nur so enden „7", „48" und „1981" auf derselben Kante und die Spalte
   ist eine. `ch` ist bei tabular-nums genau eine Ziffernbreite — dasselbe
   Verfahren wie bei den Wissens-Zahlen (.know-num). */
.topic-count {
  flex: 0 0 auto;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.9em;
  min-width: 4ch;
  text-align: right;
}
/* Vom Oberthema mitgemeint: sichtbar dabei, aber nicht einzeln abwählbar —
   „Natur ohne Vögel" gibt es im Datenmodell nicht. */
/* Mitgemeint über ein Oberthema: zurückgenommen dargestellt, damit man sieht,
   woher die Wahl kommt — aber BEDIENBAR. Ein Abhaken löst das Oberthema in
   seine übrigen Kinder auf; deshalb kein `cursor: default`, das versprach
   „hier geht nichts". */
.topic-row--implied { opacity: 0.55; }

.topic-actions { display: flex; justify-content: flex-end; padding-top: 10px; }

/* ── CrabsWords: die Eingabe ───────────────────────────────────────────────
   Eigene Ebene ÜBER dem Brett. Sie verkleinert es NICHT — sonst liefe der
   ResizeObserver des Bretts an, es rechnete eine neue Feldgröße, und das
   Rätsel spränge beim Öffnen auf einen anderen Zoom. Deshalb `position:
   absolute` im Spielbereich statt eines Flex-Kindes, das Platz beansprucht. */
/* `display: flex` unten schlägt sonst das eingebaute `[hidden] {display:none}`
   (gleiche Spezifität, spätere Regel) — die Eingabe stünde IMMER da und
   reservierte unten Platz, auch wenn gar keine Tastatur gebraucht wird. */
.crab-entry[hidden] { display: none; }

.crab-entry {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px;
  max-height: 100%;
  background: var(--bg);
  background-image: var(--paper);
  border-top: 1px solid var(--border);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.25);
  /* Die Eingabe führt ihre Zeiger selbst — kein Durchreichen ans Brett. */
  touch-action: none;
}

/* Die Bedeutungen sehen aus wie die Slots der Sinn-Suche und sitzen wie sie
   OBEN: grauer Kasten mit Rahmen, der den Löwenanteil der Ebene bekommt. Vom
   Rätsel bleibt darüber zu sehen, was danach noch übrig ist — die Aufgabe zu
   lesen ist wichtiger, als das Brett im Blick zu behalten.

   Sie können lang sein; dann scrollen SIE, nicht die Ebene. */
/* Träger des Hinweises und der Wisch-Vorführung darüber. Geblättert wird mit
   dem Finger über dem Hinweis selbst — vorher lagen dafür zwei Pfeil-Streifen
   an seinen Rändern, die ihm Breite und Bild nahmen. Der Träger erbt das
   Wachstum des Hinweises, damit die Vorführung dessen ganze Fläche hat. */
.crab-clue {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}

/* ── Vorführung der Wisch-Geste ───────────────────────────────────────────────
   Eine Fingerkuppe zieht über den Hinweis, in die Richtung, in die auch
   wirklich geblättert wird (--back: zurück). Sie erscheint erst nach einer
   Weile ohne Eingabe (CRAB_HINT_IDLE_MS) — wer tippt, weiß, was er tut.

   Sie liegt über dem Hinweis und nimmt KEINE Zeiger an: sie ist eine Auskunft,
   kein Bedienelement — ein Tipp darauf soll den Hinweis darunter treffen. */
.crab-swipe-hint {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 10px;
  pointer-events: none;
  z-index: 3;
}
.crab-swipe-hint[hidden] { display: none; }

/* Weiße Fläche, schwarze Kontur — beides steckt im Bild und ist BEWUSST nicht
   an das Thema gebunden: die Hand liegt über einem beliebigen Foto, und dort
   hilft kein Themenwert, sondern der Kontrast in der Zeichnung selbst.
   Das Seitenverhältnis 83:150 ist das der Datei; wer sie neu erzeugt
   (scripts/make-swipe-hand.js), prüft es hier nach. */
.crab-swipe-hint-hand {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 50px;
  margin: -25px 0 0 -14px;
  background: url('../swipe-hand.png') center / contain no-repeat;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
  opacity: 0;
  animation: crab-swipe-hint 5s ease-in-out infinite;
}
/* Der Schweif liegt HINTER der Hand und zeigt damit, woher sie kommt — also
   auf der Seite, von der weg gewischt wird. Er trägt dieselbe Zeichnung wie
   die Hand: weiße Fläche, dunkle Kontur (hier als Schlagschatten-Ring). */
.crab-swipe-hint-hand::after {
  content: '';
  position: absolute;
  top: 62%;
  left: 100%;
  margin-left: 8px;
  width: 30px;
  height: 4px;
  border-radius: 2px;
  background: #FBF3E3;
  box-shadow: 0 0 0 1px rgba(20, 16, 14, 0.85);
  opacity: 0.8;
}
/* Nach links = vorwärts (siehe onCommit in main.js). Rückwärts ist dieselbe
   Vorführung, gespiegelt — Hand UND Schweif zusammen. Über `animation-
   direction: reverse` ginge es auch, aber dann käme die eingebaute Pause
   zuerst und die erste Vorführung ließe Sekunden auf sich warten. */
.crab-swipe-hint--back { transform: scaleX(-1); }

/* Eine Runde = eine Wischbewegung (1,4 s) und danach Pause, zusammen 5 s: so
   oft wiederholt sich die Vorführung, solange niemand tippt. Die Pause steckt
   IN den Keyframes und die Wiederholung in `infinite` — ein JS-Timer daneben
   wäre eine zweite Uhr, die mit dieser hier auseinanderlaufen kann. Der Takt
   steht deshalb NUR hier. */
@keyframes crab-swipe-hint {
  0%   { transform: translateX(74px)  scale(0.85); opacity: 0; }
  5%   { transform: translateX(56px)  scale(1);    opacity: 0.95; }
  22%  { transform: translateX(-56px) scale(1);    opacity: 0.95; }
  28%  { transform: translateX(-74px) scale(0.85); opacity: 0; }
  100% { transform: translateX(-74px) scale(0.85); opacity: 0; }
}
/* Ohne Bewegung bleibt die Hand stehen, statt zu verschwinden: sie mit
   `animation: none` unsichtbar zu machen (opacity 0 im ersten Keyframe) nähme
   genau denen die Auskunft, die sie am ehesten brauchen. Der Schweif zeigt
   weiterhin die Richtung an. */
@media (prefers-reduced-motion: reduce) {
  .crab-swipe-hint-hand { animation: none; opacity: 0.95; }
}

/* Der Platz für das Bild, bevor es da ist. `aspect-ratio` ist genau dafür
   gedacht: der Kasten bekommt eine Höhe aus seiner Breite, ohne dass jemand
   die Zahl kennen muss. 2:1 ist eine Schätzung ins Mittel — sobald der Abruf
   die echten Maße liefert, stehen sie als width/height am <img> und der
   Browser reserviert selbst.

   Ohne das klappte das Feld beim Eintreffen des Bildes auf: die Tastatur
   rutschte nach unten, und das Brett bekam mitten in der Bewegung eine neue
   verdeckte Höhe gemeldet. */
.crab-clue-loading {
  aspect-ratio: 2 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
}

.crab-entry-senses {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 4.5em;
  max-height: 42vh;
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior-x: none;
  font-size: 0.95em;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
}
/* Keine Aufzählungszeichen: getrennt wird mit Linien, wie in der Sinn-Suche.
   Ein Punkt oder Strich davor legte nahe, die Bedeutungen gehörten zu einer
   Aufzählung mit fester Ordnung — sie sind aber gleichrangige Lesarten
   desselben Wortes, und gezeigt wird ohnehin nur eine Auswahl. */
/* Kein Bild, obwohl eines bestellt war. Zurückhaltend, aber sichtbar: es ist
   eine Entschuldigung, keine Fehlermeldung — gespielt werden kann weiter, nur
   eben mit der Bedeutung. */
.crab-no-image {
  margin: 0 0 6px;
  font-size: 0.72rem;
  color: var(--muted);
  font-style: italic;
}

.crab-senses { margin: 0; padding: 0; list-style: none; }
.crab-senses li { margin: 0; padding: 7px 0; }
.crab-senses li + li { border-top: 1px solid var(--border); }

/* Die Kästchen des Wortes. Sie dürfen umbrechen: ein zwölfbuchstabiges Wort
   passt sonst auf schmalen Geräten nicht nebeneinander, und seitwärts
   scrollen soll in einem Dialog niemand (CLAUDE.md, Layout-Regel). */
/* Kästchen und Rücktaste in einer Zeile. Die Rücktaste sitzt rechts daneben,
   mit Abstand: sie gehört zum getippten Wort, nicht zur Tastatur — dort nahm
   sie der obersten Reihe den Platz weg. */
.crab-entry-row {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
}
.crab-del {
  flex: 0 0 auto;
  /* Rund zwei Tastenbreiten — sie wird beim Korrigieren blind getroffen. */
  width: 68px;
  height: 38px;
  border: 0;
  border-radius: 8px;
  background: #C3BFB6;
  color: #241F1C;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.crab-del:active { background: #ADA89E; }
:root:not(.light-theme) .crab-del { background: #57504A; color: #EFE7DA; }
:root:not(.light-theme) .crab-del:active { background: #6B635C; }

/* Das getippte Wort steht in EINER Zeile, solange es in die Breite passt — und
   in ZWEIEN, wenn nicht. Ein zwölfbuchstabiges Wort auf einem schmalen Gerät
   ließ sich sonst nur noch geschrumpft oder seitwärts scrollend eintippen, und
   beides trifft man nicht mehr sicher.

   Aufgeteilt wird gleichmäßig und in renderCells(), nicht vom Browser: ein
   `flex-wrap` schöbe nur den Rest in die zweite Zeile („elf und einer"), und
   das liest sich nicht mehr als ein Wort. Die beiden Werte, mit denen dort
   gerechnet wird, stehen deshalb hier am Wirt — vererbt bekommt sie das
   Kästchen ohnehin.

   Das Schrumpfen bleibt als letzte Reserve im Kästchen stehen: reicht selbst
   eine halbierte Zeile nicht, schrumpft sie bis 65 % und läuft danach über. */
.crab-entry-cells {
  --crab-cell-w: 30px;
  --crab-cell-gap: 2px;
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  max-width: 100%;
}
/* Eine Reihe von Kästchen — eine oder zwei davon, siehe oben. */
.crab-cell-line {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  gap: var(--crab-cell-gap);
  max-width: 100%;
  overflow-x: auto;
  /* Waagerechtes Wischen darf keine Zurück-Navigation auslösen (CLAUDE.md). */
  overscroll-behavior-x: contain;
  touch-action: pan-y;
}
.crab-cell {
  width: var(--crab-cell-w);
  flex: 0 1 var(--crab-cell-w);
  min-width: calc(var(--crab-cell-w) * 0.65);
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  font-weight: 600;
}
/* Schon bekannt — aus einem Tipp oder einer Kreuzung. Steht fest und lässt
   sich nicht wegtippen: die Rücktaste überfährt so ein Feld nur.

   Deshalb eine eigene FARBE (Petrol, dieselbe Hervorhebungsfarbe wie beim
   Pangramm — sie ist in beiden Themes auf der Kästchenfläche klar lesbar) und
   ein farbiger Rahmen dazu, statt der früheren blasseren Tinte: „grau" liest
   sich als „unwichtig", der Buchstabe ist aber das Gegenteil davon. Fett wie
   der Tipp auf dem Brett (`.cw-cell--hinted`) — geschenkt, nicht erarbeitet.

   Die Fläche trägt den Unterschied bewusst NICHT mehr: `--hex-key` ist im
   Hell-Theme dasselbe wie `--surface`, ein bekanntes Kästchen sah dort also
   aus wie ein leeres; im Dunkel-Theme war die graue Fläche zwar sichtbar,
   nahm aber jeder farbigen Schrift darauf den Kontrast. */
.crab-cell--known {
  color: var(--purple);
  border-color: var(--purple);
  font-weight: 800;
}
.crab-cell--typed { color: var(--text); }
/* Die Schreibmarke: rot, damit sie sich von den Kästchen und vom gefundenen
   Grün abhebt — und weil man sie durch Antippen selbst setzt, muss sie ohne
   Suchen zu finden sein. `cursor: pointer` sagt, dass das geht. */
.crab-cell { cursor: pointer; }
.crab-cell--cursor {
  outline: 2px solid var(--red, #C7502A);
  outline-offset: -2px;
}
/* Dieselbe Marke, aber HINTER dem letzten Kästchen: ein voll getipptes Wort
   lässt ihr kein Feld mehr, auf dem sie stehen könnte. Als schmaler Strich in
   derselben Farbe — sie sagt dasselbe und muss deshalb dasselbe sein. */
.crab-caret-end {
  flex: 0 0 auto;
  width: 2px;
  height: 34px;
  border-radius: 1px;
  background: var(--red, #C7502A);
}

/* KEINE feste Höhe: das Tastatur-Modul bekommt gesagt, wie viel es haben darf,
   und macht sich so hoch, wie es braucht. Ein `min-height` reservierte den
   Platz auch dann, wenn gar keine Tastatur darin steht. */
.crab-entry-keyboard { flex: 0 0 auto; }

/* Brücke zum Tastatur-Modul (public/keyboard/): es kennt keine App-Variablen,
   nur eigene --kb-*-Tokens mit Rückfallwerten. */
.crab-entry-keyboard {
  --kb-key:          var(--hex-key);
  --kb-key-hover:    var(--hex-key-hover);
  --kb-key-text:     var(--text);
  /* Die Rücktaste hebt sich als GRAUE Taste ab — sie wird blind gesucht, und
     `--surface` lag zu nah an den Buchstabentasten. Bewusst ein eigener
     Grauton statt `--muted`: das ist verblasste TINTE, darauf wäre die
     Beschriftung (--kb-key-text) unlesbar. */
  --kb-action:       #C3BFB6;
  --kb-action-hover: #ADA89E;
  --kb-enter:        var(--violet);
  --kb-enter-hover:  var(--violet-d);
  --kb-enter-text:   #FBF3E3;
  --kb-flash:        var(--gold);
  --kb-flash-text:   #241F1C;
}

/* Die Ebene misst sich an der ganzen Spielfläche, nicht am oberen Drittel:
   die Tastatur braucht mehr Höhe, als das Wörter-Gitter je hätte. */
body.crab-mode .game-left { position: relative; }

/* Kein Buchstabenset, also auch kein Rad. Der Platz gehört dem Brett. */
body.game-mode.crab-mode .hex-region,
body.game-mode.crab-mode .word-display-wrap { display: none; }

/* Zoom-Knöpfe über dem Brett — sie liegen darüber und nehmen ihm keinen
   Platz. Zwei Finger bzw. Strg+Rad tun dasselbe, aber das muss man wissen. */
.crab-zoom {
  position: absolute;
  right: 6px;
  top: 6px;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.crab-zoom-btn {
  width: 34px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: 600 18px/1 system-ui, sans-serif;
  opacity: 0.85;
  cursor: pointer;
}
.crab-zoom-btn:hover { background: var(--hex-key-hover); opacity: 1; }
/* Am Anschlag ausgegraut. Ein Knopf, der nichts tut, ist schlechter als
   keiner — man drückt ihn und zweifelt am Gerät. */
.crab-zoom-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.crab-zoom-btn:disabled:hover { background: var(--surface); opacity: 0.35; }


/* Fehlversuch. Kurz und deutlich — die Eingabe bleibt stehen, nur das
   Getippte ist weg, also muss das Blinken die Auskunft tragen. */
@keyframes crab-wrong {
  0%, 100% { border-color: var(--border); }
  25%, 75% { border-color: var(--red, #C7502A); }
}
.crab-entry--wrong .crab-entry-keyboard,
.crab-entry--wrong .crab-cell {
  animation: crab-wrong 420ms ease-in-out;
}
.crab-entry--wrong .crab-entry-keyboard { border: 1px solid var(--border); border-radius: 10px; }

@media (prefers-reduced-motion: reduce) {
  .crab-entry--wrong .crab-entry-keyboard,
  .crab-entry--wrong .crab-cell { animation: none; }
}


/* ── Krebsworträtsel-Auflösung: Bezugsrand ist der KARTENRAND ─────────────────
   Überall sonst auf dem Spielende-Bildschirm ist die Lesespalte das Maß. Hier
   nicht: das Gitter braucht jeden Punkt Breite, den es kriegen kann, und die
   Bildzeilen sind Karten — Karten dürfen bis an den Rand. Also wird die
   Polsterung der Lesespalte einmal herausgezogen, und alles darin rechnet ab
   dem Kartenrand. Zurückgezogen wird nur um `--go-pad`; ein größerer
   Safe-Area-Anteil bleibt also stehen, die Aussparung bleibt frei.

   `display: block` statt der Pillen-Wolke: dort ist jedes Wort ein Flex-Kind
   mit Umbruch, hier ist jedes Kind eine eigene volle Zeile. Als Flex-Kinder
   zögen sich Gitter und Galerien auf ihre Inhaltsbreite zusammen. */
.solution-grid--crab {
  display: block;
  padding-right: 0; /* der 4px-Rest der Pillen-Wolke verschöbe die Mitte */
  margin-inline: calc(-1 * var(--go-pad));
}
/* Die Überschriften der Galerien sind Text und bleiben in der Lesespalte. */
.solution-grid--crab .crab-gallery-title { padding-inline: var(--go-pad); }
/* Die Bildzeilen reichen bis an den Rand — dort fallen seitlicher Rahmen und
   Rundung weg: eine Ecke, die sichtbar in die Kante schneidet, sieht nach
   Fehler aus, nicht nach Absicht. Oben und unten bleiben beide. */
.solution-grid--crab .crab-gallery-row {
  border-inline: 0;
  border-radius: 0;
}

/* ── Bilder-Runde: die Galerien UNTER dem Gitter ──────────────────────────── */

.crab-gallery { margin-bottom: 14px; }
.crab-gallery-title {
  margin: 0 0 6px; font-size: 0.9rem; font-weight: 700; color: var(--text);
}
.crab-gallery-list { display: flex; flex-direction: column; gap: 8px; }

/* Zwei Spalten: links das Bild, rechts die Lösung. Die Bildspalte ist fest,
   damit die Wörter untereinander an derselben Kante beginnen — sonst tanzt die
   Liste mit jedem Seitenverhältnis. */
.crab-gallery-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: 10px;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  cursor: pointer;
}
.crab-gallery-row:hover { background: var(--surface2); }

/* Noch keine Karte, sondern eine Zeile in einer Wortliste: was der Spieler nie
   aufgeschlagen hat, trägt kein Bild und holt sich auch keines. Ohne Fläche und
   ohne Rahmen — der Unterschied zu den aufgedeckten Karten soll zu sehen sein,
   bevor man tippt, und nicht erst danach. Es steht nur das Wort da; beim
   Aufdecken rückt das Bild davor und die Zeile sieht aus wie jede andere. */
.crab-gallery-row--verdeckt {
  grid-template-columns: auto 1fr;
  padding-block: 2px;
  background: none;
  border-color: transparent;
}
.crab-gallery-row--verdeckt:hover { background: none; }
/* Das Wort ist hier zugleich der Weg zum Bild und sagt das so, wie es jeder
   Link tut. Ein Hinweis daneben stünde in jeder Zeile noch einmal; die
   Unterstreichung sagt dasselbe und nimmt keinen Platz. */
.crab-gallery-word--link { text-decoration: underline; text-underline-offset: 3px; }

/* Höchstens ein Sechstel des Bildschirms — mehr wäre eine Bildergalerie mit
   angehängtem Rätsel. Der Nachweis darunter zählt NICHT zur Bildhöhe, er darf
   die Grenze überschreiten: er ist Pflicht, das Bild ist Zugabe. */
.crab-gallery-img { min-width: 0; }
.crab-gallery-img .wikt-image img { max-height: 16vh; }
.crab-gallery-none { font-size: 0.75rem; color: var(--muted); }

/* Das Lösungswort ist zugleich eine .sol-word-Pille (siehe `solWordPaint`):
   die Fläche trägt die Farbe dessen, der das Wort hatte — dieselbe Auskunft
   und dieselbe Farbsprache wie am Ende des Wort-Fangs. Fläche, Rand und
   Polsterung kommen deshalb von dort und dürfen hier NICHT zurückgesetzt
   werden (`background: none` löschte genau die Aussage); der Knopf steuert nur
   noch Schrift und Ausrichtung bei.

   `justify-self: start`: als Raster-Kind zöge sich der Knopf sonst über die
   ganze Spalte, und die Farbfläche stünde als breiter Balken da statt als
   Pille um das Wort. */
.crab-gallery-word {
  min-width: 0;
  justify-self: start;
  font: inherit; font-weight: 700; font-size: 1.05rem;
  color: var(--text); text-align: left; cursor: pointer;
  overflow-wrap: anywhere;
}

/* Steht dasselbe Wort zweimal im Gitter (je Thema einmal), nennt jede Zeile
   ihr Thema — leise, hinter dem Wort: die Angabe unterscheidet, sie trägt
   nicht. Bei einmaligen Wörtern fehlt sie ganz. */
.crab-gallery-topic {
  font-weight: 400; font-size: 0.85em;
  color: var(--muted);
}

/* Allein gespielt trägt die Pille nicht die Farbe eines Finders (es gibt nur
   einen, von dem sie niemanden unterscheiden könnte), sondern den ZUSTAND des
   Worts: gelöst grün, angesehen und nicht gelöst hellgrau, nie aufgeschlagen
   weiß. Dieselbe Auskunft und dieselben Farben wie im aufgelösten Gitter
   darüber — beide fragen `crabSoloState`.

   Die Schrift bleibt die normale Tinte: die drei Flächen sind je Theme so
   gewählt, dass sie darauf lesbar sind (siehe --crab-solo-* in den
   Theme-Dateien). */
.sol-word.crab-solo         { color: var(--text); border-color: var(--border); }
.sol-word.crab-solo--solved { background: var(--crab-solo-solved); }
.sol-word.crab-solo--seen   { background: var(--crab-solo-seen); }
.sol-word.crab-solo--unseen { background: var(--crab-solo-unseen); }

/* Der Bildnachweis, so klein wie er lesbar bleibt. Kleiner ginge, wäre aber
   keine Namensnennung mehr — unlesbar erfüllt die Pflicht nicht. */
.crab-gallery-img .wikt-image-credit { font-size: 0.55rem; line-height: 1.3; }

/* Die HÖHE kommt aus dem Skript (`neededBoardSize`) — so hoch wie das
   eingepasste Brett und keinen Punkt höher. Fest eingestellt stand der Kasten
   zur Hälfte leer, und leerer Platz IM Brett ist kein neutraler Platz: das
   Modul führt seine Zeiger selbst, also verschluckte er jede Wischgeste.

   In der BREITE bleibt es ein Stück hinter dem Inhalt daneben zurück, und der
   Rahmen wird sichtbar mitgeführt: das Brett führt seine Zeiger selbst, also
   ist überall dort, wo es liegt, das Wischen seins. Der Streifen links und
   rechts gehört dagegen der Seite — dort greift man zu, um zu scrollen, ohne
   im Rätsel zu landen. Der Rahmen sagt, wo die Grenze verläuft; ohne den
   Streifen liefe sie unsichtbar am Seitenrand entlang.

   Gemessen wird er ab dem KARTENRAND, nicht ab der Lesespalte: der Wirt zieht
   deren Polsterung heraus (.solution-grid--crab). Vorher lagen zwischen Brett
   und Bildschirmkante 44 + 20 + 16 px — der Streifen war fast doppelt so breit
   wie gewollt, und die Breite fehlte dem Rätsel. Jetzt sind es die 44, die hier
   stehen, und keine verdeckte Zutat mehr.

   Er ist FINGERKUPPENBREIT (44 px, dieselbe Untergrenze wie bei den
   Buchstabentasten, siehe CLAUDE.md „Touch targets"). Zehn Punkte waren es
   vorher, und das ist keine Grifffläche, sondern eine Ziellinie: man trifft
   sie nicht, landet im Brett und die Seite rührt sich nicht. Ein Streifen, den
   man nur mit Zielen erwischt, ist so gut wie keiner — lieber ein schmaleres
   Brett, das sich ohnehin zoomen und schieben lässt.

   `content-box` ist Absicht: das Skript setzt die Höhe des BRETTS, Rahmen und
   Streifen kommen außen dazu — mit `border-box` schnitten sie von der
   gerechneten Höhe ab. Die Breite nimmt sich beides selbst wieder heraus,
   sonst stünde das Rätsel über die Zeilen darunter hinaus und die Seite ließe
   sich seitlich schieben. */
.crab-solution {
  position: relative;
  /* Eine Quelle für beides — Breitenabzug und Rahmen dürfen nicht
     auseinanderlaufen. */
  --crab-solution-border: 2px;
  /* Der Griffstreifen für die Seite, links wie rechts — fingerkuppenbreit. */
  --crab-solution-inset: 44px;
  box-sizing: content-box;
  margin-inline: var(--crab-solution-inset);
  /* Der Abstand zur ersten Galerie darunter. Er stand früher als Flex-Abstand
     im Wirt; der ist mit `display: block` fort. */
  margin-block-end: 14px;
  width: calc(100% - 2 * (var(--crab-solution-border) + var(--crab-solution-inset)));
  overflow: hidden;
  /* Ein deutlicher Rahmen, damit die Grenzen des Rätsels klar sind. */
  border: var(--crab-solution-border) solid var(--border);
  border-radius: 10px;
  --cw-cell-bg:       var(--hex-key);
  --cw-cell-border:   var(--border);
  --cw-text:          var(--text);
  --cw-found-default: var(--violet);
  --cw-found-text:    #FBF3E3;
  --cw-multi:         var(--muted);
  /* Von allen gefunden. `--text` ist die dunkelste Farbe des Themas und damit
     im hellen wie im dunklen Theme die richtige Antwort auf „erledigt". */
  --cw-all:           var(--text);
}


/* Alleine gespielt: keine Rangliste, nur das Ergebnis. Groß gesetzt, weil es
   die einzige Zahl auf dem Schirm ist, die zählt. */
.final-solo { text-align: center; padding: 8px 0 4px; }
.final-solo-score {
  font-size: 2.6rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--accent-text);
}
.final-solo-label {
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 6px;
}
