:root {
  --bg: #050b14;
  --panel: #0c1826;
  --cyan: #34e5ff;
  --cyan-dim: #1a5f6e;
  --text: #d9f4ff;
  --danger: #ff3b5c;
  --speaker: #34e5ff;
  color-scheme: dark;
}

body.blackout {
  filter: grayscale(0.85) sepia(0.35) brightness(0.75);
  transition: filter .8s ease;
}
body:not(.blackout) { transition: filter 1.5s ease; }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0; padding: 0; height: 100%; overflow: hidden;
  background: var(--bg); color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

/* Nutzerkritik 2026-07-31 (iOS Safari): "die untere Leiste ist nicht
   bedienbar" -- klassischer iOS-Safari-Bug: `position:fixed; inset:0`
   spannt den Container ueber den GROSSEN Viewport (Browser-Chrome
   eingeklappt), Safaris eigene, unten schwebende Adressleiste liegt aber
   bei eingeblendetem Chrome PHYSISCH ueber dem unteren Rand -- #bottom-bar
   war dadurch da, aber nicht antippbar (Taps gingen an Safaris UI statt
   an die Seite). `100svh` (small viewport height, seit iOS 15.4/Safari
   15.4 unterstuetzt) ist garantiert nie groesser als der tatsaechlich
   sichtbare Bereich, egal ob Safaris Chrome gerade ein- oder ausgeblendet
   ist -- deshalb hier explizite Hoehe statt `bottom:0`/`inset:0`, mit
   100vh als Fallback fuer Browser ohne svh-Support (zweite Deklaration,
   wird von aelteren Browsern einfach ignoriert). */
#app {
  position: fixed; top: 0; left: 0; right: 0;
  height: 100vh;
  height: 100svh;
  display: flex; flex-direction: column;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
/* Auf Mobile hat #sidebar keinerlei Layout-Wirkung -- ihre Kinder
   (#decision-text/#buttons/#avatar-wrap/#bottom-bar) verhalten sich exakt
   wie zuvor als direkte Flex-Kinder von #app. Erst der Desktop-Breakpoint
   unten macht daraus eine echte Spalte. */
#sidebar { display: contents; }

/* HUD ~7% -- Glass-Panel-Rezept (VISUAL_UPGRADE_ANALYSIS.md #3.4): Hairline
   als erster box-shadow-Layer statt border, Abhebe-Schatten als zweiter. */
/* Nutzerkritik 2026-07-31: "der kontakte button und was da sonst noch
   rechts oben auftaucht ist auch nicht sauber sichtbar" -- #hud reiht bis
   zu 5 Ressourcen-Chips + Galaxie-/Sol-Label + bis zu 3 bedingte Buttons
   (uplink-btn/soeldner-btn/held-chip) + help-btn OHNE Umbruch/Overflow-
   Handling in einer einzigen 7vh-Zeile auf -- auf schmalen Mobile-
   Viewports (390px) laeuft das rechts aus dem sichtbaren Bereich, statt
   sichtbar zu bleiben. overflow-x:auto macht die Zeile horizontal
   wischbar statt die ueberschuessigen Elemente unsichtbar abzuschneiden;
   flex-shrink:0 auf den Kindern (unten) verhindert, dass der Browser
   stattdessen Text/Buttons bis zur Unlesbarkeit zusammenquetscht.
   Scrollbar per Utility-Klasse ausgeblendet (aesthetisch, Wischen bleibt
   funktional). */
#hud {
  flex: 0 0 7vh; display: flex; align-items: center; gap: 10px;
  padding: 0 10px; font-size: 13px;
  overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  background: radial-gradient(circle at top left, rgba(20,50,80,.9), rgba(5,11,20,.92));
  backdrop-filter: blur(12px);
  box-shadow: 0 0 0 1px rgba(52,229,255,.25), 0 4px 16px rgba(0,0,0,.6);
}
#hud::-webkit-scrollbar { display: none; }
#hud > * { flex-shrink: 0; }
#hud .res { color: var(--cyan); white-space: nowrap; }
#hud .res b { color: var(--text); font-weight: 600; margin-left: 2px; }
/* Mining-Juice-Nachtrag: HUD-Zahl pulst kurz auf, wenn sie steigt (game.js
   pulseResIfIncreased) -- macht Ressourcenzuwachs am Ort der eigentlichen
   Zahl sichtbar, nicht nur im Popup ueber dem Mutterschiff. */
#hud .res b.res-pulse { animation: resPulse .6s ease-out; }
/* Energiesparmodus-HUD-Warnung (Nutzerkritik: "rote Rufzeichen oder was
   auch immer") -- der Energie-Chip selbst wird rot und pulst, kein neues
   Icon-System noetig. */
/* Konzept 12 Abschnitt D ("Gestaffelte Energieeskalation"): "caution" ist
   die neue, schwaechste Stufe (low_reserve -- unter zwei Unterhaltszyklen
   Reserve, naechster Sol aber noch sicher) -- deutlich dezenter als
   "warning" (kein Puls), damit sie nicht mit einer bereits laufenden Krise
   verwechselt wird, aber trotzdem sichtbar VOR der ersten Negativ-Sol-Warnung
   erscheint. */
#res-energie-chip.energy-caution { color: #ffd166; }
#res-energie-chip.energy-caution b { color: #ffd166; }
#res-energie-chip.energy-warning { color: #ffb03a; }
#res-energie-chip.energy-warning b { color: #ffb03a; }
#res-energie-chip.energy-critical { color: var(--danger); animation: energyChipPulse 1.1s ease-in-out infinite; }
#res-energie-chip.energy-critical b { color: var(--danger); }
@keyframes energyChipPulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@keyframes resPulse {
  0% { color: #6fe38a; transform: scale(1.35); display: inline-block; }
  100% { color: var(--text); transform: scale(1); }
}
#galaxy-label { margin-left: auto; letter-spacing: .5px; color: var(--text-dim, #9fb3c0); font-size: 11px; white-space: nowrap; }
#sol-label { margin-left: 8px; letter-spacing: 1px; color: var(--cyan); font-size: 12px; white-space: nowrap; }
/* Split-Second-Fleet-Controller-Persona-Kritik (Review 2026-07-29): auf
   schmalen Mobile-Viewports (390px) hatte #hud nicht genug Platz fuer 5
   Ressourcen-Chips + Galaxiename + Sol -- ohne white-space:nowrap oben oder
   eine Verkleinerung hier brach der Text mitten im Wort um ("Vela-\nNeun",
   "SOL\n14"), was die feste 7vh-Hoehe der Leiste sprengte. Galaxiename ist
   reiner Flavor-Text (nicht handlungsrelevant wie die Ressourcenzahlen),
   deshalb bei sehr schmalen Screens ausgeblendet statt weiter verkleinert. */
@media (max-width: 480px) {
  #hud { gap: 6px; font-size: 11px; padding: 0 6px; }
  #galaxy-label { display: none; }
  #sol-label { margin-left: auto; }
}
/* Kapitelende-Wartezeit-Fix (Task #124, "fuehlt sich tot an -- bis zu 45s
   ohne Interaktion") + Synthese 3.2 (P1: "Ein HUD-Badge allein reicht fuer
   die QA nicht"): urspruenglich ein 10px-Text mitten in der dichten
   #hud-Zeile -- dort ging er sowohl fuer Spieler als auch fuer den
   QA-Bot (scripts/10_capture_playthrough_log.py) leicht unter. position:
   fixed loest dasselbe Element (kein zweites UI-Element) bewusst aus dem
   #hud-Flow und platziert es als eigene, permanente Banner-Karte zentriert
   ueber dem Spielfeld -- sichtbar nur, waehrend das Sol server-seitig am
   Kapitelende eingefroren ist. Sanftes Pulsieren macht sichtbar, dass der
   Zustand lebt/sich aufloest, statt eines regungslosen Bildschirms. */
#kapitelende-badge {
  position: fixed; top: 34px; left: 50%; transform: translateX(-50%);
  z-index: 20; max-width: min(92vw, 420px); text-align: center;
  background: rgba(10, 22, 34, .92); border: 1px solid #ffd166; border-radius: 8px;
  padding: 8px 16px; color: #ffd166; font-size: 13px; line-height: 1.4;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .4);
  animation: kapitelendePulse 1.8s ease-in-out infinite;
}
#kapitelende-badge[hidden] { display: none; }
@keyframes kapitelendePulse { 0%, 100% { opacity: 1; } 50% { opacity: .72; } }
@media (max-width: 480px) {
  #kapitelende-badge { top: 30px; font-size: 12px; padding: 6px 12px; }
}

/* Resilienz-Fallback-Badge (Konzept konzepte/codex_review_2026-07-30/
   08_offline_fallback_konzept.md §3/§5a, kleinste Ausbaustufe) -- gleiches
   Muster wie #kapitelende-badge (fixed, aus dem #hud-Flow geloest), aber
   OHNE Puls-Animation: dieser Zustand kann laenger anhalten (z.B. ein
   dauerhaft unerreichbares LLM-Backend), staendiges Pulsieren waere dafuer
   unpassend/ermuedend. Farblich neutral-orange statt kapitelendes Gelb,
   damit beide Badges im seltenen Ueberlappungsfall (Kapitelende UND
   Resilienz-Modus gleichzeitig) unterscheidbar bleiben; top-Offset
   knapp darunter, statt exakt gleich zu positionieren. */
#mode-status-badge {
  position: fixed; top: 76px; left: 50%; transform: translateX(-50%);
  z-index: 19; max-width: min(92vw, 380px); text-align: center;
  background: rgba(10, 22, 34, .92); border: 1px solid #ff9f5a; border-radius: 8px;
  padding: 5px 14px; color: #ff9f5a; font-size: 12px; line-height: 1.4;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .4); cursor: pointer;
}
#mode-status-badge[hidden] { display: none; }
@media (max-width: 480px) {
  #mode-status-badge { top: 66px; font-size: 11px; padding: 4px 10px; }
}

#help-btn {
  width: 20px; height: 20px; border-radius: 50%; border: 1px solid var(--cyan-dim);
  background: transparent; color: var(--cyan); font-size: 11px; line-height: 1; padding: 0;
}

/* Uplink-Protokoll-HUD-Button (Sprecher-Spezialisierung-Konzept 07 §1/§4) --
   minimal-funktional: ein kompakter Chip statt eines eigenen Panels. Drei
   Zustaende ueber Klassen von updateUplinkButton() in game.js gesetzt:
   bereit (klickbar, Cyan), aktiv (laufender Countdown, Cyan pulsierend),
   cooldown (gesperrt, gedaempft). */
#uplink-btn {
  border-radius: 8px; border: 1px solid var(--cyan-dim); background: rgba(20,50,80,.55);
  color: var(--cyan); font-size: 10px; line-height: 1.3; padding: 3px 8px; white-space: nowrap;
  font-family: inherit; cursor: pointer;
}
#uplink-btn[hidden] { display: none; }
#uplink-btn:disabled { color: var(--text-dim, #9fb3c0); border-color: rgba(159,179,192,.35); cursor: default; }
#uplink-btn.uplink-active { color: #ffd166; border-color: #ffd166; animation: uplinkPulse 1.4s ease-in-out infinite; }
@keyframes uplinkPulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* Soeldner-Kontakte-HUD-Button (Helden-Faehigkeiten-Konzept 07 §5) -- selber
   Chip-Stil wie #uplink-btn, eigene Farbe (Bernstein statt Cyan) fuers
   sofortige Unterscheiden zweier gleichzeitig sichtbarer HUD-Chips. Pulst,
   solange ein Angebot offen ist (updateSoeldnerButton() setzt .offer-active),
   damit ein befristetes Fenster sich auch wie eines anfuehlt. */
#soeldner-btn {
  border-radius: 8px; border: 1px solid #ffb347; background: rgba(80,52,20,.5);
  color: #ffb347; font-size: 10px; line-height: 1.3; padding: 3px 8px; white-space: nowrap;
  font-family: inherit; cursor: pointer;
}
#soeldner-btn[hidden] { display: none; }
#soeldner-btn.offer-active { animation: soeldnerPulse 1.4s ease-in-out infinite; }
@keyframes soeldnerPulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* Feuerbefehl-Chip -- selber Grund-Stil wie #uplink-btn/#soeldner-btn, eigene
   Farbe (Rot) als Kampfaktion. Pulst durchgehend, solange er sichtbar ist:
   anders als die anderen Chips ist er nur waehrend eines laufenden Anflugs
   ueberhaupt da, das Zeitfenster IST die Information. */
#feuerbefehl-btn {
  border-radius: 8px; border: 1px solid #ff6b5e; background: rgba(88,26,22,.55);
  color: #ff6b5e; font-size: 10px; line-height: 1.3; padding: 3px 8px; white-space: nowrap;
  font-family: inherit; cursor: pointer;
  animation: feuerbefehlPulse 1.2s ease-in-out infinite;
}
#feuerbefehl-btn[hidden] { display: none; }
#feuerbefehl-btn:disabled {
  color: var(--text-dim, #9fb3c0); border-color: rgba(159,179,192,.35);
  cursor: default; animation: none;
}
@keyframes feuerbefehlPulse { 0%, 100% { opacity: 1; } 50% { opacity: .6; } }

/* Autopilot-Protokoll -- steht waehrend des Autopilotbetriebs ANSTELLE der
   Optionsknoepfe im Entscheidungsbereich. Bewusst im Stil einer Funkmitschrift
   (Monospace, juengster Zug hervorgehoben), damit sofort klar ist: hier
   entscheidet gerade nicht der Spieler. */
.autopilot-log {
  /* #buttons ist ein zweispaltiges Grid -- ohne grid-column bekam das
     Protokoll nur eine Spalte und war in der ohnehin schmalen Seitenspalte
     nur 134px von 319px breit (Nutzerkritik "das log ist zu schmal"). */
  grid-column: 1 / -1;
  border: 1px solid rgba(255,209,102,.5); border-radius: 10px;
  background: rgba(28,22,8,.45); padding: 8px 10px;
}
.autopilot-log-kopf {
  font-size: 10px; letter-spacing: .08em; color: #ffd166;
  margin-bottom: 6px; font-weight: bold;
}
.autopilot-log-zeile {
  font-size: 11px; line-height: 1.45; color: var(--text-dim, #9fb3c0);
  padding: 3px 0; border-top: 1px solid rgba(255,209,102,.12);
}
.autopilot-log-zeile:first-of-type { border-top: none; }
/* Ein Feld pro Zeile: die Seitenspalte ist schmal, eine durchlaufende
   Textzeile brach mitten im Satz um. Praefix links, Wert dahinter. */
.autopilot-log-feld { display: block; overflow-wrap: anywhere; }
.autopilot-log-tag {
  display: inline-block; min-width: 3.2em; color: #ffd166;
  font-size: 9px; letter-spacing: .06em; text-transform: uppercase;
}
.autopilot-log-zeile.neu { color: var(--text, #e8f4ff); }
/* Ausgeklappt scrollt der volle Verlauf in fester Hoehe, statt den
   Entscheidungsbereich beliebig aufzublaehen. */
.autopilot-log.ausgeklappt { max-height: 40vh; overflow-y: auto; }
.autopilot-log-mehr {
  display: block; margin-top: 6px; padding: 0;
  background: none; border: none; text-align: left;
  color: #ffd166; font-family: inherit; font-size: 10px;
  text-decoration: underline; cursor: pointer;
}
/* COM-Empfehlung waehrend des Autopiloten: als Beobachtung markiert, nicht
   als Handlungsaufforderung an den Spieler. */
.com-recommend-autopilot {
  border: 1px dashed rgba(255,209,102,.45); border-radius: 8px;
  padding: 6px 9px; font-size: 11px; color: var(--text-dim, #9fb3c0);
}

/* Autopilot-Bedienung im Testmodus-Block -- schlicht gehalten, es ist ein
   QA-Werkzeug und soll optisch nicht mit Spielfunktionen konkurrieren. */
#autopilot-strategy {
  display: block; margin: 4px 0 8px; padding: 4px 6px;
  background: rgba(10,18,30,.9); color: var(--text, #e8f4ff);
  border: 1px solid rgba(52,229,255,.4); border-radius: 6px;
  font-family: inherit; font-size: 12px;
}
#autopilot-strategy-label { font-size: 11px; opacity: .8; }
#autopilot-btn.laeuft { border-color: #ffd166; color: #ffd166; }
#autopilot-status { font-size: 11px; opacity: .75; min-height: 1.2em; margin: 4px 0 0; }

/* Taktik-Chips im laufenden Gefecht -- Grund-Stil wie die uebrigen HUD-Chips,
   eigene Farben zur Unterscheidung (Rueckzug bernstein = Vorsicht, Verfolgen
   gruen = Zugriff). Kein Dauerpulsen wie beim Feuerbefehl: die beiden stehen
   laenger zur Verfuegung, ein Dauerblinken waere reine Unruhe. */
#rueckzug-btn, #verfolgen-btn, #spaeher-btn {
  border-radius: 8px; border: 1px solid currentColor; background: rgba(20,32,48,.6);
  font-size: 10px; line-height: 1.3; padding: 3px 8px; white-space: nowrap;
  font-family: inherit; cursor: pointer;
}
#rueckzug-btn { color: #ffb347; }
#verfolgen-btn { color: #6fe38a; }
#spaeher-btn { color: #c9a8ff; }
#rueckzug-btn[hidden], #verfolgen-btn[hidden], #spaeher-btn[hidden] { display: none; }
#rueckzug-btn:disabled, #verfolgen-btn:disabled, #spaeher-btn:disabled {
  color: var(--text-dim, #9fb3c0); border-color: rgba(159,179,192,.35); cursor: default;
}

/* Autopilot-Helden-HUD-Chip (Konzept konzepte/codex_review_2026-07-30/
   10_autopilot_helden_konzept.md §4/§5) -- minimaler permanenter Chip,
   selber Grund-Stil wie #uplink-btn/#soeldner-btn, eigene Farbe (helles
   Violett) fuers Unterscheiden. Rein informativ (title-Tooltip traegt
   Aura/Unterhalt/Status), kein Klick-Verhalten in dieser ersten Fassung. */
#held-chip {
  border-radius: 8px; border: 1px solid #b98cff; background: rgba(48,24,80,.5);
  color: #b98cff; font-size: 10px; line-height: 1.3; padding: 3px 8px; white-space: nowrap;
  font-family: inherit; cursor: default;
}
#held-chip[hidden] { display: none; }
#held-chip.held-offline { color: var(--text-dim, #9fb3c0); border-color: rgba(159,179,192,.35); }

/* Heldenangebote teilen sich die .soeldner-offer-Kachel (dieselbe
   Kontaktanzeige, siehe renderSoeldnerBody() in game.js), nur der
   Kachelrand/Anwerben-Button wechseln auf die Helden-Farbe. */
.held-offer { border-color: rgba(185,140,255,.4); }
.held-offer .soeldner-cost { color: #b98cff; }
.held-offer .soeldner-hire-btn { border-color: #b98cff; color: #b98cff; background: #21123a; }

#soeldner-overlay {
  position: absolute; inset: 0; z-index: 26; display: flex; align-items: center; justify-content: center;
  background: rgba(2,8,14,.9);
}
#soeldner-overlay[hidden] { display: none; }
#soeldner-card {
  background: radial-gradient(circle at top left, rgba(80,52,20,.85), rgba(6,14,24,.95));
  backdrop-filter: blur(16px);
  box-shadow: 0 0 0 1px rgba(255,179,71,.35), 0 16px 40px rgba(0,0,0,.7);
  border-radius: 14px; padding: 16px; width: 88%; max-width: 360px; max-height: 78dvh; overflow-y: auto;
}
#soeldner-title { color: #ffb347; font-size: 15px; margin: 0 0 10px; font-weight: 700; }
#soeldner-body { font-size: 13px; line-height: 1.5; display: flex; flex-direction: column; gap: 10px; }
.soeldner-offer {
  border: 1px solid rgba(255,179,71,.4); border-radius: 10px; padding: 10px; background: rgba(0,0,0,.2);
}
.soeldner-offer p { margin: 0 0 6px; }
.soeldner-offer .soeldner-cost { color: #ffb347; font-size: 12px; margin-bottom: 6px; }
.soeldner-window { color: rgba(255,255,255,.55); font-size: 11px; font-style: italic; margin-bottom: 6px; }
.soeldner-hire-btn {
  width: 100%; padding: 8px; border-radius: 8px; border: 1px solid #ffb347;
  background: #2a1c0a; color: #ffb347; font-size: 13px; cursor: pointer;
}
.soeldner-hire-btn:disabled { opacity: .5; cursor: default; }
.soeldner-active-row {
  display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,.08); padding: 4px 0;
}
#soeldner-close {
  margin-top: 4px; width: 100%; padding: 10px; border-radius: 10px; border: 1px solid var(--cyan-dim);
  background: #0a1622; color: var(--text); font-size: 14px;
}

#manual-overlay {
  position: absolute; inset: 0; z-index: 26; display: flex; align-items: center; justify-content: center;
  background: rgba(2,8,14,.9);
}
#manual-overlay[hidden] { display: none; }
#manual-card {
  background: radial-gradient(circle at top left, rgba(20,50,80,.92), rgba(6,14,24,.95));
  backdrop-filter: blur(16px);
  box-shadow: 0 0 0 1px rgba(52,229,255,.3), 0 16px 40px rgba(0,0,0,.7);
  border-radius: 14px; padding: 16px; width: 88%; max-width: 320px; max-height: 78dvh; overflow-y: auto;
}
#manual-title { color: var(--cyan); font-size: 15px; margin: 0 0 10px; font-weight: 700; }
#manual-body { font-size: 13px; line-height: 1.5; }
#manual-body h3 { color: var(--cyan); font-size: 12px; margin: 10px 0 2px; }
#manual-close {
  margin-top: 14px; width: 100%; padding: 10px; border-radius: 10px; border: 1px solid var(--cyan-dim);
  background: #0a1622; color: var(--text); font-size: 14px;
}
#alarm-bar {
  width: 60px; height: 8px; background: #1a2634; border-radius: 4px; overflow: hidden;
  border: 1px solid var(--cyan-dim);
}
#alarm-fill { height: 100%; width: 0%; background: var(--danger); transition: width .6s ease; }
#schild-bar {
  width: 60px; height: 8px; background: #1a2634; border-radius: 4px; overflow: hidden;
  border: 1px solid var(--cyan-dim); cursor: pointer;
}
#schild-fill { height: 100%; width: 0%; background: var(--cyan); transition: width .6s ease; }

/* Spielfeld -- vergroessert auf Kosten von Entscheidungstext/Buttons
   (Layout-Diaet, VISUAL_UPGRADE_ANALYSIS.md Stufe 3 + explizite Anfrage:
   "Entscheidungsfelder kompakter, Spielfeld groesser"). 46->53vh. */
/* Endlos-Kampagne: Galaxie-Theme (game.js applyGalaxyTheme()) ueberschreibt
   --field-bg1/--field-bg2 pro Session -- Default = bisheriges Vela-Blau,
   damit ohne JS-Aufruf (z.B. vor dem ersten Session-Response) nichts anders
   aussieht als vorher. */
#field {
  flex: 0 0 53vh; position: relative; overflow: hidden;
  --field-bg1: #0e2233; --field-bg2: #050b14;
  background: radial-gradient(ellipse at 50% 30%, var(--field-bg1), var(--field-bg2) 70%);
}
#fieldmap { width: 100%; height: 100%; display: block; }
/* Galaxiewechsel-Flug (game.js playFieldTransitionOut/In): reiner CSS-
   Transform/Opacity-Uebergang auf dem GESAMTEN Canvas -- absichtlich keine
   neue Server-Flugphase in der Leg-FSM, siehe Plan. */
#warp-flash {
  position: absolute; inset: 0; background: #eaffff; opacity: 0;
  pointer-events: none; z-index: 5;
}
/* Energiesparmodus-Vignette (Nutzerkritik 2026-07-30: "rot hintermalter
   Hintergrund oder Blitze oder rote Rufzeichen") -- Radialverlauf von den
   Raendern her (Mitte bleibt frei, Spielfeld bleibt lesbar), auf
   body.energy-warning/-critical umgeschaltet statt eines dritten Zustands
   hier -- so bleibt die Opazitaets-/Pulslogik an einer Stelle. */
#energy-vignette {
  position: absolute; inset: 0; pointer-events: none; z-index: 4;
  background: radial-gradient(ellipse at center, transparent 35%, rgba(255,59,92,.4) 100%);
  opacity: 0; transition: opacity .8s ease;
}
/* "caution" (Konzept 12 Abschnitt D, low_reserve-Stufe): sichtbar, aber
   bewusst OHNE Puls -- reine Vorwarnung, keine Krise. Reuse derselben
   #energy-vignette statt eines eigenen Elements. */
body.energy-caution #energy-vignette { opacity: .3; background: radial-gradient(ellipse at center, transparent 40%, rgba(255,209,102,.35) 100%); }
body.energy-warning #energy-vignette { opacity: .55; animation: energyPulseSlow 3.2s ease-in-out infinite; }
body.energy-critical #energy-vignette { opacity: .8; animation: energyPulseFast 1.1s ease-in-out infinite; }
@keyframes energyPulseSlow { 0%, 100% { opacity: .4; } 50% { opacity: .7; } }
@keyframes energyPulseFast { 0%, 100% { opacity: .55; } 50% { opacity: .95; } }

#toast {
  /* FIXED und ueber allen Overlays. Vorher: absolute mit z-index 6 -- damit
     lag der Toast UNTER der Stadtansicht (z-index 60), und jede Meldung von
     sendOrder() war dort unsichtbar. Der Nutzer klickte auf "bauen", bekam
     eine Fehlermeldung, die niemand sehen konnte, und meldete zurecht
     "bauen ging nicht" (2026-08-02). Ein Hinweis, den man nicht sieht, ist
     kein Hinweis. */
  position: fixed; top: 8px; left: 50%; transform: translateX(-50%) translateY(-12px);
  z-index: 200; background: rgba(10,22,34,.95); border: 1px solid var(--cyan-dim);
  border-radius: 8px; padding: 6px 12px; font-size: 12px; color: var(--text);
  opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .2s ease;
  max-width: 85%; text-align: center;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Radiales Iris-Baumenue (ersetzt das Tab/Karten-Modal, Nutzerkritik:
   "mir gefaellt das baumenu ueberhaupt nicht" -- Konzept mit Opus 5
   entwickelt). Tap aufs Mutterschiff oeffnet eine Blenden-Animation
   (Clip-Path-Kreis vom Mutterschiff-Bildschirmpunkt aus), danach ein Ring
   aus 4 Kategorie-Kacheln auf denselben vier Achsen, die das modulare
   Mutterschiff-Rendering schon nutzt (Winkel=Kategorie, Radius=Tier). Alle
   Positionen laufen ueber CSS custom properties --a (Winkel) und --r
   (Radius), die einzige Trigonometrie bleibt in JS (Winkel-/Radius-
   Zuweisung), die eigentliche Platzierung ist reine CSS-Transform-Kette
   (Gegenrotation haelt Kacheln aufrecht). */
#radial-menu {
  position: fixed; inset: 0; z-index: 25; pointer-events: none;
}
#radial-menu[hidden] { display: none; }
#radial-menu.open { pointer-events: auto; }
#radial-backdrop {
  position: absolute; inset: 0; background: rgba(2,8,14,.82);
  clip-path: circle(0px at var(--mx, 50%) var(--my, 50%));
  transition: clip-path .38s cubic-bezier(.16,1,.3,1), backdrop-filter .38s ease;
}
#radial-menu.open #radial-backdrop { clip-path: circle(150vmax at var(--mx, 50%) var(--my, 50%)); backdrop-filter: blur(10px); }
#radial-menu.closing #radial-backdrop { transition-duration: .26s; }

#radial-ring { position: absolute; left: var(--mx, 50%); top: var(--my, 50%); width: 0; height: 0; }

.radial-connector {
  position: absolute; left: 0; top: 0; height: 1px; width: var(--r); background: rgba(52,229,255,.25);
  transform-origin: left center; transform: rotate(var(--a)) scaleX(0);
  transition: transform .3s ease .05s;
}
#radial-menu.open .radial-connector { transform: rotate(var(--a)) scaleX(1); }

.radial-tile {
  position: absolute; left: 0; top: 0; width: var(--size, 84px); height: var(--size, 84px);
  margin: calc(var(--size, 84px) / -2) 0 0 calc(var(--size, 84px) / -2);
  transform: rotate(var(--a)) translateX(var(--r)) rotate(calc(-1 * var(--a))) scale(0);
  opacity: 0; transition: transform .3s cubic-bezier(.16,1,.3,1) var(--delay, 0s), opacity .3s ease var(--delay, 0s), filter .2s ease, box-shadow .15s ease;
  clip-path: polygon(29% 0, 71% 0, 100% 29%, 100% 71%, 71% 100%, 29% 100%, 0 71%, 0 29%);
  background: radial-gradient(circle at 30% 25%, rgba(20,50,80,.92), rgba(6,14,24,.95));
  box-shadow: 0 0 0 1px rgba(52,229,255,.35);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  color: var(--text); cursor: pointer; text-align: center; padding: 4px;
}
.radial-tile.show { transform: rotate(var(--a)) translateX(var(--r)) rotate(calc(-1 * var(--a))) scale(1); opacity: 1; }
.radial-tile.dim { transform: rotate(var(--a)) translateX(calc(var(--r) * .68)) rotate(calc(-1 * var(--a))) scale(.62); filter: saturate(.4) brightness(.65); }
.radial-tile.active { box-shadow: inset 0 0 0 2px var(--cyan); }
.radial-tile.selected { box-shadow: inset 0 0 0 2px var(--cyan); }
/* Eindeutige Drei-Wege-Kennzeichnung (Nutzerkritik: "eindeutige Markierung
   was baubar ist und was nicht, auch was mit den vorhandenen Ressourcen
   leistbar ist und was nicht") -- vorher sah "kaufbar" identisch zu
   "technisch gesperrt" aus, nur ein dünner Rahmen unterschied sie:
   buildable = klar gruen mit Haken-Badge und leichtem Glimmen,
   locked-tech = stark abgedunkelt + Schloss-Badge (Voraussetzung fehlt --
   bewusst nicht ausgeblendet, bleibt als Tech-Baum-Info sichtbar),
   locked-resource = normale Helligkeit, aber Warnfarbe + Fehlbetrag direkt
   auf der Kachel (kein Detail-Sheet-Umweg noetig, um zu sehen was fehlt).
   WICHTIG: box-shadow MUSS inset sein -- die Kachel hat clip-path (Oktagon),
   und clip-path schneidet das gesamte Rendering inkl. box-shadow auf die
   Polygonflaeche zu. Ein normaler (outset) Ring liegt komplett ausserhalb
   der Box-Kante und wird dadurch vollstaendig unsichtbar (per Playwright-
   Screenshot-Review durch vier unabhaengige Personas aufgedeckt: "kaufbar"
   sah pixelgleich zu "ressourcengesperrt" aus). Zusaetzlich zur Ring-Farbe
   bekommt jeder Zustand einen eigenen Flaechen-Hintergrund, damit die
   Unterscheidung nicht an einem einzelnen 2px-Rand haengt. */
.radial-tile.buildable {
  background: radial-gradient(circle at 30% 25%, rgba(58,150,96,.95), rgba(8,32,18,.95));
  box-shadow: inset 0 0 0 2px var(--ok, #6fe38a), 0 0 14px rgba(111,227,138,.45);
}
/* brightness .55->.72: Persona-Review-Befund "mittelgrau auf fast-schwarz,
   das ist unlesbar, nicht nur gedaempft" -- Label muss trotz Tech-Sperre
   noch lesbar bleiben (die Kachel ist bewusst SICHTBAR als Tech-Baum-Info,
   siehe Kommentar oben, das nuetzt nichts wenn man sie nicht lesen kann). */
.radial-tile.locked-tech { filter: saturate(.25) brightness(.72); }
.radial-tile.locked-resource {
  background: radial-gradient(circle at 30% 25%, rgba(94,76,18,.92), rgba(30,22,4,.95));
  box-shadow: inset 0 0 0 2px #ffd166, 0 0 10px rgba(255,209,102,.35);
}
.radial-tile.locked-resource .sub { color: #ffd166; }
/* Konzept 12 Abschnitt D ("Gestaffelte Energieeskalation"): Risiko-Chip fuer
   Schiffskarten, deren Kauf laut Server (entry.energie_risiko) die
   Energiebilanz naechsten Sol ins Minus bringen wuerde -- NUR "art"=="schiff"
   traegt dieses Feld (Gebaeude/Tuerme haben keinen Energieunterhalt, siehe
   GameSession.yard_entries()-Kommentar). Eigene Ecke unten links, damit sich
   der Chip nicht mit dem Lock-/Haken-Badge oben oder dem ⓘ rechts
   ueberlagert; dieselbe Warnfarbe wie die HUD-"caution"-Stufe (#ffd166). */
.radial-tile .energy-risk-badge {
  position: absolute; bottom: 3px; left: 4px; font-size: 10px; line-height: 1;
  color: #ffd166; filter: drop-shadow(0 0 2px rgba(0,0,0,.9));
}
.radial-tile.energy-risk { box-shadow: inset 0 0 0 2px #ffd166, 0 0 10px rgba(255,209,102,.35); }
.radial-tile .badge {
  position: absolute; top: 3px; left: 50%; transform: translateX(-50%); font-size: 11px; line-height: 1;
  filter: drop-shadow(0 0 2px rgba(0,0,0,.9));
}
.radial-tile.buildable .badge { color: var(--ok, #6fe38a); }
.radial-tile.locked-tech .badge { color: var(--text-dim, #9fb3c0); }
.radial-tile.locked-resource .badge { color: #ffd166; }
/* Kategorie-Ring-Kacheln (Ebene 1): Zaehler-Badge statt Zustands-Icon --
   zeigt schon vor dem Reinklicken, wie viele Elemente in dieser Kategorie
   gerade kaufbar sind (Nutzerkritik: "man muss sich immer durch das
   gesamte Menue durchklicken"). */
.radial-tile .badge.count {
  background: var(--ok, #6fe38a); color: #04150a; font-weight: 700;
  border-radius: 50%; min-width: 15px; height: 15px; padding: 0 2px;
  display: flex; align-items: center; justify-content: center; font-size: 10px;
  filter: none;
}
/* Nutzerkritik: "kein (i) wofuer die jeweiligen Einheiten/Tuerme gut
   sind" -- rechte Kante statt Ecke, weil die Kachel ein Oktagon ist
   (clip-path oben) und Ecken abgeschnitten sind; bei y=50% liegt die
   rechte Kante im flachen (nicht abgeschnittenen) Segment. Eigener Klick-
   Handler mit stopPropagation-Effekt (siehe #radial-menu-Listener) --
   Toast statt Navigation, dieselbe Vergroesserte-Trefferflaeche-Technik
   wie .choice-info-btn. */
.tile-info-btn {
  position: absolute; top: 50%; right: 1px; transform: translateY(-50%);
  width: 15px; height: 15px; display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: rgba(52,229,255,.18); color: var(--cyan);
  font-size: 9px; line-height: 1; cursor: help; z-index: 1;
}
.tile-info-btn:hover, .tile-info-btn:active { background: rgba(52,229,255,.4); }
.tile-info-btn::before { content: ""; position: absolute; inset: -8px; }
.radial-tile .glyph { font-size: 22px; line-height: 1; }
/* width:100% ist noetig, WEIL der Elternflex align-items:center setzt --
   ohne das schrumpfen Label/Sub-Spans auf ihre eigene Textbreite und
   laufen ueber die Oktagon-Kachel hinaus statt umzubrechen (bei deutschen
   Komposita wie "Verteidigungszentrale" sonst garantiert). */
.radial-tile .label {
  width: 100%; font-size: 8px; line-height: 1.15; text-transform: uppercase; letter-spacing: .02em;
  color: var(--cyan); overflow-wrap: break-word; hyphens: auto;
}
.radial-tile .sub { width: 100%; font-size: 8px; color: var(--text-dim, #9fb3c0); overflow-wrap: break-word; line-height: 1.1; }
/* Produktion: belegte Slots werden zum Fortschrittsring (--p 0..1 aus
   1-sols_remaining/sols_total, per rAF-Loop per style.setProperty
   fortgeschrieben, kein innerHTML-Rebuild noetig). Freie Slots gestrichelt. */
.radial-tile.slot-busy { background: conic-gradient(var(--cyan) calc(var(--p, 0) * 1turn), rgba(6,14,24,.95) 0); }
.radial-tile.slot-busy .glyph, .radial-tile.slot-busy .label { background: rgba(6,10,16,.55); border-radius: 6px; padding: 0 2px; }
.radial-tile.slot-free { background: transparent; box-shadow: 0 0 0 2px dashed rgba(52,229,255,.4); border: 2px dashed rgba(52,229,255,.4); }

/* Detailkarte (Ebene 3) -- Desktop freischwebend neben dem Bogen, Mobile
   als Bottom-Sheet (siehe Media-Query unten). */
#radial-detail {
  position: fixed; z-index: 27; background: radial-gradient(circle at top left, rgba(20,50,80,.94), rgba(6,14,24,.97));
  backdrop-filter: blur(16px); box-shadow: 0 0 0 1px rgba(52,229,255,.3), 0 16px 40px rgba(0,0,0,.7);
  border-radius: 14px; padding: 16px; width: 260px; text-align: left;
  opacity: 0; transform: scale(.92); transition: opacity .16s ease, transform .16s ease; pointer-events: none;
}
#radial-detail.show { opacity: 1; transform: scale(1); pointer-events: auto; }
#radial-detail .rd-title { font-size: 15px; color: var(--cyan); margin: 0 0 8px; }
#radial-detail .rd-kosten { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
#radial-detail .rd-chip { font-size: 11px; padding: 3px 8px; border-radius: 999px; border: 1px solid var(--cyan-dim); color: var(--cyan); }
#radial-detail .rd-wirkung { font-size: 12px; color: var(--text-dim, #9fb3c0); margin: 0 0 8px; }
#radial-detail .rd-vergleich { font-size: 11px; margin: 2px 0; display: flex; justify-content: space-between; }
#radial-detail .rd-vergleich .delta.pos { color: var(--ok, #6fe38a); }
#radial-detail .rd-vergleich .delta.neg { color: var(--danger); }
#radial-detail .rd-grund { font-size: 12px; color: #ffd166; margin: 0 0 8px; }
/* Konzept 12 Abschnitt D: Kauf-Risiko-Hinweis in der Detailkarte, dieselbe
   Warnfarbe wie .rd-grund/die HUD-"caution"-Stufe -- bewusst eine eigene
   Klasse, da die Karte hier (anders als bei .rd-grund) trotzdem kaufbar
   bleibt, nur eine Folge des Kaufs gewarnt wird. */
#radial-detail .rd-energie-risiko { font-size: 12px; color: #ffd166; margin: 0 0 8px; }
/* Persona-Review-Befund: dunkles Navy auf dunklem Panel las sich wie ein
   deaktivierter Button, obwohl BESTELLEN die Primaeraktion der Detailkarte
   ist -- kraeftige Cyan-Flaeche statt Outline-Look, damit "aktiv/klickbar"
   sofort erkennbar ist. [disabled] bleibt sichtbar abgedunkelt. */
#radial-detail .rd-buy {
  width: 100%; margin-top: 6px; padding: 10px; border-radius: 8px; border: 1px solid var(--cyan);
  background: var(--cyan); color: #04141c; font-size: 14px; font-weight: 700;
}
#radial-detail .rd-buy[disabled] { opacity: .35; pointer-events: none; background: #123246; color: var(--text-dim, #9fb3c0); border-color: var(--cyan-dim); }
/* Rot ist im Rest der UI reserviert fuer Gefahr/Zerstoerung (--danger) --
   "Schliessen" ist ein neutraler Abbruch, kein destruktiver Akt. background/
   border/padding explizit neutralisiert, sonst rendert der native <button>
   ein graues UA-Chrome-Rechteck, das wie ein zweiter, aber unlesbarer
   Button neben BESTELLEN wirkt (grauer Text auf grauem Standard-Button). */
#radial-detail .rd-close {
  display: block; margin-top: 8px; width: 100%; text-align: center; padding: 4px;
  background: transparent; border: none; color: var(--text-dim, #9fb3c0); font-size: 12px;
}

#radial-close-hint { position: absolute; left: var(--mx, 50%); top: var(--my, 50%); width: 0; height: 0; }
#radial-close-hint span {
  /* top 96px->200px: die Verteidigung-Kachel sitzt bei RING_R1=120px unter
     dem Hub mit ~84px Kantenlaenge (Bereich ~78-162px) -- 96px lag mitten
     in der Kachel (Persona-Review-Befund: Hinweistext ueberlappt sie). */
  position: absolute; left: 0; top: 200px; transform: translateX(-50%); font-size: 10px; color: var(--text-dim, #9fb3c0);
  opacity: 0; transition: opacity .2s ease .3s; white-space: nowrap;
}
#radial-menu.open #radial-close-hint span { opacity: .6; }

/* Mobile (<880px, einziger bestehender Breakpoint im Projekt): der
   Kategorie-Ring (Ebene 1) bleibt wie auf Desktop -- 4 Kacheln passen --
   ab Ebene 2 wechselt game.js auf ein Bottom-Sheet mit zwei horizontal
   scrollbaren Kachelreihen (Tier-1/Tier-2) statt eines zweiten Bogens, der
   auf schmalen Bildschirmen keinen Platz haette. Der gedimmte Ring bleibt
   sichtbar darueber. */
#radial-sheet {
  display: none;
}
@media (max-width: 879px) {
  #radial-sheet {
    display: block; position: fixed; left: 0; right: 0; bottom: 0; z-index: 26;
    background: rgba(6,14,24,.96); backdrop-filter: blur(12px);
    border-top: 1px solid rgba(52,229,255,.3); border-radius: 16px 16px 0 0;
    padding: 10px 0 calc(10px + env(safe-area-inset-bottom));
    transform: translateY(100%); transition: transform .28s cubic-bezier(.16,1,.3,1);
  }
  #radial-sheet.show { transform: translateY(0); }
  #radial-sheet .rs-title { font-size: 12px; color: var(--cyan); padding: 0 14px 6px; }
  .radial-row { display: flex; gap: 8px; overflow-x: auto; padding: 4px 12px 10px; scroll-snap-type: x proximity; }
  .radial-row .radial-tile {
    position: static; margin: 0; transform: none !important; opacity: 1 !important; flex: 0 0 auto;
    scroll-snap-align: start; transition: box-shadow .15s ease;
  }
  #radial-detail { left: 0 !important; right: 0; bottom: 0; top: auto !important; width: auto; max-width: none;
    border-radius: 16px 16px 0 0; transform: translateY(100%); z-index: 28; }
  #radial-detail.show { transform: translateY(0); }
}

.yard-turm-built { display: flex; align-items: center; gap: 6px; font-size: 12px; margin-bottom: 6px; }
.yard-turm-built .rang-badge {
  display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px;
  border-radius: 4px; background: rgba(52,229,255,.2); color: var(--cyan); font-size: 10px; font-weight: 700;
}
.yard-turm-built .ammo-pips { display: flex; gap: 2px; flex: 1; }
.yard-turm-built .ammo-pip { width: 6px; height: 6px; border-radius: 50%; background: #334155; }
.yard-turm-built .ammo-pip.filled { background: #ffd166; }
.yard-turm-built button {
  font-size: 11px; padding: 3px 8px; border-radius: 6px; border: 1px solid var(--cyan-dim);
  background: #0d1e2b; color: var(--text);
}
.yard-turm-built button[disabled] { opacity: .4; pointer-events: none; }

/* Buttons -- kompakter (22->18vh), jetzt ÜBER dem Chatverlauf statt darunter
   (Nutzerkritik: "unten ein Chatverlauf, oben wie bisher die vorgeschlagenen
   Optionen"). padding-right:108px war eine Ausweichung vor #avatar-wrap, als
   die Buttons noch direkt ueber dem Avatar sassen -- jetzt sitzen sie oben im
   Sidebar, der Avatar unten, keine Ueberlappung mehr moeglich.

   Nutzerkritik 2026-07-31 ("die untere Leiste ist nicht bedienbar"):
   #com-recommend (die proaktive COM-Empfehlungskarte) sass als eigener
   `flex: 0 0 auto`-Geschwister-Knoten von #buttons -- seine Hoehe (Button +
   Hint-Text, real gemessen ~74px auf einem 700px hohen Mobile-Viewport)
   war NIRGENDS im vh-Budget eingerechnet (hud 7 + field 44 + buttons 15 +
   bottom-bar 12 + chat-log min 18 = 96vh, +com-recommend obendrauf ohne
   Abzug). Da hud/field/buttons/bottom-bar alle flex-shrink:0 sind (feste
   vh-Basis), hatte die Ueberschreitung nirgends Platz und schob
   #bottom-bar komplett aus dem sichtbaren Bereich, sobald COM etwas
   empfahl. Fix: #com-recommend + #buttons jetzt gemeinsam in #decision-
   area (siehe dort) -- EIN Budget-Posten statt zwei, EIN Overflow-Rand
   statt eines verschachtelten zweiten. #buttons selbst ist jetzt ein
   flex:1-Kind von #decision-area (kein eigenes vh-Budget, kein eigenes
   overflow-y mehr -- vermeidet verschachteltes Doppel-Scrollen). */
#decision-area {
  flex: 0 0 20vh; display: flex; flex-direction: column;
  overflow-y: auto; min-height: 0;
}
#buttons {
  flex: 1 1 auto; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: start; gap: 6px;
  padding: 6px 14px; min-height: 0;
}
#buttons > .choice-btn:only-child { grid-column: 1 / -1; }
/* Proaktiver COM-Berater (Nutzerkritik: "COM soll wirklich vorschlagen was
   sinnvoll waere zu bauen") -- eigener Container UEBER #buttons, unabhaengig
   von renderSituation() befuellt, damit ein Klick (Direktbestellung ueber
   /api/game/order) nicht mit dem Situationswechsel kollidiert. Leer = kein
   Kind-Element = keine Hoehe, faellt optisch nicht auf, wenn nichts zu
   empfehlen ist. */
#com-recommend { flex: 0 0 auto; padding: 6px 14px 0; }
#com-recommend .com-recommend-btn .hint {
  font-size: 11px; color: var(--text-dim, #9fb3c0); line-height: 1.3; white-space: normal;
}
.choice-btn {
  width: 100%; min-height: 44px; text-align: left; border-radius: 10px;
  background: linear-gradient(180deg, #123246, #0c2230);
  border: 1px solid var(--cyan-dim); color: var(--text);
  font-size: 14px; padding: 7px 12px; display: flex; flex-direction: column; gap: 2px;
  box-shadow: 0 0 0 1px rgba(52,229,255,.08), 0 3px 8px rgba(0,0,0,.5);
  transition: transform .08s ease, box-shadow .08s ease, filter .08s ease;
  position: relative;
}
.choice-btn:active { transform: scale(.97); background: var(--cyan-dim); }
.choice-btn:disabled { opacity: .4; }
/* Nutzerkritik 2026-07-29 ("kein Hover-Erklaertext, keine i-Buttons"):
   kleines Info-Icon oben rechts jeder Option, tippbar auch ohne Hover
   (Mobile) -- zeigt den vom Server mitgelieferten Wirkungstext als Toast. */
.choice-info-btn {
  position: absolute; top: 4px; right: 6px; width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: rgba(52,229,255,.12); color: var(--cyan);
  font-size: 12px; line-height: 1; cursor: help;
}
.choice-info-btn:hover, .choice-info-btn:active { background: rgba(52,229,255,.3); }
/* Trefferflaeche 20px ist fuer echte Finger knapp (Apple HIG empfiehlt
   44pt) -- unsichtbares ::before vergroessert nur die Tap-Zone, ohne die
   optische Groesse des Icons zu aendern. */
.choice-info-btn::before {
  content: ""; position: absolute; inset: -9px;
}
.choice-btn .chips { font-size: 11px; color: var(--cyan); }
/* Konzept 12 Abschnitt B ("Hints als echte Vorschau"): eine sichtbare
   Kurzvorschau der Wirkung DIREKT unter den Kostenchips -- vorher stand die
   einzige Erklaerung hinter dem (i)-Icon, das viele Spieler nie antippen.
   Eigene, gedimmtere Zeile statt Chip-Farbe, damit Kosten (auffaellig) und
   Wirkungsvorschau (informativ, aber ruhiger) optisch unterscheidbar bleiben. */
.choice-btn .option-preview-summary {
  font-size: 11px; color: var(--text-dim, #9fb3c0); line-height: 1.3; white-space: normal;
}
/* COMs Empfehlung nach einer Beratungsfrage ("was soll ich tun?") --
   pulsierender Rahmen statt stiller Textantwort ohne erkennbare Wirkung. */
.choice-btn.com-suggested { border-color: var(--cyan); animation: comSuggestPulse 1.3s infinite; }
@keyframes comSuggestPulse {
  0%, 100% { box-shadow: 0 0 0 1px rgba(52,229,255,.2), 0 3px 8px rgba(0,0,0,.5); }
  50% { box-shadow: 0 0 0 3px rgba(52,229,255,.7), 0 3px 8px rgba(0,0,0,.5); }
}
/* Nutzerkritik ("keine der Optionen ist auswaehlbar" bei 0 KP -- eigentlich
   war eine kostenlose Option da, ging aber neben den ausgegrauten optisch
   unter): einzige noch waehlbare Option bekommt einen eigenen, warnfarbenen
   Rahmen + Badge statt sich nur durch "nicht transparent" von den
   deaktivierten Buttons abzuheben. */
.choice-btn.only-affordable {
  border-color: #ffd76a; box-shadow: 0 0 0 1px rgba(255,215,106,.35), 0 3px 10px rgba(0,0,0,.5);
  animation: onlyAffordablePulse 1.6s infinite;
}
.only-affordable-badge {
  font-size: 10px; color: #1a1508; background: #ffd76a; font-weight: 700;
  border-radius: 5px; padding: 1px 6px; align-self: flex-start; margin-top: 2px;
}
@keyframes onlyAffordablePulse {
  0%, 100% { box-shadow: 0 0 0 1px rgba(255,215,106,.3), 0 3px 10px rgba(0,0,0,.5); }
  50% { box-shadow: 0 0 0 3px rgba(255,215,106,.75), 0 3px 10px rgba(0,0,0,.5); }
}

/* Kompakte aktuelle Sprechzeile. Der Verlauf bleibt im DOM, ist aber nur
   nach explizitem Antippen sichtbar; damit gehoert der freie Raum im
   Normalbetrieb dem Schlachtfeld. */
#speech-area {
  flex: 1 1 auto; min-height: 0; position: relative;
  padding: 4px 12px 6px; background: var(--panel);
  border-top: 1px solid rgba(52,229,255,.16);
}
#live-speech {
  max-height: 100%; overflow-y: auto; padding: 8px 88px 8px 10px;
  border: 1px solid rgba(52,229,255,.35); border-radius: 12px 12px 4px 12px;
  background: linear-gradient(135deg, rgba(18,50,70,.92), rgba(7,18,29,.96));
  font-size: 13px; line-height: 1.35;
  box-shadow: 0 5px 18px rgba(0,0,0,.35);
  transition: border-color .2s ease, box-shadow .2s ease;
}
#live-speech:not(.has-message) { display: none; }
#live-speech.speaking {
  border-color: var(--speaker); box-shadow: 0 0 10px rgba(52,229,255,.28), 0 5px 18px rgba(0,0,0,.4);
}
#live-speech .chat-msg { border-left: 0; padding-left: 0; }
#chat-toggle {
  position: absolute; right: 98px; top: 10px; z-index: 7;
  min-height: 24px; padding: 3px 9px; border-radius: 999px;
  border: 1px solid var(--cyan-dim); background: #0a1622; color: var(--cyan);
  font: 600 10px/1 inherit; letter-spacing: .02em;
}
#chat-log {
  min-height: 0; max-height: 38vh; overflow-y: auto;
  padding: 8px 14px; font-size: 13px; line-height: 1.35;
  margin-top: 6px; background: #08121d; border: 1px solid var(--cyan-dim); border-radius: 10px;
  display: flex; flex-direction: column; gap: 8px;
}
#chat-log[hidden] { display: none; }
body.chat-history-open #speech-area {
  position: absolute; inset: 8px 8px 70px; z-index: 12;
  display: flex; flex-direction: column; min-height: 0; max-height: none;
  padding: 38px 12px 12px; border: 1px solid var(--cyan-dim); border-radius: 14px;
  background: rgba(5,11,20,.97); box-shadow: 0 16px 40px rgba(0,0,0,.75);
}
body.chat-history-open #live-speech { flex: 0 0 auto; max-height: 30%; padding-right: 12px; }
body.chat-history-open #chat-log { flex: 1 1 auto; max-height: none; }
body.chat-history-open #chat-toggle { top: 8px; right: 16px; }
.chat-msg {
  border-left: 3px solid var(--cyan-dim); padding-left: 8px;
  animation: chatMsgIn .2s ease;
}
@keyframes chatMsgIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.chat-speaker { font-weight: 700; margin-right: 6px; }
/* Karaoke-Wort-Highlighting (Task #120) -- .tts-word umschliesst jedes
   edge-tts-Wort (buildWordSpans() in game.js), .active wird per audioEl-
   "timeupdate" gegen die Wort-Start-/End-Zeitstempel gesetzt, synchron zur
   tatsaechlichen Wiedergabe. --speaker ist dieselbe CSS-Variable, die
   setSpeakerAvatar() schon fuer den Avatar-Glow pflegt -- Highlight-Farbe
   folgt damit automatisch dem aktuellen Sprecher, kein eigener Zustand noetig. */
.tts-word { border-radius: 3px; }
.tts-word.active {
  background: rgba(255, 255, 255, .16);
  text-shadow: 0 0 6px var(--speaker, #34e5ff);
}
/* Nutzerkritik "com text kann ich nicht lesen": #avatar-wrap ist
   position:absolute (bottom:13vh, right:8px, 92px breit) und lag auf
   schmalen Bildschirmen ohne Platzreservierung direkt UEBER den letzten
   Chatzeilen -- die Avatar-Grafik verdeckte den Text komplett statt nur
   daneben zu stehen. padding-right raeumt dem Avatar seine Spalte frei,
   statt ihn zu verschieben (bleibt an seiner fuer Talk-Glow/Emotion-Rig
   eingespielten Position). Muss NACH der #chat-log-Basisregel stehen --
   gleiche Spezifitaet, sonst gewinnt die spaetere Basisregel unabhaengig
   von der Media Query. */
/* Codex-Konzept 12_offene_punkte_konzept_2026-07-31.md Abschnitt F: die
   Portrait-Regel unten griff nur bis max-width:879px -- ein breites, aber
   niedriges Landscape-Geraet (z.B. 932x430) faellt weder in dieses Mobile-
   Fenster (Breite >=880px) noch in den Desktop-Breakpoint (der zusaetzlich
   min-height:480px verlangt, style.css unten bei "min-width: 880px"), landet
   also bei den unveraenderten vh-Basiswerten und zeigt denselben
   #chat-log-Quetsch-Bug erneut. Komma-getrennte Media-Query-Liste = ODER:
   dieselben Werte gelten jetzt zusaetzlich fuer JEDES Geraet mit
   max-height:480px im Querformat, unabhaengig von der Breite. */
@media (max-width: 879px), (max-height: 480px) and (orientation: landscape) {
  /* dvh (dynamic viewport height) rechnet mobile Browser-Toolbars ein, die
     100vh sonst durch ein-/ausblendende Adressleisten falsch machen --
     progressive enhancement: Browser ohne dvh-Unterstuetzung ignorieren die
     zweite Deklaration und behalten die vh-Basiswerte, kein Fallback-Code
     noetig. */
  #chat-log { font-size: 14px; max-height: 48vh; max-height: 48dvh; }
  /* Nutzerkritik 2026-07-31 "text anzeige bereich ... ist im mobilen
     browser zu wenig hoch, man kann es nicht lesen": #field/#buttons/
     #bottom-bar sind auf Mobile alle in vh fixiert (53+18+12=83vh, plus
     7vh #hud = 90vh), #chat-log bekam ueber flex:1 nur den mageren Rest
     (~10vh minus Rahmen/Padding). #field verliert 9vh (bleibt mit 44vh
     immer noch der mit Abstand groesste Bereich), #buttons 3vh (hat
     ohnehin schon overflow-y:auto als Fallback bei vielen Optionen) --
     macht zusammen die min-height oben tatsaechlich nutzbar statt nur
     auf dem Papier zu stehen.  */
  #field { flex-basis: 50.5vh; flex-basis: 50.5dvh; }
  #decision-area { flex-basis: 20vh; flex-basis: 20dvh; }
  #com-recommend .com-recommend-btn .hint,
  .choice-btn .option-preview-summary {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
  }
  #app #avatar-wrap { width: 61px; height: 86px; bottom: 11vh; bottom: 11dvh; }
  #live-speech { padding-right: 90px; }
  #chat-toggle {
    right: 65px; width: 28px; min-width: 28px; padding: 0; font-size: 0;
  }
  #chat-toggle::before { content: "☰"; font-size: 14px; line-height: 1; }
  #app #bottom-bar { flex-basis: 9vh; flex-basis: 9dvh; }
  body.chat-history-open #speech-area { inset: 8vh 8px 10vh; inset: 8dvh 8px 10dvh; }
}

/* Avatar */
#avatar-wrap {
  position: absolute; right: 8px; bottom: 13vh; width: 92px; height: 130px;
  z-index: 5; pointer-events: none;
}
#avatar-placeholder {
  position: relative; width: 100%; height: 100%;
  border-radius: 46px 46px 30px 30px;
  background: radial-gradient(ellipse at 50% 35%, rgba(52,229,255,.35), rgba(52,229,255,.05) 70%);
  filter: drop-shadow(0 0 14px rgba(52,229,255,.5));
}
#avatar-placeholder .ring {
  position: absolute; inset: -6px; border-radius: 50px;
  border: 2px solid rgba(52,229,255,.4); animation: pulse-ring 3s infinite ease-in-out;
  z-index: 2; pointer-events: none;
}
#avatar-placeholder .avatar-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* Quellbild hat viel Leerraum ueber dem Kopf (Full-Body-A-Pose-Render) --
     "top" zeigt daher Leerraum oben und schneidet die Fuesse unten ab.
     "bottom" behaelt die Fuesse und reduziert den Leerraum oben deutlich. */
  object-fit: cover; object-position: bottom center; border-radius: 46px 46px 30px 30px;
  transition: filter .3s ease; display: none;
}
#avatar-placeholder.has-rig .avatar-img { display: block; }
/* talk-glow animiert opacity statt filter (REDESIGN_V5.md §8.1/§8.3) -- vorher
   gewann die Animation in der Kaskade ueber die Emotionsregeln, die
   ebenfalls "filter" setzen: waehrend des Sprechens war die Emotion also
   unsichtbar, genau dann, wenn sie zaehlen wuerde. */
#avatar-placeholder.speaking .avatar-img { animation: talk-glow .28s infinite ease-in-out; }
/* Kein hue-rotate mehr fuer Foto-Portraits (SCHMIEDE/SCHILD) -- auf einem
   Hologramm ist eine Farbverschiebung ein Effekt, auf einem Gesichtsfoto
   verfaerbte "concerned"/"surprised" bislang die Haut gruen. Saettigung/
   Helligkeit funktioniert auf beidem sicher. */
#avatar-placeholder.emotion-skeptical .avatar-img { filter: sepia(.25) saturate(1.3) brightness(.92); }
#avatar-placeholder.emotion-concerned .avatar-img,
#avatar-placeholder.emotion-surprised .avatar-img { filter: saturate(1.35) brightness(1.08) contrast(1.05); }
@keyframes talk-glow { 0%,100% { opacity: 1;} 50% { opacity: .82;} }

/* Foto-Portraits (SCHMIEDE/SCHILD/AUGE, image_url) vs. COMs Hologramm-Sprite
   (sprite, data-material via game.js#setSpeakerAvatar) -- REDESIGN_V5.md §8.1:
   1) Framing: die Quellfotos sind randlose Kopf-/Schulter-Ausschnitte, deren
      Seitenverhaeltnis fast exakt dem Rahmen entspricht, "object-fit: cover"
      beschneidet also kaum etwas -- ein grosses, kantenbuendiges Gesicht.
      COMs Sprite ist dagegen ein Ganzkoerper-Render mit viel Leerraum ueber
      dem Kopf, das im selben Rahmen als kleine, von Glow umgebene Figur
      erscheint. Verkleinern + oben verankern gibt den Fotos denselben
      "Figur im Glow-Rahmen statt randabschneidendes Passfoto"-Look.
   2) Sprech-Indikator: talk-glow (oben) pulst opacity auf dem Bildinhalt --
      bei COMs halbtransparentem Hologramm liest sich das als Flackern, bei
      einem opaken Foto ist eine Opacity-Aenderung kaum wahrnehmbar. Fuer
      Fotos daher: Opacity-Puls aus, stattdessen ein Glow-Ring am Rahmen
      selbst (Box-Shadow auf #avatar-placeholder, nicht am Bildinhalt) in
      Sprecherfarbe (--speaker). */
#avatar-placeholder[data-material="photo"] .avatar-img {
  object-position: top center;
  /* Nutzerkritik 2026-07-31 "die portraits muessen ein bisschen weiter
     nach unten im rahmen": scale(.8) mit transform-origin:top haelt die
     Bildoberkante am Rahmenrand fest und schrumpft nach unten weg --
     die Figur sass dadurch hoch im Rahmen, mit leerem Raum darunter.
     translateY(10%) NACH dem scale verschiebt das bereits verkleinerte
     Bild zusaetzlich nach unten (wirkt im durch scale gestauchten
     Koordinatenraum, daher reicht ein vergleichsweise kleiner Wert). */
  transform: scale(.8) translateY(10%);
  transform-origin: top center;
}
#avatar-placeholder[data-material="photo"].speaking .avatar-img {
  animation: none; opacity: 1;
}
#avatar-placeholder[data-material="photo"].speaking {
  animation: photo-speak-ring .5s infinite ease-in-out;
}
@keyframes photo-speak-ring {
  0%,100% { box-shadow: 0 0 10px 1px var(--speaker), inset 0 0 0 2px rgba(255,255,255,.12); }
  50%     { box-shadow: 0 0 26px 6px var(--speaker), inset 0 0 0 2px rgba(255,255,255,.45); }
}

/* Fallback-Visor/Mund -- nur wenn COMs echtes Bild fehlschlaegt (has-rig.no-avatar) */
#avatar-placeholder .visor, #avatar-placeholder .mouth { display: none; }
#avatar-placeholder.has-rig.no-avatar .visor {
  display: block; position: absolute; top: 30%; left: 20%; right: 20%; height: 14px;
  background: var(--speaker); border-radius: 6px; box-shadow: 0 0 12px var(--speaker);
}
#avatar-placeholder.has-rig.no-avatar .mouth {
  display: block; position: absolute; top: 55%; left: 35%; right: 35%; height: 6px;
  background: rgba(52,229,255,.6); border-radius: 3px; transform: scaleY(1);
  transition: transform .12s ease;
}
#avatar-placeholder.has-rig.no-avatar.speaking .mouth { animation: talk .28s infinite ease-in-out; }

/* Funk-Portrait fuer Sprecher ohne Rig (SCHMIEDE/SCHILD/AUGE) -- rein CSS,
   keine Assets: Ring + Pegelbalken in Sprecherfarbe (REDESIGN_V2.md #1/#6) */
#avatar-placeholder .radio-portrait {
  display: none; position: absolute; inset: 10px; border-radius: 50%;
  border: 2px solid var(--speaker); align-items: flex-end; justify-content: center;
  gap: 4px; padding-bottom: 18px;
}
#avatar-placeholder.no-rig .radio-portrait { display: flex; }
#avatar-placeholder .radio-bar {
  width: 6px; height: 14px; background: var(--speaker); border-radius: 2px;
  box-shadow: 0 0 8px var(--speaker); animation: radio-idle 1.6s infinite ease-in-out;
}
#avatar-placeholder.speaking .radio-bar { animation: radio-talk .35s infinite ease-in-out; }
#avatar-placeholder .radio-bar:nth-child(1) { animation-delay: 0s; }
#avatar-placeholder .radio-bar:nth-child(2) { animation-delay: .1s; }
#avatar-placeholder .radio-bar:nth-child(3) { animation-delay: .2s; }
#avatar-placeholder .radio-bar:nth-child(4) { animation-delay: .3s; }
#avatar-placeholder .radio-bar:nth-child(5) { animation-delay: .15s; }
@keyframes radio-idle { 0%,100% { height: 8px; opacity:.5;} 50% { height: 16px; opacity:.9;} }
@keyframes radio-talk { 0%,100% { height: 10px;} 50% { height: 34px;} }

.callsign {
  position: absolute; left: 0; right: 0; bottom: -2px; text-align: center;
  font-size: 9px; letter-spacing: 1px; color: var(--speaker); text-shadow: 0 0 6px var(--speaker);
  pointer-events: none;
}

#avatar-placeholder.emotion-skeptical.no-rig .ring,
#avatar-placeholder.emotion-concerned.no-rig .ring,
#avatar-placeholder.emotion-surprised.no-rig .ring { border-color: var(--danger); }

@keyframes pulse-ring { 0%,100% { opacity:.3; transform:scale(1);} 50% { opacity:.8; transform:scale(1.04);} }
@keyframes talk { 0%,100% { transform: scaleY(1);} 50% { transform: scaleY(2.4);} }

/* Bottom bar ~12% */
#bottom-bar {
  flex: 0 0 12vh; display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; background: var(--panel); border-top: 1px solid var(--cyan-dim);
}
#play-btn {
  width: 44px; height: 44px; border-radius: 50%; border: 2px solid var(--cyan);
  background: transparent; color: var(--cyan); font-size: 16px; flex: 0 0 auto;
}
#play-btn.playing { background: var(--cyan); color: #04141c; animation: pulse-ring 2s infinite; }
#sfx-btn, #music-btn, #sfx-settings-btn, #lang-btn {
  width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--cyan-dim);
  background: transparent; color: var(--cyan); font-size: 14px; flex: 0 0 auto; opacity: .85;
}
#sfx-btn.muted, #music-btn.muted { color: var(--text-dim); opacity: .5; }
/* #sfx-settings-btn etwas kleiner/dezenter als die Hauptschalter -- ist ein
   Zusatz-Werkzeug, kein primaerer Ein/Aus-Schalter wie #sfx-btn/#music-btn. */
#sfx-settings-btn { width: 30px; height: 30px; font-size: 12px; opacity: .6; }
/* Sprachwechsel (Task #144): DE/EN ist Text statt Icon -- eigene, etwas
   kleinere Schrift, damit "DE"/"EN" in den 36px-Kreis passt wie die
   Emoji-Icons der Nachbar-Buttons. */
#lang-btn { font-size: 11px; font-weight: 700; letter-spacing: .02em; }

#sfx-settings-overlay {
  position: absolute; inset: 0; z-index: 26; display: flex; align-items: center; justify-content: center;
  background: rgba(2,8,14,.9);
}
#sfx-settings-overlay[hidden] { display: none; }
#sfx-settings-card {
  background: radial-gradient(circle at top left, rgba(20,50,80,.92), rgba(6,14,24,.95));
  backdrop-filter: blur(16px);
  box-shadow: 0 0 0 1px rgba(52,229,255,.3), 0 16px 40px rgba(0,0,0,.7);
  border-radius: 14px; padding: 16px; width: 88%; max-width: 320px; max-height: 78dvh; overflow-y: auto;
}
#sfx-settings-title { color: var(--cyan); font-size: 15px; margin: 0 0 10px; font-weight: 700; }
#sfx-settings-card h3 { color: var(--text); font-size: 12px; margin: 10px 0 6px; }
#sfx-settings-body { font-size: 13px; line-height: 1.5; display: flex; flex-direction: column; gap: 10px; }
.sfx-setting-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  border-bottom: 1px solid rgba(255,255,255,.08); padding: 4px 0;
}
.sfx-setting-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--cyan); }
#test-mode-settings {
  margin-top: 14px; padding-top: 10px; border-top: 1px solid rgba(255,209,102,.35);
}
#test-mode-settings p { margin: 0 0 9px; color: var(--text-dim, #9fb3c0); font-size: 12px; line-height: 1.4; }
#test-mode-btn {
  width: 100%; padding: 10px; border-radius: 9px; border: 1px solid #ffd166;
  background: rgba(90,62,10,.35); color: #ffd166; font: 700 13px/1.2 inherit;
}
#test-mode-btn:disabled { opacity: .55; }
#sfx-settings-close {
  margin-top: 14px; width: 100%; padding: 10px; border-radius: 10px; border: 1px solid var(--cyan-dim);
  background: #0a1622; color: var(--text); font-size: 14px;
}
#text-input {
  flex: 1 1 auto; min-width: 0; height: 40px; border-radius: 20px; border: 1px solid var(--cyan-dim);
  background: #0a1622; color: var(--text); padding: 0 14px; font-size: 14px;
}
#mic-btn, #send-btn {
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--cyan-dim);
  background: #0a1622; color: var(--cyan); font-size: 16px; flex: 0 0 auto;
}
#mic-btn.listening { background: var(--danger); color: white; }

/* Overlays */
#start-overlay, #end-overlay, #pin-overlay {
  position: absolute; inset: 0; z-index: 20; display: flex; align-items: center; justify-content: center;
  background: rgba(2,8,14,.94); text-align: center; padding: 24px;
}
#start-overlay[hidden], #end-overlay[hidden], #pin-overlay[hidden] { display: none; }
#start-card h1, #end-card h1, #pin-card h1 { color: var(--cyan); font-size: 22px; letter-spacing: 1px; }
#start-card p, #pin-card p, #end-subtitle { font-size: 14px; opacity: .8; line-height: 1.6; }
#start-btn, #continue-btn, #pin-btn {
  margin-top: 20px; padding: 14px 22px; border-radius: 24px; border: 2px solid var(--cyan);
  background: transparent; color: var(--cyan); font-size: 15px;
}
/* "Kampagne verwerfen" ist ein selten gebrauchter Eskalationsweg, kein
   gleichwertiger Primaerknopf neben "Weiter"/"Erneut versuchen" -- bewusst
   klein und zurueckhaltend (dasselbe neutrale Muster wie .rd-close). */
#retry-galaxy-btn.secondary-link,
#restart-btn.secondary-link {
  display: block; margin: 14px auto 0; padding: 0; border: none; background: transparent;
  color: var(--text-dim, #9fb3c0); font-size: 12px; text-decoration: underline;
}
#retry-galaxy-btn[hidden] { display: none; }
#pin-input {
  margin-top: 18px; width: 140px; height: 52px; text-align: center; letter-spacing: 10px;
  font-size: 22px; border-radius: 12px; border: 1px solid var(--cyan-dim);
  background: #0a1622; color: var(--text);
}
#pin-card.shake { animation: shake .4s; }
.pin-err { color: var(--danger); font-size: 13px; min-height: 18px; margin-top: 8px; }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-8px); }
  75% { transform: translateX(8px); }
}

/* ---- Desktop-Layout ------------------------------------------------------
   Nutzerkritik: "im Desktopmodus sieht alles sehr dumm aus, die breit
   gezogenen Dialoge mit kaum Text, Asteroiden und Spielfeld im mittleren
   Bereich eingepfercht und draussen nichts." Ursache: #field bekam auf jeder
   Breite nur 53vh Hoehe -- toCanvas() letterboxt dann bei breiten Viewports
   auf die Feldhoehe und laesst links/rechts riesige leere Raender, waehrend
   #decision-text/#buttons/#bottom-bar volle Bildschirmbreite gestreckt
   wurden, obwohl sie kaum Inhalt tragen. Ab hier: echtes Zwei-Spalten-Layout
   -- grosses Spielfeld links, feste Seitenleiste rechts (RTS-Vorbild
   Command & Conquer/StarCraft: grosser zentraler Viewport + Seitenpanel
   statt gestapelter Voll-Breite-Leisten). */
@media (min-width: 880px) and (min-height: 480px) {
  #app { display: grid; grid-template-columns: minmax(0, 1fr) clamp(320px, 30vw, 380px); grid-template-rows: 56px 1fr; }
  #hud { grid-column: 1 / -1; flex: unset; }
  #field { grid-column: 1; grid-row: 2; flex: unset; }
  #sidebar {
    display: flex; flex-direction: column; position: relative;
    grid-column: 2; grid-row: 2; overflow: hidden;
    background: var(--panel); border-left: 1px solid var(--cyan-dim);
  }
  /* vh-basierte Fixgroessen (fuer schmale Mobile-Screens kalibriert) weichen
     natuerlicher Inhaltsgroesse -- #chat-log behaelt sein flex:1 und fuellt
     den Rest, damit auf einem hohen Desktop-Monitor kein riesiges leeres
     Feld unter den Buttons entsteht. Bottom-Bar landet automatisch am
     unteren Rand, weil #chat-log den ganzen Zwischenraum aufsaugt -- kein
     margin-top:auto mehr noetig. */
  #decision-area { flex: 0 1 auto; max-height: 48%; overflow-y: auto; }
  #buttons { padding: 16px 20px; gap: 10px; }
  .choice-btn { font-size: 14px; min-height: 58px; padding: 10px 12px; }
  /* Dieselbe Luecke wie die Mobile-Regel oben (Nutzerkritik "com text kann
     ich nicht lesen") -- #avatar-wrap ist bei JEDER Breite position:absolute
     rechts unten, diese Desktop-Regel hatte die Reservierung bisher nicht
     uebernommen: Chatzeilen liefen unter dem Avatar-Kreis weiter und wurden
     dort abgeschnitten (Persona-Review 2026-07-30, siehe konzepte/). */
  #speech-area { margin-top: auto; flex: 0 1 auto; min-height: 74px; max-height: 52%; padding: 8px 12px; }
  #live-speech { font-size: 14px; padding: 11px 96px 11px 12px; }
  #chat-log { font-size: 14px; max-height: 34vh; padding: 12px; }
  #bottom-bar { flex: 0 0 auto; }

  /* Detailkarte etwas breiter als die Mobile-Basis (260px) -- mehr Platz
     fuer Wirkungstext/Vergleichszeilen, ohne haesslich umzubrechen. */
  #radial-detail { width: 300px; }
}


/* ---- Stadtansicht (Nutzerauftrag 2026-08-02) --------------------------
   Vollflaechiges Overlay: "nur das mutterschiff, maximal als stadt, ohne
   weltraum und ohne abbau kampf stress rumherum". Deshalb bewusst KEIN
   Sternenhintergrund und kein HUD -- ein ruhiger Bauraum. */
/* MUSS vor der display-Regel stehen und sie ueberstimmen: das
   hidden-Attribut setzt nur display:none mit UA-Spezifitaet, ein eigenes
   "display: grid" schlaegt es. Ohne diese Zeile liegt die unsichtbare
   Stadtansicht ueber dem ganzen Spiel und faengt jeden Klick ab -- im Test
   scheiterte prompt schon die PIN-Eingabe. */
#city-view[hidden] { display: none !important; }
#city-view {
  position: fixed; inset: 0; z-index: 60;
  background: radial-gradient(circle at 50% 45%, #0e1a28 0%, #070d16 70%, #050910 100%);
  display: grid; grid-template-rows: auto 1fr; grid-template-columns: 1fr;
}
#city-head {
  display: flex; align-items: center; gap: 14px;
  min-height: 56px; padding: 8px 16px; border-bottom: 1px solid rgba(52,229,255,.18);
}
#city-title { color: #34e5ff; font-size: 16px; font-weight: 700; line-height: 1; letter-spacing: .16em; }
#city-sub { color: #7fa8bd; font-size: 13px; line-height: 1.25; }
#city-close {
  margin-left: auto; background: transparent; border: 1px solid rgba(52,229,255,.35);
  color: #9fd6e8; border-radius: 8px; width: 34px; height: 34px; cursor: pointer;
}
#city-close:hover { background: rgba(52,229,255,.12); }
/* min-height:0 ist hier der entscheidende Teil: ein Canvas ist ein ersetztes
   Element mit Eigengroesse, und in einer Grid-Zeile (1fr) waechst es sonst
   ueber die Zeile hinaus, statt sich einzupassen. Mobil schob es dadurch das
   Baupanel komplett unter den Bildschirmrand -- die Stadtansicht oeffnete,
   aber bauen war unmoeglich (Nutzerkritik 2026-08-02: "stadtansicht geht nur
   im browser und nicht in der app view"). */
#city-canvas { width: 100%; height: 100%; min-height: 0; display: block; cursor: pointer; }
#city-panel {
  position: absolute; right: 0; top: 55px; bottom: 0; width: 320px;
  padding: 14px; overflow-y: auto;
  background: rgba(6,12,22,.88); border-left: 1px solid rgba(52,229,255,.18);
}
#city-panel:empty { display: none; }
#city-panel h3 { color: #cfe9f5; font-size: 15px; margin: 0 0 4px; }
#city-panel .city-hint { color: #7fa8bd; font-size: 12px; margin-bottom: 12px; }
.city-opt {
  display: block; width: 100%; text-align: left; margin-bottom: 8px;
  padding: 9px 11px; border-radius: 9px; cursor: pointer;
  background: rgba(20,38,54,.85); border: 1px solid rgba(52,229,255,.25);
  color: #cfe9f5; font: inherit; font-size: 13px;
}
.city-opt:hover:not(:disabled) { background: rgba(52,229,255,.16); }
.city-wait {
  margin-top: 12px; border-color: rgba(126,255,232,.55);
  color: #7effe8; font-weight: 700; text-align: center;
}

/* FARBROLLEN der Stadtansicht -- eine Bedeutung je Farbe.
   Der UI-Perfektionist im Persona-Review 2026-08-02: "Cyan markiert
   gleichzeitig Navigation, freie Plaetze, Auswahl und kaufbare Aktionen ...
   Orange Sperren und Baufortschritt". Diese Doppelbelegung ist aufgeloest:

     Cyan   #34e5ff  -- baubar / anklickbar (freie Parzelle, kaufbarer Eintrag)
     Mint   #7effe8  -- AUSWAHL, sonst nichts
     Bernst #ffd166  -- IM BAU, sonst nichts
     Grau   #8fa3b3  -- gesperrt (dazu Schraffur und Schloss, siehe unten)
     Modulfarbe      -- Identitaet des Gebaeudes, nie Zustand

   Gesperrt heisst NICHT unleserlich. Vorher: opacity .45 auf die ganze Karte,
   wodurch ausgerechnet die Voraussetzung ("Benoetigt: Kaserne") zur
   schwaechsten Information wurde. Und die Sperre war allein farbcodiert --
   der Color-Independent-Auditor: "Kaufbare und gesperrte Eintraege brauchen
   neben Schloss und Text auch verschiedene Flaechenmuster". Deshalb traegt
   die Sperre jetzt drei voneinander unabhaengige Merkmale: Schraffur,
   Schloss-Zeichen und Randfarbe -- lesbar auch ohne Farbwahrnehmung. */
.city-opt:disabled { cursor: not-allowed; }
.city-opt.gesperrt {
  border-color: rgba(143,163,179,.4);
  color: #9fb3c0;
  /* Diagonale Schraffur: das farbunabhaengige Merkmal. */
  background:
    repeating-linear-gradient(135deg,
      rgba(143,163,179,.09) 0 5px,
      rgba(143,163,179,0) 5px 11px),
    rgba(14,24,36,.85);
}
.city-opt.gesperrt .city-cost { color: #6f8fa3; }
.city-opt .city-cost { display: block; color: #7fd6ff; font-size: 12px; margin-top: 3px; }
/* Grau statt Orange: Bernstein ist ab jetzt ausschliesslich "im Bau". */
.city-opt .city-lock { display: block; color: #a8bccb; font-size: 12px; margin-top: 3px; }
@media (max-width: 880px) {
  #city-head { gap: 12px; padding: 8px 16px; }
  #city-title { font-size: 15px; letter-spacing: .14em; }
  #city-sub { margin-left: auto; max-width: 160px; text-align: left; }
  #city-close { margin-left: 0; flex: 0 0 36px; width: 36px; height: 36px; }
  /* Panel als EIGENE, feste Grid-Zeile statt "auto": mit auto haengt die
     Hoehe am Inhalt, und eine lange Bauliste drueckt den Canvas weg. Feste
     Zeile plus eigenes Scrollen haelt beides im Bild. */
  #city-panel { position: static; width: auto; border-left: none;
                border-top: 1px solid rgba(52,229,255,.18);
                min-height: 0; overflow-y: auto; }
  /* Panel nur so hoch wie noetig, hoechstens 42vh. Der Typografie-Review
     2026-08-02 nannte es "eine riesige, nahezu leere Flaeche" -- eine feste
     Zeile ist bei kurzem Inhalt tatsaechlich verschenkter Platz. */
  #city-view { grid-template-rows: auto minmax(0, 1fr) minmax(0, auto); }
  #city-panel { max-height: 42vh; }
  /* Leeres Panel darf die Zeile nicht offen halten -- dann gehoert der
     Platz dem Schiff. */
  #city-panel:empty { display: none; }
  #city-view:has(#city-panel:empty) { grid-template-rows: auto minmax(0, 1fr) 0; }
}


/* Verbindungslinien sind reine Grafik -- sie duerfen keine Klicks abfangen.
   Die zentrale Stadt-Wabe (Nutzerauftrag 2026-08-02) sitzt bei r=0 und lag
   damit genau unter ihnen; im Test war sie schlicht nicht anklickbar. */
.radial-connector { pointer-events: none; }
.radial-tile.city-tile { z-index: 6; }
.radial-tile.city-tile .glyph { font-size: 22px; }


/* Einheitliche Radien und Strichstaerken in der Stadtansicht. Der
   Typografie-Auditor 2026-08-02: "weder einheitliche Radien noch konsistente
   Strichstaerken" -- Schliessen-Knopf 8px, Karten 9px, Panelraender 1px
   gegen 1px, aber unterschiedliche Farben. Eine Groesse, ein Wert. */
#city-close, .city-opt { border-radius: 9px; border-width: 1px; }
#city-head { align-items: baseline; }
#city-close { align-self: center; }
