* { box-sizing: border-box; }

html { height: 100%; }

/* body ist jetzt ein Flex-Container: #adventure-header liegt als erster Kindknoten IMMER oben
   (siehe index.html - Header ist nicht mehr Teil von #adventure-view, sondern globaler Body-
   Nachbar von #setup-view/#adventure-view). WICHTIG: height (nicht min-height) - #adventure-view
   braucht eine DEFINITE Hoehe von body, damit sein "flex:1; min-height:0" tatsaechlich auf "Rest
   der Viewport-Hoehe" aufloest und #chat-log (flex:1; min-height:0; overflow-y:auto darin) intern
   scrollen kann statt endlos mitzuwachsen (min-height:100% machte body de facto hoehen-indefinit,
   sobald Kinder groesser als der Viewport werden - flex:1 verlor dadurch seine Wirkung, #chat-log
   wuchs unbegrenzt mit statt zu scrollen). Ein langes #setup-view (kein flex-grow, natuerliche
   Content-Hoehe) laesst die Seite trotzdem ganz normal nach unten wachsen (Seiten-Scroll) - ein
   ueberlaufendes Flex-Kind wird NICHT abgeschnitten (kein overflow:hidden gesetzt), es macht das
   Dokument einfach hoeher als die feste body-Hoehe. */
body {
  height: 100%;
  display: flex;
  flex-direction: column;
  font-family: system-ui, sans-serif;
  margin: 0;
  background: #14151a;
  color: #e8e6e0;
}

/* Scrollbalken durchgaengig im Seiten-Farbschema statt der grauen Browser-Vorgabe - gilt global
   (jeder scrollbare Container: #chat-log, #party-panel, #info-panel, #debug-panel,
   #location-map-panel, Cost-/Voice-Overlays, ...), kein einzelnes Element muss extra markiert
   werden. Firefox (scrollbar-width/-color) + WebKit/Blink (::-webkit-scrollbar) getrennt, da es
   dafuer keine gemeinsame Syntax gibt. */
* {
  scrollbar-width: thin;
  scrollbar-color: #4a3f2a #1a1b21;
}
*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
*::-webkit-scrollbar-track {
  background: #1a1b21;
}
*::-webkit-scrollbar-thumb {
  background: #4a3f2a;
  border-radius: 6px;
  border: 2px solid #1a1b21;
}
*::-webkit-scrollbar-thumb:hover {
  background: #7a4f2b;
}
*::-webkit-scrollbar-corner {
  background: #1a1b21;
}

h1 { font-size: 1.5rem; margin: 0; }
h2 { font-size: 1.1rem; margin-top: 1.5rem; color: #c9a876; }

.hint { font-size: 0.85rem; color: #888; font-weight: normal; }

.hidden { display: none; }

/* Setup-Wizard: breiter als der alte schmale Setup-Screen, damit die Card-Grids (Abenteuer/
   Charakterbibliothek/Party) genug Platz haben und auf breiten Bildschirmen (Full HD+) mehr als
   die Haelfte der Breite genutzt wird - siehe #setup-steps/.card-grid weiter unten.
   WICHTIG: width:100% ist hier Pflicht, nicht nur max-width - #setup-view ist ein Flex-Item von
   body (siehe body-Regel oben) UND hat "margin: ... auto ..." zur Zentrierung. Auto-Margins auf
   der Kreuzachse eines Flex-Items schalten laut Spec die sonst uebliche Stretch-Ausrichtung ab -
   ohne width:100% waere #setup-view deshalb nur so breit wie sein Inhalt (shrink-to-fit) und
   haette dadurch je nach GERADE SICHTBAREM Schritt eine andere tatsaechliche Breite (der
   ausgeblendete Rest traegt zur Breitenberechnung nichts bei) - das liess den Container UND alles
   darin (z.B. #setup-steps) beim Schrittwechsel sichtbar springen/schrumpfen. */
#setup-view {
  width: 100%;
  max-width: 1600px;
  margin: 1.5rem auto 3rem;
  padding: 0 1rem;
}

input[type="text"] {
  background: #1e1f26;
  border: 1px solid #3a3b45;
  color: #e8e6e0;
  padding: 0.4rem 0.6rem;
  border-radius: 4px;
  margin-left: 0.5rem;
}

button {
  background: #2a2b34;
  border: 1px solid #3a3b45;
  color: #e8e6e0;
  padding: 0.5rem 0.9rem;
  border-radius: 4px;
  cursor: pointer;
  margin: 0.25rem 0.4rem 0.25rem 0;
}
button:hover { background: #34353f; }
button:disabled { opacity: 0.5; cursor: default; }
button:disabled:hover { background: #2a2b34; }
button.primary { background: #7a4f2b; border-color: #9a6a3f; }
button.primary:hover { background: #8f5c33; }

.stat-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.3rem 0;
}
.stat-label { width: 7rem; font-weight: 600; }
.stat-value { width: 2rem; text-align: center; }
.stat-mod { width: 2.2rem; text-align: center; color: #a89a7f; }

#setup-story-io { margin: 0 0 1rem; }

/* Dreistufige Farbskala fuer Wurf/Modifikator/Ergebnis in der Wuerfel-Zeile im
   Nachrichtenverlauf (appendDiceMessage in app.js). */
.dice-tier-good { color: #6fcf7c; font-weight: 600; }
.dice-tier-mid { color: #d9a441; font-weight: 600; }
.dice-tier-bad { color: #e0605a; font-weight: 600; }

.msg-bubble.narrator-loading,
.msg-bubble.artist-loading,
.msg-bubble.player-loading {
  display: flex;
  align-items: center;
}

.loading-dots {
  display: inline-flex;
  gap: 0.25rem;
  margin-left: 0.4rem;
}
.loading-dots span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #c9a876;
  animation: loading-bounce 1.2s infinite ease-in-out both;
}
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }

/* Kuenstler-Bubble: gleiche Bounce-Animation wie beim Erzaehler, aber warme Malerfarbe
   statt Gold, damit auf den ersten Blick klar ist, dass hier ein Bild statt Text entsteht. */
.msg-bubble.artist-loading .loading-dots span { background: #d97757; }

/* Spieler-Bubble: erscheint sofort wenn Discord-Voice+STT eine Aeusserung erkennt
   (discord-stt.ts "start"-Event), blaeulich statt Gold/Orange, damit auf den ersten Blick
   klar ist, dass hier gerade jemand transkribiert wird statt der Erzaehler/Kuenstler. */
.msg-bubble.player-loading .loading-dots span { background: #6f9bd9; }

/* Live-Zwischenergebnis waehrend eine Aeusserung noch andauert (discord-stt.ts periodisches
   Nachtranskribieren, siehe STT_PARTIAL_INTERVAL_MS/app.js updatePlayerPartial) - gedaempft/
   kursiv, damit klar bleibt, dass der Text noch nicht final ist und sich bis "done" noch
   aendern kann. Ueberschreibt die Lade-Punkte-Ausrichtung von .player-loading bewusst nicht -
   beide Klassen sind gleichzeitig aktiv, bis resolveLoadingLine sie ablegt. */
.msg-bubble.player-partial {
  opacity: 0.7;
  font-style: italic;
}

@keyframes loading-bounce {
  0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
  40% { transform: scale(1); opacity: 1; }
}

/* ---------- Chat-Bubbles (Nachrichtenverlauf) ---------- */
/* Erzaehler/Kuenstler/Erzaehler-Loading rechts (warmer Gold-Ton, Erzaehler heller als Spieler),
   Spieler links (gedeckterer Gold-Ton), Preflight/State-JSON/Erzaehler-Rohantwort/System(Wuerfel)
   rechts in Grau - reine "Buchhaltung", optisch bewusst zurueckhaltender als Spielinhalt. Gilt
   gleichermassen im normalen wie im Debug-Modus (appendMsgRow in app.js). Jede Zeile ist eine
   .msg-row (links/rechts ausgerichtet) mit einer .msg-col darin - die Spalte traegt oben optional
   ein kleines .msg-speaker-Label (Sprechername), darunter die eigentliche .msg-bubble (nur noch
   reiner Inhalt, kein "Name: "-Praefix mehr im Bubble-Text selbst). */
.msg-row {
  display: flex;
  margin: 0.35rem 0;
}
.msg-row.msg-left { justify-content: flex-start; }
.msg-row.msg-right { justify-content: flex-end; }
.msg-row.msg-center { justify-content: center; }

.msg-col {
  display: flex;
  flex-direction: column;
  max-width: 90%;
  min-width: 0;
}
.msg-row.msg-left .msg-col { align-items: flex-start; }
.msg-row.msg-right .msg-col { align-items: flex-end; }
.msg-row.msg-center .msg-col { align-items: center; }

.msg-speaker {
  font-size: 0.7rem;
  color: #8a7c62;
  padding: 0 0.3rem 0.15rem;
}

/* Erneut-Abspielen-Button neben dem Erzaehler-Label (siehe attachReplayButton in app.js) - spielt
   die archivierten Audio-Segmente dieser Runde ueber den Discord-Bot im Voice-Channel ab, ohne
   erneut XTTS/Piper zu bemuehen. Kompakt und unauffaellig, eckiger/kleiner als die uebrigen
   Buttons (kein eigenes "Wurf"-Gewicht, nur eine Nebenfunktion des Labels). Waehrend die
   Wiedergabe laeuft (Button zeigt Stop-Symbol, siehe setReplayButtonState) deutlich hervorgehoben,
   damit klar ist WELCHE der u.U. mehreren Erzaehler-Bubbles gerade zu hoeren ist. */
.replay-btn {
  background: transparent;
  border: 1px solid #5a4f3f;
  color: #a89a7f;
  width: 1.1rem;
  height: 1.1rem;
  padding: 0;
  border-radius: 3px;
  font-size: 0.6rem;
  line-height: 1;
  cursor: pointer;
  margin: 0 0 0 0.3rem;
  vertical-align: middle;
}
.replay-btn:hover { background: #3a2f1f; color: #e8e6e0; }
.replay-btn.replay-btn-playing {
  background: #7a4f2b;
  border-color: #c9a876;
  color: #fff;
}

.msg-bubble {
  max-width: 100%;
  border-radius: 12px;
  padding: 0.5rem 0.85rem;
  line-height: 1.45;
  word-wrap: break-word;
}

.msg-bubble.msg-narrator {
  background: #3a2f1f;
  border: 1px solid #7a4f2b;
}
.msg-bubble.msg-player {
  background: #241c12;
  border: 1px solid #5a3d24;
}
/* Kuenstler-Bubble: waehrend des Ladens (Punkte-Animation) wie eine normale Bubble, aber sobald
   das Szenenbild da ist (artist-loading-Klasse entfernt, siehe resolveArtistMessage in app.js)
   OHNE Bubble-Chrome - das Bild soll freistehend und zentriert erscheinen statt rechtsbuendig in
   einer Sprechblase (Nutzerwunsch). */
.msg-bubble.msg-artist {
  background: none;
  border: none;
  padding: 0;
}
.msg-bubble.msg-artist.artist-loading {
  background: #3a2f1f;
  border: 1px solid #7a4f2b;
  padding: 0.5rem 0.85rem;
}
.msg-bubble.msg-meta {
  background: #24252c;
  border: 1px solid #3a3b45;
  color: #a8a29a;
  font-size: 0.85rem;
}

/* Rundenmarker (Playtest "kampf-testarena-40": "mittig zentriert aber sehr dezent") - bewusst
   ohne Bubble-Chrome (kein Hintergrund/Rahmen), nur ein leiser Trenntext. */
.msg-bubble.msg-round-marker {
  background: none;
  border: none;
  padding: 0.15rem 0;
  color: #5c5952;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Orts-/Ziel-/Weltereignis-Marker (Diskussion "Orts-/Objective-Marker im Chatverlauf", siehe
   appendLocationChangeMarker/appendObjectiveChangeMarker/appendWorldEventMarker in app.js) - genau
   dieselbe Bubble-Chrome (Hintergrund/Rahmen/Schriftgroesse) wie ein Wuerfelwurf (.msg-meta oben),
   damit diese Spielverlaufs-Ereignisse optisch als gleichrangige Kategorie erkennbar sind - nur die
   Textfarbe unterscheidet die drei Marker-Arten voneinander. */
.msg-bubble.msg-location-marker,
.msg-bubble.msg-objective-marker,
.msg-bubble.msg-world-event-marker {
  background: #24252c;
  border: 1px solid #3a3b45;
  font-size: 0.85rem;
}
.msg-bubble.msg-location-marker { color: #9b9690; }
.msg-bubble.msg-objective-marker { color: #c9a876; }
.msg-bubble.msg-world-event-marker { color: #7fb0c9; }

/* Modell-Zuverlaessigkeits-Feedback (siehe appendModelReliabilityFeedback in app.js) - mittig
   zentrierte Karte, dezentes/schwaches Hellblau (Nutzerwunsch), damit sie sich klar von normalen
   Erzaehler-/System-Bubbles abhebt, ohne grell zu wirken. Dieselbe Akzentfarbe wie der
   Weltereignis-Marker (#7fb0c9), hier nur als sehr schwacher Flaechenton statt als reine Textfarbe. */
.msg-bubble.msg-model-feedback {
  background: rgba(127, 176, 201, 0.14);
  border: 1px solid rgba(127, 176, 201, 0.4);
  color: #dce8ee;
  max-width: 30rem;
  font-size: 0.88rem;
  text-align: center;
}
.model-feedback-prompt {
  margin: 0 0 0.6rem 0;
}
.model-feedback-form {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
}
.model-feedback-radio {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  cursor: pointer;
}
.model-feedback-comment {
  width: 100%;
  min-height: 2.5rem;
  resize: vertical;
  background: #1c232a;
  color: #e8e6e0;
  border: 1px solid rgba(127, 176, 201, 0.4);
  border-radius: 6px;
  padding: 0.3rem 0.5rem;
  font-family: inherit;
  font-size: 0.85rem;
}
.model-feedback-submit {
  margin-top: 0.2rem;
  padding: 0.3rem 1.2rem;
  border-radius: 6px;
  border: 1px solid #7fb0c9;
  background: rgba(127, 176, 201, 0.25);
  color: #dce8ee;
  cursor: pointer;
}
.model-feedback-submit:hover {
  background: rgba(127, 176, 201, 0.4);
}
.model-feedback-submit:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Nachbesprechung (generateDebrief) - bewusst deutlich abgesetzt von normalen Erzaehler-Bubbles
   (andere Hintergrundfarbe, Akzent-Rahmen, breiter, weiches Absatz-Layout via pre-wrap), damit
   sofort klar ist: das hier ist Meta-/Out-of-Character-Inhalt, kein In-Fiction-Erzaehltext. */
.msg-bubble.msg-debrief {
  background: #1f2a24;
  border: 1px solid #4a7a5f;
  color: #e8e6e0;
  max-width: 46rem;
  white-space: pre-wrap;
}

/* Preflight/State-JSON/Erzaehler-Rohantwort sind <details>-Elemente (nativ auf-/zuklappbar,
   siehe appendDebugTurnBlock in app.js) mit ZWEI verschachtelten <details> darin ("Prompt"/
   "Antwort", huebsch formatierte JSON-Antwort falls moeglich) - alle drei Ebenen starten
   zugeklappt. Werden IMMER erzeugt (siehe .msg-debug unten), nicht nur wenn der Debug-Modus
   gerade aktiv ist - so kann rueckwirkend jeder bisherige Zug eingesehen werden. */
.msg-bubble.msg-meta summary {
  cursor: pointer;
  font-weight: 600;
  color: #c3bdb3;
}
.msg-bubble.msg-meta details {
  margin: 0.4rem 0 0 0.9rem;
  border-left: 1px dashed #3a3b45;
  padding-left: 0.6rem;
}
.msg-bubble.msg-meta details summary {
  font-weight: 500;
  color: #a8a29a;
}
.msg-bubble.msg-meta pre {
  white-space: pre-wrap;
  margin: 0.4rem 0 0;
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 0.75rem;
  line-height: 1.4;
}

/* Debug-Bloecke (Preflight/Erzaehler/State-JSON/Szenenbild-/Portrait-Prompts) werden IMMER
   erzeugt (siehe appendDebugTurnBlock in app.js), aber standardmaessig ausgeblendet - #chat-log
   bekommt beim Umschalten des "Debug-Modus"-Buttons die Klasse "debug-visible", das wirkt dank
   reiner CSS-Steuerung SOFORT auf ALLE bereits vorhandenen UND alle kuenftigen .msg-debug-Zeilen
   (kein erneutes Rendern noetig). */
.msg-debug { display: none; }
#chat-log.debug-visible .msg-row.msg-debug { display: flex; }

.voice-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  background: #1e1f26;
  padding: 0.4rem 0.7rem;
  border-radius: 4px;
  margin: 0.3rem 0;
}
.voice-row select {
  background: #2a2b34;
  color: #e8e6e0;
  border: 1px solid #3a3b45;
  border-radius: 4px;
  padding: 0.2rem;
}

/* ---------- Adventure-Screen: 100% Breite, 35/65-Split, volle Hoehe ---------- */

#adventure-view {
  width: 100%;
  /* Kein festes height:100vh mehr - #adventure-header ist jetzt ein Body-Geschwister DAVOR (siehe
     body-Flex-Layout oben), #adventure-view fuellt per flex:1 exakt die restliche Viewport-Hoehe. */
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* Ohne dieses Override gewinnt #adventure-view's eigenes "display: flex" IMMER gegen ".hidden
   { display: none }" (ID-Selektor schlaegt Klassen-Selektor unabhaengig von der Stylesheet-
   Reihenfolge) - das Panel blieb dadurch waehrend des gesamten Setup-Wizards sichtbar (leerer
   Chat-/Party-Bereich unter #setup-view), obwohl die "hidden"-Klasse korrekt gesetzt war. Gleicher
   Fehler/Fix wie #debug-panel.hidden/#combat-panel.hidden weiter unten. */
#adventure-view.hidden { display: none; }

#adventure-header {
  width: 100%;
  padding: 0.7rem 1.2rem;
  border-bottom: 1px solid #3a3b45;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
#header-top-row {
  display: flex;
  align-items: center;
  gap: 1rem;
}
/* h1 behaelt seine natuerliche Breite, #status-bar nimmt den Rest der Zeile ein und zentriert
   seinen Inhalt darin selbst (justify-content:center weiter unten) - dieselbe Zentrierung wie
   #ttfss-detail-row darunter, statt wie zuvor rechtsbuendig (justify-content:space-between). */
#header-top-row h1 { flex-shrink: 0; }

/* Live-Teilzeiten-Zeile unter der normalen Statusleiste (siehe ttfss-hub.ts) - per Klick auf
   #ttfss-widget/#ntpt-widget/#ttra-widget/#wtnt-widget ein-/ausgeblendet (app.js, alle vier
   steuern dieselbe Zeile), Elemente zentriert statt an einer Seite ausgerichtet. Enthaelt ZWEI
   unabhaengige Tracks (TTFT/Satz1/TTS/Discord -> #ttfss-total, SD/State JSON -> #ttra-total)
   PLUS #ntpt-total (playback->speechDone) und #wtnt-total, siehe app.js fuer die Zaehllogik. NUR
   die vier Boxen, die auch oben als eigenes KPI-Widget stehen (#ttfss-total/#ntpt-total/
   #ttra-total/#wtnt-total), bekommen einen leicht blaeulichen Ton - die reinen Zwischenschritte
   (TTFT/Satz1/TTS/Discord/SD/State JSON) bleiben im neutralen Braun-Ton der uebrigen .sys-mod-
   Widgets, damit klar bleibt, welche Werte "Kennzahlen" und welche nur Zwischenschritte sind.
   Die vier Widgets oben bekommen denselben blauen Ton, solange die Zeile sichtbar ist
   (.ttfss-widget-active, von app.js beim Toggle gesetzt). .ttfss-phase-active markiert
   zusaetzlich die GERADE live hochzaehlende Box in Gold, unabhaengig davon ob blau oder neutral
   (siehe handleTtfssEvent in app.js). */
#ttfss-detail-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  justify-content: center;
}
#ttfss-detail-row.hidden { display: none; }
#ttfss-widget, #ntpt-widget, #ttra-widget, #wtnt-widget, #cost-widget { cursor: pointer; }

#ttfss-total,
#ntpt-total,
#ttra-total,
#wtnt-total,
#ttfss-widget.ttfss-widget-active,
#ntpt-widget.ttfss-widget-active,
#ttra-widget.ttfss-widget-active,
#wtnt-widget.ttfss-widget-active,
#cost-widget.ttfss-widget-active {
  background: #1a2531;
  border-color: #3d5266;
}
#ttfss-total .sys-mod-label,
#ntpt-total .sys-mod-label,
#ttra-total .sys-mod-label,
#wtnt-total .sys-mod-label,
#ttfss-widget.ttfss-widget-active .sys-mod-label,
#ntpt-widget.ttfss-widget-active .sys-mod-label,
#ttra-widget.ttfss-widget-active .sys-mod-label,
#wtnt-widget.ttfss-widget-active .sys-mod-label,
#cost-widget.ttfss-widget-active .sys-mod-label {
  color: #6f95b0;
}
#ttfss-total .sys-mod-value,
#ntpt-total .sys-mod-value,
#ttra-total .sys-mod-value,
#wtnt-total .sys-mod-value,
#ttfss-widget.ttfss-widget-active .sys-mod-value,
#ntpt-widget.ttfss-widget-active .sys-mod-value,
#ttra-widget.ttfss-widget-active .sys-mod-value,
#wtnt-widget.ttfss-widget-active .sys-mod-value,
#cost-widget.ttfss-widget-active .sys-mod-value {
  color: #a9c4d8;
}

.sys-mod.ttfss-phase.ttfss-phase-active {
  border-color: #d9a441;
}
.sys-mod.ttfss-phase.ttfss-phase-active .sys-mod-value {
  color: #d9a441;
}
/* #ntpt-total/#wtnt-total sind zugleich blau getoent (ID-Regel oben) UND ein .ttfss-phase (fuers
   Ticken) - ID-Spezifitaet schlaegt sonst die .ttfss-phase-active-Regel, deshalb hier explizit
   per ID nachgezogen, damit die Gold-Hervorhebung waehrend des Tickens tatsaechlich sichtbar
   wird. */
#ntpt-total.ttfss-phase-active,
#wtnt-total.ttfss-phase-active {
  border-color: #d9a441;
}
#ntpt-total.ttfss-phase-active .sys-mod-value,
#wtnt-total.ttfss-phase-active .sys-mod-value {
  color: #d9a441;
}

/* Markiert einen Zug, bei dem der zugehoerige Call trotz aller Versuche scheiterte (aktuell nur
   "stateCall", siehe ttfss-hub.ts/handleTtfssEvent) - bewusst eigene Regel statt nur ueber
   .ttfss-phase-active, da die Box zu diesem Zeitpunkt bereits fertig getickt hat (nicht mehr
   aktiv) und die rote Faerbung bis zum naechsten Zug (reset()) stehen bleiben soll. */
.sys-mod.ttfss-phase.ttfss-phase-error {
  border-color: #c0392b;
}
.sys-mod.ttfss-phase.ttfss-phase-error .sys-mod-value {
  color: #e0564a;
}

/* Container fuer beide Widget-Gruppen (System-Auslastung + Dienste-Status) - eine Zeile,
   durch #status-divider optisch in zwei Bereiche getrennt statt gestapelt. flex:1 + center
   zentriert den Inhalt ueber die gesamte verbleibende Breite (nach h1), genau wie
   #ttfss-detail-row darunter zentriert ist. */
#status-bar {
  display: flex;
  flex: 1;
  flex-direction: row;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  justify-content: center;
}
#status-divider {
  width: 1px;
  height: 22px;
  background: #3a3b45;
  flex-shrink: 0;
}

/* Vier kompakte System-Module (CPU/RAM/GPU/VRAM), je Label + kleiner Balken + Wert -
   platzsparende Variante des grossen Balkens aus xtts-prototype/index.html. Siehe
   pollSystemWidget in app.js, Daten von GET /api/system (web-server.ts). */
#sys-widget {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.sys-mod {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.78rem;
  color: #a89a7f;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.3rem 0.55rem;
}
.sys-mod-label {
  font-weight: 600;
  color: #8a7c62;
}
.sys-mod-track {
  width: 38px;
  height: 5px;
  background: #14151a;
  border: 1px solid #3a3b45;
  border-radius: 3px;
  overflow: hidden;
  flex-shrink: 0;
}
.sys-mod-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #6fcf7c, #d9a441);
  transition: width 0.4s;
}
.sys-mod.sys-mod-err .sys-mod-value { color: #e0605a; }

/* "Inference-Costs"-Widget (siehe cost-stats.ts) - #cost-widget sitzt in derselben Toggle-Gruppe
   wie #ttfss-widget/#ntpt-widget/#ttra-widget/#wtnt-widget (teilen sich alle #ttfss-detail-row,
   siehe .ttfss-widget-active weiter oben und toggleTtfssDetailRow in app.js) und bekommt daher
   dieselbe .ttfss-widget-active-Klasse gesetzt - hier aber grueneingefaerbt (statt dem blauen Ton
   der Timing-Widgets), passend zu .cost-category unten. Ueberschreibt die blaue Basis-Regel
   weiter oben rein durch spaetere Reihenfolge im Stylesheet (gleiche Selektor-Spezifitaet). */
#cost-widget { cursor: pointer; }
#cost-widget.ttfss-widget-active {
  background: #1a2e22;
  border-color: #3d664a;
}
#cost-widget.ttfss-widget-active .sys-mod-label { color: #6fb085; }
#cost-widget.ttfss-widget-active .sys-mod-value { color: #a9d8bc; }

/* .cost-category sitzt als Kosten-Eintrag INNERHALB derselben Zeile wie die Timing-Phasen,
   gruener statt blauer/neutraler Ton um die vier optisch von den Zeit-Boxen abzuheben. Klick auf
   irgendeine der vier oeffnet #cost-detail-overlay mit der Gesamtuebersicht (nicht nur dieser
   einen Kategorie), siehe openCostDetailOverlay in app.js. */
.cost-category {
  cursor: pointer;
  background: #1a2e22;
  border-color: #3d664a;
}
.cost-category .sys-mod-label { color: #6fb085; }
.cost-category .sys-mod-value { color: #a9d8bc; }
.cost-category:hover { border-color: #6fb085; }

/* Popup mit der Kosten-Gesamtuebersicht (alle vier Kategorien + Summe + zugrunde liegende
   .env-Preise) - position:fixed (anders als #dice-overlay, das relativ zu seinem Spiel-Panel
   positioniert ist) damit es unabhaengig von der Scroll-Position ueber der GESAMTEN Seite liegt,
   siehe openCostDetailOverlay in app.js. */
#cost-detail-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 14, 0.78);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
#cost-detail-overlay.hidden { display: none; }
#cost-detail-panel {
  position: relative;
  background: #1e1f26;
  border: 1px solid #3d664a;
  border-radius: 8px;
  padding: 1.2rem 1.4rem;
  width: 95%;
  max-height: 85vh;
  overflow-y: auto;
  color: #d8cdb8;
}
#cost-detail-panel h3 {
  margin: 0 0 0.8rem;
  color: #a9d8bc;
  font-size: 1rem;
}
#cost-detail-close {
  position: absolute;
  top: 0.6rem;
  right: 0.7rem;
  background: none;
  border: none;
  color: #a89a7f;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
}
#cost-detail-close:hover { color: #d8cdb8; }

/* Vier Spalten (LLM/STT/TTS/SD) nebeneinander, siehe renderCostDetailBody in app.js - jede
   Spalte listet zuerst die zugrunde liegenden .env-Preise dieser Komponente, dann die
   Nutzungs-Felder, dann ihre eigene Teilsumme. Faellt auf schmaleren Bildschirmen auf 2, dann 1
   Spalte zurueck (Media-Queries unten). */
.cost-detail-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.9rem;
  align-items: start;
}
.cost-detail-col {
  background: #14151a;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.7rem 0.8rem;
  min-width: 0; /* Grid-Items schrumpfen sonst nicht unter die Inhaltsbreite (lange Preis-Labels) */
}
@media (max-width: 900px) {
  .cost-detail-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
  .cost-detail-grid { grid-template-columns: 1fr; }
}

#cost-detail-body table { width: 100%; border-collapse: collapse; font-size: 0.8rem; table-layout: fixed; }
#cost-detail-body th, #cost-detail-body td {
  text-align: left;
  padding: 0.25rem 0.35rem;
  border-bottom: 1px solid #3a3b45;
  overflow-wrap: break-word;
}
#cost-detail-body th { color: #8a7c62; font-weight: 600; }
#cost-detail-body td.num { text-align: right; font-variant-numeric: tabular-nums; }
#cost-detail-col h4 {
  margin: 0 0 0.5rem;
  color: #a9d8bc;
  font-size: 0.88rem;
  font-weight: 600;
}
/* Preis-Zeilen (.env-Variablen, IMMER die erste Tabelle jeder Spalte) optisch leicht abgesetzt
   von den Nutzungs-Feldern darunter - eigene Farbe statt der neutralen Feld-Werte. */
.cost-detail-col table:first-of-type td.num { color: #d9a441; }
#cost-detail-body .cost-detail-total {
  margin-top: 0.6rem;
  padding-top: 0.5rem;
  border-top: 1px solid #3a3b45;
  font-weight: 600;
  color: #a9d8bc;
}
#cost-detail-body .cost-detail-grand-total {
  margin-top: 1.2rem;
  padding-top: 0.8rem;
  border-top: 2px solid #6fb085;
  font-size: 1.05rem;
  color: #d9a441;
}
/* Runden/Spieldauer-Zeile ganz oben im Popup (siehe #game-stats-widget unten fuers Pendant in
   der Statuszeile) - eigene Zeile VOR dem 4-Spalten-Grid. */
#cost-detail-body .cost-detail-game-stats {
  margin-bottom: 0.9rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid #3a3b45;
  color: #a89a7f;
  font-size: 0.85rem;
}

/* "N Runden | XhYm"-Widget links vom Kosten-Widget (siehe cost-stats.ts getGameStats) - reine
   Anzeige ohne eigene Interaktion, deshalb kein cursor:pointer/Label wie bei den anderen
   .sys-mod-Widgets (der Wert ist selbsterklaerend, kein Kuerzel noetig). */
#game-stats-widget { color: #a89a7f; }

/* Fuenf Dienste-Status-Module (LLM/XTTS/STT/SD=Forge/Discord), je Status-Lampe + Label.
   Fuenf Zustaende (siehe service-status.ts; Discord nutzt nur offline/ready/busy):
     offline  - rot, kein Puls   - nicht erreichbar/nicht geladen/nicht verbunden
     starting - gelb, Puls       - startet gerade (nicht bei SD, siehe unten)
     idle     - gelb, kein Puls  - NUR SD: Prozess bereit, Checkpoint aus dem VRAM entladen
     ready    - gruen, kein Puls - bereit und untaetig (Discord: im Voice-Channel verbunden)
     busy     - gruen, Puls     - aktiv in Benutzung (Zug/Synthese/Transkription/Bildgenerierung/
                Discord: liest gerade im Voice-Channel vor)
   Siehe pollSystemWidget in app.js, Daten von GET /api/system (web-server.ts, services-Feld). */
#svc-widget {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.svc-mod {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: #a89a7f;
  white-space: nowrap;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.3rem 0.55rem;
}
.svc-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #5a5f6e;
  flex-shrink: 0;
}
.svc-mod.svc-offline .svc-dot { background: #e0605a; }
.svc-mod.svc-starting .svc-dot {
  background: #d9a441;
  animation: svc-pulse 1.2s infinite ease-in-out;
}
.svc-mod.svc-idle .svc-dot { background: #d9a441; }
.svc-mod.svc-ready .svc-dot { background: #6fcf7c; }
.svc-mod.svc-busy .svc-dot {
  background: #6fcf7c;
  animation: svc-pulse 1.2s infinite ease-in-out;
}
@keyframes svc-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* Discord-Widget ist klickbar (app.js): pausiert/reaktiviert Sprachausgabe + STT-Zuhoeren
   gemeinsam fuer Spielpausen (siehe /api/discord-pause, web-server.ts). Blaeulich statt der
   ueblichen rot/gelb/gruen-Ampel, damit "pausiert" auf den ersten Blick ein eigener, neutraler
   Zustand ist statt ein Fehler (rot) oder Uebergang (gelb). */
.svc-mod[data-svc="discord"] { cursor: pointer; }
.svc-mod[data-svc="discord"].svc-paused .svc-dot {
  background: #6f9bd9;
  animation: none;
}

#adventure-layout {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
}

#party-panel {
  flex: 0 0 32%;
  max-width: 32%;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
  border-right: 1px solid #3a3b45;
}

/* Ersetzt #party-panel komplett im Debug-Modus (applyDebugMode in app.js) - Charaktere/Plan als
   read-only huebsch formatiertes JSON + Export, darunter alle Prompts aus prompts.json editierbar
   + Speichern-Buttons (siehe #debug-prompts-list). Violetter Debug-Ton wie .debug-panel/
   button.debug, damit auf den ersten Blick klar ist, dass das kein Spielinhalt ist. */
#debug-panel {
  flex: 0 0 32%;
  max-width: 32%;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
  border-right: 1px solid #5a4a6a;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
/* Ohne dieses Override gewinnt #debug-panel's "display: flex" IMMER gegen ".hidden { display:
   none }" (ID-Selektor schlaegt Klassen-Selektor unabhaengig von der Reihenfolge im Stylesheet) -
   das Panel blieb dadurch dauerhaft sichtbar, selbst mit gesetzter hidden-Klasse (app.js
   applyDebugMode togglet nur die Klasse, nicht das display selbst). #party-panel hatte dasselbe
   Problem nie, weil dort nie ein eigenes "display" auf der ID gesetzt ist. */
#debug-panel.hidden { display: none; }
#debug-panel h3 {
  margin: 0.8rem 0 0.2rem;
  color: #b89ad0;
  font-size: 0.95rem;
}
#debug-panel h3:first-child { margin-top: 0; }
#debug-panel textarea {
  width: 100%;
  min-height: 110px;
  background: #1a1621;
  border: 1px dashed #5a4a6a;
  color: #c9b8d9;
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 0.75rem;
  line-height: 1.4;
  padding: 0.5rem;
  border-radius: 6px;
  resize: vertical;
}
.debug-prompt-field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin-bottom: 0.5rem;
}
.debug-prompt-field label {
  font-size: 0.75rem;
  color: #a89a7f;
}
.debug-prompt-field textarea { min-height: 90px; }

/* NPC-Aufzaehlung + Discovery-State-Visualisierung (siehe renderDiscoveryDebug in app.js,
   dm-prompt.ts DiscoveryState) - Status-Farben wiederverwenden dieselbe Ampel-Logik wie
   .dice-tier-good/-mid (Wuerfel-Zeile): gruen = entdeckt/freigeschaltet, amber = teilweise/
   offen, gedaempftes Grau = noch gesperrt/nicht erreicht. */
#discovery-debug-panel {
  margin-top: 0.8rem;
  padding-top: 0.6rem;
  border-top: 1px dashed #5a4a6a;
}
#discovery-debug-panel h3 {
  margin: 0.8rem 0 0.2rem;
  color: #b89ad0;
  font-size: 0.95rem;
}
#discovery-state-details summary {
  margin: 0 0 0.2rem;
  color: #b89ad0;
  font-size: 0.95rem;
  font-weight: bold;
  cursor: pointer;
  user-select: none;
}
#discovery-state-details[open] summary { margin-bottom: 0.2rem; }
#discovery-state-details #debug-discovery { margin-top: 0.2rem; }
.debug-list {
  list-style: none;
  padding: 0;
  margin: 0 0 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.discovery-item {
  font-size: 0.8rem;
  padding: 0.15rem 0;
}
.discovery-status-full { color: #6fcf7c; }
.discovery-status-partial { color: #d9a441; }
.discovery-status-locked { color: #766a86; }
#debug-discovery .discovery-group { margin-bottom: 0.6rem; }
#debug-discovery .discovery-group-title {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #8a7c9a;
  margin-bottom: 0.2rem;
}
#debug-prompt-actions {
  display: flex;
  gap: 0.5rem;
  margin: 0.4rem 0 0.2rem;
  position: sticky;
  bottom: 0;
  background: #14151a;
  padding-top: 0.5rem;
}

#chat-panel {
  flex: 0 0 50%;
  max-width: 50%;
  min-height: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 1rem;
}

#chat-log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.6rem;
  background: #1a1b21;
}
.scene-image {
  display: block;
  width: 100%;
  max-width: 500px;
  border-radius: 6px;
  border: 1px solid #3a3b45;
}

#debrief-actions { display: flex; gap: 0.6rem; padding: 0.6rem 0; }
#debrief-actions.hidden { display: none; }

/* Debug-UI: bewusst optisch abgesetzt (violett statt Gold), damit klar ist, dass das kein
   spielrelevantes UI ist, sondern nur zum Konsistenz-Check (Abenteuer-Plan, Prompts, Bild-
   Prompts). */
button.debug { border-color: #5a4a6a; color: #b89ad0; }
button.debug:hover { background: #362c40; }
button.debug.debug-active { background: #5a4a6a; color: #fff; }

.checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: #a8a29a;
  margin: 0.4rem 0;
  cursor: pointer;
}

/* Zug-Steuerung: fester Balken unter dem Nachrichtenverlauf statt schwebendem Widget -
   Live-Transkripte landen direkt im Chat-Log (siehe .player-loading unten), hier bleiben
   nur noch die Bild-Checkbox und der Senden-Button. */
#turn-controls {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  padding-top: 0.6rem;
}
#turn-controls .checkbox-row { margin: 0; }

/* Rollenbasiertes Ausblenden (siehe applyRoleVisibility in app.js, GET /api/me) - greift per
   Body-Klasse unabhaengig davon, ob/wann #turn-controls sein eigenes "hidden" (Setup-Wizard-
   Schritt) sonst noch gesetzt/entfernt bekommt. Nur ein bestaetigter Spieler-Login blendet aus -
   ohne Login bleibt der volle (Host-)Funktionsumfang wie bisher erhalten. */
body.role-player #turn-controls,
body.role-anonymous #turn-controls {
  display: none !important;
}
/* Spieler sehen statt des Host-Setup-Wizards die Lobby (siehe lobby.js/#lobby-view) - "hidden"
   auf #lobby-view selbst steuert lobby.js abhaengig vom Ladezustand (Abenteuer gewaehlt? Runde
   schon live?), diese Regel sorgt nur dafuer, dass #setup-view fuer Spieler NIE aufblitzt. */
body.role-player #setup-view,
body.role-anonymous #setup-view {
  display: none !important;
}

/* Rechte 18%-Spalte in #adventure-layout (siehe index.html #info-panel) - Objective/History oben,
   Ortskarte in der Mitte, aktueller Ort unten, vertikal gestapelt. */
#info-panel {
  flex: 0 0 18%;
  max-width: 18%;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
  border-left: 1px solid #3a3b45;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
}

/* Aktueller Ort der Gruppe (state.location, siehe renderLocation in app.js) - leer/kollabiert vor
   dem ersten Zug (kein GameState), sonst eine eigene volle-Breite-Karte (wie .char-card, aber
   einzeilig) - bewusst prominent, da der Ort staendig relevanter Kontext ist, keine Nebeninfo.
   Feste, grosszuegige Hoehe (Nutzerwunsch: #info-panel-Rahmen oben/unten fest, Ortskarte in der
   Mitte fuellt den Rest) statt content-abhaengiger Hoehe - Text bleibt dabei vertikal zentriert,
   ein eigener Scrollbalken faengt den seltenen Fall eines ungewoehnlich langen Ortsnamens ab. */
#party-location {
  flex: 0 0 4.5rem;
  height: 4.5rem;
  overflow-y: auto;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 0.6rem 0.9rem;
  font-size: 1.15rem;
  font-weight: 600;
  color: #c9a876;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
#party-location:empty { display: none; }

/* Aktuelle Aufgabe der Gruppe (DiscoveryState.activeObjective, siehe renderObjective in app.js) -
   dieselbe Karten-Optik wie #party-location, aber unauffaelliger (kein Titel-Ton) und mit ".hidden"
   statt ":empty" versteckt, weil der Text bei leerem activeObjective in app.js unveraendert bleiben
   soll (kein Ueberschreiben eines vorherigen Standes durch einen Zug, der keine neue Aufgabe setzt).
   Ebenfalls feste, grosszuegige Hoehe wie #party-location (siehe Kommentar dort). */
#party-objective {
  flex: 0 0 7rem;
  height: 7rem;
  overflow-y: auto;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-left: 3px solid #c9a876;
  border-radius: 8px;
  padding: 0.5rem 0.9rem;
  font-size: 0.95rem;
  color: #d8d5c9;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}
#party-objective.hidden { display: none; }

/* Historie bereits abgeloester Ziele (siehe objectiveHistory/renderObjectiveHistory in app.js) -
   per Klick auf #party-objective auf-/zugeklappt, direkt darunter (Abstand ueber #info-panels
   eigenen "gap", siehe dort), gleiche Kartenoptik aber ohne Akzentrahmen (kein aktives Ziel mehr). */
#objective-history {
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 0.5rem 0.9rem;
  font-size: 0.85rem;
  color: #a8a29a;
}
#objective-history.hidden { display: none; }
.objective-history-row {
  padding: 0.15rem 0;
}
.objective-history-row + .objective-history-row {
  border-top: 1px dashed #3a3b45;
}

/* Ortskarte (siehe renderLocationMap in app.js) - Panel als deterministisches SVG-Diagramm ohne
   Physik-Layout (Rueckgrat laeuft nach UNTEN, improvisierte Zweige nach rechts - vertikal statt
   horizontal, passend zur schmalen #info-panel-Spalte). Dauerhaft sichtbarer Teil von #info-panel
   (kein Ein-/Ausklapp-Header mehr). flex:1 (statt einer festen max-height) fuellt genau den Raum
   zwischen #party-objective (fest, oben) und #party-location (fest, unten) - siehe Kommentare dort.
   Eigenes Scrollen bleibt (statt endlos mitzuwachsen), falls eine lange Kette die verfuegbare Hoehe
   sprengt. Zentriert sein Kind (svg bzw. der Platzhaltertext) IMMER horizontal UND vertikal, statt
   wie zuvor am linken Rand zu kleben (Nutzerfund: bei einer rein linearen Karte mit nur einer Spur -
   Plan-Orte ohne improvisierte Abzweigung - macht die schmale SVG-Breite die Karte optisch
   linksbuendig, obwohl das Panel selbst die volle Spaltenbreite hat). */
#location-map-panel {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 0.6rem;
  overflow: auto;
  width: 100%;
  box-sizing: border-box;
}
#location-map-svg {
  display: block;
  padding: 2px;
  /* #location-map-panel ist jetzt ein Flex-Container (siehe dort) - ohne flex-shrink:0 wuerde das
     SVG bei knappem Platz unter seine per width/height-Attribut gesetzte, aus der Knotenzahl
     berechnete Groesse gequetscht statt (wie zuvor mit max-height+overflow:auto) unveraendert zu
     bleiben und bei Bedarf im Panel zu scrollen. */
  flex-shrink: 0;
  /* Knoten-Labels sind text-anchor:middle und koennen bei laengeren Ortsnamen etwas ueber die
     rechnerische viewBox hinausragen (siehe LOCATION_MAP_MARGIN in app.js) - overflow:visible
     verhindert, dass die SVG-eigene Standard-Clip-Box (wie bei jedem Wurzel-<svg>) das letzte
     Zeichen abschneidet; #location-map-panel darueber faengt echten Overflow via overflow:auto ab. */
  overflow: visible;
}
#location-map-svg.hidden { display: none; }
#location-map-empty {
  color: #766a86;
  font-size: 0.85rem;
  text-align: center;
  padding: 0.6rem 0;
}
#location-map-empty.hidden { display: none; }
.location-map-edge {
  stroke: #3a3b45;
  stroke-width: 1.5;
  fill: none;
}
.location-map-edge--improvised {
  stroke-dasharray: 3 3;
}
.location-map-node-circle {
  fill: #14151a;
  stroke: #6b6f7d;
  stroke-width: 1.5;
}
.location-map-node-circle--real {
  stroke: #c9a876;
}
.location-map-node-circle--improvised {
  stroke: #6b6f7d;
  stroke-dasharray: 2 2;
}
.location-map-node-circle--current {
  fill: #c9a876;
  stroke: #f0decb;
  stroke-width: 2;
}
.location-map-node-label {
  fill: #a8a29a;
  font-size: 9px;
  text-anchor: middle;
  pointer-events: none;
}
.location-map-node-label--current {
  fill: #d8d5c9;
  font-weight: 600;
}
.location-map-node-hit {
  fill: transparent;
  cursor: pointer;
}
.location-map-tooltip {
  position: absolute;
  background: #0f1014;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  font-size: 0.8rem;
  color: #d8d5c9;
  pointer-events: none;
  white-space: nowrap;
  z-index: 20;
}
.location-map-tooltip.hidden { display: none; }

/* ---------- Charakter-Cards ---------- */
/* Zweispaltiges Grid: bei 4 Spielern zwei Reihen a zwei Karten, passt ohne
   Scrollen in den 35%-Panel bei ueblichen Bildschirmhoehen. */
#character-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.7rem;
}

.char-card {
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Fuer das langsame Ausfaden bei 0 HP im Kampf (.char-card--down) - selbes Muster wie
     .npc-card--defeated (siehe dort), Klasse wird per requestAnimationFrame erst nach dem
     Einfuegen gesetzt, damit die Transition tatsaechlich ablaeuft statt zu springen. */
  transition: opacity 2s ease, filter 2s ease;
}
/* Charakter im Kampf auf 0 HP - analog zu .npc-card--defeated (renderCharacterCards in app.js). */
.char-card--down { opacity: 0.15; filter: grayscale(1); }

.char-card-name {
  font-weight: 600;
  color: #c9a876;
  font-size: 0.92rem;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* HP-Anzeige unter dem Namen: Zahl + Balken, Balkenfarbe je nach verbleibendem Anteil
   (siehe char-hp-tier-* unten). Leer/unsichtbar (keine Hoehe), solange kein GameState mit
   HP fuer diesen Charakter bekannt ist (buildHpRow in app.js laesst die Zeile dann leer). */
.char-hp-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.char-hp-row:empty { display: none; }
.char-hp-label {
  font-size: 0.75rem;
  color: #a89a7f;
  white-space: nowrap;
}
.char-hp-bar {
  flex: 1;
  height: 6px;
  border-radius: 3px;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  overflow: hidden;
}
.char-hp-bar-fill { height: 100%; transition: width 0.3s ease; }
.char-hp-tier-good { background: #6fcf7c; }
.char-hp-tier-mid { background: #d9a441; }
.char-hp-tier-bad { background: #e0605a; }

/* Kampfwerte-Zeile (Ruestungsklasse + Schadensformel, siehe buildCombatStatsRow in common.js) -
   beides rein abgeleitet aus den Attributen (kein eigener Angriffswert existiert), daher schlicht
   als kleine Info-Zeile statt einer eigenen Kartensektion. */
.char-combat-row {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.72rem;
  color: #a89a7f;
}

/* Inventar-/Zustandszeile (Migrationsplan Phase 5 "Items, Inventar, Conditions", siehe
   buildInventoryConditionsRow in app.js) - bleibt leer/unsichtbar ohne Inhalt, kein fester
   Platzverbrauch fuer Charaktere ohne Inventar/Bedingungen. */
.char-inventory-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  font-size: 0.7rem;
  color: #a89a7f;
}
.char-inventory-row:empty { display: none; }
.char-condition-badge {
  background: #4a2f2f;
  color: #e0a0a0;
  border-radius: 4px;
  padding: 0.05rem 0.35rem;
}

/* Item-Grid (Playtest "kampf-testarena-32"/"kampf-testarena-39", siehe buildInventoryConditionsRow
   in app.js) - Nutzerwunsch: "die Items muessen deutlich groesser, sodass 4 pro Reihe in eine
   Charakter-Card passen". Eigene Grid-Zeile statt in dieselbe Flex-Zeile wie die (kleinen, reinen
   Text-)Bedingungs-Badges gemischt - braucht die volle Kartenbreite fuer sich, deshalb width:100%
   innerhalb des umgebenden .char-inventory-row (display:flex), damit die Bedingungen darunter in
   eine eigene Zeile umbrechen statt sich in dieselbe Reihe wie die Item-Kacheln zu quetschen. */
.char-item-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.4rem;
  width: 100%;
}

/* Item-Kachel: Portrait GROSS oben, Name darunter (statt vorher klein nebeneinander) - siehe
   buildItemChip in app.js. */
.item-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  background: #2f2a1f;
  border: 1px solid #4a3f2a;
  border-radius: 6px;
  padding: 0.3rem 0.2rem;
  min-width: 0;
}
.item-chip-portrait {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 5px;
  background-color: #3a3b45;
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  font-weight: 600;
  color: #c9a876;
}
.item-chip-label {
  font-size: 0.62rem;
  line-height: 1.15;
  text-align: center;
  color: #c9a876;
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  word-break: break-word;
}

.char-card-body {
  display: flex;
  gap: 0.5rem;
  /* stretch statt flex-start: .char-stats (s.u.) soll die volle Portrait-Hoehe ausfuellen,
     damit sich die 6 Attribut-Zeilen automatisch ueber genau diese Hoehe verteilen. */
  align-items: stretch;
  min-width: 0;
}

/* Quadratisches Portrait (wie .npc-portrait) statt des frueheren 9:16-Formats - wirkt in der
   Angriffsanimation neben dem ebenfalls quadratischen NPC-Portrait stimmiger. */
.char-portrait {
  width: 150px;
  height: 150px;
  flex-shrink: 0;
  border-radius: 6px;
  background-color: #2a2b34;
  background-size: cover;
  background-position: center;
  border: 1px solid #3a3b45;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  font-weight: 600;
  color: #c9a876;
  cursor: pointer;
}

.portrait-gen {
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
/* Ohne dieses Override gewinnt ".portrait-gen"'s eigenes "display: flex" IMMER gegen ".hidden
   { display: none }" (gleiche Spezifitaet, aber ".portrait-gen" steht im Stylesheet SPAETER als
   ".hidden" - bei gleicher Spezifitaet gewinnt die spaetere Regel) - das Beschreibungsfeld blieb
   dadurch trotz standardmaessig gesetzter .hidden-Klasse sichtbar UND liess sich per Portrait-Klick
   nicht mehr ausblenden (nur wieder einblenden, da die Klasse selbst korrekt togglete, nur ohne
   sichtbaren CSS-Effekt). Gleicher Fehler/Fix wie #combat-panel.hidden weiter unten. */
.portrait-gen.hidden { display: none; }
.portrait-gen input.portrait-prompt {
  margin-left: 0;
  width: 100%;
  font-size: 0.78rem;
  padding: 0.35rem 0.5rem;
}
.char-stats {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* Verteilt die 6 Attribut-Zeilen ueber die volle (gestreckte, siehe .char-card-body)
     Portrait-Hoehe, statt oben eng zusammenzustehen - sie schliessen so immer buendig mit dem
     Portrait ab, unabhaengig von dessen genauer Hoehe. */
  justify-content: space-between;
}

.char-stat-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.03rem 0;
  border-top: 1px solid #2a2b34;
}
.char-stat-row:first-child { border-top: none; }

.char-stat-row .stat-name {
  flex: 1;
  min-width: 0;
  font-size: 0.7rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.char-stat-row .stat-val {
  font-size: 0.7rem;
  color: #a8a29a;
  flex-shrink: 0;
}

/* ---------- NPC-Cards ---------- */
/* Eigenes Grid unter den Charakter-Cards im party-panel - bleibt leer (0 Hoehe) ohne aktive
   NPCs, da renderNpcCards() den Container dann einfach nicht befuellt. Vierspaltig statt
   zweispaltig (wie #character-cards), damit eine NPC-Card genau halb so breit wird wie eine
   Charakter-Card - zusaetzlich zur Namensfarbe ein zweites optisches Unterscheidungsmerkmal. */
#npc-cards {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.7rem;
  margin-top: 0.7rem;
}

.npc-card {
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 0.5rem;
  display: flex;
  flex-direction: column;
  /* NICHT "center" - das wuerde .char-hp-row (und ihre Balken-Breite: flex:1) auf ihre
     Content-Breite zusammenschrumpfen lassen statt sie ueber die volle Kartenbreite zu
     strecken (Default-Verhalten, das .char-card fuer Charakterkarten schon nutzt). Zentrierung
     von Name/Platzhalter-Buchstabe kommt stattdessen ueber text-align unten. */
  align-items: stretch;
  gap: 0.35rem;
  text-align: center;
  /* Fuer das langsame Ausfaden bei 0 HP (.npc-card--defeated) - siehe requestAnimationFrame-
     Kommentar in renderNpcCards (app.js), der die Klasse erst NACH dem Einfuegen setzt. */
  transition: opacity 2s ease, filter 2s ease;
}
.npc-card--defeated { opacity: 0.15; filter: grayscale(1); }
/* Noch nicht entdeckte NPCs in der Nachbesprechung (npc.locked, siehe buildDebriefNpcs in
   web-server.ts) - sichtbar als "es gab sie", aber deutlich zurueckhaltender als aktive Karten. */
.npc-card--locked { opacity: 0.4; filter: grayscale(1); }

/* Namensfarbe zeigt die Gesinnung (disposition, vom Erzaehler gepflegt): rot fuer Gegner,
   weiss/hell fuer alles andere (Verbuendete, neutrale Mitkaempfer). Ueberschreibt die goldene
   Standardfarbe von .char-card-name (gleiche Spezifitaet, spaeter im Stylesheet definiert). */
.npc-name-hostile { color: #e0605a; }
.npc-name-neutral { color: #e8e6e0; }

/* Quadratisches Portrait (Kopf/Oberkoerper) statt des 9:16-Charakterportraits - unterscheidet
   NPC- von Charakter-Karten auf den ersten Blick. */
.npc-portrait {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 6px;
  background-color: #2a2b34;
  background-size: cover;
  background-position: center;
  border: 1px solid #3a3b45;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  font-weight: 600;
  color: #c9a876;
}

/* ---------- Kampf-Panel ---------- */
/* Siehe CombatState (dm-prompt.ts) - sichtbar nur waehrend ein Kampf laeuft (renderCombatControls
   in app.js togglet .hidden). Der Angriffs-/Schadenswurf selbst braucht KEINEN eigenen Button hier
   (laeuft seit Phase 1 ueber den generischen Gate-Button im Chatverlauf, siehe .gate-roll-btn) -
   nur Rundenzaehler + Fliehen leben hier. */
#combat-panel {
  margin-top: 0.7rem;
  padding: 0.6rem 0.7rem;
  background: #24191a;
  border: 1px solid #6a3a3a;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
/* Ohne dieses Override gewinnt #combat-panel's eigenes "display: flex" IMMER gegen ".hidden
   { display: none }" (ID-Selektor schlaegt Klassen-Selektor unabhaengig von der Stylesheet-
   Reihenfolge) - das Panel blieb dadurch dauerhaft sichtbar, selbst mit gesetzter hidden-Klasse
   (renderCombatPanel in app.js togglet nur die Klasse). Gleicher Fehler/Fix wie #debug-panel oben. */
#combat-panel.hidden { display: none; }
#combat-panel-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
#combat-panel-title { font-weight: 600; color: #e0605a; font-size: 0.9rem; }
#combat-round-label { font-size: 0.78rem; color: #a89a7f; }

/* Gegner-Aufgebot (siehe CombatState.enemies) - dieselben .npc-card-Elemente wie #npc-cards, nur
   hierher relocated (siehe buildNpcCard/renderNpcCards in app.js) statt neu als Text gezeichnet.
   Vierspaltig (statt zweispaltig) UND mit halbierter Kartengroesse (Schriftgroessen/Padding/
   Portrait-Platzhalter unten ueberschrieben), damit auch ein Aufgebot mit vier Gegnern noch in
   einer Reihe Platz findet. */
#combat-enemy-cards {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.35rem;
}
#combat-enemy-cards .npc-card {
  padding: 0.3rem;
  gap: 0.2rem;
}
#combat-enemy-cards .char-card-name {
  font-size: 0.62rem;
}
#combat-enemy-cards .char-hp-label {
  font-size: 0.6rem;
}
#combat-enemy-cards .char-hp-bar {
  height: 4px;
}
#combat-enemy-cards .npc-portrait {
  font-size: 0.85rem;
}

/* "Wer ist noch dran" (siehe CombatState.actedCharacters) - leer/unsichtbar ausserhalb einer
   Mehrfach-Charakter-Runde, siehe .char-hp-row:empty fuer dasselbe Leer-Ausblenden-Muster. */
#combat-turn-label { font-size: 0.78rem; color: #c9a876; }
#combat-turn-label:empty { display: none; }
/* Roter Akzent statt button.primary (goldbraun) - eigene "Gefahr/Ausweichen"-Farbsprache, dieselbe
   Rot-Nuance wie .npc-name-hostile/.dice-tier-bad, damit sie sich vom normalen Weiter-Button
   abhebt. */
#flee-button {
  background: transparent;
  border-color: #8a4a4a;
  color: #e0605a;
  margin: 0;
}
#flee-button:hover { background: #6a3a3a; color: #fff; }

/* Generischer Wuerfel-Gate-Trigger (Phase 1 des Migrationsplans, siehe app.js appendGateRollRow/
   revealGate) - seit Playtest "kampf-testarena-32" eine zentrierte Zeile IM Chatverlauf
   (.msg-row.msg-center, siehe appendMsgRow) statt eines statischen Footer-Buttons, damit der
   Button erst erscheint, nachdem der dazugehoerige Erzaehltext bereits sichtbar ist. Gleiche
   Gold-Akzentfarbe wie .dice-btn/#dice-overlay, damit klar ist: hier steht ein Wuerfelwurf an. */
.gate-roll-btn {
  background: #2a2b34;
  border-color: #9a6a3f;
  color: #c9a876;
  font-weight: 600;
}
.gate-roll-btn:hover { background: #7a4f2b; color: #fff; }
.gate-roll-btn:disabled { opacity: 0.6; cursor: default; }

/* Passive Zeile fuer Zuschauer-Clients, die diesen Wurf nicht selbst machen duerfen (siehe
   appendGateRollRow in app.js, Mehrspieler-Plan) - dieselbe Position/Zentrierung wie
   .gate-roll-btn, aber ohne Button-Chrome, leicht gedaempfte Farbe statt Gold-Akzent. */
.gate-roll-waiting {
  margin: 0;
  padding: 0.5rem 1rem;
  color: #9a927f;
  font-style: italic;
}

/* Umschliessende Zeile des Gate-Buttons (appendGateRollRow) - wie .msg-artist ohne Bubble-Chrome,
   der goldene Button selbst traegt schon genug visuellen Akzent. */
.msg-bubble.msg-gate-roll {
  background: none;
  border: none;
  padding: 0;
}

/* ---------- Wuerfel-Overlay ---------- */

#dice-overlay,
#damage-overlay {
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 14, 0.78);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  border-radius: 6px;
}
#dice-overlay.hidden,
#damage-overlay.hidden { display: none; }

.dice-overlay-inner { text-align: center; }

.d20 { width: 140px; height: 140px; margin: 0 auto; }
.d20.d20-spin { animation: dice-spin 1.1s cubic-bezier(.36,.07,.19,.97); }

@keyframes dice-spin {
  0%   { transform: rotate(0deg) scale(1); }
  60%  { transform: rotate(720deg) scale(1.15); }
  100% { transform: rotate(1080deg) scale(1); }
}

/* Schadenswurf (W6, siehe #damage-overlay in index.html) - N Kacheln statt einer W20-SVG, gleiche
   dice-spin-Animation. */
#damage-dice-row {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
}
.damage-die {
  width: 48px;
  height: 48px;
  border-radius: 8px;
  background: #2a2b34;
  border: 2px solid #9a6a3f;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  font-weight: 700;
  color: #e8e6e0;
}
.damage-die.damage-die-spin { animation: dice-spin 1.1s cubic-bezier(.36,.07,.19,.97); }

#dice-result-panel,
#damage-result-panel { margin-top: 0.8rem; }
#dice-result-panel.hidden,
#damage-result-panel.hidden { display: none; }

#dice-result-label,
#damage-result-label {
  font-size: 1.15rem;
  color: #c9a876;
  font-weight: 600;
  margin: 0;
}

.dice-result-actions {
  display: flex;
  gap: 0.6rem;
  justify-content: center;
  margin-top: 0.7rem;
}
.dice-result-actions button { margin: 0; }

/* ---------- Angriffs-Animation (Chat-Log) ---------- */
/* Kurzes Zwischenspiel bei einem aufgeloesten Angriff (Treffer ODER Fehlschlag, siehe
   playAttackAnimation in app.js) - anders als der erste Entwurf BLEIBT die Zeile nach Ablauf
   dauerhaft im Chat-Log stehen (Teil der Kampf-Historie, siehe Playtest "kampf-testarena-10":
   "im Anschluss stehen bleiben") statt sich selbst zu entfernen. Feste Seiten-Konvention: Spieler
   IMMER links (.attack-anim-player), NPC IMMER rechts (.attack-anim-npc), unabhaengig davon, wer
   angreift - nur die Bewegungsrichtung (--attack-dx, per JS gemessen) unterscheidet sich. Portraits
   statt Buchstaben-Kuerzel, deutlich groesser als der erste Entwurf. */
.attack-anim-row {
  display: flex;
  justify-content: center;
  padding: 0.5rem 0;
}
.attack-anim-stage {
  position: relative;
  width: min(420px, 85%);
  height: 100px;
}
.attack-anim-token {
  position: absolute;
  top: 0;
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background-color: #2a2b34;
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.7rem;
  color: #e8e6e0;
}
.attack-anim-player { left: 0; z-index: 2; box-shadow: 0 0 0 3px #9a6a3f; }
.attack-anim-npc { right: 0; box-shadow: 0 0 0 3px #8a4a4a; }

.attack-anim-token.attack-anim-dash {
  animation: attack-dash 0.5s cubic-bezier(.36,.07,.19,.97) forwards;
}
/* Kein Ausblenden/Schrumpfen am Ende (anders als der erste Entwurf) - der bewegte Token bleibt
   voll sichtbar an seiner neuen Position stehen, da die ganze Zeile dauerhaft erhalten bleibt. */
@keyframes attack-dash {
  0%   { transform: translateX(0) scale(1); }
  60%  { transform: translateX(var(--attack-dx, 200px)) scale(1.08); }
  100% { transform: translateX(var(--attack-dx, 200px)) scale(1); }
}

.attack-anim-damage {
  position: absolute;
  top: -6px;
  width: 84px;
  text-align: center;
  font-weight: 800;
  opacity: 0;
  pointer-events: none;
  text-shadow: 0 1px 4px #0a0a0e;
  /* Muss ueber BEIDEN Tokens liegen, auch ueber .attack-anim-player (z-index: 2) - ohne eigenes,
     hoeheres z-index landete die Schadenszahl bei einem NPC-Angriff (Zahl liegt ueber dem
     Charakter-Token) hinter dessen z-index:2 und war kaum lesbar; bei Spieler-Angriffen fiel das
     nicht auf, weil der NPC-Token gar kein erhoehtes z-index hat. */
  z-index: 3;
}
.attack-anim-damage-left { left: 0; }
.attack-anim-damage-right { right: 0; }
.attack-anim-damage-hit { font-size: 2rem; color: #e0605a; }
.attack-anim-damage-miss { font-size: 1.3rem; color: #a89a7f; }
/* Endet SICHTBAR (opacity 1), nicht ausgeblendet - bleibt als permanente "Einschlag"-Markierung
   ueber dem getroffenen Token stehen statt wegzufaden. */
.attack-anim-damage-show { animation: attack-damage-float 0.9s ease-out forwards; }
@keyframes attack-damage-float {
  0%   { opacity: 0; transform: translateY(10px) scale(0.7); }
  25%  { opacity: 1; transform: translateY(-4px) scale(1.25); }
  55%  { opacity: 1; transform: translateY(-14px) scale(1); }
  100% { opacity: 1; transform: translateY(-22px) scale(1); }
}

/* ---------- Setup-Wizard: Schritt-Indikator + Card-Grids + Modal ---------- */

#setup-steps {
  display: flex;
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.setup-step-indicator {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.8rem;
  border-radius: 999px;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  color: #766a86;
  font-size: 0.85rem;
}
.setup-step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  background: #2a2b34;
  font-size: 0.75rem;
  font-weight: 600;
}
.setup-step-indicator.reachable { color: #c3bdb3; cursor: pointer; }
.setup-step-indicator.reachable:hover { border-color: #9a6a3f; }
.setup-step-indicator.active {
  color: #e8e6e0;
  border-color: #9a6a3f;
  background: #2a2118;
}
.setup-step-indicator.active .setup-step-num { background: #7a4f2b; color: #fff; }

.setup-step { padding-bottom: 1rem; }
.setup-step h2 { margin-top: 0; }
.setup-step-actions {
  display: flex;
  gap: 0.6rem;
  margin-top: 1.2rem;
  padding-top: 0.8rem;
  border-top: 1px solid #3a3b45;
}

/* Gemeinsames responsives Grid fuer Abenteuer-/Charakterbibliotheks-/Party-Cards - Basisbreite
   220px, #adventure-cards/#library-cards/#party-cards ueberschreiben sie gezielt (s.u.). */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0.9rem;
  margin-top: 0.8rem;
}

/* Abenteuer-Cards: doppelte Basisbreite (440px statt 220px) - Platz fuer den laengeren Teaser-
   Text UND das optionale Titelbild, das dadurch auch nicht winzig wirkt. */
#adventure-cards.card-grid { grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); }

/* Bibliotheks-/Party-Cards (.char-card, Schritt 2/3): auto-fit statt auto-fill, damit bei wenigen
   Karten NICHT eine Reihe leerer Spuren reserviert bleibt, die die vorhandene(n) Karte(n) ueber
   1fr auf die volle (bei 1600px-#setup-view sehr breite) Zeilenbreite aufblaesst - stattdessen
   teilen sich nur die tatsaechlich vorhandenen Karten den Platz, zusaetzlich per max-width auf der
   Karte selbst hart gedeckelt, damit eine einzelne Karte nie riesig wird. min-width:0 auf der Karte
   (Grid-Item) verhindert, dass ihr eigener Inhalt eine groessere Mindestbreite erzwingt, als die
   zugewiesene Spur eigentlich hat. */
#library-cards.card-grid,
#party-cards.card-grid { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }

#library-cards .char-card,
#party-cards .char-card {
  min-width: 0;
  max-width: 420px;
}

/* Etwas kleineres Portrait als im laufenden Abenteuer (dort 150px, siehe .char-portrait weiter
   oben), passend zur jetzt groesseren Kartenbreite (320px Basis). */
#library-cards .char-portrait,
#party-cards .char-portrait {
  width: 130px;
  height: 130px;
  font-size: 1.4rem;
}

.adventure-card {
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.adventure-card--selected { border-color: #9a6a3f; background: #2a2118; }
/* Titelbild (siehe hasHeaderImage/buildAdventureCard in setup.js) - optional, vom Plan-Autor als
   Datei gleichen Namens neben dem Plan-JSON mitgeliefert. 16:9 statt quadratisch (wie die
   Charakter-/NPC-Portraits), damit es als Szenen-/Titelbild statt als Portrait wirkt. Negative
   Randwerte lassen es bis an den Kartenrand reichen, statt vom Card-Padding eingerueckt zu sein. */
.adventure-card-header-image {
  aspect-ratio: 16 / 9;
  margin: -1rem -1rem 0;
  background-color: #14151a;
  background-size: cover;
  background-position: center;
  border-radius: 8px 8px 0 0;
}
.adventure-card-title { font-weight: 600; color: #c9a876; font-size: 1rem; }
.adventure-card-teaser {
  margin: 0;
  font-size: 0.85rem;
  color: #a8a29a;
  line-height: 1.4;
  flex: 1;
}

/* Bibliotheks-/Party-Cards nutzen dieselben .char-card/.char-portrait/.char-stats/.char-stat-row
   Klassen wie die Charakterkarten im laufenden Abenteuer (siehe weiter oben) - hier nur die
   Ergaenzungen, die dort nicht existieren (eine Aktionszeile statt Wuerfel-Buttons). */
.char-card-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.2rem;
}
.char-card-actions button { flex: 1; margin: 0; }

.char-card--add {
  align-items: center;
  justify-content: center;
  color: #766a86;
  border-style: dashed;
  cursor: pointer;
  min-height: 8rem;
  text-align: center;
}
.char-card--add:hover { border-color: #9a6a3f; color: #c9a876; }

.char-card--in-party { border-color: #6fb085; }

button.danger { border-color: #8a4a4a; color: #e0605a; }
button.danger:hover { background: #6a3a3a; color: #fff; }

/* ---------- Erstellen/Bearbeiten-Modal (Schritt 2) ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 14, 0.78);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
.modal-overlay.hidden { display: none; }
.modal-panel {
  position: relative;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 1.2rem 1.4rem;
  width: 95%;
  max-width: 32rem;
  max-height: 88vh;
  overflow-y: auto;
}
.modal-panel label { display: block; margin: 0.5rem 0; }
.modal-panel input[type="text"] {
  display: block;
  width: 100%;
  max-width: none;
  margin: 0.3rem 0 0;
}
.modal-close {
  position: absolute;
  top: 0.6rem;
  right: 0.7rem;
  background: none;
  border: none;
  color: #a89a7f;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
}
.modal-close:hover { color: #e8e6e0; }

#modal-portrait-row {
  display: flex;
  gap: 0.8rem;
  align-items: center;
  margin: 0.8rem 0;
}
#modal-portrait-row .char-portrait { width: 100px; height: 100px; cursor: default; }
#modal-portrait-actions { flex: 1; min-width: 0; }
#modal-portrait-hint { margin: 0.3rem 0 0; }

.modal-actions {
  display: flex;
  gap: 0.6rem;
  margin-top: 1rem;
  padding-top: 0.8rem;
  border-top: 1px solid #3a3b45;
}
.modal-actions .primary { margin-left: auto; }

/* ---------- Audio-Teaser (Schritt 5, siehe teaser-audio-row in setup.js) ---------- */
#teaser-audio-row {
  margin-top: 0.8rem;
  max-width: 26rem;
}
#teaser-audio-button {
  width: 100%;
}

/* ---------- Bereitschafts-Checkliste (Schritt 5) ---------- */
#readiness-checklist {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.8rem;
  max-width: 26rem;
}
.readiness-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.5rem 0.8rem;
}
.readiness-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #e0605a;
  flex-shrink: 0;
}
.readiness-row--ready .readiness-dot { background: #6fcf7c; }
.readiness-label { flex: 1; }
.readiness-status {
  font-size: 0.8rem;
  color: #a89a7f;
  text-transform: capitalize;
}

/* ---------- Charaktererstellungs-Lobby (Mehrspieler-Plan, siehe lobby.js) ---------- */

#lobby-view { max-width: 1400px; margin: 0 auto; padding: 1.5rem 1rem; }
#lobby-layout {
  display: flex;
  gap: 2rem;
  align-items: flex-start;
}
#lobby-editor {
  flex: 0 0 380px;
}
/* Nimmt den restlichen Platz ein (Roster-Karten brauchen deutlich mehr Breite als der schmale
   #party-panel-Sidebar-Kontext, aus dem .char-card sonst stammt, siehe Nutzer-Vorgabe "viel mehr
   Details" - volle Attributliste + Kampfwerte statt einer kompakten Uebersichtszeile). */
#lobby-right-column {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
}
#lobby-roster-panel {
  background: #24252d;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 1rem;
}
#lobby-roster-panel h3 { margin-top: 0; }
#lobby-roster {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.7rem;
}

#lobby-editor label { display: block; margin-bottom: 0.6rem; }
#lobby-editor input[type="text"] { width: 100%; }

#lobby-recall-row { margin-bottom: 0.8rem; }

#lobby-portrait-row {
  display: flex;
  gap: 0.8rem;
  align-items: center;
  margin: 0.8rem 0;
}
/* Deutlich groesser als die Modal-Miniatur (#modal-portrait-row .char-portrait, 100px) - siehe
   Nutzer-Vorgabe "Portrait waehrend der Erstellung deutlich groesser anzeigen". */
.lobby-portrait-large { width: 220px; height: 220px; cursor: default; }
#lobby-portrait-actions { flex: 1; min-width: 0; }
#lobby-portrait-hint { margin: 0.3rem 0 0; }

#lobby-claim-button { margin-top: 0.6rem; }
#lobby-claim-status { margin-top: 0.4rem; }

/* Roster-Karten (siehe buildLobbyCharacterCard in lobby.js) sind dieselben .char-card wie in
   #character-cards (Nutzer-Vorgabe: "Charakter-Cards aus der Abenteuer-View wiederverwenden") -
   nur die zusaetzliche Meta-/Beschreibungszeile + der Entfernen-Button sind lobby-spezifisch. */
.lobby-card-meta {
  font-size: 0.78rem;
  color: #a89a7f;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.lobby-card-status {
  font-size: 0.72rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: #3a3b45;
  color: #c9a876;
  white-space: nowrap;
}
.lobby-card--claimed .lobby-card-status { background: #2f4d38; color: #7ec98f; }
.lobby-card-description {
  margin: 0;
  font-size: 0.8rem;
  color: #a89a7f;
}
.lobby-card-remove {
  margin-top: 0.2rem;
  align-self: flex-start;
}

/* Eigene Karte (Nutzer-Vorgabe: "Host-Werkzeuge in eine eigene Card") - gleiche Optik wie
   #lobby-roster-panel, nur separat statt darin verschachtelt. */
#lobby-host-extra {
  background: #24252d;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  padding: 1rem;
}
#lobby-host-extra h3 { margin-top: 0; }
#lobby-host-extra label { display: block; margin-bottom: 0.6rem; }
#lobby-host-extra select { width: 100%; margin-top: 0.2rem; }

/* ---------- Discord-Login-Widget (Mehrspieler-Plan, siehe app.js applyUserWidget) ---------- */

#user-widget { position: relative; flex-shrink: 0; }
#user-widget-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: #1e1f26;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.3rem 0.55rem;
  color: #e8e6e0;
  cursor: pointer;
  font-size: 0.78rem;
}
#user-widget-toggle:hover { background: #2a2b34; }
#user-widget-avatar {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background-color: #3a3b45;
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 0.7rem;
  flex-shrink: 0;
}
#user-widget-menu {
  position: absolute;
  top: calc(100% + 0.3rem);
  left: 0;
  z-index: 20;
  background: #24252d;
  border: 1px solid #3a3b45;
  border-radius: 6px;
  padding: 0.6rem;
  min-width: 160px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
}
#user-widget-menu.hidden { display: none; }
#user-widget-role { margin: 0 0 0.5rem; text-transform: capitalize; }
#user-widget-logout { width: 100%; }

/* ---------- Login-Zwang / Lobby-Warteschirm (Mehrspieler-Plan, siehe lobby.js) ---------- */

.splash-box {
  max-width: 420px;
  margin: 4rem auto;
  padding: 2rem;
  background: #24252d;
  border: 1px solid #3a3b45;
  border-radius: 8px;
  text-align: center;
}
.splash-box h2 { margin-top: 0; }
.splash-box .primary { display: inline-block; margin-top: 1rem; text-decoration: none; }
