/* ============================================================
   Chat Tower — Desktop-Viewer-CSS (ui-456, Frontend-Neubau Phase 5,
   P5-Kette Glied 7/10, "Hüllen"-Block)

   1:1 aus viewer.htmls vormaligem Inline-<style id="ct-viewer-style">-Block
   verschoben (Lock: reine Verschiebung, kein Redesign — Selektoren/Werte
   unangetastet). Byte-identische Regeln mit dem Mobile-Viewer leben seither
   EINMAL in viewer-base.css (s. dortiger Kopfkommentar); alles Übrige hier
   ist Desktop-eigen (inkl. Regeln, die es auf Mobile mit ABWEICHENDEM Wert
   ebenfalls gibt — Bestand gewinnt bei Abweichung, keine Vereinheitlichung).

   Ladereihenfolge (Kaskade-Lock, Ticket ui-456): tokens.css →
   <style id="ct-viewer-style"> (leerer Boot-/Fragment-Anker, VOR dieser
   Datei — nicht danach, s. viewer-base.css-Kopfkommentar für die volle
   Herleitung + den empirisch gefundenen Cascade-Bug, den diese Reihenfolge
   fixt) → viewer-base.css → DIESE DATEI (gewinnt zuletzt, auch gegen ein
   evtl. injiziertes Companion-CSS-Fragment). Gelinkt unter /client/css/desktop.css
   (no-store-Mount, s. viewer-base.css-Kopfkommentar für Mount-Begründung).
   ============================================================ */
  * { box-sizing: border-box; }
  /* ui-282: Panel-Zoom skaliert jetzt höhen-abhängig (s. #panel-Kommentar unten) —
     die VISUELLE #panel-Breite (280px unskaliert × jeweiliger zoom-Faktor) ist
     dadurch KEINE feste Zahl mehr. #stage (Geometrie-Anker, ui-033) reserviert
     rechts exakt diese visuelle Breite (`width: calc(100vw - <var> - 288px)`) —
     vorher war `364px` (280×1.3) hart codiert, was bei der neuen zoom:1.0-Stufe
     eine 84px-Lücke zwischen Stage-Ende und Panel-Anfang erzeugt hätte (Canvas/
     Tower-Zentrierung hätte 84px zu früh aufgehört, VERIFIZIERT per Browser-Smoke
     vor diesem Fix: stageRight=1556 vs. panelLeft=1640 bei 1920×920). Default hier
     = Stream-Wert (280×1.3=364px), die Media-Query-Stufe unten überschreibt NUR
     diese eine Variable. */
  :root { --panel-w-visual: 364px; }
  body {
    margin: 0; padding: 0;
    background: var(--bg);
    color: var(--text);
    font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    position: relative;
    width: 100vw; height: 100vh; height: 100svh; /* ui-265 DL-4: svh-Fallback, OBS-CEF bleibt auf vh */
    overflow: hidden;
  }

  /* ui-413: die bildschirmbreite #viewer-app-nav (ui-405, oben) ist am
     Desktop wieder weg — ersetzt durch den Bereichs-Cluster #nav-cluster
     unten links (s. dortiger Kommentar) + die Sub-Nav-Zeile #ph-subnav im
     Popin-Kopf. Mit ihr entfällt auch --app-nav-h: die drei Verbraucher
     #hud/#stage/#panel unten sind auf ihre Vor-ui-405-Formeln zurückgebaut
     (kein top-Offset/Höhen-Abzug mehr nötig — die Leiste, für die reserviert
     wurde, existiert nicht mehr). Mobile behält die
     Leiste unverändert (ui-412, out of scope hier).
     ============================================================ */

  /* ============================================================
     LINKE HUD-SÄULE — #hud (DL-3, kanonische Umsetzung)
     zoom:1.3 → 222px × 1.3 ≈ 288px visuell = Stage-left-Inset
     pointer-events:none — read-only Spectator-Client
     z-index: 2 (ui-033 DL-4): ueber Canvas (0) und Stage (1), deckt seitlichen Bleed.
     ============================================================ */
  #hud {
    zoom: 1.3;
    position: absolute;
    left: 0; top: 0;
    width: 222px;
    height: calc(100vh / 1.3); height: calc(100svh / 1.3); /* ui-265 DL-4: svh-Fallback */
    background: var(--bg2);
    border-right: 1px solid var(--border);
    padding: 14px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    color: var(--muted);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
    z-index: 2;
  }
  #hud .big   { color: var(--text); font-size: 18px; }
  #hud .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; margin-top: 6px; }
  #hud .dmg   { color: var(--cyan); font-weight: 600; font-size: 22px; }
  #hud .wave  { color: var(--yellow); font-weight: 600; font-size: 22px; letter-spacing: 0.02em; }
  #hud .wave.boss { color: var(--magenta); text-shadow: 0 0 10px var(--magenta); }
  #hud .wave-phase { color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
  #hud .wave-phase.pausing { color: var(--cyan); }

  /* ui-122: #hud-queue ist aus #hud herausgezogen und lebt jetzt in #left-info.
     Die alten #hud-Queue-Styles entfallen; neue Styles stehen beim #left-info-Block. */

  /* ============================================================
     STAGE — unsichtbarer Geometrie-Anker zwischen HUD und Panel (ui-033)
     Box-Geometrie UNVERAENDERT bei fixem zoom (DL-1): left:288px /
     width:calc(100vw - var(--panel-w-visual) - 288px)
     → Tower, Rim-Cards und In-Stage-Overlays bleiben unveraendert zentriert.
     background: transparent (der Canvas darunter fuellt alles mit #06080f).
     overflow: visible (Canvas ist aus dem Flow raus → kein Clip noetig).
     Kein z-index (auto): #stage bildet KEINEN eigenen Stacking-Context.
     → In-Stage-Overlays (Endscreen z-index:120)
       partizipieren weiter im body-Context → bleiben ueber #settings-bar (92).
       (DL-4: Schicht-Reihenfolge korrekt, kein Stacking-Context-Bruch.)
       ui-169: #enemy-intro-stage entfernt — Gegner-Intro-Karten liegen jetzt
       im #left-info-Panel (#enemy-log, 4. Body). Kein In-Stage-Overlay mehr.
     #canvas ist ein Kind von #stage; position:fixed reisst es aus dem Flow in den
     body-Stacking-Context (z-index:0). #stage (z-index:auto positioned) liegt laut
     CSS-Spec darueber, da positioned-auto-Elemente ueber z-index:0-fixed-Kindern rendern.
     left: 288px  (= HUD zoom×222px, HUD bleibt IMMER zoom:1.3 — ui-282-Scope ist
           nur das rechte Panel, s.u.)
     width: calc(100vw - var(--panel-w-visual) - 288px)
            (var(--panel-w-visual) = Panel zoom×280px, HÖHEN-abhängig seit ui-282
             — s. #panel-Kommentar; 288px = HUD zoom×222px, unveraendert)
     @ 2560×1440 (Stream, zoom:1.3): Stage = 2560 - 364 - 288 = 1908px breit
     @ 1920×920 (ui-282, zoom:1.0):  Stage = 1920 - 280 - 288 = 1352px breit
     ============================================================ */
  #stage {
    position: absolute;
    left: 288px; top: 0;
    width: calc(100vw - var(--panel-w-visual) - 288px);
    height: 100vh; height: 100svh; /* ui-265 DL-4: svh-Fallback, OBS-CEF bleibt auf vh */
    background: transparent;
    overflow: visible;
  }
  /* ui-033 (DL-2): Canvas voll-viewport, hinter allem (z-index:0).
     Fuellt den gesamten Viewport mit dem Spiel-Hintergrund → kein leeres
     Dahinter bei Browser-Zoom. worldToScreen-Origin = Stage-Mitte (canvas.js).
     position:fixed → Canvas liegt als Kind von #stage im body-Stacking-Context
     bei z-index:0; #stage (z-index:auto) liegt als positioned Parent darueber.
     ui-054: width/height explizit (100vw/100vh) zusaetzlich zu inset:0.
     Grund: inset:0 allein reicht nicht immer — bei bestimmten Render-Zustaenden
     (fruehes load-Event, OBS-CEF) gibt getBoundingClientRect() {width:0,height:0}
     zurueck wenn der Canvas als position:fixed noch nicht gerendert wurde. Dann
     setzt resizeCanvas() canvas.width/height auf 0 → alle Draw-Calls sind No-Ops.
     Explizite width:100vw/height:100vh fixiert das CSS-Layout unabhaengig davon.
     ui-265 DL-4: height zusaetzlich als 100svh (Doppel-Deklaration, OBS-CEF-Fallback
     bleibt auf der vh-Zeile) — verhindert den iOS-URL-Leisten-Resize-Jank. */
  #canvas { display: block; position: fixed; inset: 0; width: 100vw; height: 100vh; height: 100svh; z-index: 0; }

  /* ============================================================
     RECHTES PANEL — #panel, read-only (DL-2, kanonische Umsetzung)
     zoom:1.3 → 280px × 1.3 = 364px visuell
     Inhalt: nur Command-Referenz-Tabelle (renderTable() aus ui.js)
     KEINE klickbaren Upgrade-Zellen, KEIN #log, KEINE Buttons.
     pointer-events:none — kein Klick-Feedback im Spectator-Client (DL-2)
     z-index: 2 (ui-033 DL-4): ueber Canvas (0) und Stage (1), deckt seitlichen Bleed.

     ui-279-Fix (Schaden B, Nachfix zu ui-278): #panel selbst ist NICHT mehr der
     scrollende Knoten — es ist nur noch der äußere Flex-Column-Rahmen (Position/
     Größe/zoom/z-index unverändert). Scrollen + Rearm-Sichtbarkeit werden in
     ZWEI Kind-Knoten aufgeteilt:
       #panel-scroll (flex:1 1 auto; min-height:0; overflow-y:auto) — Titel,
         Chip-Kopfzeile, Stats-Tabelle, Template-Button. Genau das, was vorher
         direkt in #panel lag, minus die Rearm-Box.
       #panel-footer (flex:0 0 auto; pointer-events:auto) — NUR die Rearm-Box
         (`.ip-rearm`, Run-Modus). Liegt AUSSERHALB des Scroll-Containers → bei
         JEDER Panel-Höhe ohne Scrollen sichtbar (Lock: Rearm-Sichtbarkeit ist
         nicht verhandelbar, kein "Panel höher machen"/"Inhalt kleiner zoomen").
     Warum zwei Kinder statt eines internen zweiten scrollenden Wrappers direkt
     auf #panel: #panel selbst bleibt padding-tragend (14px allseitig) und
     bekommt NUR noch flex/overflow:visible — dadurch bleiben `min-width:0`-
     Flex-Kollaps-Regeln von Kind-Elementen unverändert wirksam (kein neuer
     Flex-Kontext, der die bestehenden `.ip-rearm-txt`-min-width:0-Fixes aus
     ui-278 bräche). Beide Kinder erben padding-lose Breite (100%), #panel
     selbst behält sein 14px-Padding auf ALLEN Seiten — #panel-scroll und
     #panel-footer brauchen daher kein eigenes horizontales Padding.
     Reload-freie Rebuilds (Poll/Snapshot/Idiomorph-Morph): #panel-scroll +
     #panel-footer werden aus JEDEM Render-Pfad des Panels aufgebaut (_ipRender-
     RunPanel/_ipRenderCtaPanel/_ipRenderQueuedPanel/_ipRenderHoveredPanel/
     _ipRenderPanel-loading-Zweig) — nicht nur beim Erst-Aufbau, da `panelEl.
     innerHTML` bei jedem Modus-Render komplett neu gesetzt wird (kein Idiomorph
     auf #panel selbst, s. Kommentar bei _ipRenderRunPanel).

     ui-280-Fix (Fiftys eigentliches PTR-Finding über 3 Runden — "unten rechts
     ist die sidebar abgeschnitten"): der ui-278-Fix hatte #panel um 70px
     (unskaliert, zoom:1.3 → ~91px visuell) VERKÜRZT, um die #settings-bar-
     Kollision zu lösen — Ergebnis war ein Loch zwischen Panel-Unterkante und
     Bildschirmrand, in dem Seiten-Hintergrund durchscheint und die Status-
     Icons frei "schweben". Das war die abgeschnittene BOX selbst, nicht nur
     ein Kollisions-Detail. Architektur-Lock (ui-280): Box-Geometrie ≠ Inhalts-
     Geometrie — Kollisionen werden über INNEREN Freiraum gelöst (Padding/
     Spacer), nie über Verkürzen der sichtbaren Box. #panel ist daher wieder
     VOLLE Höhe (kein "- 70px" mehr); der Freiraum für die #settings-bar-Zone
     wandert stattdessen in ein zusätzliches `padding-bottom` AUF #panel selbst
     (s.u.) — das wirkt unabhängig davon, ob #panel-footer gerade befüllt
     (Run-Modus) oder leer ist (`:empty{display:none}`, CTA/Queued/Hovered/
     loading): der Freiraum sitzt am Ende der Flex-Column, hinter #panel-scroll
     UND #panel-footer, nicht "unter dem Footer" — sonst hätte der CTA-/
     Queued-/Hovered-/loading-Zustand (leerer Footer) gar keine Reserve gehabt.

     ui-281-Fix (Fifty: "das scrolling selbst ist schwierig, weil es durch die
     ständige aktualisierung eh wieder hoch scrollt"): jeder komplette
     innerHTML-Rebuild von #panel-scroll (Poll/Snapshot/Refetch) setzt den
     scrollenden DOM-Knoten neu auf — scrollTop springt sonst bei jedem
     Rebuild auf 0. `_ipRenderPanelBody(panelEl, scrollKey, buildFn)` (JS,
     direkt vor _ipRenderPanel) ist der EINE zentrale Save/Restore-Punkt, den
     alle 5 Render-Pfade nutzen (kein 5-facher Einzel-Fix, Architektur-Lock):
     scrollTop von #panel-scroll VOR dem Rebuild sichern, buildFn() ausführen,
     danach restaurieren — aber NUR wenn scrollKey identisch zum vorherigen
     Aufruf ist (gleicher Panel-Modus/-Kontext, z.B. Run→Run beim nächsten
     Poll-Tick). Bei unterschiedlichem scrollKey (echter Modus-/Kontext-
     Wechsel, z.B. Run→CTA nach Run-Ende oder ein anderer gehoverter Tower)
     bleibt der frische Knoten bewusst bei scrollTop 0 (Ticket-AK: Reset bei
     Modus-Wechsel ist erwünscht). Live-Daten laufen währenddessen unverändert
     weiter (kein "Update pausieren beim Scrollen" — Architektur-Lock ui-281).

     ui-282-Fix (Fifty: "es ist so schade das wir da einen scrolling balken
     brauchen"): #panel-zoom ist jetzt HÖHEN-abhängig statt fix 1.3, in zwei
     diskreten Stufen (Media-Query auf Viewport-HÖHE, kein `calc`-Stufenlos —
     Architektur-Lock: Chromium-zoom-Bug-Fläche klein halten + vorhersagbare
     Baselines, s. ui-278/280-Historie). Stream-Kanon (OBS 2560×1440,
     Viewport-Höhe 1440) bleibt bei `zoom:1.3` UNANTASTBAR — die Grenze
     `(min-height: 1200px)` liegt mit deutlichem Puffer darüber, wo `zoom:1.3`
     bereits ohne Scrollen passt (empirisch gemessen: bündig bis ~1150px
     runter, erst bei ~1100px kippt es — 1200px ist also die harte Ticket-
     Vorgabe MIT Sicherheitsabstand, nicht die exakte Kipp-Grenze).
     Bei Viewport-Höhe < 1200px (die meisten realen 1080p-Browserfenster
     liegen bei ~900-940px Viewport-Höhe, Browser-Chrome/Taskleiste ziehen
     ab, s. ui-279-Preset-Herleitung) skaliert `zoom` auf 1.0 — bei diesem
     Wert passt der komplette Run-Panel-Inhalt (inkl. AllMode = ALLE
     Stat-Zeilen) bis runter auf ~860px Viewport-Höhe ohne Scrollen
     (empirisch gemessen: bündig bei 900px/850px kippt knapp). ui-478-
     Korrektur: dieser ~860px-Wert ist durch nachfolgende Panel-Zuwächse
     (ui-446 Template-Block, ui-475 Modus-Paar-Zeile) überholt — der reale
     Kipppunkt lag VOR dem ui-478-Fix längst bei ~944-1000px (Fiftys
     Prod-Fund, s. Ticket-Blocker-Messtabelle). Bleibt hier als historische
     Marke der ursprünglichen ui-282-Messung stehen (zeigt die Ausgangslage
     vor den Inhalts-Zuwächsen), ist aber NICHT mehr der aktuell gültige
     Kipppunkt — nach dem ui-478-Fix (s. Absatz "ui-478-Fix" unten UND der
     Kommentar direkt vor der Media-Query-Regel) ist 944px wieder
     komfortabel im Plus. NUR EINE Zwischenstufe (nicht 3) — der
     Ticket-Zielwert 1920×920 liegt komfortabel in der 1.0-Stufe
     (Overflow-Puffer ~270px), eine dritte Stufe für den schmalen
     860-900px-Korridor hätte keinen belegten Nutzen (kein reales
     1080p-Fenster ist so knapp) und mehr zoom-Bug-Fläche gekostet (Lock).
     Extremfälle < ~860px (z.B. stark verkleinerte Fenster, ~750px) fallen
     durch die 1.0-Stufe durch und scrollen weiter intern — das
     ui-281-Scroll-Persistenz-Netz bleibt dafür unverändert bestehen
     (Architektur-Lock: kein Rückbau).
     WARUM height statt width: das Problem ist die Fensterhöhe (Browser-
     Chrome frisst Vertikal-Raum), Breite ist bei Desktop nie der Engpass
     (Lock, Ticket-Vorgabe). `padding-bottom: 84px` (Icon-Zone-Reserve,
     s.u.) war BIS ui-478 bei BEIDEN Stufen unverändert — die reale
     benötigte Zone ist absolut (`#settings-bar` ist `position:fixed`,
     zoom-unabhängig, real gemessen 58px = Pill-Höhe 44px + `bottom:14px`),
     84px unskaliert deckte das bei jedem Zoom-Wert ≤1.3 komfortabel ab
     (bei 1.0 visuell 84px, bei 1.3 visuell ~109px — beides ausreichend,
     verifiziert per Browser-Smoke + Bündigkeits-Assertion, s. Ticket-
     Notes). Seit ui-478 gilt das NUR NOCH für den zoom-1.3-Zweig
     (Stream-Kanon, Lock ui-282 unverändert) — der zoom-1.0-Zweig
     überschreibt auf den engeren Realbedarf, s. Absatz "ui-478-Fix"
     direkt unten für die volle Herleitung.

     ui-478-Fix (Fifty-Prod-Fund 2026-08-18, "auf 1920×1080 blinkt die
     Scrollbar" + Fifty-Entscheid "Paket A" nach Blocker-Messung): 84px war
     bei zoom 1.0 ~26px MEHR als der reale Bedarf (Pill-Höhe war zuvor auf
     ~42px GESCHÄTZT — für diesen engeren Boden real mit
     `getBoundingClientRect()` gemessen: 44px; + `bottom:14px` = 58px Boden,
     + 6px AK3-Luft = 64px Minimum). Durch die seither gewachsenen
     Panel-Inhalte (ui-446 Template-Block, ui-475 Modus-Paar-Zeile) reichte
     der verbleibende Platz in `#panel-scroll` bei echten 1080p-
     Fensterhöhen (~944px, Browser-Chrome abgezogen) nicht mehr — der
     Inhalt oszillierte exakt um die Overflow-Schwelle, die native
     Scrollbar blendete bei jedem ~1Hz-Poll-Rebuild sichtbar/unsichtbar
     ein/aus ("Flackern") und verengte dabei selbst nochmal den
     Inhaltsbereich (Kaskaden-Effekt, s. Ticket-Blocker-Messung). Fix NUR
     im zoom-1.0-Media-Query-Zweig unten (Lock ui-282: zoom-1.3-Stream-
     Kanon bleibt bei 84px/gap:12px UNANGETASTET): `padding-bottom: 64px`
     (58 Realbedarf + 6px AK3-Luft zur Pill) UND `gap: 12px → 8px` — beide
     Werte REDUZIEREN nur die Reserve, ändern nichts an Inhalt/Struktur
     (Lock ui-280 bleibt eingehalten: Box-Geometrie unverändert, nur der
     innere Freiraum schrumpft auf Realbedarf). Selbst 64/8 reichte für den
     größten Zustand (`pausiert`, 2-zeiliger Status-Subtext) NICHT
     (weiterhin Overflow bei 944px, s. Ticket-Blocker) — den fehlenden Rest
     liefert KEIN drittes CSS-Reserve-Hebel, sondern zwei gekürzte
     Inhalts-Strings (Fifty-Entscheid Paket A, hebt die "Rearm-/Template-
     Text bleiben"-Sperre NUR für diese zwei Strings auf): Rearm-`small`-
     Text 3→1 Zeile (`rearm.js`) UND `pausiert`-Subtext 2→1 Zeile
     (`template.js`) — macht den 64/8-Boden bei ALLEN fünf
     Template-Zuständen wieder komfortabel positiv (Messtabelle: Ticket-
     Notes).
     ============================================================ */
  #panel {
    zoom: 1.3;
    position: absolute;
    right: 0; top: 0;
    width: 280px;
    /* ui-280-Fix: volle Höhe (kein Verkürzen mehr, s. Erklär-Kommentar oben +
       Architektur-Lock). ui-265 DL-4: svh-Fallback bleibt (Doppel-Deklaration,
       OBS-CEF-Fallback auf der vh-Zeile). */
    height: calc(100vh / 1.3); height: calc(100svh / 1.3);
    background: var(--bg2);
    border-left: 1px solid var(--border);
    padding: 14px;
    /* ui-280-Fix: zusätzliche untere Innen-Reserve für die #settings-bar-Zone
       (bottom:14px + Pill-Höhe ~42px, z-index:92 > #panel 2) — überschreibt
       NUR padding-bottom, die anderen drei Seiten bleiben beim 14px-Kurz-
       schreibweise-Wert oben. 70px (unskaliert, zoom:1.3 → visuell ~91px)
       ist dieselbe Herleitung wie der frühere ui-278-Höhen-Abzug, jetzt aber
       INNERHALB der vollen Box statt die Box selbst zu kürzen. */
    padding-bottom: 84px;
    /* ui-279-Fix: overflow-y:auto ist von #panel auf #panel-scroll gewandert —
       #panel selbst clippt nicht mehr, damit #panel-footer (Kind NACH dem
       Scroll-Bereich) nie im Overflow landen kann. */
    display: flex;
    flex-direction: column;
    gap: 12px;
    pointer-events: none;
    z-index: 2;
  }
  /* ui-279-Fix (Schaden B): scrollbarer Inhaltsbereich. min-height:0 ist in
     einem Flex-Column-Parent zwingend — ohne das würde der Content-Bereich
     NIE unter seine intrinsische (volle) Höhe schrumpfen und #panel-footer
     bekäme keinen Platz mehr (identische Flex-Falle wie `.ip-rearm-txt`
     min-width:0 aus ui-278, nur auf der Kreuzachse).
     ui-481-Fix (AK5, "zweite Linie"): `overflow-x: hidden` — eine
     horizontale Scrollbar darf hier konstruktiv NIE erscheinen, auch wenn
     eine künftige Zeile trotz `table-layout: fixed` (s. .ip-stbl-Kommentar)
     einmal breiter wird als der Container. Rein additiv zu `overflow-y:
     auto` (zwei unabhängige Achsen) — vertikales Scrollen/ui-281-
     Scroll-Persistenz bleiben unangetastet. */
  /* ui-522 (Fifty-PTR-Feedback "Der Scroll-Balken muss noch gestylt werden."):
     IST geprüft (live @1920×1080, Run-Modus) — #panel-scroll ist der EINZIGE
     Desktop-Panel-Container, der tatsächlich überläuft (scrollHeight >
     clientHeight, 44px gemessen; #hud links bleibt in jedem geprüften Zustand
     exakt gleich hoch wie sein Content). Repo-Kanon übernommen statt neu
     erfunden (`scrollbar-width:thin` + dezent eingefärbter Thumb statt
     Komplett-Hide, s. #hud-queue-list oben [ui-122] + .gameover .es-body +
     .ge-legend [ui-464 R2, dort als "bereits etablierter Projekt-Kanon"
     dokumentiert] — Farbe hier `--line-2` [neutrale Line-Familie, kein
     Team-/Gruppen-Farbton wie #hud-queue-list's `--group-a42`], Breite 8px
     [Ticket-Vorgabe "~8px"], Track transparent). `scrollbar-gutter: stable`
     zusätzlich (Ticket "Kein Layout-Shift, scrollbar-gutter prüfen") — ohne
     das würde ein Umschalten zwischen überlaufendem und nicht-überlaufendem
     Inhalt (z. B. Template-Box-Zustandswechsel, CTA→Run-Moduswechsel) die
     Inhaltsbreite um die Scrollbar-Breite verschieben; live @1920×1080
     geprüft: kein sichtbarer Sprung mehr, keine Firefox-only-Lücke (Chrome
     ignoriert den Hinweis bei Nicht-Firefox-Renderern der Custom-Bar nicht —
     beide Deklarationen sind unabhängig wirksam).
     ui-572 (Fifty-PTR-Feedback "Popin-Scrollbar ungestylt", Rang/Setup-Popin):
     #popin-mount (`.embed-surface.ct-embed-mount`, dashboard.css — der
     tatsaechliche vertikale Scroll-Container ALLER Flaechen-Popins/-Screens,
     s. surfaces/surface-host.js-Kopfkommentar "Boot-Reihenfolge" fuer die
     volle Konsument-Liste: Setup UND Rang/Profil/Glossar/Guide/Patch-Notes
     teilen sich denselben Mount) trug bislang GAR KEINE Scrollbar-Regel
     (Browser-Default) — IST per CDP geprueft: `#popin-mount` ist der Container
     mit `overflow-y:auto`, der beim Popin tatsaechlich scrollt (`#popin-body`
     selbst hat `overflow:hidden`, s. dortiger Kommentar). GENERALISIERT statt
     kopiert: die Scrollbar-KOSMETIK (scrollbar-width/-color + die drei
     ::-webkit-scrollbar*-Regeln + scrollbar-gutter) ist jetzt eine
     Selektor-Liste ueber BEIDE Container — #panel-scroll BEHAELT seine
     eigenen Layout-Deklarationen (flex/display/gap) exklusiv, die sind
     Panel-spezifisch und wuerden #popin-mounts eigenes Sizing aus
     dashboard.css (width/height:100%) kaputt ueberschreiben. NUR
     #popin-mount (Desktop-Popin) — NICHT #mob-setup-mount/#mob-surface-mount
     (dieselbe `.embed-surface.ct-embed-mount`-Klasse auf Mobile, aber dort
     bewusst unangetastet: kein Popin/Modal auf Mobile, keine gemeldete
     PTR-Beschwerde dort, "Mobile: bestehende None-/Touch-Konventionen NICHT
     brechen"-Vorgabe des Tickets — native Touch-Overlay-Scrollbars bleiben
     unveraendert).
     ui-577 (Fifty-PTR-Testrunde-2-Fund "settings haben wieder ungestylte
     scroolbars"): #settings-panel (Zahnrad-Popover unten rechts, s.
     #settings-panel-Regelblock weiter unten) trug — anders als seine beiden
     Nachbarn hier — NOCH KEINE Scrollbar-Kosmetik, obwohl es dieselbe
     overflow-y:auto-Falle hat (bei aufgeklappter Highlights-Sektion wird der
     Panel-Inhalt > 640px = eigenes max-height, Scroll aktiv, s. dortiger
     Kommentar) — breite, helle native Scrollbar im dunklen Panel (Fiftys
     Screenshot). GENERALISIERT statt kopiert, exakt das ui-572-Muster:
     dritter Selektor in dieser Liste (#panel-scroll BEHAELT seine eigenen
     Layout-Deklarationen exklusiv, s.o.). AK2-Gegenpruefung (Ticket ## Notes
     fuer die volle Messung): Fiftys Screenshot deutete eine ZWEITE,
     ungestylte Leiste am rechten Fensterrand ueber die volle Hoehe an — per
     CDP am lebenden Viewer @1920x1080 (echter Klickpfad: Zahnrad -> Highlights
     aufklappen) NICHT reproduzierbar. documentElement/body scrollHeight ===
     clientHeight in beiden Achsen (kein Overflow), body{overflow:hidden}
     propagiert unveraendert auf den Viewport (kein Transform/Filter/
     Perspective/Contain in der Ahnenkette #settings-bar -> body -> html, das
     die containing-block-Regel fuer position:fixed umleiten wuerde),
     elementsFromPoint() am rechten Rand (window.innerWidth-2) trifft ueber
     die GESAMTE Fensterhoehe nur #canvas/BODY/HTML — keine zweite Leiste,
     kein zweiter Scroll-Container. Die einzige real vorhandene Scrollbar ist
     #settings-panels EIGENE (s.o.), deren rechte Aussenkante nur ~17px vom
     wahren Fensterrand sitzt (#settings-panel `right:0` relativ zu
     #settings-bar, das selbst `right:16px` traegt) und exakt die
     Panel-eigene Hoehe (~640 von 1080px, NICHT "volle Hoehe") ueberspannt —
     am ehesten dieselbe Bar, auf einem Screenshot als naeher am Rand/hoeher
     wahrgenommen als sie tatsaechlich ist. Kein zweiter Fix noetig, keine
     Overflow-Ursache zu beheben (AK2 erledigt durch Befund, s. Notes). */
  #panel-scroll,
  #popin-mount,
  #settings-panel {
    scrollbar-gutter: stable;
    scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; /* Firefox */
  }
  #panel-scroll {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
    display: flex; flex-direction: column; gap: 12px;
  }
  #panel-scroll::-webkit-scrollbar,
  #popin-mount::-webkit-scrollbar,
  #settings-panel::-webkit-scrollbar { width: 8px; }                    /* Chrome/Safari */
  #panel-scroll::-webkit-scrollbar-thumb,
  #popin-mount::-webkit-scrollbar-thumb,
  #settings-panel::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; }
  #panel-scroll::-webkit-scrollbar-track,
  #popin-mount::-webkit-scrollbar-track,
  #settings-panel::-webkit-scrollbar-track { background: transparent; }
  /* ui-443: #ip-upgrades-mount (Element-Territorium, web/client/elements/
     controls/upgrades.js) ist jetzt der EINZIGE Direkt-Kind-Träger von
     .ip-stbl-head + der Zeilen-Tabelle (statt vorher zwei separate
     #panel-scroll-Flex-Kinder) — #panel-scroll hat seither nur noch DREI
     Flex-Kinder (.ip-panel-headrow, #ip-upgrades-mount, #ip-tplbtn-mount,
     ui-446 — vormals #ip-tplbtn direkt, s. dortige Namensänderung unten).
     Ohne diese Regel würde die vormalige 12px-Lücke zwischen Tabellenkopf
     und Tabelle (aus dem `gap:12px` oben, das NUR zwischen DIREKTEN
     Flex-Kindern wirkt) verloren gehen — dieselbe Flex-Spalten-Konfiguration
     hier repliziert sie INNERHALB des Mounts (Optik bleibt byte-gleich). */
  #ip-upgrades-mount { display: flex; flex-direction: column; gap: 12px; }
  /* ui-446: #ip-tplbtn-mount (Element-Territorium, web/client/elements/
     controls/template.js) — DRITTES Flex-Kind von #panel-scroll, s. Regel
     oben. Anders als #ip-upgrades-mount braucht dieser Mount KEIN eigenes
     `display:flex`/`gap` — er umschließt genau EIN Kind (den Button selbst,
     `#ip-tplbtn`, unverändert), kein internes Zeilen-Gap nötig. Der Button
     behält seinen eigenen `margin-top` (s. .ip-tpl-btn unten) — ein Flex-Item
     etabliert einen eigenen Block-Formatting-Context, das Kind-Margin
     kollabiert deshalb NICHT nach außen (Optik bleibt byte-gleich, Fixture-
     Beweis: Ticket ## Notes). */
  /* ui-279-Fix (Schaden B): NICHT-scrollender Footer — trägt ausschließlich die
     Rearm-Box (Run-Modus). flex:0 0 auto hält ihn auf seiner intrinsischen Höhe,
     außerhalb des Scroll-Containers → bei jeder Panel-Höhe ohne Scrollen
     erreichbar. pointer-events:auto nötig, weil #panel selbst pointer-
     events:none hat (DL-2/DL-3) und dieser Footer klickbar sein muss (Rearm-
     Toggle) — identisches Re-Enable-Pattern wie #panel.ip-active bereits für
     den Scroll-Bereich nutzt. margin-top:auto NICHT verwendet: #panel-footer
     ist immer entweder leer (kein Run-Modus) oder trägt genau die Rearm-Box —
     kein zusätzlicher Freiraum-Bedarf, gap auf #panel deckt den Abstand zum
     Scroll-Bereich bereits ab.
     :empty prüft NICHT auf Whitespace — _ipRenderRunPanel setzt bewusst
     `.innerHTML = ''` (kein Leerzeichen/Kommentar) für die Nicht-Run-Modi. */
  #panel-footer { flex: 0 0 auto; pointer-events: auto; }
  #panel-footer:empty { display: none; }
  #panel h1 {
    margin: 0; font-size: 14px; color: var(--cyan); font-weight: 600;
  }
  #panel h1 small { color: var(--muted); font-weight: 400; letter-spacing: 0.04em; margin-left: 8px; }
  /* ui-239/ui-278/ui-279/ui-280: Run-Panel-h1.
     ui-280-Fix (Fifty-Feedback nach ui-279-PTR: "dass 'Raum N · X Tower'
     umbricht finde ich ist völlig unnötig — damit verlieren wir wieder massig
     Höhe und das hilft nirgends"): Titel + Chip sind WIEDER in EINER Zeile
     (wie vor ui-279 auf Prod), aber per Flexbox statt der ui-278-`position:
     absolute`+pauschaler-`max-width`-Reservierung (der ui-278-Fehler war die
     FESTE 150px-Reservierung, nicht die gemeinsame Zeile an sich — Flex mit
     `min-width:0` verhandelt die Breite pro Render dynamisch, keine feste
     Zahl, die bei einem kürzeren Chip Platz verschenkt oder bei einem
     längeren nicht reicht).
     `.ip-h1` ist jetzt `flex:1 1 auto; min-width:0` (schrumpft zugunsten des
     Chips) + `overflow:hidden;white-space:nowrap;text-overflow:ellipsis`
     (Ellipsis nur im echten Platzmangel-Fall — der Standardfall "Dein Tower"
     + "Raum 13 · 13 Tower" bei 280px Panel-Breite passt ungekürzt, per
     Browser-Smoke nachgemessen, s. Ticket-Notes). `overflow:hidden` kommt
     damit zurück (war seit ui-279 entfernt) — der Chromium-Zoom-Bug (zoom:1.3
     + overflow:hidden + inline-block-Kind kollabiert die berechnete
     Content-Höhe auf 0, ui-278-Fund) ist deshalb wieder ein Risiko;
     min-height/line-height/display:block bleiben genau deshalb als Fix
     bestehen (per Browser-Smoke re-verifiziert: h1-Höhe > 0). */
  .ip-h1 {
    /* min-height (NICHT nur line-height, NICHT height!) explizit gesetzt:
       Chromium kollabiert bei zoom:1.3 (proprietäres #panel-Feature) +
       overflow:hidden + einem inline-block-Kind (.ip-num, vertical-align:
       middle) die berechnete Content-Höhe auf 0 (verifiziert per
       getBoundingClientRect() — Entfernen NUR von overflow:hidden behebt es
       sofort; eine explizite `height` half NICHT, `min-height` schon —
       offenbar wird bei diesem Zoom-Bug die berechnete Auto-Höhe auf 0
       geklemmt, aber ein min-height-Constraint respektiert). display:block
       + line-height nur kosmetisch für den Rest des Layouts, min-height ist
       der eigentliche Fix. */
    min-height: 20px; line-height: 20px;
    /* ui-280: flex-Kind in .ip-panel-headrow (s.u.) — display:block wird durch
       den Flex-Kontext des Elternteils ersetzt, min-width:0 ist der Flex-
       Analog-Fix zu min-height:0 aus ui-279 (ohne das würde die h1 NIE unter
       ihre intrinsische Breite schrumpfen, der Chip bekäme keinen Platz). */
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  }
  /* ui-280: Kopfzeile ist wieder EINE Flex-Row (Titel + Chip nebeneinander,
     nicht mehr untereinander wie ui-279) — Design-Rückbau auf Fiftys
     Feedback. Chip bekommt flex:0 0 auto (behält seine intrinsische Breite,
     schrumpft nie), h1 (flex:1 1 auto/min-width:0 s.o.) gibt zugunsten des
     Chips nach. gap statt margin zwischen den beiden Flex-Kindern.
     ui-284-Fix: das readonly-Hover-Panel wrappt seine h1 SEIT DIESEM Fix
     ebenfalls in `.ip-panel-headrow` — dort OHNE Chip (nur ein Flex-Kind).
     Grund: `.ip-h1`s `flex:1 1 auto` braucht IMMER einen Flex-Container als
     direkten Parent — ohne ihn wird h1 zum wachsenden Flex-ITEM im
     Flex-COLUMN-Kontext von `#panel-scroll` (Leerraum-Bug, volle Herleitung
     seit ui-453 in /client/elements/tower-detail.js beim `mount.innerHTML=`-
     Template, layout:'hover' — die Datenaufbereitung des Hover-Panels lebt
     dort, s. dortiger Datei-Kommentar). */
  .ip-panel-headrow {
    display: flex; align-items: center; gap: 8px;
    margin: 0 0 10px;
  }
  .ip-panel-headrow > .ip-h1 { margin: 0; }
  .ip-panel-headrow > .ip-room { flex: 0 0 auto; margin: 0; }

  /* ============================================================
     ui-282: HÖHEN-abhängiger Panel-Zoom — zweite (kleinere) Stufe.
     Nur `zoom` + die davon abhängige `height`-Formel werden überschrieben —
     `width: 280px` bleibt unskaliert IDENTISCH in beiden Stufen (Lock:
     kleinere Zoom-Stufe bedeutet NICHT weniger verfügbare CSS-Breite; alle
     Breiten-/Worst-Case-Garantien aus ui-280 — Chip max-width:140px,
     Kopfzeile ungekürzt — bleiben dadurch unverändert gültig, unabhängig
     davon welche Stufe aktiv ist. Nur die VISUELLE Bildschirm-Pixel-Größe
     schrumpft mit dem zoom-Faktor, nicht das CSS-Pixel-Verhältnis von
     Inhalt zu Panel-Breite).
     `(pointer: fine)` grenzt bewusst gegen den bestehenden Mobile-Media-
     Query-Block ab (`@media (max-width:950px) and (pointer:coarse)`,
     s.o.) — beide Queries dürfen sich NICHT überschneiden (Ticket-AK).
     Ein Touch-Gerät mit zufällig kleiner Viewport-Höhe (z.B. Tablet
     Landscape) matcht durch `pointer:coarse` NIE diese Regel, sondern
     ausschließlich den bestehenden Mobile-Block (Breite-basiert). Reine
     Maus-/Trackpad-Desktop-Fenster (`pointer:fine`) sind die einzige
     Zielgruppe dieser Stufe.
     Grenzwert `max-height: 1199.98px` (statt 1200px) — Media-Queries in
     CSS sind inklusiv (`max-height:1200px` würde auch GENAU 1200px matchen,
     was den Stream-Lock-Grenzwert "≥1200px bleibt 1.3" nicht exakt
     abbilden würde; 0.02px Puffer schließt exakt 1200px sauber aus der
     kleineren Stufe aus, ohne Rundungsrisiko bei Sub-Pixel-Viewporthöhen).
     Empirisch gemessen (Ticket-Notes): zoom:1.0 bleibt ohne Overflow bis
     runter auf ~860px Viewport-Höhe (inkl. AllMode = voller Stat-Zeilen-
     Inhalt); darunter greift unverändert das ui-281-Scroll-Persistenz-Netz.
     ui-478-Korrektur: der ~860px-Wert ist überholt (volle Herleitung im
     großen Kommentarblock oberhalb der #panel-Regel, Absatz "ui-478-Fix")
     — durch Panel-Zuwächse (ui-446/ui-475) lag der reale Kipppunkt bei
     944px längst im Minus, BEVOR ui-478 die Reserve unten auf
     `padding-bottom: 64px`/`gap: 8px` senkte UND zwei Inhalts-Strings
     kürzte (Rearm-`small`, `pausiert`-Subtext). Die zwei zusätzlichen
     Deklarationen unten sind NUR für diesen Zweig aktiv (Lock ui-282:
     zoom-1.3-Zweig bleibt bei 84px/gap:12px, unverändert).
     ============================================================ */
  @media (max-height: 1199.98px) and (pointer: fine) {
    /* :root-Override statt Redeklaration auf #panel — dieselbe Variable wird
       von #stage (Geometrie-Anker, ui-033) UND #panel selbst gelesen. Ohne
       diesen Override würde #stage weiterhin die 364px-Stream-Breite
       reservieren, obwohl #panel jetzt visuell nur noch 280px breit ist —
       eine 84px-Lücke zwischen Stage-Ende und Panel-Anfang (verifiziert vor
       diesem Fix per Browser-Smoke, s. Ticket-Notes). 280px = 280px
       unskaliert × zoom:1.0 (diese Stufe). */
    :root { --panel-w-visual: 280px; }
    #panel {
      zoom: 1.0;
      height: calc(100vh / 1.0); height: calc(100svh / 1.0);
      /* ui-478-Fix: Realbedarf statt Icon-Zone-Pauschale (volle Herleitung
         im großen Kommentarblock oben, Absatz "ui-478-Fix") — NUR dieser
         Zweig, zoom-1.3 bleibt bei 84px/12px (Lock ui-282). 64px = 58px
         real (Pill 44px + bottom:14px) + 6px AK3-Luft. Reicht ALLEIN (mit
         gap weiterhin 12px) noch nicht für AK1 (bleibt Overflow bei
         944px) — gap 12→8 gibt weitere 4px, macht zusammen mit den zwei
         gekürzten Inhalts-Strings (rearm.js/template.js, Fifty-Entscheid
         Paket A) alle fünf Template-Zustände wieder overflow-frei
         (Messtabelle: Ticket-Notes). */
      padding-bottom: 64px;
      gap: 8px;
    }
  }

  /* Command-Referenz-Tabelle (#stats, s. viewer-shell.js #panel-Markup — der
     EINZIGE bare <table> im Viewer-eigenen statischen HTML).
     ui-510-Fix: diese fuenf Regeln waren bis hierher UNSCOPED (bare `table`/
     `th`/`td`), matchten also JEDES th/td im GESAMTEN Dokument — seit ui-402
     (kein iframe mehr) lebt das Companion-Popin-Fragment (#popin-mount) im
     SELBEN Dokument, jede darin gerenderte <table> (Rang-/Profil-Teilnehmer-
     tabelle, Run-Listen-Tabelle, Achievement-/Admin-Tabellen …) erbte dadurch
     UNGEWOLLT `white-space:nowrap`/`text-align:right`/`padding:6px 8px` von
     HIER. Konkreter Schaden (Ticket ## Notes, Live-CDP-Beleg): das geleakte
     `white-space:nowrap` VERERBTE sich auf `.es-breakdown`/`.bd-pact`
     (Pakt+-Chip) — ui-490 hatte dort das LOKALE nowrap bewusst entfernt und
     sich auf den ererbten Default "normal" verlassen (Kommentar dort: "Default
     normal darf bei Platzmangel mehrzeilig umbrechen"), was NUR auf der
     eigenstaendigen Companion-Seite stimmte, NICHT im Viewer-Popin — dort war
     der ererbte Wert wegen dieses Leaks weiterhin "nowrap", `overflow-wrap:
     anywhere` (ui-490s eigene zweite Sicherung) greift bei nowrap NICHT (kein
     Umbruch erlaubt = kein Ansatzpunkt zum Umbrechen) → der Pakt+-Chip sprengte
     seine Zelle sichtbar (100px gemessener Overflow bei 1920×1080, `.hs-scroll
     --part.scrollWidth` > `.clientWidth`) — Fiftys "Scrollbar ist noch da".
     `#hexa-ranking`/`.ip-stbl` (die beiden ANDEREN <table>-Konsumenten in
     diesem File) sind gegengeprueft NICHT betroffen — beide setzen
     text-align/white-space bereits vollstaendig selbst (eigene `#hexa-ranking
     thead th`/`tbody td`-Regeln weiter unten in dieser Datei, `.ip-strow
     .ip-val` in viewer-base.css), verlassen sich nirgends auf diesen Leak.
     Scope auf `#stats` (einzige ID
     dieser Tabelle) macht den Leak strukturell unmoeglich, unabhaengig von
     jeder zukuenftigen Companion-Markup-/CSS-Aenderung. */
  .table-wrap { overflow-x: auto; }
  #stats {
    width: 100%; border-collapse: collapse;
    font-size: 12px; font-variant-numeric: tabular-nums; table-layout: fixed;
  }
  #stats th, #stats td {
    padding: 6px 8px; text-align: right;
    border-bottom: 1px solid var(--border);
    white-space: nowrap; overflow: visible;
  }
  #stats th:first-child, #stats td:first-child { text-align: left; color: var(--muted); width: 86px; }
  #stats th { color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; text-align: center; }
  td.attr-cell { line-height: 1.2; }
  td.attr-cmd-cell {
    color: var(--yellow);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 11px; letter-spacing: 0.03em; text-align: right; line-height: 1.3;
  }
  .attr-cmd-sub { opacity: 0.55; display: block; }
  #stats-body td { padding: 4px 8px; }
  tr.cmd-group-tower td { color: var(--cyan); font-weight: 600; border-bottom: 2px solid var(--border); }
  tr.cmd-group-tower td.attr-cmd-cell { color: var(--cyan); }
  tr.section-stat-end td { border-bottom: 2px solid var(--border); }
  tr.cmd-group-other td { color: var(--text); }
  tr.cmd-group-other td.attr-cmd-cell { color: var(--yellow); }

  /* ============================================================
     INTERAKTIVES PANEL (ui-020) — nur wenn eingeloggt + im Run / nicht im Run
     pointer-events werden per JS freigeschaltet (#panel.ip-active)
     ============================================================ */
  #panel.ip-active { pointer-events: auto; }

  /* ui-475: Stats-Tabellen-Header — geteilte Layout-Klasse mit ZWEI
     Konsumenten: upgrades.js (eigener Tower) zeigt seither NUR den Titel
     (Design-Lock 6, AllMode-Toggle entfallen); tower-detail.js' read-only
     Hover-Panel (FREMDER Tower) zeigt weiterhin Titel + Read-only-Badge +
     `.ip-allmode-toggle` (s. dortiger Kommentar unten — die Klasse ist NICHT
     tot, nur nicht mehr interaktiv). */
  .ip-stbl-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 6px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
  }
  .ip-stbl-title {
    font-size: 11px; color: var(--muted);
  }
  /* ui-475: AllMode-Toggle (Pill mit Switch) — als INTERAKTIVES Element seit
     ui-475 entfallen (upgrades.js baut es nicht mehr). Bleibt als CSS-Regel
     bestehen: tower-detail.js' read-only Hover-Panel (`_createHoverPanel()`)
     rendert dieselbe Klasse weiterhin `aria-hidden="true"` als reine
     Zustandsanzeige für FREMDE Tower (Out-of-Scope dieses Tickets). */
  .ip-allmode-toggle {
    display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none;
    border: 1px solid var(--border); border-radius: 999px; padding: 4px 9px 4px 7px;
    font-size: 11px; color: var(--muted); transition: border-color .12s, color .12s;
    min-height: 32px;
  }
  .ip-allmode-toggle .ip-sw {
    width: 26px; height: 15px; border-radius: 999px; background: #243250;
    position: relative; transition: background .14s; flex: 0 0 auto;
  }
  .ip-allmode-toggle .ip-sw::after {
    content: ""; position: absolute; top: 2px; left: 2px;
    width: 11px; height: 11px; border-radius: 50%;
    background: var(--muted); transition: transform .14s, background .14s;
  }
  .ip-allmode-toggle.on { border-color: var(--magenta); color: var(--magenta); }
  .ip-allmode-toggle.on .ip-sw { background: var(--magenta); }
  .ip-allmode-toggle.on .ip-sw::after { transform: translateX(11px); background: #fff; }

  /* Stats-Tabelle.
     ui-481-Fix: `table-layout: fixed` klemmt die Tabellenbreite HART auf den
     Container (#panel-scroll, 252px unskaliert bei 14px Panel-Padding —
     zoom-unabhängig, s. Ticket-Notes). Ohne das berechnet die Auto-Layout-
     Tabelle (Default) ihre Spaltenbreite aus dem MIN-CONTENT jeder Zelle —
     die einzige Zelle pro Zeile enthält `.ip-strow` (CSS-Grid), dessen
     `.ip-lvl`/`.ip-val`-Spalten `white-space: nowrap` sind (Zahlen dürfen nie
     umbrechen/clippen, s. `.ip-label`-Kommentar unten) und deshalb ihre volle
     Text-Breite als Minimum einfordern — bei langen Werten (McEs_-Fund,
     Prod v0.8.7: "Proj-Speed · lvl 36 (+44) · 181,1 m/s") wächst die Tabelle
     dadurch über 252px hinaus, `#panel-scroll` zeigt eine horizontale
     Scrollbar. `table-layout: fixed` macht die `<td>`-Breite unabhängig vom
     Inhalt (= exakt die Tabellenbreite, hier 100% von #panel-scroll) — erst
     DAMIT hat `.ip-strow` (block-level, width:auto = volle Elternbreite)
     einen DEFINITEN Container, gegen den die Grid-Spalten unten tatsächlich
     schrumpfen können. `td { overflow: hidden }` ist eine zweite, redundante
     Sicherung auf Zell-Ebene (Live-Messung: ohne sie bereits 0 Overflow mit
     der McEs_-Fixture, s. Ticket-Notes) — zusätzlich zu `#panel-scroll
     { overflow-x: hidden }` weiter unten (AK5, "zweite Linie"). */
  .ip-stbl { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; table-layout: fixed; }
  .ip-stbl td { padding: 0; overflow: hidden; }
  /* ui-193: Grid mit getrennter Checkbox-Zone (DL-3).
     Spalten: icon | label | level | value | [divider] | checkbox-zone
     ui-481: Raster UNVERÄNDERT gelassen (Messung zeigte: die DL-1-Alternative
     `18px minmax(0,1fr) auto auto 1px 30px` verschiebt im Normalfall die
     Level-Spalte vom Label weg zum Wert (0 freie Fläche mehr zwischen Label
     und Level) — AK6-Pixel-Diff wäre NICHT 0, s. Ticket-Notes-Messwerte.
     Der `1fr`-Wert-Spalte reicht als alleiniger Flex-Träger: ihre
     automatische Minimalgröße ist ihr eigener Min-Content (kein
     `overflow:hidden` auf `.ip-val`, AK2), sie kann also nie unter ihre
     Textbreite schrumpfen — schrumpft bei Platzmangel NIE zuerst. */
  /* ui-482-Fund (Live-CDP, K2/K3-Beispiele in #ob-c2/#ob-c3 rendern OHNE
     jede Optik auf Mobile): die eigentliche .ip-strow-Optik (Grid + Front-/
     Div-/Checkbox-Zone + in-set/active/locked/ip-disabled-Zustände) ist
     nach viewer-base.css umgezogen (jetzt geteilt — AK3 "eine Änderung an
     .ip-strow-Optik schlägt automatisch durch", vormals nur auf Desktop
     wirksam, weil mobile.css diese Datei nie lädt). Byte-identische Werte,
     reine Verschiebung. Die TABELLEN-/PANEL-BREITEN-spezifischen Regeln
     (.ip-stbl/.ip-stbl td oben, .ip-readonly weiter unten) bleiben hier —
     die betreffen nur das echte Desktop-Panel, nicht die K2/K3-Beispiele. */

  /* Template-Button */
  .ip-tpl-btn {
    margin-top: 10px; width: 100%; cursor: pointer; font-size: 12.5px;
    background: var(--bg); color: var(--yellow); border: 1px solid var(--border);
    border-radius: 8px; padding: 10px 12px; text-align: center;
    transition: border-color .12s, background .12s; min-height: 40px;
  }
  .ip-tpl-btn:hover { border-color: var(--yellow); }
  .ip-tpl-btn .ip-tpl-sub {
    display: block; font-size: 11px; color: var(--muted); margin-top: 2px;
  }
  .ip-tpl-btn.ip-tpl-active {
    color: var(--green); border-color: rgba(94,255,138,.4); background: rgba(94,255,138,.06);
  }
  .ip-tpl-btn.ip-tpl-active .ip-tpl-sub { color: var(--green); opacity: .8; }
  /* ui-088: Template fertig — grün erreicht, kein Aktiv-Tint, gedimmt (kein Handlungsbedarf) */
  .ip-tpl-btn.ip-tpl-done { color: var(--green); opacity: .7; }
  .ip-tpl-btn.ip-tpl-done:hover { border-color: var(--border); cursor: default; }

  /* ui-472 (seit ui-475 IMMER da, s. Ticket AK1): Zeile 1 unter dem
     Template-Block — GESCHWISTER-Div NACH #ip-tplbtn, niemals dessen Kind
     (Design-Lock 1, template.js `_syncSecondaryRows()`). Ghost-Button volle
     Breite (Fifty-Entscheid 2026-08-17, Variante B) — Cyan (interaktive
     Handlung), bewusst NICHT grün/gold wie der Zustandsblock oben
     (Design-Lock 2: Cyan trennt "Handlung" vom "Zustand"). Container trägt
     seit ui-475 SOWOHL Zeile 1 (dieser Button) ALS AUCH Zeile 2
     (`.ip-tpl-modes` unten) — `flex-direction:column` staffelt beide
     untereinander, Breite/Zentrierung kommen vom Button selbst
     (width:100%). */
  .ip-tpl-actions { margin-top: 4px; display: flex; flex-direction: column; gap: 6px; }
  .ip-tpl-action {
    display: flex; align-items: center; justify-content: center; gap: 4px;
    width: 100%; min-height: 32px; padding: 4px 8px; font-family: inherit;
    font-size: 11.5px; color: var(--cyan); background: transparent;
    border: 1px solid var(--line-2); border-radius: 8px; cursor: pointer;
    transition: border-color .15s, background .15s;
  }
  .ip-tpl-action:hover, .ip-tpl-action:focus-visible {
    border-color: var(--cyan); background: rgba(56,225,255,.06); outline: none;
  }
  .ip-tpl-action .chev { opacity: .7; }

  /* ui-475: Zeile 2 — Fokus-Set/AllMode-Paar, EIGENE Mode-Button-Familie
     (Design-Lock 2 "ganz anders, darum eigene Buttons" — Zustands-Schalter,
     KEINE Navigation wie Zeile 1 oben). Aktiver Button gefüllt (Cyan-Fläche
     + Border + Inset-Ring), inaktiver Button neutral mit Hover/Focus-Cyan-
     Rahmen (Guide: Cyan = interaktiv, kein Glow — Zustand, nicht Highlight). */
  .ip-tpl-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
  .ip-tpl-mode {
    min-height: 40px; display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 1px; padding: 4px 6px;
    background: rgba(126,150,200,.06); border: 1px solid var(--line-2); border-radius: 8px;
    color: var(--text); font-family: inherit; font-size: 11.5px; cursor: pointer;
    transition: border-color .15s, background .15s, color .15s;
  }
  .ip-tpl-mode .m-lbl { font-weight: 600; }
  /* ui-504 (M2, K1): 10px -> 11px Lese-Boden ("aktiv"/"aktivieren"/"—" -- echter
     Zustandstext, keine Deko). Mobile-Pendant .u-tpl-mode .m-sub war schon 11px. */
  .ip-tpl-mode .m-sub { font-size: 11px; color: var(--muted); }
  .ip-tpl-mode:hover:not([disabled]):not(.on), .ip-tpl-mode:focus-visible:not(.on) {
    border-color: var(--cyan); outline: none;
  }
  .ip-tpl-mode.on {
    background: rgba(56,225,255,.16); border-color: var(--cyan); color: var(--cyan);
    box-shadow: inset 0 0 0 1px rgba(56,225,255,.35); cursor: default;
  }
  .ip-tpl-mode.on .m-sub { color: var(--cyan); opacity: .8; }
  .ip-tpl-mode[disabled] { opacity: .45; cursor: not-allowed; }
  /* ui-475: Busy bis der nächste Tower-Poll den neuen Zustand liefert (Server-SSOT, Design-Lock 4) */
  .ip-tpl-mode[aria-busy="true"] { opacity: .55; cursor: progress; }
  /* ui-475 (Design-Lock 3): genau EINE Zeile, nie umbrechen (Desktop-Panel-
     Breite 340px darf nicht sprengen) — sonst reservierte, aber LEERE Zeile
     (min-height) — der Block darf beim Zustandswechsel/Hover über fremde
     Tower nicht in der Höhe springen (Fifty-Fund am Prototyp 2026-08-18). */
  /* ui-504 (M2, K1): 10.5px -> 11px Lese-Boden; die 340px-Ein-Zeile-Regel oben
     (white-space:nowrap+ellipsis) faengt den seltenen Overflow-Fall weiterhin ab,
     kein Layout-Sprung. Mobile-Pendant .u-tpl-modehint war schon 11px. */
  .ip-tpl-modehint {
    grid-column: 1 / -1; font-size: 11px; color: var(--muted); text-align: center;
    margin-top: -2px; min-height: 15px; line-height: 15px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* srv-140/ui-276: "Nächste Runde wieder dabei"-Haken (Design-Referenz: ui-275-
     Simulator .rearm/.rearm.on/.box). Grün = angehakt (spiegelt --green-Rolle wie
     .ip-tpl-btn.ip-tpl-active), min-height 44px (ui-214 Touch-Target-Kanon). */
  .ip-rearm {
    display: flex; align-items: center; gap: 10px; margin: 10px 0 14px;
    padding: 9px 10px; min-height: 44px; cursor: pointer; user-select: none;
    border: 1px solid var(--border); border-radius: 8px;
    transition: border-color .12s, background .12s;
  }
  .ip-rearm:hover { border-color: var(--text-dim); }
  .ip-rearm.on { border-color: rgba(94,255,138,.55); background: rgba(94,255,138,.07); }
  .ip-rearm-box {
    width: 18px; height: 18px; flex: none; border: 2px solid var(--muted); border-radius: 4px;
    display: flex; align-items: center; justify-content: center;
    font-size: 13px; color: var(--green); transition: border-color .12s;
  }
  .ip-rearm.on .ip-rearm-box { border-color: var(--green); }
  /* ui-278-Fix (PTR Finding 2): min-width:0 auf dem Flex-Kind — ohne das
     verhindert der Flexbox-Default (min-width:auto) das Schrumpfen des
     Textblocks unter seine intrinsische Breite und der Subtext ragt/clippt
     rechts über den Panel-Rand hinaus, statt sauber umzubrechen. */
  .ip-rearm-txt { font-size: 12.5px; min-width: 0; }
  .ip-rearm-txt b { color: var(--text); }
  .ip-rearm-txt small { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; line-height: 1.4; overflow-wrap: break-word; }
  /* ui-587 (DL-1/DL-4, Prototyp-Vorlage "PROTO ui-587"): Select ersetzt den
     statischen Intent-Text INNERHALB derselben <small> (11px erbt via
     font:inherit) — appearance:none + eigener Data-URI-Chevron statt des
     hellen nativen Chrome/Windows-Selects. Hoehe 1.45em der 11px-Subtext-
     Schrift haelt das Blockwachstum <= 2px (Live-CDP-Messung Ticket ui-587:
     54.9->56.7px am echten Desktop-Panel), kein Zeilenumbruch. --border ist
     Desktops eigener Name fuer denselben Rahmen-Ton wie Mobiles --line
     (tokens.css "Companion-Alias"-Kommentar) — hier wie die Nachbarregeln
     oben bei --border geblieben statt --line, Konsistenz mit dieser Datei.

     Live-CDP-Fund (Ticket-Notes): OHNE festes `max-width` sizt Chrome ein
     natives <select> auf die BREITESTE <option> (hier "Auf große Runde
     warten", ~119px Textbreite + Padding = ~145px) — UNABHÄNGIG davon,
     welche Option gerade ausgewählt ist (auch bei "Sofort spielen"
     ausgewählt bleibt das Select 145px breit). Am realen Desktop-Panel
     (~220-280px Innenbreite je nach DPI/Scroll-Zustand) reißt das die
     "· gilt 1 Runde"-Suffix-Zeile um (DL-1-Verstoß, per Live-CDP-Bisektion
     gefunden: Wrap-Schwelle exakt zwischen 133px/no-wrap und 135px/wrap am
     1920×1080-Referenzpanel — der Prototyp-Demo-Frame hatte diesen
     Breiten-Konflikt nicht, weil sein isolierter 364px-Container großzügiger
     ist als das echte, schmalere `#panel`). Fix: `max-width:100px` (deutlich
     unter der gemessenen Wrap-Schwelle, zeigt "Sofort spielen" vollständig —
     Textbreite ~67px + Padding — UND ellipsis-kürzt "Auf große Runde warten"
     lesbar) + `overflow:hidden;text-overflow:ellipsis;white-space:nowrap`
     (nötig, weil `appearance:none` die native Truncation-Darstellung nicht
     mehr übernimmt). */
  .ip-rearm-intent {
    appearance: none; -webkit-appearance: none;
    font: inherit; line-height: 1.2; color: var(--text);
    background: var(--bg-2) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23838fa7' stroke-width='1.6'/></svg>") no-repeat right 6px center;
    border: 1px solid var(--border); border-radius: 6px;
    height: 1.45em; padding: 0 18px 0 6px; max-width: 100px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    cursor: pointer; vertical-align: middle;
  }
  .ip-rearm-intent:hover { border-color: var(--text-dim); }
  .ip-rearm-intent:focus-visible { outline: 2px solid var(--cyan); outline-offset: 1px; }
  .ip-rearm-intent:disabled { opacity: .6; cursor: default; }
  .ip-readonly .ip-rearm { cursor: default; }
  .ip-readonly .ip-rearm:hover { border-color: var(--border); }

  /* ui-515: Gast-Login-Hinweis — umgezogen aus dem Endscreen-Overlay
     (vormals `.gameover .es-guest-cta`/`.gc-*`, s. dortiger Kommentar + Kopf-
     Kommentar in `elements/controls/guest-hint.js` sowie dessen
     Instanziierung/`.update()`-Aufrufe in desktop/main.js) in den Panel-Footer, wo
     er sich mit `.ip-rearm` direkt darüber den Slot teilt (nie gleichzeitig —
     Rearm nur im Run-Modus, dieser Hinweis nur für Gäste). PERSISTENT statt
     einmaliges Endscreen-Popup: zeigt sich "auch in der Intro-Phase" (Ticket-
     AK1), deshalb kompakter als das vormalige, deutlich breitere Endscreen-
     Pendant — Zeilen-Sprache/Touch-Target an `.ip-rearm`/`.ip-cta` angelehnt,
     auf die 252px-Panel-Innenbreite herunterskaliert. Schriftgrößen ≥ 11px
     (K1-Lese-Boden, web/client/README.md „Schriftgrößen"-Regel) — literale
     px, kein `em`, außer den beiden echten Icon-Skalierungsbezügen unten. */
  .ip-guest-hint {
    display: flex; flex-direction: column; gap: 6px;
    margin: 10px 0 0; padding: 10px 10px 12px;
    border: 1px solid var(--line-2); border-radius: 8px;
    background: var(--bg2);
  }
  .ip-gh-title { font-weight: 700; font-size: 12.5px; color: var(--cyan); }
  .ip-gh-note { margin: 0 0 2px; font-size: 11px; line-height: 1.4; color: var(--text-dim); }
  .ip-gh-opt {
    display: flex; align-items: center; gap: 8px;
    width: 100%; min-height: 44px; padding: 7px 9px;
    border-radius: 8px; border: 1px solid var(--line-2);
    background: transparent; color: var(--text); font: inherit; text-align: left;
    cursor: pointer; text-decoration: none; box-sizing: border-box;
    transition: border-color .12s;
  }
  .ip-gh-opt:hover { border-color: var(--cyan); }
  /* DL-2 (1:1 aus dem vormaligen .gc-opt--primary übernommen, s. dortiger
     Kommentar): Twitch bleibt PRIMÄR, aber bewusst CYAN statt var(--twitch) —
     Cyan ist überall sonst im Panel die Primärrolle (Konsistenz zu .ip-cta.primary). */
  .ip-gh-opt--primary { background: var(--cyan); border-color: var(--cyan); color: var(--text-on-accent); }
  .ip-gh-opt--primary .ip-gh-opt-sub { color: rgba(7, 9, 26, 0.72); }
  .ip-gh-opt-ic { font-size: 1.1em; line-height: 1; flex: 0 0 auto; }
  .ip-gh-opt-ic.tw { width: 1em; height: 1em; fill: var(--text-on-accent); }
  .ip-gh-opt-txt { min-width: 0; }
  .ip-gh-opt-main { display: block; font-weight: 700; font-size: 11.5px; }
  .ip-gh-opt-sub { display: block; font-size: 11px; color: var(--muted); line-height: 1.3; margin-top: 1px; }
  .ip-gh-opt:disabled { opacity: .6; cursor: default; }
  .ip-gh-status { font-size: 11px; color: var(--text-dim); min-height: 1.2em; }
  /* ui-522 (Fifty-PTR-Feedback "Der Abstand nach dem 'Auf diesem Gerät
     sichern'-Button ist ein wenig zu groß."): IST gemessen (live @1920×1080,
     Run- UND CTA-Modus identisch) — im Leerlauf (idle, kein Passkey-Klick
     erfolgt) ist `.ip-gh-status` ein LEERES Element, das trotzdem `gap:6px`
     (Elternflex) + seine eigene `min-height:1.2em` (~13px) VOR dem
     `padding-bottom:12px` des Kartenrands reserviert — 32px toter Raum
     zwischen Button-Unterkante und Kartenrand (Vorher: 32,1875px, s. Ticket
     `## Notes`). `:empty` matcht exakt den Idle-Fall (`_paint()` in
     guest-hint.js setzt `textContent=''`, ergibt 0 Kindknoten — kein
     Whitespace im Markup-Template) — `display:none` nimmt das Element
     vollständig aus dem Flex-Layout (kein `gap`-Beitrag mehr davor), das
     verbleibende Innenmaß ist danach exakt `.ip-guest-hint`s eigenes
     `padding-bottom:12px` (Karten-übliches Maß, s. `.ip-guest-hint`
     oben) statt der bisherigen ~32px. Sobald ein Status-Text gesetzt wird
     (busy/done/error), ist das Element nicht mehr `:empty` und erscheint
     wieder normal (kein Verhaltensverlust — nur der LEERE Default-Zustand
     verliert seine Geister-Reservierung). */
  .ip-gh-status:empty { display: none; }

  /* ============================================================
     ui-096: READ-ONLY PANEL (Hover fremder Tower-Card)
     Design-Lock DL-1/DL-3: Stats voll lesbar, Interaktions-Affordances entwertet.
     pointer-events:none kommt vom #panel-Default (kein .ip-active → DL-3).
     ============================================================ */
  /* #N-Tower-Badge — ui-075-Rezeptur verbatim (wie .rc-num / .gb-num).
     Background wird inline aus player.color gesetzt (DL-8). */
  .ip-num {
    display: inline-block; min-width: 28px; text-align: center; vertical-align: middle;
    font-weight: 800; font-size: 11px; border-radius: 5px; padding: 2px 6px;
    color: var(--text-on-accent); margin-right: 8px;
  }
  /* „nur ansehen"-Badge — muted Pill, kein neuer Glyph (DL-1, Style-Guide-konform) */
  .ip-ro-badge {
    font-size: 11px; color: var(--muted);
    border: 1px solid var(--border); border-radius: 999px; padding: 3px 9px;
  }
  /* ui-193 DL-8: Read-only-Treatment: Stats bleiben lesbar, Interaktion entwertet */
  .ip-readonly .ip-front { cursor: default; }
  .ip-readonly .ip-cbz { cursor: default; }
  .ip-readonly .ip-cbz:hover { background: transparent; }
  .ip-readonly .ip-strow:hover { background: transparent; }
  .ip-readonly .ip-tpl-btn { cursor: default; opacity: .9; }
  .ip-readonly .ip-tpl-btn:hover { border-color: var(--border); }
  /* ui-472/473 (Design-Lock 4): zweite Verteidigungslinie — template.js'
     createTemplateElement() läuft strukturell NIE im Read-only-Panel (eigene
     Kopie in tower-detail.js, s. dortiger Datei-Kommentar), diese Regel
     greift also heute nie, bleibt aber als dokumentierte Absicherung stehen. */
  .ip-readonly .ip-tpl-actions { display: none; }
  /* AllMode-Toggle im read-only: Zustand sichtbar, aber nicht klickbar */
  .ip-readonly .ip-allmode-toggle { cursor: default; }
  /* Drift-Notiz ui-096: .rc-num + .gb-num + .ip-num = 3× dieselbe Rezeptur →
     Kandidat für spätere Styleguide-Konsolidierung zu einer Chip-Klasse. */

  /* ============================================================
     ui-219: Raum-/Warteschlange-Chip (ip-room)
     Pill oben im "Dein Tower"-Panel — zeigt Raum-Zugehörigkeit oder globale Warteliste-Position.
     Drei Zustände (DL-2): is-run (Cyan = aktiv), is-wait (Queued-Orange = warten).
     Farb-Rollen via Tokens (kein Hex-Hardcode, DL-7).
     Nur bei Multi-Room (≥2 Räume) sichtbar oder wenn global gewartet wird (DL-3).
     ~0.15s Übergänge bei State-Wechsel (DL-7).
     ============================================================ */
  .ip-room {
    display: flex; align-items: center; gap: 7px;
    margin: 0 0 10px; font-size: 12.5px; line-height: 1; padding: 5px 9px;
    border-radius: 999px; border: 1px solid var(--border);
    background: rgba(255,255,255,.02); width: fit-content;
    transition: border-color .15s, background .15s;
  }
  .ip-room[hidden] { display: none; }
  .ip-room .rk {
    font-size: 11px; letter-spacing: .10em; text-transform: uppercase; color: var(--muted);
    transition: color .15s;
  }
  .ip-room .rv { color: var(--text); }
  .ip-room .rn { font-weight: 700; transition: color .15s; }
  .ip-room .rsub { color: var(--text-dim); }
  /* Rolle: in-run = cyan (aktiv) */
  .ip-room.is-run { border-color: var(--cyan-a55); background: var(--cyan-a12); }
  .ip-room.is-run .rn { color: var(--cyan); }
  /* Rolle: warten = queued-orange (Raum-Lobby oder Intent-Queue) */
  .ip-room.is-wait { border-color: var(--queued-a40); background: var(--queued-a10); }
  .ip-room.is-wait .rn { color: var(--queued); }
  .ip-room.is-wait .rk { color: var(--queued); opacity: .85; }

  /* ui-239/ui-278/ui-279/ui-280: Run-Panel-Chip. Trifft NUR den Run-Panel-Chip
     (_ipRenderRunPanel, direktes Kind von .ip-panel-headrow); der Queued-Chip
     steckt weiterhin in #ipq-chip → unberührt.
     ui-280-Fix (Design-Rückbau auf Fifty-Feedback, s. .ip-h1-Kommentar oben):
     Titel + Chip wieder in EINER Flex-Row. Der ui-278-FEHLER war nicht "eine
     Zeile" an sich, sondern die FESTE 150px-`max-width`-Reservierung auf der
     h1, die bei kurzen Titeln Platz verschenkte und bei "Dein Tower" (11
     Zeichen, 280px Panel) zu wenig Rest ließ. Flexbox löst das dynamisch:
     Chip bekommt `flex:0 0 auto` (behält seine intrinsische Breite, nie
     erzwungen breiter/schmaler) + `max-width:140px` als Sicherheitsnetz gegen
     einen absurd langen Chip-Text (der Titel würde sonst in Extremfällen auf
     0 kollabieren, `min-width:0` auf h1) — 140px deckt den längsten realen
     Chip-Text ("Raum 13 · 13 Tower") komfortabel. Ellipsis auf `.rv` bleibt
     als Fallback für den (im Run-Chip praktisch nie erreichten) Fall, dass
     der Chip-Inhalt selbst die 140px sprengt. */
  .ip-panel-headrow > .ip-room { max-width: 140px; overflow: hidden; }
  .ip-panel-headrow > .ip-room .rv { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Zwei-Intent-CTA (ui-276) — KEIN Raum-Wähler mehr (ui-269/ui-273-Erklärzeile
     + Zustands-Branching auf dem sekundären Button entfallen), primär (#ip-cta-sofort,
     immer cyan) + sekundär (#ip-cta-gross7, immer dezent) — beide statisch klickbar,
     nur die Sub-Kontextzeilen ändern sich (In-Place-Patch, kein Rebuild). */
  .ip-cta-wrap { margin-top: 6px; display: flex; flex-direction: column; gap: 8px; }
  .ip-cta {
    width: 100%; cursor: pointer; border-radius: 10px; text-align: center;
    letter-spacing: .02em;
    border: 1px solid; background: var(--bg);
    transition: transform .08s, box-shadow .12s, border-color .12s;
  }
  .ip-cta:active { transform: translateY(1px); }
  .ip-cta .ip-cta-sub { display: block; font-weight: 400; margin-top: 4px; opacity: .85; }
  /* Primär = "Sofort spielen" (immer cyan/statisch, kein Zustands-Branching). */
  .ip-cta.primary {
    padding: 16px 12px; font-size: 15px; font-weight: 800; min-height: 48px;
    color: var(--cyan); border-color: var(--cyan); box-shadow: 0 2px 16px rgba(56,225,255,.20);
  }
  .ip-cta.primary .ip-cta-sub { font-size: 11px; }
  .ip-cta.primary:hover { box-shadow: 0 2px 24px rgba(56,225,255,.30); }
  /* Sekundär = "Auf große Runde warten" (dezenter/kleiner, statisch — kein
     Zustands-Branching mehr, immer klickbar). */
  .ip-cta.secondary {
    padding: 10px 9px; font-size: 13px; font-weight: 600; min-height: 44px;
    color: var(--text-dim); border-color: var(--line-2);
  }
  .ip-cta.secondary .ip-cta-sub { font-size: 11px; }
  .ip-cta.secondary:hover { border-color: var(--text-dim); }

  /* ============================================================
     AUSGELOGGTER VIEWER-EXPLAINER (ui-064)
     Spiegelt .ip-cta.primary — Tokens via var(--*), kein Hex-Hardcode.
     ============================================================ */
  .vx-explainer { display: flex; flex-direction: column; gap: 12px; }
  .vx-lead {
    margin: 0; color: var(--text); font-size: 12.5px; line-height: 1.5;
  }
  .vx-lead .vx-channel { color: var(--cyan); font-weight: 600; }

  .vx-points {
    list-style: none; margin: 0; padding: 0;
    display: flex; flex-direction: column; gap: 9px;
  }
  .vx-points li {
    position: relative; padding-left: 16px;
    font-size: 12px; line-height: 1.45; color: var(--text);
  }
  .vx-points li::before {
    content: "›"; position: absolute; left: 2px; top: 0;
    color: var(--cyan); font-weight: 700;
  }
  .vx-points code {
    color: var(--yellow); font-family: ui-monospace, Menlo, monospace;
    font-size: 11.5px; letter-spacing: 0.02em;
  }
  .vx-points b { color: var(--text); font-weight: 600; }
  .vx-cred { color: var(--yellow); font-weight: 600; }

  /* Login-CTA — spiegelt .ip-cta.primary (Cyan-Border, Glow, 48px-Touch-Target) */
  .vx-cta-wrap { margin-top: 4px; pointer-events: auto; display: flex; flex-direction: column; gap: 10px; }
  .vx-cta {
    display: block; width: 100%; text-decoration: none; cursor: pointer;
    border-radius: 10px; padding: 16px 12px; text-align: center;
    font-size: 15px; font-weight: 700; letter-spacing: .02em;
    border: 1px solid var(--cyan); color: var(--cyan); background: var(--bg);
    box-shadow: 0 2px 14px rgba(56,225,255,.18);
    transition: transform .08s, box-shadow .12s; min-height: 48px;
    font: inherit; appearance: none;
  }
  .vx-cta:hover  { box-shadow: 0 2px 22px rgba(56,225,255,.28); }
  .vx-cta:active { transform: translateY(1px); }
  .vx-cta .vx-cta-sub { display: block; font-size: 11px; font-weight: 400; margin-top: 4px; opacity: .85; }

  /* ui-267: "Jetzt spielen"-Gast-CTA — führend, gefüllt-cyan (Funnel-Aktion, spiegelt
     landing.js .cta-primary, aber mit viewer-eigenen var(--*)-Tokens). */
  .vx-cta-play {
    border-color: var(--cyan); background: var(--cyan); color: var(--bg);
    box-shadow: 0 2px 18px rgba(56,225,255,.3);
  }
  .vx-cta-play:hover  { box-shadow: 0 2px 26px rgba(56,225,255,.45); }
  .vx-cta-play .vx-cta-sub { opacity: .8; }
  .vx-cta-play[aria-busy="true"] { cursor: progress; opacity: .75; }
  .vx-cta-play[disabled] { cursor: not-allowed; }

  /* ui-274: Sofort-Intent-Deep-Link (?autojoin=1) ausgeloggt — Hervorhebung beider
     CTAs (Gast-Start + Twitch-Login), damit klar ist "hier geht's direkt weiter". */
  .vx-deeplink-highlight {
    box-shadow: 0 0 0 2px var(--cyan), 0 2px 22px rgba(56,225,255,.4);
  }

  .vx-play-status {
    min-height: 1.2em; font-size: 12px; line-height: 1.4; color: var(--text);
  }
  .vx-play-status.err { color: var(--red); }
  .vx-play-status .retry {
    margin-left: 6px; color: var(--cyan); text-decoration: underline; cursor: pointer;
  }

  /* web-226 (DL-4): leise dritte Zeile — Passkey-Login, nur sichtbar wenn dieses
     Gerät (localStorage) einen Passkey kennt. Bewusst kein drittes .vx-cta-Design
     (kein drittes gleichrangiges CTA) — reiner Text-Link-Stil, Muster aus dem
     abgenommenen Prototyp. [hidden] explizit erzwungen (display:block unten
     schlägt sonst die native [hidden]-UA-Regel, gleiche Spezifität). */
  .vx-cta-quiet {
    display: block; width: 100%; min-height: 44px; margin-top: 2px; padding: 0;
    background: none; border: 0; color: var(--cyan); font: inherit; font-size: 13px;
    cursor: pointer; text-decoration: underline; text-underline-offset: 3px;
  }
  .vx-cta-quiet[hidden] { display: none; }
  .vx-cta-quiet[aria-busy="true"] { cursor: progress; opacity: .75; }
  .vx-cta-quiet[disabled] { cursor: not-allowed; }

  .vx-foot {
    margin: 2px 0 14px; color: var(--muted); font-size: 11px; line-height: 1.45;
  }
  .vx-foot a { color: var(--cyan); text-decoration: none; }
  .vx-foot a:hover { text-decoration: underline; }

  /* Warteschlangen-Status (ui-062) — Muted-Rolle: bestätigend, kein CTA */
  .ip-queued {
    width: 100%; border: 1px solid var(--border); background: var(--bg);
    border-radius: 10px; padding: 16px 14px; cursor: default;
  }
  /* Status-Kopfzeile: pulsierender Dot + Label (wie HUD-Label, uppercase muted) */
  .ipq-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
  .ipq-dot {
    width: 8px; height: 8px; border-radius: 999px; background: var(--muted);
    animation: ipq-pulse 1.4s ease-in-out infinite; flex: 0 0 auto;
  }
  @keyframes ipq-pulse { 0%,100% { opacity: .35; } 50% { opacity: 1; } }
  @media (prefers-reduced-motion: reduce) { .ipq-dot { animation: none; opacity: .8; } }
  .ipq-label {
    font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em;
  }
  /* Hero-Wert: Position groß (wie #hud .wave) */
  .ipq-pos { font-size: 26px; font-weight: 600; letter-spacing: .02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
  .ipq-pos .ipq-me  { color: var(--text); }
  .ipq-pos .ipq-sep { color: var(--muted); margin: 0 2px; }
  .ipq-pos .ipq-tot { color: var(--muted); }
  /* Kontextzeile (Sub-State) */
  .ipq-ctx { color: var(--muted); font-size: 11px; margin-top: 8px; line-height: 1.4; }
  /* srv-154: gross7-Countdown-Zeile — nur sichtbar (display via Inline-Style aus
     run-panel.js) waehrend die aktive Queue "Auf große Runde warten" ist. Design-
     Lock 2: immer reservierte Zeile (min-height/line-height, kein Layout-Sprung
     beim Wortlaut-Wechsel "Ohne Neuzugang: 2er-Runde in mm:ss" -> "Große Runde
     jetzt ab 2 Mitspielenden"). Wortlaut re-fixt (2026-08-18, srv-154-Review-
     must-fix): der reale Zeilen-Container ist nur ~221px breit (#panel 280px −
     2×14px Panel-Padding − 2×14px .ip-queued-Padding − 2×1px .ip-queued-Border),
     NICHT die 300px aus dem ursprünglichen Prototyp-Mockup — beide Text-Varianten
     sind jetzt ≤ 200px natürliche Breite bei 11px Segoe-UI gemessen
     (Sicherheitsmarge zu 221px für andere OS-Fonts/OBS-CEF). nowrap/ellipsis
     bleiben als Sicherheitsnetz, sind aber NICHT mehr die primäre Fit-Strategie.
     Design-Lock 4: KEINE neue Farbe — Ziffern --text (im <b>), Rest --muted,
     kein Glow, keine Animation. */
  .ipq-countdown {
    color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums;
    min-height: 16px; line-height: 16px; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; margin-top: 8px;
  }
  .ipq-countdown b { color: var(--text); font-weight: 600; }
  /* Big-Run-Teaser (optional, nur queueLen >= 8) */
  .ipq-bigrun { color: var(--yellow); opacity: .85; font-size: 11px; margin-top: 8px; letter-spacing: .02em; }
  .ipq-bigrun .gly { margin-right: 4px; }

  /* Leave-Button (ui-063, Styling ui-269 DL-4) — dezent-umrandet (--line-2), kein
     Destruktiv-Rot mehr: "Warteschlange verlassen" ist der Kern-Escape des harte-
     Schwelle-Designs (Umentscheiden), keine gefährliche Aktion. Hover/Focus → queued-orange. */
  .ipq-leave {
    width: 100%; margin-top: 14px; cursor: pointer; min-height: 40px;
    background: var(--bg); color: var(--muted); border: 1px solid var(--line-2);
    border-radius: 8px; padding: 9px 12px; font-size: 12px; text-align: center;
    transition: border-color .12s, color .12s;
  }
  .ipq-leave:hover  { border-color: var(--queued); color: var(--queued); }
  .ipq-leave:focus-visible { outline: 2px solid var(--queued); outline-offset: 2px; }
  .ipq-leave:disabled { opacity: .5; cursor: default; border-color: var(--line-2); color: var(--muted); }

  /* Toast-Feedback (ui-020) */
  #ip-toast {
    position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%) translateY(20px);
    background: var(--bg2); border: 1px solid var(--border); color: var(--text);
    padding: 8px 14px; border-radius: 8px; font-size: 12.5px;
    opacity: 0; pointer-events: none; transition: opacity .18s, transform .18s;
    z-index: 999;
  }
  #ip-toast.ip-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); }
  #ip-toast.ip-toast-warn { border-color: var(--red); color: var(--red); }

  /* ============================================================
     RIM-CARDS (DL-3, kanonische Umsetzung)
     ============================================================ */
  #rim-cards { position: absolute; inset: 0; pointer-events: none; }
  .rim-card {
    position: absolute;
    width: 198px;
    transform: translate(-50%,-50%) scale(var(--tower-card-scale));
    background: var(--card-bg);
    border: var(--card-border-width) solid var(--card-accent);
    border-radius: var(--card-radius);
    padding: 8px 12px;
    font-size: 13px; line-height: 1.25;
    pointer-events: auto;
    cursor: default;
    /* ui-056: Hover-Affordance-Transition (DL-3).
       Hinweis: transition animiert border-color, aber nach ui-013 überschreibt Hover/Self
       den --card-accent-Slot — der Browser interpoliert border-color weiterhin korrekt. */
    transition: border-color .15s ease, box-shadow .15s ease;
    box-shadow: var(--card-shadow);
    color: var(--text);
    overflow: hidden; min-height: 96px;
  }
  /* ui-056: Hover-Affordance — dezente Border/Shadow-Hervorhebung (DL-3). Kein transform/scale.
     :not(.rim-self) stellt sicher dass .rim-self (Spielerfarbe, stärker) nicht überschrieben wird. */
  .rim-card:hover:not(.rim-self) {
    --card-accent: var(--cyan-a55);
    box-shadow: var(--card-shadow), 0 2px 12px rgba(56, 225, 255, 0.18);
  }
  /* ui-084: Self-Highlight in Spielerfarbe (--rim-self-c, von renderRimCards gesetzt), Fallback cyan.
     Spread-Ring 0 0 0 1px = visuell 2px Border ohne border-width-Änderung (kein Layout-Shift). */
  .rim-card.rim-self {
    --card-accent: var(--rim-self-c, var(--cyan));
    box-shadow:
      var(--card-shadow),
      0 0 0 1px var(--rim-self-c, var(--cyan)),
      0 2px 14px color-mix(in srgb, var(--rim-self-c, var(--cyan)) 40%, transparent);
  }
  .rim-card.empty { visibility: hidden; }
  .rim-card.dead  { opacity: 0.45; }
  .rim-card .rc-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 5px; }
  /* ui-075: #n-Token — gb-num-Rezeptur verbatim (DL-2), align-self:center gegen 16px-Username (DL-3). */
  .rim-card .rc-num { flex: 0 0 auto; min-width: 28px; text-align: center; align-self: center; font-weight: 800; font-size: 11px; border-radius: 5px; padding: 2px 6px; color: var(--text-on-accent); }
  .rim-card .rc-user { font-weight: 700; font-size: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; letter-spacing: 0.01em; }
  /* ui-113: rc-weapon Text → Icon (18px Hex+Glyph Data-URL, Spielerfarbe, statischer Frame) */
  .rim-card .rc-weapon { flex-shrink: 0; align-self: center; display: block; width: 18px; height: 18px; line-height: 0; }
  .rim-card .rc-weapon img { width: 18px; height: 18px; display: block; }
  .rim-card .rc-hp-bar { height: 6px; background: var(--red-a18); border-radius: 3px; overflow: hidden; margin-bottom: 2px; }
  .rim-card .rc-hp-fill { height: 100%; background: var(--green); border-radius: 3px; transition: width 0.18s ease, background-color 0.18s ease; }
  .rim-card .rc-hp-fill.warn { background: var(--yellow); }
  .rim-card .rc-hp-fill.crit { background: var(--red); }
  .rim-card .rc-hp-text { font-size: 11px; color: var(--muted); text-align: right; line-height: 1.0; }
  .rim-card .rc-wallet { color: var(--yellow); font-variant-numeric: tabular-nums; }
  .rim-card .rc-focus  { color: var(--cyan); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rim-card .rc-stats { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; margin-top: 5px; font-size: 12px; align-items: baseline; }

  /* srv-021: Credit-Icon CSS (DL-3). Benötigt von renderRimCards() (ui.js-Reuse).
     ============================================================ */
  .credit-icon {
    display: inline-block;
    width: 1.05em; height: 1.05em;
    vertical-align: -0.18em;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10.5' fill='none' stroke='%2338e1ff' stroke-opacity='0.4' stroke-width='1.5'/><circle cx='12' cy='12' r='8.5' fill='%230d1322' stroke='%2338e1ff' stroke-width='1.4'/><path d='M16.4 8.6 A5.2 5.2 0 1 0 16.4 15.4' fill='none' stroke='%23ffd84a' stroke-width='2.3' stroke-linecap='round'/></svg>");
    background-repeat: no-repeat; background-position: center; background-size: contain;
  }
  .rim-card .rc-dmg-row {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 8px; margin-top: 4px; font-size: 11px; line-height: 1.1;
  }
  .rim-card .rc-dmg-row .rc-dmg-label  { color: var(--muted); }
  .rim-card .rc-dmg-row .rc-dmg-share  { color: var(--text); font-variant-numeric: tabular-nums; }
  .rim-card .rc-dmg-row .rc-dmg-value  { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 600; }
  .rim-card .rc-dmg-bar {
    position: relative; height: 4px;
    background: rgba(255,255,255,0.06); border-radius: 2px; overflow: hidden;
    margin-top: 2px; margin-bottom: 2px;
  }
  .rim-card .rc-dmg-layer {
    position: absolute; top: 0; left: 0; height: 100%;
    background: var(--green); border-radius: 2px;
  }
  .rim-card .rc-dmg-layer.pastel { opacity: 0.35; z-index: 1; }
  .rim-card .rc-dmg-layer.strong { opacity: 1.0;  z-index: 2; }
  /* ui-585 (AK6): `.rim-card .rc-allmode-indicator` entfernt (vormals hier,
     seit ui-193 tote Regel ohne Aufrufer, s. ui-504-Herleitung/-Test in
     ui-504-viewer-sub11-fontsize.test.js — ersetzt der Sache nach durch
     `.ip-allmode-toggle` im Hover-Panel). */
  .rim-card .rc-progress {
    height: 4px; background: rgba(255,255,255,0.08);
    border-radius: 2px; overflow: hidden; margin-top: 5px;
  }
  .rim-card .rc-progress-fill { height: 100%; width: 0%; border-radius: 2px; transition: width 0.15s ease; }
  .rim-card.dead .rc-progress-fill { background: var(--muted) !important; opacity: 0.4; }
  .rim-card .rc-level-info {
    margin-top: 3px; padding: 2px 4px; text-align: center;
    font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums;
    letter-spacing: 0.03em; border-radius: 3px; transition: background-color 60ms linear;
  }
  .rim-card .rc-template-next {
    margin-top: 5px; padding-top: 5px;
    border-top: 1px dashed rgba(255,255,255,0.07);
    font-size: 11px; color: var(--text); line-height: 1.2;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .rim-card .rc-template-next .rc-tpl-label {
    color: var(--muted); font-size: 11px; text-transform: uppercase;
    letter-spacing: 0.06em; margin-right: 4px;
  }
  .rim-card .rc-template-next.ended { color: var(--muted); font-style: italic; }
  .rim-card .rc-template-next.ended .rc-tpl-label {
    text-transform: none; letter-spacing: 0; font-size: 11px; font-style: italic; margin-right: 0;
  }
  .rim-card .rc-special {
    margin-top: 5px; padding-top: 5px; border-top: 1px dashed rgba(255,255,255,0.07);
  }
  .rim-card .rc-special-row {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 6px; font-size: 11px; line-height: 1.2;
  }
  .rim-card .rc-special-label {
    color: var(--special); font-size: 11px; text-transform: uppercase;
    letter-spacing: 0.06em; opacity: 0.85; flex-shrink: 0;
  }
  .rim-card .rc-special-value { color: var(--special); font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
  .rim-card .rc-special-bar {
    height: 3px; background: var(--special-a12); border-radius: 2px; overflow: hidden; margin-top: 3px;
  }
  .rim-card .rc-special-bar-fill { height: 100%; background: var(--special); border-radius: 2px; opacity: 0.7; }

  /* ============================================================
     GAME-OVER OVERLAY (srv-022)
     Positioniert in #stage, inset:0, z-index:50 (über Canvas, unter WS-Badge).
     ============================================================ */
  .gameover {
    position: absolute;
    inset: 0;
    background: rgba(6,8,15,0.85);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 28px;
    font-weight: 700;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--red);
    z-index: 120; /* srv-059: über #settings-bar(92), unter nichts */
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s;
  }
  .gameover.show { opacity: 1; }
  .gameover .sub { color: var(--text); font-size: 14px; letter-spacing: 0.05em; margin-top: 12px; }
  .gameover .hint { color: var(--text); font-size: 16px; letter-spacing: 0.05em; margin-top: 24px; text-transform: none; }
  .gameover .hint code { color: var(--cyan); background: rgba(56, 225, 255, 0.10); padding: 2px 8px; border-radius: 4px; font-family: ui-monospace, monospace; font-size: 18px; }
  /* ==========================================================================
     ui-198 — Hexa-Endscreen: 3-Zonen-Grid (HEAD / BODY-scroll / DOCK)
     Kern-Fix: kein position:absolute auf #achievement-cards mehr — beide im Flow.
     ========================================================================== */

  /* ── Container: 3-Zonen-Grid ─────────────────────────────────────────────── */
  .gameover.hexa-endscreen {
    height: 100%; zoom: var(--es-scale, 1.3);
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;   /* head / body / dock */
    /* ui-548: die (einzige, implizite) Spalte war vor diesem Ticket NIE
       explizit deklariert — ein "auto"-Grid-Track wächst mit dem breitesten
       Kind-Inhalt, statt hart auf die verfügbare Breite geklemmt zu sein
       (dasselbe Problem, das `minmax(0, 1fr)` bei den ZEILEN oben schon
       loest, hier aber nie noetig war, weil bis zu diesem Ticket NIE ein
       Dock-Kind absichtlich breiter als der Viewport sein wollte). Erst
       `.ac-row{width:max-content}` (Marquee-Ueberbreite-Messung, s.
       Achievement-Band weiter unten) deckte die Luecke auf: die Spalte wuchs
       live mit auf die Kartenreihen-Breite, wodurch NIE echte Ueberbreite
       gegen den (dadurch mitgewachsenen) Viewport gemessen werden konnte —
       Live-Messung (CDP): 13 Karten × 166px trafen den ANGEBLICH
       verfuegbaren Platz IMMER exakt, weil "verfuegbar" in Wahrheit "so viel
       wie die Karten brauchen" war. `minmax(0, 1fr)` klemmt die Spalte hart
       auf die tatsaechlich verfuegbare Breite — exakt dasselbe Muster wie
       die Zeilen. */
    grid-template-columns: minmax(0, 1fr);
    gap: 18px;
    padding: 30px clamp(24px, 4vw, 64px) 22px;
    font-size: 18px; letter-spacing: 0.06em;
    overflow: hidden;   /* Body ist der einzige Scroll-Bereich */
  }

  /* ── HEAD ─────────────────────────────────────────────────────────────────── */
  .gameover .es-head { text-align: center; }
  .gameover .hexa-title {
    color: var(--cyan); font-size: 30px; font-weight: 700;
    letter-spacing: 0.20em; margin: 0 0 8px; text-transform: uppercase;
  }
  .gameover .hexa-subtitle {
    display: inline-flex; flex-wrap: wrap; justify-content: center;
    gap: 8px 10px; font-size: 13px; letter-spacing: 0.04em; color: var(--text-dim);
  }
  .gameover .es-meta-chip {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 2px 10px; border: 1px solid var(--border); border-radius: 999px;
    background: rgba(255,255,255,0.02); white-space: nowrap;
  }
  .gameover .es-meta-chip b { color: var(--text); font-weight: 600; }
  .gameover .es-meta-chip .mk { color: var(--muted); text-transform: uppercase; font-size: 11px; letter-spacing: .1em; }

  /* ── ui-492: "Wieder dabei"-Banner (DL-1) ──────────────────────────────────
     Rearm/Sticky-Rejoin hat den Tower bereits in einem NEUEN Raum platziert
     (Server/Queue unverändert, Lock "Platzierung ≠ Ansicht") — die ANSICHT
     wartet noch auf den bestehenden Runden-Timer (`.hexa-cooldown` weiter
     unten bleibt die EINZIGE Countdown-Anzeige, DL-2 — kein zweiter hier).
     Skeleton ist IMMER Teil des Erst-Renders (endscreen.js `_renderDesktop`),
     nur `.show` + der Zahlwert togglen pro Tick (`_syncRejoinBanner()`) —
     dadurch reserviert das Banner im Default-Fall (kein Rearm) exakt 0px
     (max-height:0), der Endscreen bleibt pixelgleich zum Vor-ui-492-Stand.
     "Sanftes Einschieben" (Ticket-AK3) statt hartem display-Toggle: max-
     height/opacity/padding/margin transitionen gemeinsam auf ~konstanter
     Fläche (Prototyp-Optik: grüner Rand .5 Alpha, Fläche .08 Alpha). */
  .gameover .rejoin-banner {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    max-width: 640px; margin: 0 auto; padding: 0 14px;
    border: 1px solid rgba(94,255,138,.5); border-radius: 999px;
    background: rgba(94,255,138,.08);
    font-size: 13px; letter-spacing: 0.02em; text-transform: none; color: var(--text);
    max-height: 0; opacity: 0; overflow: hidden; pointer-events: none;
    transition: max-height .25s ease, opacity .2s ease, padding .25s ease, margin .25s ease;
  }
  .gameover .rejoin-banner.show {
    max-height: 52px; opacity: 1; padding: 8px 14px; margin: 0 auto 14px;
    pointer-events: auto; /* ui-322-Muster: einzige interaktive Insel in diesem Bereich */
  }
  .gameover .rejoin-banner b { color: var(--green); }
  .gameover .rejoin-banner .dot {
    flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
    background: var(--green); box-shadow: 0 0 0 3px rgba(94,255,138,.18);
    animation: rejoin-pulse 1.6s ease-in-out infinite;
  }
  @keyframes rejoin-pulse { 50% { box-shadow: 0 0 0 6px rgba(94,255,138,.06); } }
  @media (prefers-reduced-motion: reduce) {
    .gameover .rejoin-banner .dot { animation: none; }
  }
  .gameover .rejoin-banner .go {
    flex: 0 0 auto; min-height: 30px; padding: 4px 14px;
    border: 0; border-radius: 999px; background: var(--green); color: var(--text-on-accent);
    font-weight: 700; font-size: 13px; font-family: inherit; text-transform: none;
    letter-spacing: 0.02em; cursor: pointer; transition: box-shadow .15s;
  }
  .gameover .rejoin-banner .go:hover { box-shadow: 0 0 14px rgba(94,255,138,.4); }

  /* ── BODY (Ranking-Tabelle, einziger Scroll-Bereich) ─────────────────────── */
  .gameover .es-body { min-height: 0; overflow-y: auto; padding-right: 4px; }
  .gameover .es-body::-webkit-scrollbar { width: 9px; }
  .gameover .es-body::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 5px; }

  /* Ranking-Tabelle im Highscore-Stil (DL-2) */
  /* ui-246: table-layout:fixed + <colgroup> (s. endscreen.js) erzwingen Spaltenbreiten —
     Tower-Name kann nie mehr in DMG-done-Spalte laufen (Wurzel der Ueberlappung). */
  #hexa-ranking {
    width: 100%; max-width: 980px; margin: 0 auto;
    border-collapse: collapse; table-layout: fixed;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-variant-numeric: tabular-nums;
    font-size: 15px; letter-spacing: 0; text-transform: none;
  }
  /* ui-246/ui-262/ui-618: colgroup-Breiten (Richtwerte bei max-width 980px).
     ui-262: c-hexas 274→410 (Pakt+-Komponente sprengte die alte Breite, Regression aus ui-260);
     c-dmg/c-credits getrimmt, um das 980px-Gesamtbudget einzuhalten (DMG-done/Credits-Inhalt
     braucht die alte volle Breite nicht — empirisch headless verifiziert, s. Ticket-Notes).
     ui-618: c-dmg 160→195 — die deutsche Zahlen-Kurzform (ui-615, `46.4M`→`46,4 Mio`) macht
     den Worst-Case-Inhalt `999,9 Mio · 100,0 %` (19 Zeichen bei 15px ui-monospace ≈ 157px Text
     + 28px Zell-Padding ≈ 185px Minimum) breiter als die alte 160px — Range-basiert (NICHT
     scrollWidth/clientWidth: unzuverlässig auf `overflow:visible`+`text-align:right`-Zellen,
     s. Ticket-Notes) gegen 999,9 Mio/Mrd/Bio + 1,0 Mrd + den realen Prod-Wert 46,4 Mio
     nachgemessen, 195px lässt ~10px Puffer. c-credits/c-hexas bewusst UNVERÄNDERT gelassen —
     eine erneute Range-Messung ihres jeweiligen Worst-Case-Inhalts (`999,9k` bzw. die
     ui-262-Caption `Rank+ … · Slot 7`) fand nur ~4–5px echten Spielraum, zu wenig, um sicher
     etwas an c-dmg abzugeben (ui-262s historische „~19px Puffer"-Angabe beruhte auf derselben
     unzuverlässigen scrollWidth-Messung); die Differenz geht deshalb zu Lasten von c-tower
     (auto, 246px→211px — geschützt durch den bestehenden Ellipsis-Fallback, ui-246-Lock). */
  #hexa-ranking col.c-rank    { width: 64px; }
  #hexa-ranking col.c-tower   { width: auto; }
  #hexa-ranking col.c-dmg     { width: 195px; }
  #hexa-ranking col.c-credits { width: 100px; }
  #hexa-ranking col.c-hexas   { width: 410px; }
  /* Kopfzeile: muted, uppercase, Highscore-Konvention */
  #hexa-ranking thead th {
    color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 11px; font-weight: 500; text-align: right;
    padding: 0 14px 8px; border-bottom: 1px solid var(--border);
    position: sticky; top: 0; background: rgba(3, 5, 12, 0.96); z-index: 1;
  }
  #hexa-ranking thead th.col-rank  { text-align: center; }
  #hexa-ranking thead th.col-tower { text-align: left; }
  /* Daten-Zellen */
  #hexa-ranking tbody td {
    padding: 9px 14px; vertical-align: middle; text-align: right;
    border-bottom: 1px solid rgba(31, 39, 64, 0.55);
  }
  #hexa-ranking tbody tr:last-child td { border-bottom: none; }
  /* Rang-Zelle: zentriert + dezenter linker Farb-Akzent (Spielerfarbe) */
  #hexa-ranking tbody td.col-rank {
    text-align: center; position: relative;
    color: var(--yellow); font-weight: 700;
  }
  #hexa-ranking tbody td.col-rank::before {
    content: ''; position: absolute; left: 0; top: 6px; bottom: 6px; width: 3px;
    border-radius: 2px; background: var(--row-color, transparent);
  }
  #hexa-ranking tbody tr:hover td { background: rgba(56, 225, 255, 0.045); }
  /* Tower-Zelle: Waffen-Icon + Username */
  .gameover .col-tower { text-align: left !important; }
  .gameover .es-tower { display: inline-flex; align-items: center; gap: 9px; min-width: 0; max-width: 100%; }
  .gameover .es-tower .wpn-icon { display: block; flex: 0 0 auto; image-rendering: pixelated; }
  /* ui-246: max-width:100% statt fixem 200px — Ellipsis greift innerhalb der auto-Tower-Spalte.
     table-layout:fixed + col.c-tower:auto stellt sicher, dass der Span nie die Nachbarspalte schiebt. */
  /* ui-408 (DL-3/DL-4): jetzt ein <a> statt eines <span> — Farbe bleibt die
     Spieler-Zeilenfarbe (Inline-style pro Row, s. endscreen.js), nur der
     Browser-Link-Unterstrich wird zurueckgesetzt; ein dezenter Unterstrich
     bei Hover bleibt als einzige "klickbar"-Affordanz (kein Farbwechsel —
     die Zeilenfarbe ist hier das Identitaets-Signal, nicht austauschbar). */
  .gameover .es-user { display: inline-block; max-width: 100%; font-weight: 600; letter-spacing: 0.02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; text-decoration: none; }
  .gameover .es-user:hover { text-decoration: underline; }
  /* game-153: Killing-Blow-Icon — dezent hinter dem Namen (Ticket-AK "kein
     Layout-Sprung"), flex:0 0 auto haelt es auf fester Groesse neben dem
     schrumpfenden .es-user (analog .wpn-icon). Kleiner als das Waffen-Icon
     (16px vs. 20px) — sekundaere Info, konkurriert nicht mit dem Tower-Icon. */
  .gameover .es-tower .es-kill { display: inline-flex; align-items: center; flex: 0 0 auto; }
  .gameover .es-tower .es-kill svg { display: block; width: 16px; height: 16px; overflow: visible; }
  /* ui-618: DMG-done-Zelle NICHT umbrechen — nur AUF DIESER Zelle (Lock:
     kein breiterer `#hexa-ranking tbody td`-Selektor), damit die
     Aufschlüsselungs-Zeile (`.es-breakdown`/`.bd-pact`, s. u.) ihre eigene,
     unabhängige white-space-Regel behält statt sie über einen gemeinsamen
     Vorfahren zu erben (ui-510-Lehre: ein zu breit gescopter nowrap-Leak
     sprengte dort einen Pakt-Chip). ` ` (NBSP) statt eines normalen
     Leerzeichens vor `%` in `_dmgPctStr()` (endscreen.js) ist eine zweite,
     unabhängige Absicherung an der historisch beobachteten Bruchstelle. */
  #hexa-ranking tbody td.col-dmg { white-space: nowrap; }
  /* DMG-Anteil */
  .gameover .es-dmg-pct { color: var(--text-dim); }
  /* Credits */
  .gameover .col-credits { color: var(--text); white-space: nowrap; }
  .gameover .col-credits .credit-icon { margin-right: 2px; }
  /* ui-246: kombinierte Hexas-Zelle (Summe + Caption) — Variante A "Summe + Caption" */
  /* Grosse Hexas-Summe (Anker, Magenta + Glow) */
  #hexa-ranking .col-total-cell { white-space: nowrap; }
  #hexa-ranking .es-total {
    color: var(--magenta); font-weight: 700; font-size: 16px;
    text-shadow: 0 0 8px var(--magenta);
  }
  /* Caption-Zeile: kleine Aufschluesselung unter der Summe */
  #hexa-ranking .es-breakdown {
    display: block; margin-top: 2px;
    font-size: 12px; color: var(--text-dim);
    white-space: nowrap; letter-spacing: 0.01em;
  }
  #hexa-ranking .es-breakdown .bd-k { color: var(--muted); }
  /* Boss-Anteil: gelb wenn > 0, gedimmt wenn 0 */
  #hexa-ranking .es-breakdown .bd-boss      { color: var(--yellow); }
  #hexa-ranking .es-breakdown .bd-boss--zero { color: var(--text-dim); }
  /* ui-260: Pakt-Anteil: rot (Pakt-Identitaet), Marker ⚠. Zero = gar nicht gerendert (kein --zero-Selektor). */
  #hexa-ranking .es-breakdown .bd-pact { color: var(--red); }
  /* Top-3-Highlight (Podium) — Rang-Zelle + Hexas-Summe */
  #hexa-ranking tr.rank-1 td.col-rank,
  #hexa-ranking tr.rank-1 .es-total { color: var(--rank-1); font-weight: 700; text-shadow: 0 0 10px rgba(94, 184, 255, 0.7); }
  /* ui-382: text-shadow wirkt nicht auf Inline-SVG — Podest-Glow als drop-shadow
     aufs Hexa-Icon gespiegelt (gleiche Farb-/Radius-Werte wie die Zeile daneben). */
  #hexa-ranking tr.rank-1 .es-total svg.hexa-icon { filter: drop-shadow(0 0 10px rgba(94, 184, 255, 0.7)); }
  #hexa-ranking tr.rank-2 td.col-rank,
  #hexa-ranking tr.rank-2 .es-total { color: var(--rank-2); font-weight: 700; text-shadow: 0 0 8px rgba(192, 200, 212, 0.6); }
  #hexa-ranking tr.rank-2 .es-total svg.hexa-icon { filter: drop-shadow(0 0 8px rgba(192, 200, 212, 0.6)); }
  #hexa-ranking tr.rank-3 td.col-rank,
  #hexa-ranking tr.rank-3 .es-total { color: var(--rank-3); font-weight: 700; text-shadow: 0 0 8px rgba(205, 127, 50, 0.6); }
  #hexa-ranking tr.rank-3 .es-total svg.hexa-icon { filter: drop-shadow(0 0 8px rgba(205, 127, 50, 0.6)); }
  /* Slot-Chip (in Caption-Zeile) */
  #hexa-ranking .hr-slot-chip {
    display: inline-block; margin-left: 6px; padding: 1px 5px;
    background: rgba(138, 160, 192, 0.10); color: var(--text-dim);
    border: 1px solid rgba(138, 160, 192, 0.30); border-radius: 3px; font-size: 11px;
  }

  /* ── DOCK (Summary-Bar + Achievement-Band, beide IM FLOW) ─────────────────── */
  .gameover .es-dock { display: flex; flex-direction: column; gap: 14px; }
  .gameover .es-summary {
    display: flex; align-items: center; justify-content: center;
    gap: 12px 22px; flex-wrap: wrap;
    padding-top: 14px; border-top: 1px solid var(--border);
  }
  .gameover .hexa-footer { color: var(--text); font-size: 14px; letter-spacing: 0.02em; text-transform: none; }
  .gameover .hexa-footer .hexa-total { color: var(--cur-hexa); font-weight: 700; } /* ui-623: SSOT-Token-Alias, Wert unverändert */

  /* ── ui-264/web-226: Gast-CTA „Fortschritt sichern" (nur guest_-Identität, non-blocking) ──
     web-226 (DL-1/DL-2/DL-3): erweitert den Block um einen zweiten Weg (Passkey) neben
     Twitch. Vertikal gestapelte Optionen statt der alten Ein-Button-Zeile — Layout/Klassen
     1:1 aus dem abgenommenen Prototyp (.claude/sprint/prototypes/web-226.html).
     DL-2: Twitch bleibt PRIMÄR, aber bewusst CYAN (nicht var(--twitch)) — s. Kommentar
     in src/endscreen.js guestCtaHtml für die Begründung (DL-7-Tokenliste). */
  .gameover .es-guest-cta {
    width: 100%;
    display: flex; flex-direction: column; gap: 0.5em;
    padding: 0.9em 1em; border-radius: 8px;
    border: 1px solid var(--line-2);
    background: var(--bg2);
    /* ui-322: pointer-events:auto — die einzige interaktive Insel im sonst read-only
       .gameover-Overlay (pointer-events:none, Basis-Regel oben, Lock bleibt unverändert für
       den Rest des Endscreens). Ohne dieses Feld erben die Buttons/Links darin
       pointer-events:none vom Overlay und sind klick-tot. */
    pointer-events: auto;
  }
  .gameover .gc-title { font-weight: 700; font-size: 1.02em; color: var(--cyan); }
  .gameover .gc-note { margin: 0 0 0.3em; font-size: 0.8em; line-height: 1.4; color: var(--text-dim); }
  .gameover .gc-opt {
    display: flex; align-items: center; gap: 0.7em;
    width: 100%; min-height: 52px; padding: 0.6em 0.9em;
    border-radius: 8px; border: 1px solid var(--line-2);
    background: transparent; color: var(--text); font: inherit; text-align: left;
    cursor: pointer; text-decoration: none; box-sizing: border-box;
    transition: border-color 0.15s;
  }
  .gameover .gc-opt:hover { border-color: var(--cyan); }
  .gameover .gc-opt--primary { background: var(--cyan); border-color: var(--cyan); color: var(--text-on-accent); }
  .gameover .gc-opt--primary .gc-opt-sub { color: rgba(7, 9, 26, 0.72); }
  .gameover .gc-opt-ic { font-size: 1.25em; line-height: 1; flex: 0 0 auto; }
  .gameover .gc-opt-ic.tw { width: 1.1em; height: 1.1em; fill: var(--text-on-accent); }
  .gameover .gc-opt-txt { min-width: 0; }
  .gameover .gc-opt-main { display: block; font-weight: 700; font-size: 0.95em; }
  .gameover .gc-opt-sub { display: block; font-size: 0.78em; color: var(--muted); line-height: 1.35; margin-top: 1px; }
  .gameover .gc-opt:disabled { opacity: 0.6; cursor: default; }
  .gameover .gc-status { font-size: 0.78em; color: var(--text-dim); min-height: 1.2em; }
  .gameover .gc-status.ok { color: var(--green); }
  .gameover .gc-status.err { color: var(--red); }
  .gameover .gc-later {
    display: block; width: 100%; background: none; border: 0; color: var(--muted);
    font: inherit; font-size: 0.8em; padding: 0.5em; cursor: pointer; min-height: 44px;
  }
  .gameover .gc-later:hover { color: var(--text-dim); }
  .gameover .gc-reach-details summary {
    font-size: 0.78em; color: var(--cyan); cursor: pointer; padding: 0.3em 0; min-height: 44px;
    display: flex; align-items: center;
  }
  .gameover .gc-reach { display: grid; grid-template-columns: auto 1fr; gap: 0.4em 0.6em; font-size: 0.78em; margin: 0.4em 0 0; }
  .gameover .gc-reach dt { color: var(--muted); white-space: nowrap; }
  .gameover .gc-reach dd { margin: 0; color: var(--text-dim); line-height: 1.4; }
  .gameover .gc-reach .yes { color: var(--green); }
  .gameover .gc-reach .no { color: var(--red); }
  .gameover .gc-warn {
    border-left: 3px solid var(--gold); background: rgba(240, 179, 80, 0.07);
    padding: 0.6em 0.8em; font-size: 0.76em; color: var(--text-dim); line-height: 1.45;
    border-radius: 0 6px 6px 0; margin-top: 0.6em;
  }
  .gameover .gc-warn b { color: var(--gold); }

  /* ── game-119: Schadens-Pool-Erklär-Zeile im DOCK ──────────────────────────
     Zentriert, gedimmt — über der .es-summary (zwischen Tabelle und Footer).
     Tokens: label = var(--text-dim), Pool-Betrag = var(--magenta), Anteil = var(--green).
     Nur gerendert wenn pool > 0 && N >= 2 (Solo/Pool=0 → kein Rauschen). */
  .gameover .es-pool-line {
    width: 100%; text-align: center; font-size: 13px; color: var(--text-dim);
    display: flex; gap: 8px; align-items: baseline; justify-content: center; flex-wrap: wrap;
  }
  .gameover .es-pool-line .pl-label  { text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.75; }
  .gameover .es-pool-line .pl-amount { color: var(--magenta); font-weight: 700; }
  .gameover .es-pool-line .pl-share  { color: var(--green); font-weight: 600; }

  /* ── ui-058: .ct-rank* — geteiltes Ranking/Table Utility-Set ──────────────
     Rollen, nicht Struktur: Font + tabular-nums + Akzent-Rollen + Podium.
     Keine Größen/Paddings — die bleiben pro Surface.
     Zwei Definitions-Orte (DL-2): hier für den Viewer (hexa + boss),
     zweite Kopie in companion/components.css (Companion: hs-table + /styleguide).
  */
  .ct-rank { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-variant-numeric: tabular-nums; }
  .ct-rank-head { color: var(--muted); text-transform: uppercase; letter-spacing: .1em; border-bottom: 1px solid var(--border); }
  .ct-rank-num { text-align: right; }
  .ct-rank-name { color: var(--cyan); font-weight: 600; }
  .ct-rank-pos  { color: var(--yellow); font-weight: 700; }
  .ct-rank-1 { color: var(--rank-1); font-weight: 700; text-shadow: 0 0 10px rgba(94, 184, 255, 0.7); }
  .ct-rank-2 { color: var(--rank-2); font-weight: 700; text-shadow: 0 0 8px rgba(192, 200, 212, 0.6); }
  .ct-rank-3 { color: var(--rank-3); font-weight: 700; text-shadow: 0 0 8px rgba(205, 127, 50, 0.6); }

  /* Status-Badge (ui-198: kein margin-top mehr — liegt im Dock-Flow; border-radius Pill) */
  .gameover .hexa-status {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 6px 14px;
    font-size: 12px; text-transform: none; letter-spacing: 0.04em;
    border-radius: 999px;
    transition: color 0.2s, background 0.2s, border-color 0.2s;
  }
  .gameover .hexa-status .hexa-status-dot {
    display: inline-block; width: 8px; height: 8px; border-radius: 50%;
    background: currentColor; box-shadow: 0 0 8px currentColor;
  }
  .gameover .hexa-status[data-status="pending"] { color: var(--muted); background: var(--muted-a10); border: 1px solid var(--muted-a30); }
  .gameover .hexa-status[data-status="pending"] .hexa-status-dot { animation: hexa-pulse 1.4s ease-in-out infinite; }
  .gameover .hexa-status[data-status="queued"]  { color: var(--queued); background: var(--queued-a10); border: 1px solid var(--queued-a40); }
  .gameover .hexa-status[data-status="synced"]  { color: var(--green); background: rgba(94, 255, 138, 0.10); border: 1px solid rgba(94, 255, 138, 0.40); }
  .gameover .hexa-status[data-status="synced"] .hexa-status-dot { animation: hexa-glow 1.8s ease-in-out infinite; }
  .gameover .hexa-status[data-status="failed"]  { color: var(--red); background: rgba(255, 77, 110, 0.10); border: 1px solid rgba(255, 77, 110, 0.40); }
  .gameover .hexa-status[data-status="timeout"] { color: var(--yellow); background: rgba(255, 216, 74, 0.10); border: 1px solid rgba(255, 216, 74, 0.40); }
  @keyframes hexa-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
  @keyframes hexa-glow  { 0%, 100% { box-shadow: 0 0 6px currentColor; } 50% { box-shadow: 0 0 14px currentColor; } }
  @media (prefers-reduced-motion: reduce) {
    .gameover .hexa-status[data-status="pending"] .hexa-status-dot { animation: none; opacity: .8; }
    .gameover .hexa-status[data-status="synced"]  .hexa-status-dot { animation: none; box-shadow: 0 0 10px currentColor; }
  }
  /* Cooldown-Text */
  .gameover .hexa-cooldown { color: var(--text-dim); font-size: 13px; letter-spacing: 0.03em; }

  /* ============================================================
     ★ ui-198 — ACHIEVEMENT-BAND im DOCK-Flow (kein position:absolute mehr)
     ui-548 (2026-08-27): der vormalige ui-019-Variante-B-Zweizeilen-Umbruch
     (flex-wrap:wrap, "@1440p: 13 Karten = 2 Reihen") ist ERSETZT — die Reihe
     ist jetzt IMMER einzeilig (`.ac-row{flex-wrap:nowrap}`) und läuft bei
     Überbreite (Referenzfall 13 Teilnehmer) stattdessen als nahtloser
     CSS-Marquee-Loop (`.ac-marquee`/`.ac-track`/`.ac-row--dup` unten,
     Ueberbreite-Messung in endscreen.js `_applyAchievementMarquee()`). Karten
     zugleich "einen Ticken größer" (132/110/150 → 166/140/186px,
     Text-Clipping-Fix: die längsten realen "WAFFE: METRIK"-Labels aus
     web/achievements.json `name_de`, z. B. "Raketenwerfer: Runden gespielt",
     passten vorher nicht immer zweizeilig ohne Abschnitt — s. `.ac-achievement`
     weiter unten, `-webkit-line-clamp:2` unverändert, UND der wahre
     Root-Cause war zusätzlich eine zu knappe `max-height` dort, s. dortiger
     Kommentar). Breite mit Marge (nicht am rechnerischen Minimum) gewählt —
     Live-Messung zeigte bei 154px einen 0px-exakten Gleichstand zwischen 13
     Karten und dem verfügbaren Platz, was den Marquee-Trigger von
     Font-Rendering-Rundung abhängig gemacht hätte (s. `.ac-card` unten).
     ============================================================ */
  #achievement-cards {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    width: 100%;
  }
  .ac-section-label {
    font-size: 11px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.14em; color: var(--muted); opacity: 0.75;
  }
  /* ui-548: Marquee-Viewport (clipt bei Überbreite) — IMMER volle Breite,
     unabhängig vom `align-items:center` des Elternteils oben (das würde
     sonst auf Inhaltsbreite schrumpfen und Überbreite könnte nie gemessen
     werden, s. endscreen.js `_applyAchievementMarquee()`). */
  .ac-marquee {
    width: 100%;
    overflow: hidden;
  }
  /* Die verschiebbare Spur: `width:max-content` sizet sie auf ihren echten
     Inhalt; `margin:0 auto` zentriert sie, SOLANGE sie in den Viewport passt
     — überschreitet sie ihn, macht dasselbe Auto-Margin sie automatisch
     bündig zum linken Rand (CSS-Spec: Auto-Margins werden bei
     überlaufendem Inhalt zu 0), genau der gewünschte "bündiger Streifen"-
     Marquee-Start, ganz ohne eigenen Sonderfall. */
  .ac-track {
    display: flex;
    flex-direction: row;
    width: max-content;
    margin: 0 auto;
  }
  /* `width: max-content` + `flex-shrink: 0` sind hier BEIDE Pflicht (nicht nur
     auf .ac-track oben) — Live-Messung (CDP): ohne diese zwei Zeilen
     schrumpfte .ac-row als verschachtelter Flex-Container trotz
     .ac-track{width:max-content} bis auf .ac-card{min-width} herunter, weil
     Chrome die max-content-Bemessung NICHT automatisch durch einen
     verschachtelten Flex-Container mit `width:auto`/Default-`flex-shrink:1`
     durchreicht (13 Karten kollabierten exakt auf 1928px statt der
     rechnerisch erwarteten ~2266px — keine Ueberbreite mehr erkennbar, der
     Marquee waere NIE getriggert). `.ac-card`s `flex-shrink:0` (unten,
     ui-548) ist die dritte, ebenfalls notwendige Zeile derselben Kette. */
  .ac-row {
    display: flex; flex-direction: row; align-items: stretch; gap: 9px; flex-wrap: nowrap;
    width: max-content; flex-shrink: 0;
  }
  /* ui-548: byte-identisches Duplikat der Reihe (endscreen.js
     `_desktopAchievementCardsHtml`) — die zweite Hälfte des nahtlosen Loops.
     Per Default unsichtbar (kein Platz-/Perf-Verbrauch ohne Überbreite);
     `.is-marquee` (JS-gesetzt) schaltet sie ein. `margin-left` = die
     Verbindungslücke zum Ende der echten Reihe = AC_MARQUEE_GAP_PX in
     endscreen.js — MUSS synchron bleiben (sonst Sprung an der Naht). */
  .ac-row--dup {
    display: none;
    margin-left: 9px;
  }
  .ac-marquee.is-marquee .ac-row--dup { display: flex; }
  /* Hover-Pause (AK „Zugaenglichkeit/Verhalten") braucht echte
     Pointer-Events — `.gameover` ist per Default `pointer-events:none`
     (Spectator-Read-only, Basis-Regel weiter oben), hier NUR eingeschaltet
     während tatsächlich animiert wird (kein neues Interaktions-Inselchen im
     statischen Default-Fall, analog `.es-guest-cta`/`.rejoin-banner.show`). */
  .ac-marquee.is-marquee {
    pointer-events: auto;
  }
  .ac-marquee.is-marquee .ac-track {
    animation: ac-marquee-scroll var(--ac-marquee-duration, 20s) linear infinite;
  }
  .ac-marquee.is-marquee:hover .ac-track {
    animation-play-state: paused;
  }
  @keyframes ac-marquee-scroll {
    from { transform: translateX(0); }
    to   { transform: translateX(var(--ac-marquee-shift, 0px)); }
  }
  /* AK „Zugaenglichkeit/Verhalten": reduced-motion → kein Autolauf. Inhalt
     bleibt vollständig erreichbar per manuellem horizontalen Scroll statt
     Loop-Animation — das Duplikat wird dafür nicht gebraucht (kein
     doppelter Karten-Anblick beim Scrollen). JS kennt die Nutzer-Präferenz
     bewusst nicht (s. `_applyAchievementMarquee()`-Kommentar) — komplett
     hier über CSS gelöst. */
  @media (prefers-reduced-motion: reduce) {
    .ac-marquee.is-marquee {
      overflow-x: auto; overflow-y: hidden;
    }
    .ac-marquee.is-marquee .ac-track {
      animation: none;
    }
    .ac-marquee.is-marquee .ac-row--dup {
      display: none;
    }
  }
  .ac-card {
    /* ui-548: 132/110/150 → 166/140/186px (Text-Clipping-Fix, s. Kommentar
       oben) — UND `flex-shrink` 1→0: dritte Pflicht-Zeile der `.ac-row`-Kette
       (s. dortiger Kommentar) — ohne sie schrumpften die Karten trotz breiterer
       Basis wieder bis auf `min-width` zusammen. `min-width`/`max-width`
       bleiben als reine Sicherheitsgrenze (bei shrink:0/grow:0 nie erreicht,
       s. .ac-row-Kommentar), kein aktiver Mechanismus mehr. Breite mit Marge
       gewählt (Live-Messung: 13 Karten bei 154px trafen den verfügbaren Platz
       EXAKT — 0px Marge, Font-Rendering-abhängiges Kippen ob Marquee greift;
       166px schafft ~140px reale Marge). */
    flex: 0 0 166px; min-width: 140px; max-width: 186px;
    background: rgba(13, 19, 34, 0.92);
    border: 1px solid var(--border); border-radius: 7px;
    padding: 8px 10px; /* ui-548: von 7px 9px */
    display: flex; flex-direction: column; gap: 3px;
    position: relative; overflow: hidden;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 11px; color: var(--text);
    box-shadow: 0 2px 8px rgba(0,0,0,0.5);
    animation: ac-card-in 0.28s ease-out both;
  }
  @keyframes ac-card-in {
    from { opacity: 0; transform: translateY(7px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .ac-card.completed {
    border-color: var(--cyan);
    box-shadow: 0 0 12px rgba(56,225,255,0.28), 0 2px 8px rgba(0,0,0,0.5);
  }
  .ac-card.completed::before {
    content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
    background: linear-gradient(90deg, var(--cyan) 0%, rgba(56,225,255,0.3) 100%);
    border-radius: 7px 7px 0 0;
  }
  .ac-card .ac-name {
    font-size: 11px; font-weight: 700; letter-spacing: 0.02em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.2;
  }
  .ac-card .ac-achievement {
    font-size: 11px; color: var(--muted); text-transform: uppercase;
    letter-spacing: 0.06em; line-height: 1.25; overflow: hidden;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    /* ui-548: 23px → 28px. Wahrer Root-Cause des "GESPIELT"-Clippings war NICHT
       nur Kartenbreite (die machte 3 Zeilen noetig) — 2 ECHTE Zeilen brauchen
       bei line-height 1.25 × font-size 11px bereits 27,5px (Live-Messung via
       CDP: gemessenes scrollHeight 27px bei clientHeight 23px, also auch ein
       korrekt 2-zeilig umgebrochener Text wurde am unteren Rand abgeschnitten,
       unabhaengig von -webkit-line-clamp). 28px = kleinste ganze Zahl >= 27,5px. */
    max-height: 28px;
  }
  .ac-card .ac-bar-wrap {
    position: relative;                   /* game-078: Anker für .ac-bar-gain */
    height: 3px; background: rgba(255,255,255,0.06);
    border-radius: 2px; overflow: hidden; margin-top: 3px;
  }
  .ac-card .ac-bar-fill {
    height: 100%; border-radius: 2px; background: var(--green);
    opacity: 0.6; transition: width 0.3s ease;
  }
  /* game-078: Gold-Abschnitt am vorderen Ende des Balkens (Runden-Zugewinn). */
  .ac-card .ac-bar-gain {
    position: absolute; top: 0; height: 100%;
    background: var(--yellow); border-radius: 2px;
    box-shadow: 0 0 5px rgba(255, 216, 74, .6);
    animation: ac-gain-grow .45s ease-out .15s both;
    transform-origin: left center;
  }
  @keyframes ac-gain-grow {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
  .ac-card.completed .ac-bar-fill { background: var(--cyan); opacity: 0.9; }
  .ac-card .ac-meta {
    display: flex; justify-content: space-between; align-items: center; gap: 4px; margin-top: 2px;
  }
  .ac-card .ac-pct {
    font-size: 11px; font-variant-numeric: tabular-nums; color: var(--muted);
    font-weight: 600; letter-spacing: 0.03em; white-space: nowrap;
  }
  .ac-card.completed .ac-pct { color: var(--cyan); }
  /* game-078: Runden-Zugewinn-Badge (Gold, rechts in .ac-meta, nur Progress-Karten). */
  .ac-card .ac-gain {
    font-size: 11px; font-variant-numeric: tabular-nums;
    color: var(--yellow); font-weight: 700; letter-spacing: 0.02em;
    white-space: nowrap; text-shadow: 0 0 6px rgba(255, 216, 74, .35);
  }
  .ac-card.completed .ac-gain { display: none; }   /* completed zeigt Reward, nicht Zugewinn */
  .ac-card .ac-reward {
    font-size: 11px; font-weight: 700;
    letter-spacing: 0.03em; white-space: nowrap;
  }
  .ac-card:not(.completed) .ac-reward { display: none; }
  /* ui-623: Hexa- und Draw-Reward tragen jetzt EIGENE Farben statt des
     vormals gemeinsamen Magenta (Fifty-Bug-Report "Hexa sind mal pink, mal
     gelb" — dieselbe Reward-Zeile zeigte beide Währungen ununterscheidbar
     ein). tokens.css ist geladen (s. dortiger Datei-Kopf), also direkt die
     Währungs-Tokens. */
  .ac-card .ac-reward--hexa { color: var(--cur-hexa); }
  .ac-card .ac-reward--draw { color: var(--cur-draw); }
  /* ui-335: Draw-Icon-Sizing im Reward-Badge (9px-Textkontext) — currentColor erbt
     die Farbe von .ac-reward--draw (ui-623, vormals --magenta von .ac-reward).
     vertical-align nur fuer Baseline-Tuning im Inline-Fluss (analog svg.draw-icon
     in companion/components.css, das hier NICHT geladen wird — eigene,
     unabhängige Deklaration für den Viewer-Kontext). */
  .ac-reward .draw-icon {
    vertical-align: -0.1em;
    margin: 0 1px;
  }

  /* ============================================================
     ui-556: SAISON-FORTSCHRITT-ZEILEN im DOCK-Flow — schlichte Zeilen (KEIN
     Marquee/keine Karte, s. endscreen.js `_seasonProgressRowsHtml()`-
     Kommentar), eine Zeile PRO SPIELER mit Fortschritt in diesem Run. Sitzt
     im selben `.es-dock` wie `#achievement-cards` oben (append-Reihenfolge:
     Achievements zuerst, dann Saison-Zeilen — beide Quellen sind vom selben
     async-Rückkanal unabhängig, s. dortiger Latch-Kommentar). Gold-Farbwelt
     wie der Achievements-/Saison-Bereich (var(--gold)). */
  #season-progress {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    /* ui-656: pointer-events:auto — die interaktive Insel des Saison-Links
       direkt darunter (ui-322-Muster, s. .es-guest-cta oben). Ohne dieses
       Feld erbt der Link pointer-events:none vom .gameover-Overlay und ist
       klick-tot. */
    pointer-events: auto;
  }
  .es-season-rows {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
  }
  .es-season-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 12px;
  }
  .es-season-name { font-weight: 700; }
  .es-season-val { color: var(--gold); font-weight: 600; white-space: nowrap; }
  .es-season-link {
    font-size: 11px; color: var(--cyan); text-decoration: none;
    display: inline-flex; align-items: center; min-height: 24px;
  }
  .es-season-link:hover { text-decoration: underline; }
  .es-season-link:focus-visible {
    text-decoration: underline; outline: 2px solid var(--cyan); outline-offset: 2px;
  }

  /* ============================================================
     web-280: HEXA-CLUB-RUN-BOOST-ZEILE im DOCK-Flow — analog #season-progress
     direkt oberhalb (schlichte Zeile, kein Marquee/Karte), aber GENAU EINE
     Zeile statt einer PRO-SPIELER-Liste (nur der eigene Viewer sieht sie
     ueberhaupt, s. endscreen.js `_clubBoostLineHtml()`-Kommentar). Cyan-
     Farbwelt (Hexa-Club-Markenfarbe, s. views/shop-club.js/dashboard.js
     Popin — dieselbe var(--cyan)-Konvention). */
  #club-boost-line {
    width: 100%;
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
    font-size: 12px;
  }
  .es-club-boost-label { font-weight: 600; }
  .es-club-boost-amt {
    color: var(--cyan);
    font-weight: 700;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 3px;
  }

  /* ============================================================
     JOIN-WINDOW-OVERLAY (srv-029, game-056) — Cold-Start-Lobby-Countdown
     Positioniert in #stage, zentriert, z-index:40 (unter Endscreen/WS-Badge).
     show-Klasse via tickJoinWindow() aus endscreen.js gesetzt.
     ============================================================ */
  #join-window-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: rgba(6, 8, 15, 0.72);
    z-index: 40;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s;
  }
  #join-window-overlay.show { opacity: 1; }
  #join-window-overlay .jw-title {
    color: var(--cyan);
    font-size: 22px;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    margin-bottom: 8px;
  }
  #join-window-overlay .jw-countdown {
    font-size: 64px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--yellow);
    letter-spacing: 0.04em;
    line-height: 1;
    margin-bottom: 12px;
  }
  #jw-countdown-display.urgent {
    color: var(--red);
    text-shadow: 0 0 18px var(--red);
  }
  #join-window-overlay .jw-hint {
    color: var(--text);
    font-size: 14px;
    letter-spacing: 0.06em;
    margin-bottom: 10px;
  }
  #jw-player-count {
    color: var(--muted);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.04em;
  }
  #jw-player-count.has-players {
    color: var(--green);
  }

  /* ============================================================
     SETTINGS-BAR (ui-017) — Lautsprecher + WLAN-Signal, unten rechts.
     Ersetzt #ws-badge (entfernt) und #audio-settings/#audio-gear-btn (entfernt).
     Pill-Container: Lautsprecher-Button | Divider | WLAN-Icon.
     Startet hidden, initSettingsBar() entfernt hidden unbedingt (P7/ui-469).
     ============================================================ */
  #settings-bar {
    position: fixed;
    right: 16px;
    bottom: 14px;
    z-index: 92;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 7px 13px;
    border-radius: 99px;
    background: rgba(13,19,34,0.78);
    border: 1px solid var(--border);
    pointer-events: auto;
  }
  #settings-bar[hidden] { display: none; }

  /* ui-568-Fix (Root-Cause "anderes", CDP-belegt): #settings-panel (ui-560/561,
     400px breit) ragt links ueber #stage's rechte Kante hinaus (DL-1: #stage
     endet an calc(100vw - var(--panel-w-visual) - 288px), zoom-abhaengig
     ~1640px @1920×1080) -- in diesem ~137px-Streifen (#settings-panel links
     vs. #stage rechts) liegt der Endscreen (.gameover, z-index:120, srv-059
     "unter nichts") laut DL-4-Default UEBER dem GESCHLOSSENEN #settings-bar
     (92) und damit auch ueber der geoeffneten #settings-panel-Sektion, die
     dort zufaellig liegt -- .gameovers halbtransparenter Backdrop
     (rgba(6,8,15,.85)) verwaescht/dimmt den Panel-Text in diesem Streifen
     sichtbar (Fiftys PTR-Fund "Benachrichtigungen-Text blutet durch" betraf
     die Push-Sektion nur zufaellig staerker als Audio/Eco, weil deren
     laengere Fliesstext-Zeilen die Naht ueberspannen — der Effekt selbst
     zieht sich ueber die GESAMTE Panel-Hoehe). Reproduziert per CDP
     elementsFromPoint()+Crop-Screenshot (Server Port 3417, Chrome Port 9651,
     `/dev/force-end-run` -> `.gameover.show.hexa-endscreen`, Panel offen):
     sichtbare Kontrast-Naht exakt bei x=1640 (= #stage-Kante), Screenshots
     lokal unter D:/ct-tmp/ui568/ (Wegwerf, nicht im Commit).
     Fix: NUR waehrend das Panel offen ist (:has(), Codebase-Praezedenz s.
     mobile.css #screen-field.on:has(#mob-side[hidden])) auf denselben Layer
     wie #room-switcher heben (ui-218, z-index:130 "ueber Endscreen:120/
     Onboarding:121, unter Popin:200") -- der GESCHLOSSENE DL-4-Default (92,
     3 Icons bleiben unter dem Endscreen) bleibt fuer den Normalfall
     unangetastet, kein Redesign der Schicht-Reihenfolge. */
  #settings-bar:has(#settings-panel.open) { z-index: 130; }

  .sb-divider {
    width: 1px; height: 20px;
    background: var(--border);
    flex-shrink: 0;
  }

  /* --- Lautsprecher-Button (DL-B, DL-G) --- */
  #audio-speaker-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0;
    background: none; border: none; cursor: pointer; outline: none;
    --spk-color: var(--text);
    opacity: 0.9;
    transition: opacity 0.2s;
  }
  #audio-speaker-btn:hover { opacity: 1; --spk-color: var(--cyan); }
  .spk-svg { width: 24px; height: 24px; display: block; overflow: visible; }
  .spk-body { fill: var(--spk-color); transition: fill 0.15s; }
  .spk-wave {
    stroke: var(--spk-color); stroke-width: 1.7;
    stroke-linecap: round; fill: none;
    opacity: 0; transition: opacity 0.15s;
  }
  .spk-mute {
    stroke: var(--red); stroke-width: 1.9; stroke-linecap: round;
    fill: none; display: none;
  }
  /* Wellen-Stufen via data-level am Button (DL-A) */
  #audio-speaker-btn[data-level="1"] .spk-wave-1 { opacity: 1; }
  #audio-speaker-btn[data-level="2"] .spk-wave-1,
  #audio-speaker-btn[data-level="2"] .spk-wave-2 { opacity: 1; }
  #audio-speaker-btn[data-level="3"] .spk-wave-1,
  #audio-speaker-btn[data-level="3"] .spk-wave-2,
  #audio-speaker-btn[data-level="3"] .spk-wave-3 { opacity: 1; }
  /* Stumm (level 0): roter Slash sichtbar, Body rot, Button voll deckend */
  #audio-speaker-btn[data-level="0"] .spk-mute { display: block; }
  #audio-speaker-btn[data-level="0"] { --spk-color: var(--red); opacity: 1; }

  /* --- WLAN-Status (DL-C, DL-E) --- */
  #ws-wifi {
    display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px;
    --wifi: var(--muted);
    transition: opacity 0.4s;
  }
  .wifi-svg { width: 22px; height: 22px; display: block; overflow: visible; }
  .wifi-arc {
    stroke: var(--wifi); stroke-width: 1.9;
    stroke-linecap: round; fill: none;
    transition: opacity 0.3s, stroke 0.3s;
  }
  .wifi-dot { fill: var(--wifi); transition: fill 0.3s; }
  /* States — Farben/Opacity gespiegelt aus dem alten #ws-badge */
  #ws-wifi[data-state="connected"]    { --wifi: var(--green-a75); opacity: 0.6; }
  #ws-wifi[data-state="reconnecting"] { --wifi: var(--orange);         opacity: 1; }
  #ws-wifi[data-state="disconnected"] { --wifi: var(--red);            opacity: 1; }
  /* disconnected: äußere Bögen verblassen → "Signal weg"-Look */
  #ws-wifi[data-state="disconnected"] .wifi-arc-2,
  #ws-wifi[data-state="disconnected"] .wifi-arc-3 { opacity: 0.2; }
  /* reconnecting: äußerer Bogen pulst */
  #ws-wifi[data-state="reconnecting"] .wifi-arc-3 {
    animation: wifi-pulse 1.4s ease-in-out infinite;
  }
  @keyframes wifi-pulse {
    0%,100% { opacity: 0.35; }
    50%     { opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    #ws-wifi[data-state="reconnecting"] .wifi-arc-3 { animation: none; opacity: .8; }
  }

  /* --- Zahnrad-Button (ui-560 AK1) — dasselbe Grundmuster wie der Lautsprecher --- */
  #settings-gear-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0;
    background: none; border: none; cursor: pointer; outline: none;
    color: var(--text);
    opacity: 0.9;
    transition: opacity 0.2s, color 0.2s;
  }
  #settings-gear-btn:hover,
  #settings-gear-btn[aria-expanded="true"] { opacity: 1; color: var(--cyan); }
  .gear-svg { width: 22px; height: 22px; display: block; overflow: visible; }

  /* ui-560 AK6/R2-Entscheid (Fifty): dezentes HUD-Signal am Zahnrad, solange Eco
     WIRKLICH drosselt (isEcoThrottling(), NIE bei blossem Eco-an) UND das Panel
     noch nie geöffnet wurde (ui-614, settingsSeen) — kleiner 🍃-Dot an der Ecke +
     sanfter Border-Puls, kein eigenes HUD-Element (s. deriveGearSignalState(),
     core/settings-panel.js). Rahmen statt Farbe: der Button selbst hat keinen
     sichtbaren Rand im Ruhezustand — der Puls zeichnet einen dezenten Fokus-Ring
     statt eine echte Border zu animieren. Diese Regel selbst ist CSS-seitig
     UNVERÄNDERT (ui-614-Lock) — nur JS setzt/entfernt .eco-active seltener. */
  #settings-gear-btn.eco-active { position: relative; animation: sgEcoPulse 2.6s ease-in-out infinite; }
  #settings-gear-btn.eco-active::after {
    content: "🍃"; position: absolute; top: -4px; right: -4px; font-size: 11px;
    line-height: 1; filter: drop-shadow(0 0 3px rgba(0,0,0,.7));
  }
  @keyframes sgEcoPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(255,179,71,0); }
    50%      { box-shadow: 0 0 0 3px rgba(255,179,71,.45); }
  }
  @media (prefers-reduced-motion: reduce) {
    #settings-gear-btn.eco-active { animation: none; box-shadow: 0 0 0 2px rgba(255,179,71,.5); }
  }

  /* --- Einstellungen-Panel (ui-560) — öffnet sich nach oben (DL-F), ersetzt das
     vormalige, kleinere #audio-panel (ui-015). Struktur/Klassen 1:1 aus dem
     Fifty-abgenommenen Prototyp (.claude/sprint/prototypes/settings-eco.html) —
     alle Selektoren unter #settings-panel gescoped (Fallen-Register "Unscoped
     Element-Selektoren in desktop.css leaken in Popin-Fragmente", ui-510). --- */
  #settings-panel {
    position: absolute;
    bottom: calc(100% + 10px);
    right: 0;
    width: 400px;
    max-width: calc(100vw - 28px);
    max-height: min(78vh, 640px);
    overflow-y: auto;
    background: rgba(13,19,34,0.96);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 14px;
    transform-origin: bottom right;
    transform: scale(0.85) translateY(4px);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1),
                opacity 0.18s ease;
    box-shadow: 0 18px 50px rgba(0,0,0,.5);
  }
  #settings-panel.open {
    transform: scale(1) translateY(0);
    opacity: 1;
    pointer-events: auto;
  }

  #settings-panel .sp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
  #settings-panel .sp-head h2 { font-size: 15px; letter-spacing: .02em; color: var(--text); font-weight: 600; }
  #settings-panel .sp-close { background: none; border: 0; color: var(--muted); font-size: 18px; cursor: pointer; padding: 4px 8px; }
  #settings-panel .sp-close:hover { color: var(--text); }

  #settings-panel .sec { border: 1px solid var(--border); border-radius: 11px; background: rgba(255,255,255,.02); margin-bottom: 10px; overflow: hidden; }
  #settings-panel .sec:last-child { margin-bottom: 0; }
  #settings-panel .sec-h { display: flex; align-items: center; gap: 9px; padding: 11px 12px; font-size: 13.5px; font-weight: 600; color: var(--text); }
  #settings-panel .sec-h .ico { width: 20px; text-align: center; font-size: 15px; }
  #settings-panel .sec-body { padding: 0 12px 11px; }
  #settings-panel .sec-sub { color: var(--muted); font-size: 12px; font-weight: 400; margin-top: 2px; }

  #settings-panel .row {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    padding: 9px 0; border-top: 1px solid var(--border); min-height: 44px;
  }
  #settings-panel .row:first-child { border-top: 0; }
  #settings-panel .row .lbl { font-size: 13.5px; color: var(--text); }
  #settings-panel .row .lbl small { display: block; color: var(--muted); font-size: 11.5px; margin-top: 1px; }

  /* Toggle (Pill) — geteiltes Motiv über Ton/Gruppen/Einzelsounds/Eco, ANDERE
     Implementierung als das Alt-Checkbox-Toggle (.as-toggle-*, ui-015): hier
     ein <button role="switch"> statt <input type=checkbox>+<label> (Prototyp-
     Vorlage, kein Formular-Kontext nötig — nur Klick-Zustand).
     ui-624-Härtung (AK2b, Lock 1: Ursache NICHT reproduzierbar — Fiftys
     Arbeitslaptop-Bug ließ sich per Headless-Chrome/CDP bei DPR 1/1,5/2,
     zwei Fenstergrößen, allen Zuständen (an/aus/mixed/disabled) UND dem
     echten unhydrierten SSR-Default NICHT nachstellen; `getComputedStyle(btn,
     '::after').top` war in jedem Fall byte-identisch "2px" für Eco UND
     Push-Schalter, s. Ticket-## Notes für die volle Rect-Tabelle). Harte
     Zentrierung ERSETZT trotzdem den festen `top: 2px`-Wert durch
     `top: 50%` + `translate` (robust gegen jede künftige Höhen-Änderung von
     `.tgl` UND — anders als `top: 2px` — unempfindlich gegen einen
     hypothetischen künftigen Kaskaden-Leck einer fremden `top`-Regel);
     `transform: none` schließt zusätzlich explizit jede geerbte
     `transform`-Regel aus (Referenzfall: `web/client/css/companion/pages/
     profile.css` `.px-switch::after` nutzt `transform: translateY(-50%)`
     für eine GRÖSSERE Geometrie (52×32/Knopf 22) — die Datei wird im Viewer
     nicht geladen, s. dort, aber Lock 3 entfernt zusätzlich die geteilte
     Klasse selbst, s. `.gold`-Kommentar unten). `box-sizing: border-box`
     explizit auf `.tgl` (bislang nur über den globalen `* { box-sizing:
     border-box }`-Reset, Z. 20) — macht die 22px-Gesamthöhe/1px-Rand-Rechnung
     unabhängig von einer künftigen Reset-Änderung. */
  #settings-panel .tgl {
    position: relative; width: 40px; height: 22px; flex: 0 0 auto;
    border-radius: 999px; border: 1px solid var(--border);
    background: rgba(0,0,0,.25); cursor: pointer; padding: 0;
    box-sizing: border-box;
    transition: background .15s, border-color .15s;
  }
  #settings-panel .tgl::after {
    content: ""; position: absolute; top: 50%; left: 2px; width: 16px; height: 16px;
    translate: 0 -50%; transform: none;
    border-radius: 50%; background: var(--muted); transition: left .15s, background .15s;
  }
  #settings-panel .tgl[aria-checked="true"] { background: rgba(56,225,255,.18); border-color: var(--cyan); }
  #settings-panel .tgl[aria-checked="true"]::after { left: 20px; background: var(--cyan); }
  /* ui-561: Gold-Variante NUR für die Push-Schalter (Prototyp-Vorlage
     settings-eco.html) — dieselbe .tgl-Mechanik, andere An-Farbe zur
     visuellen Abgrenzung von Audio/Eco (beide Cyan) im selben Panel. */
  #settings-panel .tgl.gold[aria-checked="true"] { background: rgba(240,179,80,.16); border-color: var(--gold); }
  #settings-panel .tgl.gold[aria-checked="true"]::after { background: var(--gold); }
  #settings-panel .tgl[aria-checked="mixed"] { opacity: .55; }
  #settings-panel .tgl[aria-checked="mixed"]::after { left: 11px; }
  #settings-panel .tgl:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
  /* ui-561: erster disabled-Schalter im Panel (SSR-Default „lädt/unbekannt"
     + unsupported-Outcome der Push-Sektion, s. public/push-settings.js) —
     .35-Wert bewusst vom bestehenden .tgl[aria-checked="mixed"] (.55) leicht
     abgesetzt, damit „lädt UND deaktiviert" sichtbar dunkler bleibt als das
     rein informative mixed (Gruppen-Master, nie disabled). */
  #settings-panel .tgl:disabled { opacity: .35; cursor: not-allowed; }

  /* Volume-Zeile (Desktop-exklusiv, DL-5) */
  #settings-panel .vol-row { display: flex; align-items: center; gap: 10px; padding: 6px 0 10px; }
  #settings-panel .vol-row .sp-vol-label { font-size: 12px; color: var(--muted); flex-shrink: 0; }
  #settings-panel .vol-row input[type=range] {
    flex: 1; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 2px;
    outline: none; cursor: pointer; --fill: 100%;
    background: linear-gradient(to right, var(--cyan) 0%, var(--cyan) var(--fill), var(--border) var(--fill), var(--border) 100%);
  }
  #settings-panel .vol-row input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
    background: var(--cyan); box-shadow: 0 0 6px var(--cyan-glow); cursor: pointer; transition: transform .1s;
  }
  #settings-panel .vol-row input[type=range]::-moz-range-thumb {
    width: 14px; height: 14px; border-radius: 50%; background: var(--cyan);
    box-shadow: 0 0 6px var(--cyan-glow); cursor: pointer; border: none;
  }
  #settings-panel .vol-row input[type=range]:hover::-webkit-slider-thumb { transform: scale(1.2); }
  #settings-panel .vol-row input[type=range]:hover::-moz-range-thumb { transform: scale(1.2); }
  #settings-panel .vol-row input[type=range]:disabled { opacity: .35; cursor: default; }
  #settings-panel .vol-val { width: 42px; text-align: right; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }

  /* Sound-Gruppen (aufklappbar, Gruppen-Master + Zähler — Mathematik in
     core/settings-panel.js deriveGroupToggleState()) */
  #settings-panel .grp { border-top: 1px solid var(--border); }
  #settings-panel .grp-h { display: flex; align-items: center; gap: 8px; padding: 9px 0; min-height: 44px; cursor: pointer; }
  #settings-panel .grp-h .chev { color: var(--muted); font-size: 11px; transition: transform .15s; width: 14px; }
  #settings-panel .grp[data-open="1"] .grp-h .chev { transform: rotate(90deg); }
  #settings-panel .grp-h .glabel { flex: 1; font-size: 13.5px; color: var(--text); }
  #settings-panel .grp-h .gcount { color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
  #settings-panel .grp-items { display: none; padding: 0 0 6px 22px; }
  #settings-panel .grp[data-open="1"] .grp-items { display: block; }
  #settings-panel .grp-items .row { min-height: 40px; padding: 6px 0; }
  #settings-panel .grp-items .row .lbl { font-size: 12.5px; color: var(--text); }

  /* Eco-Status-Hinweis (nur sichtbar solange isEcoThrottling()) */
  #settings-panel .eco-note {
    margin-top: 8px; padding: 8px 10px; border-radius: 8px;
    background: rgba(255,179,71,.1); border: 1px solid rgba(255,179,71,.35);
    color: var(--orange); font-size: 12px; display: none;
  }
  #settings-panel .eco-note.on { display: block; }

  /* Push-Sektion (ui-561 — Spiegel des Profil-Panels, dieselben Klassennamen
     wie web/client/css/companion/pages/profile.css .px-hint UND derselbe
     Farb-Literal 240,179,80 (== var(--gold), DL-8: kein neuer Wert) — hier
     GESCOPED unter #settings-panel statt bare (Fallen-Register "Unscoped
     Element-Selektoren", ui-510) UND eigenständig, weil profile.css in
     einem komplett anderen Seiten-Bundle liegt (kein Companion-CSS im
     Viewer, s. Modul-Karte README.md § css/). .tgl.gold/:disabled stehen
     weiter oben bei den übrigen .tgl-Regeln. */
  #settings-panel .px-hint {
    border-left: 3px solid var(--gold); background: rgba(240,179,80,.07);
    padding: 8px 10px; font-size: 11.5px; color: var(--muted);
    line-height: 1.5; border-radius: 0 6px 6px 0; margin-top: 8px;
  }
  #settings-panel .push-hint { color: var(--muted); font-size: 11.5px; margin-top: 6px; }
  /* Gast-/Anon-Notiz (AK3 — derivePushSectionState() 'guest', core/settings-panel.js) */
  #settings-panel .guest-note {
    padding: 9px 10px; border-radius: 8px; background: rgba(240,179,80,.09);
    border: 1px solid rgba(240,179,80,.3); color: var(--gold); font-size: 12.5px;
  }
  #settings-panel .guest-note a { color: var(--gold); }

  /* ============================================================
     LOGIN-INDICATOR (web-054)
     Position: fixed, unten links, eigenständig außerhalb #hud/#stage.
     DL-1: left:14px; bottom:12px; z-index:95 (über settings-bar:92)
     DL-2: kein Pill/Badge-Shape — reiner Text-Indikator
     DL-3: 11px Monospace, kein Uppercase
     DL-4: --muted Präfix + --cyan Wert (eingeloggt); --cyan Link (ausgeloggt)
     DL-5: opacity:0.65 Ruhe, 1.0 Hover
     ============================================================ */
  #login-indicator {
    position: fixed;
    left: 14px;
    bottom: 12px;
    z-index: 95;
    font-size: 11px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    color: var(--muted);
    opacity: 0.65;
    transition: opacity 0.25s;
    pointer-events: auto;
    user-select: none;
    line-height: 1;
  }
  #login-indicator:hover { opacity: 1; }
  #login-indicator .li-name { color: var(--cyan); font-weight: 600; }
  #login-indicator a {
    color: var(--cyan); font-weight: 600; text-decoration: none;
  }
  #login-indicator a:hover { text-decoration: underline; }
  /* ui-074: Draw-Count im Login-Indicator (nur eingeloggt). */
  #login-indicator .li-draws {
    margin-left: 8px; color: var(--muted); opacity: 0.75;
  }
  /* ui-623 (AK4/DL-4, "Draws-HUD-Zähler"): Icon + Zahl zusammen in --cur-draw
     statt des vormaligen --muted (Icon) + einer nie greifenden `.li-draws-val`-
     Klassenregel, die --yellow verlangte (das Element trägt eine ID, IST-Fund
     ui-623 — die Regel war seit ui-074 tote CSS). Ersetzt durch die neue
     Wrapper-Klasse .li-draws-amt (main.js), die Icon+Zahl umschließt; das
     "Draws"-Label danach bleibt außerhalb, also weiterhin --muted. */
  #login-indicator .li-draws-amt { color: var(--cur-draw); font-weight: 600; }
  /* ui-335: Draw-Icon-Sizing im Login-Indicator (11px-Textkontext) — currentColor
     erbt seit ui-623 --cur-draw von .li-draws-amt (vormals --muted von .li-draws). */
  #login-indicator .li-draws .draw-icon {
    vertical-align: -0.12em;
    margin-right: 1px;
  }

  /* ============================================================
     ui-404: LEGAL-LINKS — dezente Zeile ganz unten (bottom-center),
     eigenständig außerhalb #hud/#stage/#rim-cards (kein Layout-Fluss,
     kein Einfluss auf die Spielflächen-Geometrie — position:fixed,
     Architektur-Lock). Sitzt UNTER den bestehenden Ecken-Widgets
     (#login-indicator bottom:12px / #settings-bar bottom:14px) statt sie
     zu überlappen — eigene Zeile, kein Korb-Umbau der Ecken nötig.
     `hidden`-Attribut im Markup + initLegalLinks() entfernt es unbedingt
     (P7/ui-469, Muster #settings-bar).
     Reine Text-Links, keine Pille (analog Login-Indicator DL-2). Fünf der
     sechs Ziele sind auth-frei (Datenschutz/Impressum/Glossar/Guide —
     nachgezogen ui-411 — /Patch-Notes). Der sechste war /konto-loeschen
     (nachgezogen ui-410, auth-frei) — ui-422 tauscht ihn gegen "Account
     und Daten" (Ziel /dashboard). Anders als die Lösch-Seite VERLANGT
     /dashboard wie gehabt eine Session: abgemeldet redirected die Route
     auf die Landing-Page (kein Crash, aber kein Zugriff auf den Inhalt) —
     bestehendes Routing-Verhalten, von ui-422 nicht angefasst.
     /konto-loeschen selbst bleibt unverändert direkt (auth-frei)
     erreichbar (Play-Store-Pflicht, s. Ticket-Lock), jetzt verlinkt aus
     "Account und Daten" heraus (dashboard.js, .account-danger) statt von
     hier.
     ============================================================ */
  /* ui-504 (M2, K1): 10.5px -> 11px Lese-Boden; white-space:nowrap + `position:fixed`
     mittig -- bei 1280px+ Breite kein Kollisionsrisiko (DL-5-Kollisionsschwellen
     liegen bei 644/910px, deutlich schmaler als die gemessenen Viewports). */
  #legal-links {
    position: fixed;
    left: 50%;
    bottom: 4px;
    transform: translateX(-50%);
    z-index: 90;
    display: block;
    font-size: 11px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    color: var(--muted);
    opacity: 0.45;
    transition: opacity 0.25s;
    pointer-events: auto;
    user-select: none;
    white-space: nowrap;
  }
  #legal-links[hidden] { display: none; }
  #legal-links:hover { opacity: 0.9; }
  #legal-links a { color: inherit; text-decoration: none; }
  #legal-links a:hover { color: var(--cyan); text-decoration: underline; }

  /* ui-258: #hud-pact-banner (Top-Pakt-Banner) entfernt. .pact-banner-* / .pact-pill-* / .pact-rw-*
     bleiben — werden vom #pact-panel unten links (ui-242) genutzt. ui-456: die
     byte-identischen unter ihnen leben seither EINMAL in
     web/client/css/viewer-base.css (s. dortiger Kopfkommentar); nur
     .pact-banner-head/.pact-banner-rewards (kein flex-wrap hier) und
     .pact-pill-label (--text-dim statt Mobiles --muted) + die
     #pact-panel-Scope-Overrides unten bleiben Desktop-lokal. game-160: die
     vormaligen .pact-rw-personal-Nested-Overrides (magenta-light "dein
     Pakt"-Pille) sind mit der Pille selbst ersatzlos entfernt. */
  .pact-banner-head { display: flex; align-items: center; gap: 16px; justify-content: space-between; }
  .pact-banner-rewards { display: flex; align-items: center; gap: 10px; white-space: nowrap; }

  /* ui-256: Panel-Scope-Overrides (264px) — Basisklassen bleiben unberuehrt (geteilt mit Builder).
     .pact-rw-shared erbt white-space:nowrap via inline-flex → Pillentext bricht nicht. */
  #pact-panel .pact-banner-head    { flex-wrap: wrap; }
  #pact-panel .pact-banner-rewards { flex-wrap: wrap; white-space: normal; }

  .pact-pill-label { color: var(--text-dim); font-size: 11.5px; }

  /* ============================================================
     ui-218: RAUM-SWITCHER (#room-switcher)
     Position: top-right der Stage, direkt unter HUD-Zeile (DL-1).
     Sichtbar NUR: body.viewer-logged-in && aktiveRaeume >= 2 (DL-2).
     Segment-Pille analog bestehender Toggle-Pille (DL-3).
     Alle Farben via var(--*)-Tokens — kein Hex-Hardcode (DL-3).
     Listener via Event-Delegation am stabilen Container (DL-6 Idiomorph-Falle).
     pointer-events: auto (Klick-Interaktion).
     z-index: 130 (uber Endscreen:120/Onboarding:121, unter Popin:200).
     ============================================================ */
  #room-switcher {
    position: absolute;
    top: 12px; right: 12px;
    z-index: 130;
    display: flex;
    align-items: center;
    flex-wrap: wrap;         /* ui-283: aufgeklappt (6-Raum-Faelle) darf umbrechen */
    justify-content: flex-end;
    gap: 6px;
    max-width: min(72vw, 560px); /* ui-283: begrenzt Breite gegen #panel-Kollision beim Umbruch */
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 4px 5px 4px 10px;
    box-shadow: 0 2px 14px rgba(0,0,0,.45);
    backdrop-filter: blur(4px);
    pointer-events: auto;
    /* Guard: standardmaessig ausgeblendet — JS setzt display:flex wenn sichtbar */
  }
  #room-switcher[hidden] { display: none !important; }
  /* ui-283: bei Umbruch (2+ Zeilen) wirkt eine volle Pille (border-radius:999px)
     optisch falsch (ovale Form ueber mehrere Zeilen) — .rs-wrapped (JS gesetzt,
     s. web/client/elements/room-switcher.js seit ui-452) rundet stattdessen normal ab. */
  #room-switcher.rs-wrapped { border-radius: 16px; }
  .rs-cap {
    font-size: 11px;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--muted);
    padding-right: 2px;
    white-space: nowrap;
  }
  .rs-seg {
    appearance: none;
    border: 1px solid transparent;
    background: transparent;
    cursor: pointer;
    color: var(--text-dim);
    font: inherit;
    font-size: 12.5px;
    line-height: 1;
    padding: 6px 11px;
    border-radius: 999px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-width: 46px;
    min-height: 44px;       /* Touch-Target >= 44px (DL-9) */
    justify-content: center;
    transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
    white-space: nowrap;
  }
  .rs-seg .rs-meta { font-size: 11px; color: var(--muted); letter-spacing: 0.02em; }
  .rs-seg:hover { color: var(--text); background: rgba(255,255,255,.03); }
  /* ui-269 DL-1: Rolle-Klasse fuer den Meta-Text — wartend (orange) vs. laufend (gruen).
     Faerbt nur den Meta-Text (Segment selbst bleibt neutral bis aktiv/pressed). */
  .rs-seg.wait .rs-meta { color: var(--queued); }
  .rs-seg.run  .rs-meta { color: var(--green); }
  /* Aktives Segment: Cyan-Fill + Border (DL-3) — ueberschreibt die Rolle-Farbe */
  .rs-seg[aria-pressed="true"] {
    color: var(--cyan);
    border-color: var(--cyan);
    background: var(--cyan-a12);
  }
  .rs-seg[aria-pressed="true"] .rs-meta { color: var(--cyan-a55); }
  /* Pending-State waehrend Reconnect (DL-5) */
  .rs-seg.rs-pending {
    color: var(--yellow);
    border-color: rgba(255,216,74,.5);
  }
  .rs-seg:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
  /* ui-512 (DL-1/DL-6): virtueller "Dein Raum"-Eintrag — linker Akzent-
     Marker (Cyan, identisch zur ui-500-Kopf-Pille .ip-room.is-run/.rn, s.
     dortiger Kommentar) markiert die Zeile IMMER als Eigenraum, unabhaengig
     vom Aktiv-Zustand (der bleibt exklusiv ueber aria-pressed, DL-4 — bei
     gleichzeitig aktivem eigenem Raum addiert sich die dickere Kante einfach
     zur vollen Cyan-Fuellung). padding-left um die zusaetzliche Kantenbreite
     reduziert (11px Basis - 3px Kante = 8px), damit die Zeile nicht breiter
     wirkt als die uebrigen Segmente. Muss NACH der Basis-.rs-seg-Regel
     stehen (border-left gewinnt bei gleicher Spezifitaet ueber Deklarations-
     reihenfolge gegen border:1px solid transparent oben). */
  .rs-seg--own { border-left: 3px solid var(--cyan); padding-left: 8px; }

  /* ui-283: kompakter On-Demand-Toggle-Chip ("+N"/"−") — blendet die im Default
     ausgeblendeten (idle) Raeume ein/aus. Touch-Target >= 44px (ui-214-Kanon),
     visuell kompakter als ein .rs-seg (kein Meta-Zeile-Platz noetig). */
  .rs-toggle {
    appearance: none;
    border: 1px dashed var(--border);
    background: transparent;
    cursor: pointer;
    color: var(--muted);
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    padding: 6px 9px;
    border-radius: 999px;
    min-width: 44px;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
    white-space: nowrap;
  }
  .rs-toggle:hover { color: var(--text); background: rgba(255,255,255,.03); border-color: var(--text-dim); }
  .rs-toggle[aria-expanded="true"] { color: var(--cyan); border-color: var(--cyan); }
  .rs-toggle:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }

  /* Toast bei Auto-Fallback (Raum verschwunden, DL-7) */
  #rs-toast {
    position: absolute;
    top: 68px; right: 12px;
    z-index: 130;
    max-width: 280px;
    background: var(--card-bg);
    border: 1px solid var(--queued);
    border-radius: 8px;
    color: var(--text);
    font-size: 12px;
    line-height: 1.45;
    padding: 8px 11px;
    box-shadow: 0 2px 14px rgba(0,0,0,.5);
    pointer-events: none;
  }
  #rs-toast[hidden] { display: none !important; }
  #rs-toast b { color: var(--queued); }

  /* ============================================================
     SETUP-POP-IN (ui-073)
     Öffner: der „Hexa"-Knopf im Bereichs-Cluster #nav-cluster (ui-413 —
     vormals ein eigenständiger #setup-btn).
     Modal: #popin-scrim → #popin. Das Chrome (✕/Scrim-Klick/Escape) ist
     login-UNABHÄNGIG unbedingt initialisiert (initSurfacePopin()); der
     SETUP-INHALT selbst bleibt login-gated (initSetupPopin(), P7/ui-469:
     der vormalige zusätzliche ?obs-Guard ist entfallen). z-index 200 über
     allen Stage-Overlays.
     Lock: kein pointer-events:none-Erbe — eigenständig außerhalb #stage.
     ============================================================ */

  /* ============================================================
     BEREICHS-CLUSTER — #nav-cluster (ui-413, ersetzt #setup-btn UND die
     bildschirmbreite #viewer-app-nav aus ui-405)
     Drei waagerechte Knöpfe (Hexa · Rang · Profil/Login) unten links, in
     derselben 264-px-Spur wie #left-info (left:12px) — DL-1: nach oben ist
     kein Platz (fünf Pixel zwischen #left-info und dem früheren #setup-btn,
     s. Ticket-Notes), seitwärts schon. Kein „Spielen"-Knopf: Schließen einer
     Fläche ist am Desktop bereits die Rückkehr ins Spiel.
     Anders als das alte #setup-btn: KEIN Login-Gate auf die Sichtbarkeit
     (Struktur-Parität mit dem Mobile-Viewer seit ui-412 — „Hexa" UND „Rang"
     sind auch ausgeloggt sinnvolle Ziele, die Fragmente zeigen dann den
     öffentlichen Login-Hinweis). createAreaNavElement() (web/client/surfaces/
     area-nav.js, ui-455 — vormals initNavCluster() hier) baut den Inhalt beim
     Boot synchron — kein Flash. z-index 96 (wie vormals #setup-btn) — über
     #left-info, unter #settings-bar (unrelevant, eigene Ecke) und dem
     Popin-Backdrop (200).
     ui-476: Box-Reihe statt Pillen (Fifty-Entscheid 2026-08-18) — Grid statt
     Flex, Radius 10 statt 999, `--bg`-Fläche statt Cyan-Tönung; Werte 1:1 aus
     dem Ticket-Design-Lock „Verbindliche Werte Variante 1". Ergänzt nach
     Designer-Review: ui-476: Zellen zentriert (Prototyp-Referenz) — die
     ≤1000px-Icon-only-Regel unten braucht `justify-content` deshalb nicht
     mehr selbst (erbt von hier). */
  #nav-cluster {
    position: fixed;
    left: 12px;
    bottom: 34px; /* über #login-indicator (bottom:12px + ~18px lineheight) */
    z-index: 96;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
    width: 264px;
  }
  .nc-item {
    display: flex;
    align-items: center;
    justify-content: center; /* ui-476: Zellen zentriert (Prototyp-Referenz) */
    gap: 6px;
    min-height: 36px;
    padding: 0 8px;
    border-radius: 10px;
    background: var(--bg);
    border: 1px solid var(--cyan);
    color: var(--cyan);
    font-weight: 600;
    font-size: 12px;
    font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
    cursor: pointer;
    pointer-events: auto;
    text-decoration: none;
    white-space: nowrap;
    box-shadow: 0 2px 12px rgba(56,225,255,.12);
    transition: box-shadow .12s, background .15s;
    user-select: none;
  }
  .nc-item:hover { box-shadow: 0 2px 20px rgba(56,225,255,.28); }
  /* Aktiv: die gerade offene Fläche (Hexa ODER Rang) hebt sich leicht ab —
     kein neuer Farbwert, nur ein staerkerer Fuellton derselben Cyan-Box
     (ui-476: vormals Cyan-Pille). */
  .nc-item.active { background: rgba(56,225,255,.16); box-shadow: 0 2px 20px rgba(56,225,255,.30); }
  /* Login-Zelle (ausgeloggt) dezent wie .ip-cta.secondary — Login ist
     sekundär, Hexa/Rang bleiben primär (Design-Lock ui-476). Klasse setzt
     area-nav.js beim Login-Slot (_buildCluster()) und entfernt sie beim
     Umbau auf „Profil" (_updateIdentityCluster()) — reiner Klassen-Toggle,
     kein Verhaltens-Diff. */
  .nc-item.is-login { border-color: var(--line-2); color: var(--text-dim); box-shadow: none; }
  .nc-item:focus-visible { outline: 2px solid var(--cyan); outline-offset: -2px; }
  .nc-icon { display: inline-flex; line-height: 1; }
  /* Profil-Platzhalter vor dem ersten GET /api/me-Resolve (Muster vormals
     #va-nav-slot4) — visibility statt display, kein Sprung in der Cluster-
     Breite sobald „Login" auf „Profil" umgebaut wird. */
  #nc-profil[style*="visibility:hidden"] { pointer-events: none; }

  /* ui-413 (DL-5): schmales Fenster — zuerst die Beschriftung opfern, nicht
     die 264-px-Spur verlassen. Gemessen: die volle Icon+Label-Fassung stößt
     bei ~910px Fensterbreite an #legal-links (mittig) — schlechter als die
     bestehende #legal-links/#settings-bar-Kollision bei 644px (ui-411). Die
     Icon-only-Fassung schrumpft den Cluster auf ~105px und schiebt die
     EIGENE Kollisionsschwelle wieder in die Nähe der bestehenden 644px-Zahl
     (Rechnung wird dadurch nicht verschlechtert, DL-5-Vorgabe). aria-label/
     title bleiben (JS setzt sie unbedingt, s. area-nav.js _buildCluster())
     — Icons mit Tooltip sind der Rückfall, kein a11y-Verlust. `pointer:fine` grenzt
     bewusst gegen den bestehenden Touch-Layout-Block ab (analog der
     #panel-Zoom-Media-Query oben) — ein schmales Touch-Fenster matcht
     stattdessen den eigenen `max-width:950px`-Block. ui-476: die 84-px-Spalte
     (264/3) bleibt bei jeder Breite gleich — `justify-content:center` sitzt
     seit der Designer-Review in der Basisregel (zentriert Icon+Label
     einheitlich in jeder Breite); ohne Label reduziert sich hier nur noch
     das seitliche Label-Padding (0 8px → 0). */
  @media (max-width: 1000px) and (pointer: fine) {
    .nc-item { padding: 0; }
    .nc-label { display: none; }
  }

  /* ui-083: Ecken-Kollision unten links — logged-in Overrides.
     body.viewer-logged-in wird von initSetupPopin() gesetzt (nur eingeloggt).
     #left-info: 40px → 76px (ui-476: Cluster wächst von ~30px auf die neue
     Box-Reihe mit min-height:36px — Oberkante jetzt bottom(34)+height(36)=70,
     Ziel bleibt 6px Luft zur Metrik-Box-Unterkante → 70+6=76; vormals 72px/
     8px Luft bei ~30px Cluster-Höhe). Default 40px bleibt unangetastet.
     pointer-events je Element bleiben erhalten (#left-info bodies:none, .nc-item:auto). */
  body.viewer-logged-in #left-info { bottom: 76px; }

  /* ============================================================
     ONBOARDING-LAYER (ui-146, Sequenz-State-Machine seit ui-482)
     Overlay-Layer über Stage/Panel, unter dem Setup-Pop-in (z-index 200).
     pointer-events:none am Layer → nie blockierend; nur auf den Karten selbst auto.
     ui-482: IMMER höchstens EIN Fenster sichtbar (web/client/core/onboarding-
     flow.js) — hook → c1 → c2 → c3 (Live-Faden-Ende) → #ob-bridge
     (UNVERÄNDERT) → #ob-rearm → #ob-tpl (ab Run-2-Start seit ui-541) →
     ui-501: #ob-mates (Gate-Run-3-Start) → #ob-unlock (Gate-Abschluss,
     verweilender Endscreen, NACH einem ggf. noch offenen rearm/tpl).
     ui-533: #ob-show (Gate-Run Show-Panel, Desktop-exklusiv) ist KEIN Glied
     dieser Kette, sondern eine eigene, NIEDRIGSTE Prioritätsstufe darunter —
     Priorität hook/c1-c3/rearm/tpl/mates/unlock > #ob-bridge > #ob-show
     (D4) — weicht zustandsgetrieben (kein obMarkSeen, D2) jedem der acht
     Fenster/der Brücke, s. dortige Regel unten für Chrome + Position.
     ============================================================ */

  /* .hidden — utility für Onboarding-Karten (classList.add/remove) */
  #ob-layer .hidden { display: none !important; }

  #ob-layer {
    position: absolute;
    inset: 0;
    z-index: 120;
    pointer-events: none;
  }

  /* ---- Schritt hook: zentrierte Willkommens-Karte (top-center, unter BETA-Banner,
     ui-482 DL-2 — Inhalt/Chrome jetzt in viewer-base.css, hier NUR Position). ---- */
  #ob-hook {
    position: absolute;
    top: 84px;
    left: 38%;
    transform: translateX(-50%);
    width: min(340px, 80vw);
    pointer-events: auto;
  }

  /* ---- Schritte c1/c2/c3: "So steuerst du" — 3 Karten (ui-482 DL-3, ersetzt das
     vormalige EINE #ob-coach-Fenster), Bubble links neben Panel, Pfeil nach rechts.
     Position wie zuvor #ob-coach: JS berechnet left/top (ui-496:
     computeOnboardingCardPosition() in core/onboarding-position.js, angewendet
     via _obApplyActivePositioning() in desktop/main.js — Muster der vormaligen
     _obPositionCoach()/_obPositionAt()). Chrome (Hintergrund/Rand/Radius/Schatten/
     Kicker/CTA) kommt aus viewer-base.css (DL-8 "für alle Fenster"). ---- */
  #ob-c1, #ob-c2, #ob-c3 {
    position: absolute;
    width: 260px;
    pointer-events: auto;
  }
  /* Pfeil nach rechts (zeigt aufs Panel) — 1:1 aus dem vormaligen #ob-coach::after.
     ui-496 AK1: `.ob-fallback-pos` (vom JS gesetzt, wenn kein Ziel-Element
     gefunden wurde, computeOnboardingCardPosition() mode:'fallback') blendet den
     Pfeil aus — er würde sonst weiterhin ins Leere zeigen (Ticket-Root-Cause). */
  #ob-c1::after, #ob-c2::after, #ob-c3::after {
    content: "";
    position: absolute;
    right: -9px;
    top: var(--arrow-top, 28px);
    width: 16px;
    height: 16px;
    background: rgba(13,19,34,0.97);
    border-right: 1px solid var(--cyan);
    border-bottom: 1px solid var(--cyan);
    transform: rotate(-45deg);
  }
  #ob-c1.ob-fallback-pos::after, #ob-c2.ob-fallback-pos::after, #ob-c3.ob-fallback-pos::after {
    display: none;
  }

  /* ---- Automatisierungs-Paar rearm/tpl (ui-482 DL-4/DL-6) — dieselbe Bubble-
     Sprache wie c1/c2/c3, JS ankert an #ip-rearm bzw. #ip-tplbtn-mount
     (ui-496: computeOnboardingCardPosition(), align:'bottom'/'top'). Kein Pfeil
     (anders als c1-c3 — die Zielelemente liegen näher am Panel-Rand, ein Pfeil
     wäre hier oft außerhalb der Stage, s. Ticket ## Notes) — `.ob-fallback-pos`
     kann hier zwar ebenfalls gesetzt werden (derselbe JS-Pfad), hat aber ohne
     eine ::after-Regel keine sichtbare Wirkung. ---- */
  #ob-rearm, #ob-tpl {
    position: absolute;
    width: 280px;
    pointer-events: auto;
  }

  /* ---- ui-501 DL-2: mates (Gate-Run-3-Start, "Bots sichtbar") — Platzierung
     WIE hook (zentral über dem Feld, KEIN Element-Anker — die Bots brauchen
     keinen Pfeil, ui-496-Lehre: Anker-IDs sind die Fehlerquelle). ---- */
  #ob-mates {
    position: absolute;
    top: 84px;
    left: 38%;
    transform: translateX(-50%);
    width: min(340px, 80vw);
    pointer-events: auto;
  }

  /* ---- ui-501 DL-3: unlock (Gate-Abschluss) — erscheint auf dem
     verweilenden Endscreen (ui-492 gibt die Zeit). Bottom-center wie
     #ob-bridge (bewährtes "Coach-Fenster über dem Endscreen"-Muster,
     vermeidet eine Überlappung mit `.gameover .es-head`/der Ranking-Tabelle
     oben — anders als hooks top-center-Position, die dort mit dem
     Endscreen-Titel kollidieren würde). Eigene Box-Chrome bleibt trotzdem
     die GETEILTE .ob-*-Familie (viewer-base.css, DL-8/AK1 "ob-Optik-
     Bestand") statt #ob-bridges eigener, unabhängiger Klassen — nur die
     Positionswerte sind an das bewährte Muster angelehnt. ---- */
  #ob-unlock {
    position: absolute;
    left: 50%;
    bottom: 26px;
    transform: translateX(-50%);
    width: min(360px, 90vw);
    pointer-events: auto;
  }

  /* ---- Akt-2-Brücke: Endscreen-Meta-Brücke (bottom-center, ui-147) ---- */
  #ob-bridge {
    position: absolute;
    left: 50%;
    bottom: 26px;
    transform: translateX(-50%);
    width: min(460px, 92vw);
    pointer-events: auto;
    background: var(--bg2);
    border: 1px solid var(--cyan);
    border-radius: 10px; /* token-gap: --radius-container nicht im Viewer definiert (nur in Prototypen) */
    box-shadow: 0 0 0 1px rgba(56,225,255,.30), 0 8px 30px rgba(0,0,0,.5); /* token-gap: --cyan-a30 nicht definiert (viewer kennt --cyan-a42/50/55/12); Literal bewusst belassen — Designer-Entscheid nötig */
    padding: 16px 18px;
    animation: ob-pop-bridge .22s ease-out;
    font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
  }
  @keyframes ob-pop-bridge {
    from { opacity: 0; transform: translateX(-50%) translateY(8px) scale(.98); }
    to   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  }
  #ob-bridge .ob-kick {
    font-size: 11px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--cyan);
    font-weight: 700;
  }
  #ob-bridge .ob-big {
    margin: 6px 0 4px;
    font-size: 18px;
    font-weight: 700;
    color: var(--text);
  }
  #ob-bridge .ob-big .ob-h { color: var(--magenta); }
  #ob-bridge .ob-body {
    margin: 6px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--text);
  }
  #ob-bridge .ob-body .ob-c-hexa { color: var(--magenta); font-weight: 600; }
  #ob-bridge .ob-body .ob-c-credits { color: var(--muted); }
  #ob-bridge .ob-row {
    display: flex;
    gap: 10px;
    align-items: center;
    margin-top: 14px;
  }
  #ob-bridge .ob-cta-primary {
    font: inherit;
    cursor: pointer;
    background: var(--cyan);
    color: #04222b;
    border: none;
    border-radius: 8px;
    padding: 9px 16px;
    font-size: 13px;
    font-weight: 700;
    min-height: 34px;
  }
  #ob-bridge .ob-cta-primary:hover { box-shadow: 0 0 16px rgba(56,225,255,.45); } /* token-gap: --cyan-a45 nicht definiert; Literal bewusst belassen — Designer-Entscheid nötig */
  /* ui-584 AK4: seit diesem Ticket existiert zusätzlich eine gedeckte
     Basisregel `.ob-cta-later` in viewer-base.css (analog .ob-skip/.ob-back)
     — diese hier BLEIBT als spezifischere Override bestehen (ID-Selektor
     schlägt die Klassenregel), unverändert bis auf diesen Kommentar. */
  #ob-bridge .ob-cta-later {
    font: inherit;
    cursor: pointer;
    background: none;
    border: none;
    color: var(--muted);
    font-size: 12px;
    padding: 8px;
  }
  #ob-bridge .ob-cta-later:hover { color: var(--text); }

  /* ---- ui-533 (Fifty-Diktat "Geniess die Show"): Gate-Run Show-Panel —
     reines Status-Panel (D2: KEIN X/CTA/Scrim), deshalb SELBSTSTÄNDIGE
     Box-Chrome hier (identisches Muster zu #ob-bridge oben) statt der
     geteilten hook/c1-c3/rearm/tpl/mates/unlock-Familie in viewer-base.css —
     Desktop-EXKLUSIV (Ticket-AK "Mobile unangetastet"). Position top-center
     wie #ob-hook/#ob-mates (dieselbe Zone — nie gleichzeitig sichtbar, s.
     D1a/D4). pointer-events:none EXPLIZIT (statt nur ererbt von #ob-layer):
     das Panel hat keine Klick-Ziele, D2 "blockiert nichts". @keyframes
     ob-pop kommt unveraendert aus viewer-base.css (geteilter Name, kein
     Redefine noetig). ---- */
  #ob-show {
    position: absolute;
    top: 84px;
    left: 38%;
    transform: translateX(-50%);
    width: min(340px, 80vw);
    pointer-events: none;
    background: rgba(13,19,34,.97);
    border: 1px solid var(--cyan);
    border-radius: 12px;
    padding: 16px 18px 14px;
    box-shadow: 0 12px 40px rgba(0,0,0,.6), 0 0 24px rgba(56,225,255,.12);
    animation: ob-pop .22s ease-out;
    font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
  }
  #ob-show h2 { margin: 0 0 8px; font-size: 16px; color: var(--text); }
  #ob-show p { margin: 0 0 10px; font-size: 12.5px; line-height: 1.5; color: var(--text-dim); }
  #ob-show .ob-show-status { margin: 0; font-size: 11px; color: var(--muted); }

  /* ui-482: .ob-x (Schliessen-Affordance) + @keyframes ob-pop sind nach
     viewer-base.css umgezogen (jetzt für acht Roter-Faden-/Gate-Fenster
     gemeinsam, DL-8/ui-501 AK1) — #ob-bridge nutzt beide weiterhin unverändert
     (eigene @keyframes ob-pop-bridge bleibt oben lokal, #ob-bridge selbst
     ist Out-of-Scope dieses Tickets). */

  /* Spotlight: pulsierender Cyan-Glow auf anvisierten Panel-Elementen */
  .ob-spotlight {
    position: relative;
    z-index: 121;
    border-radius: 9px;
    animation: ob-pulse 1.6s ease-in-out infinite;
  }
  @keyframes ob-pulse {
    0%,100% { box-shadow: 0 0 0 2px rgba(56,225,255,0.55), 0 0 14px rgba(56,225,255,0.30); }
    50%      { box-shadow: 0 0 0 2px rgba(56,225,255,0.95), 0 0 22px rgba(56,225,255,0.55); }
  }

  /* Pop-in Scrim */
  #popin-scrim {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(4,6,12,0.62);
    backdrop-filter: blur(7px) saturate(0.8);
    -webkit-backdrop-filter: blur(7px) saturate(0.8);
    animation: popin-scrim-in 0.18s ease;
  }
  #popin-scrim.open { display: flex; }
  @keyframes popin-scrim-in { from { opacity: 0; } to { opacity: 1; } }

  /* Pop-in Modal.
     ui-525: Popin-Font-Kontext auf 15px normalisiert (Container-Fix statt
     40-Stellen-Token-Sweep — Vorschlag aus followups.md "[2026-08-25 von
     ui-519-Review, Struktur-Follow-up]"). Ursache (volle Herleitung am
     #popin-mount-Kommentar unten + build.css .build-invest-cell-lvl):
     diese Datei haelt body{font:13px/1.4} bewusst als LETZTE Kaskaden-
     Regel (s. Datei-Kopf) — jede Companion-em-Kette ohne eigene font-size
     rechnete im Popin dadurch gegen 13px statt der Companion-Standalone-
     Basis 15px (html,body{font-size:15px}, companion/base.css).
     font-size HIER auf #popin (statt der im Ticket vorgeschlagenen
     Kandidaten #popin-mount/#popin-body — IST-Check vor diesem Fix)
     normalisiert den GESAMTEN Popin-Subtree auf 15px:
       - #popin-head (✕/Zurück/Live-Chip/Titel) bleibt visuell unverändert
         — jede Regel dort ist ein literaler px-Wert, kein em; die Basis-
         Änderung hat dort keinen Effekt.
       - #ph-subnav ist ein GESCHWISTER von #popin-body/#popin-mount, kein
         Nachfahre — wäre #popin-mount/#popin-body der Anker, bliebe die bei
         Setup dorthin verschobene <nav class="dash-tabs"> (.dash-tab,
         .tab-icon, .tab-badge--sp/--draw/--ach, alle weiterhin em-basiert
         — Live-CDP bestätigt .tab-icon dort z.B. 16,5px statt 14,3px;
         .ref-cluster .reflink selbst rendert im Setup-Popin NIE, dash-nav.js
         `setupOnly`-Gate, s. dortiger Kommentar "ui-402: auf der
         Setup-Surface weg") auf der alten 13px-Basis stehen — ein VIERTER
         Font-Kontext mitten im selben Popin. #popin deckt #ph-subnav UND
         #popin-body gemeinsam ab (beide direkte Kinder von #popin) — EIN
         Anker für BEIDE Popin-Pfade (openSetupPopin()/ensure() UND
         openSurfacePopin()/ensureUrl(), s. Kommentar "FLÄCHEN-POPIN" oben
         — beide teilen #popin-mount, das immer unter #popin hängt).
     rem-basierte Tokens (--fs-caption/--fs-micro, tokens.css) zielen auf
     <html> und bleiben von dieser Änderung unberührt (waren nie das
     Problem, s. dortige Herleitung) — betroffen sind ausschließlich
     verbliebene Ad-hoc-em-Werte. Mobile (#mob-setup-mount, eigene
     mobile.css, 16px-Basis) und Companion-Standalone (base.css, 15px)
     bleiben unangetastet — #popin existiert nur im Desktop-Viewer-DOM. */
  #popin {
    width: min(1180px, 94vw);
    height: 95vh; /* ui-259: Puffer-Erhöhung (war 92vh) */
    background: var(--bg-2);
    border: 1px solid var(--line-2);
    border-radius: 14px;
    box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(56,225,255,0.06);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    font-size: 15px;
    animation: popin-modal-in 0.2s cubic-bezier(0.2,0.7,0.3,1);
  }
  @keyframes popin-modal-in {
    from { opacity: 0; transform: translateY(14px) scale(0.985); }
    to   { opacity: 1; transform: none; }
  }

  /* Pop-in Header */
  #popin-head {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 13px 18px;
    border-bottom: 1px solid var(--line);
    background: linear-gradient(180deg, rgba(56,225,255,0.05), transparent);
    flex-shrink: 0;
  }
  /* ui-408 (DL-2): Rückschritt — EIN Slot, kein Verlaufs-Stapel. Nur sichtbar
     (JS: openSurfacePopin()), wenn diese Fläche von einer anderen aus geöffnet
     wurde (Klick auf einen /u/…-Link INNERHALB des Mounts); über die
     Nav/Cluster geöffnet bleibt es hidden. Ein <button>, kein <a> — tauscht
     den Host-Inhalt, verspricht keine Vollseite (Prototyp .mh-back). */
  #ph-back {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 12.5px;
    font-family: inherit;
    color: var(--cyan);
    background: none;
    border: 1px solid var(--line-2);
    border-radius: 6px;
    padding: 5px 10px;
    cursor: pointer;
    flex-shrink: 0;
    transition: border-color 0.15s;
  }
  #ph-back[hidden] { display: none; }
  #ph-back:hover { border-color: var(--cyan); }
  #popin-head .ph-title {
    font-weight: 600;
    font-size: 15px;
    display: flex;
    align-items: center;
    gap: 9px;
    color: var(--text);
    flex-shrink: 0; /* ui-293: Titel behält Layout-Vorrang, schrumpft nie */
  }
  #popin-head .ph-title .ph-glyph { color: var(--cyan); font-size: 17px; }
  #popin-head .ph-spacer { margin-left: auto; flex-shrink: 0; }
  /* ui-293: .ph-live selbst ist display:flex — ohne min-width:0 bleibt die
     Flex-Item-Untergrenze bei "auto" (= intrinsische Content-Breite seiner
     white-space:nowrap-Kinder), egal welchen flex-shrink-Wert man setzt. Bei
     320px Viewport drängte das #ph-close (flex-shrink:0, korrekt) komplett
     aus dem Header/Viewport (gemessen: right=364.40625 bei viewport=320).
     min-width:0 + overflow:hidden erlauben dem Chip jetzt, unter seine
     Content-Breite zu schrumpfen; .ph-live-dot bleibt als Lauf-Indikator fix
     sichtbar (eigener flex-shrink:0), nur .ph-live-text kürzt per Ellipsis. */
  #popin-head .ph-live {
    font-size: 11.5px;
    color: var(--muted);
    display: none; /* nur sichtbar wenn Run läuft */
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border: 1px solid var(--line-2);
    border-radius: 999px;
    white-space: nowrap;
    min-width: 0;
    flex-shrink: 1;
    overflow: hidden;
  }
  #popin-head .ph-live.visible { display: flex; }
  #popin-head .ph-live .ph-live-dot { flex-shrink: 0; }
  /* text-overflow:ellipsis greift nur auf einem Nicht-Flex-Element mit
     overflow:hidden + white-space:nowrap — .ph-live selbst ist display:flex,
     daher der separate Wrapper hier (s. Dev-Prompt-Hinweis im Ticket). */
  #popin-head .ph-live .ph-live-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }
  #popin-head .ph-live .ph-lv { color: var(--cyan); font-variant-numeric: tabular-nums; }
  #popin-head .ph-live .ph-dmg { color: var(--magenta); font-variant-numeric: tabular-nums; }
  #ph-close {
    background: transparent;
    border: 1px solid var(--line-2);
    color: var(--text-dim);
    width: 30px; height: 30px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 16px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: border-color 0.15s, color 0.15s;
  }
  #ph-close:hover { border-color: var(--magenta); color: var(--magenta); }

  /* ============================================================
     POP-IN SUB-NAV — #ph-subnav (ui-413 DL-2)
     Zweite, schlanke Kopf-Zeile — NUR sichtbar während Setup/Hexa gezeigt
     wird (JS-getrieben, s. _spSyncHexaTabsHome()). Trägt die server-
     gerenderte <nav class="dash-tabs"> (Waffe/Sockel/Tech/Erfolge),
     VERSCHOBEN aus #popin-mount per appendChild — KEIN Klon, dieselbe
     Mechanik wie ui-412s _ipSyncHexaTabsHome() auf Mobile, nur
     mit dem Popin-Kopf statt der Bottom-Leiste als Ziel. dash-tabs.js/
     dash-nav.js bleiben dabei unangetastet (dokumentweite Klick-/Aktiv-
     Zustand-Abfragen dort — Position im DOM ist ihnen egal).
     Bei Rang UND Profil (seit ui-408, s. dortiger DL-10-Fix in
     views/profile.js) bleibt die Zeile weg (hidden) — beide Flächen
     unterdrücken ihre eigene <nav class="dash-tabs"> im Embed-Zweig
     komplett, genau wie mobil die Bottom-Leiste dort komplett verschwindet
     (ui-412 DL-6). .dash-tabs/.dash-tab sind UNGESCOPED Basisregeln
     (pages/dashboard.css, s. dortige "Scope-Grenze"-Notiz) — sie gelten hier unveraendert, nur
     Rand/Zeilenhöhe sind an die schlanke Kopf-Zeile angepasst (kein
     Nachbau, keine Farb-/Struktur-Aenderung). ui-525: genau WEIL diese Nav
     als Geschwister von #popin-body/#popin-mount lebt (nicht als deren
     Nachfahre), war sie der Grund, die Font-Kontext-Normalisierung auf
     #popin statt #popin-mount zu setzen (s. dortiger Kommentar) — sonst
     bliebe .dash-tab, .tab-icon, .tab-badge--sp/--draw/--ach hier auf der
     alten 13px-Basis stehen, während der Rest des Popins bereits 15px
     zeigt (.ref-cluster .reflink selbst rendert hier NIE, s. Kommentar
     an #popin oben). */
  #ph-subnav {
    border-bottom: 1px solid var(--line);
    flex-shrink: 0;
    padding: 0 18px;
  }
  #ph-subnav[hidden] { display: none; }
  #ph-subnav .dash-tabs {
    margin: 0;
    border-bottom: none; /* #ph-subnav traegt die Trennlinie bereits selbst */
  }

  /* ui-402: Fragment-Host (löst den iframe ab — kein zweiter Renderpfad,
     s. #popin-mount unten). #popin-body bleibt der äußere Flex-/Clip-
     Rahmen unverändert. */
  #popin-body {
    flex: 1;
    overflow: hidden;
    position: relative;
  }
  /* web/client/surfaces/surface-host.js hängt hier denselben
     `/dashboard?embed=1&tab=X`-Fragment-Inhalt ein, den auch
     Mobiles #mob-setup-mount
     konsumiert (Architektur-Lock DL-2). width/height:100% statt
     .ct-embed-mount's flex:1 1 auto (pages/dashboard.css) — #popin-body ist selbst
     kein Flex-Container, das direkte Sizing bleibt hier nötig (ID-Selektor
     gewinnt die Spezifitäts-Wette gegen die Klassen-Regel unabhängig von
     der Lade-Reihenfolge). Die restliche Innen-Struktur (Padding/Overflow/
     Tech-Tree-Fill-Kette) kommt aus den lazy nachgeladenen Companion-CSS-Dateien. */
  #popin-mount {
    width: 100%;
    height: 100%;
  }

  /* ============================================================
     ENEMY-INTRO-KARTE (srv-018, game-029, ui-169)
     ui-169: Karte sitzt jetzt im #enemy-log-Body (4. #left-info-Body, Panel-Breite 264px).
     Kein #enemy-intro-stage mehr (entfernt). Volle Karte = neuester Gegner;
     Verlaufs-Zeilen darunter (kompakt). Einblend-Transition bleibt identisch.
     ui-456: .enemy-intro/.ei-head/.ei-glyph/.ei-newtag/.ei-name/.ei-wave-note/
     .ei-stats/.ei-label/.ei-bar/.ei-seg/.ei-val (+ .el-empty) sind byte-
     identisch zu Mobile und leben seither EINMAL in
     web/client/css/viewer-base.css. Nur die hier folgenden .ei-trait/
     .ei-pips/.ei-special bleiben lokal (nutzen --border statt Mobiles
     --line — Bestand gewinnt, P5-Lock).
     ============================================================ */
  /* game-120: .ei-trait — wave-abhaengige Sonder-Attribut-Zeile (Label + Wert).
     Sitzt zwischen .ei-stats und .ei-special. Kein eigener border-top (Design-Lock:
     border-top bleibt exklusiv bei .ei-special). Tokens --muted / --ei vorhanden. */
  .enemy-intro .ei-trait { display:flex; justify-content:space-between; align-items:center; margin-top:8px; font-size:11.5px; }
  .enemy-intro .ei-trait .ei-tlabel { color:var(--muted); }
  .enemy-intro .ei-trait .ei-tval { font-weight:700; color:var(--ei,var(--red)); font-variant-numeric:tabular-nums; }
  /* game-121: Ausweich-Ladungs-Pips — 5-Slot-Zeile im .ei-trait-Stil. */
  .enemy-intro .ei-pips { display:flex; gap:3px; }
  .enemy-intro .ei-pip { width:10px; height:10px; border-radius:2px; background:#2a3550; }
  .enemy-intro .ei-pip.on { background:var(--ei,var(--red)); box-shadow:0 0 5px -1px var(--ei,var(--red)); }
  .enemy-intro .ei-special { font-size: 11.5px; line-height: 1.4; border-top: 1px solid var(--border); padding-top: 8px; margin-top: 8px; }

  /* ---- GEGNER-BODY (ui-169) — 4. .li-panel-Body ---- */
  /* Panel-Chrome: Cyan-Akzent (Gegner-Farbe aus --ei pro Karte via JS) */
  #enemy-log {
    border-color: var(--cyan-a42);
    box-shadow: 0 6px 28px rgba(0,0,0,.6), 0 0 22px -7px var(--cyan-a50);
    pointer-events: none;
  }
  .el-head {
    font-size: 11px; letter-spacing: 1.5px; font-weight: 700;
    color: var(--cyan); text-transform: uppercase; margin-bottom: 9px;
  }

  /* ============================================================
     ui-227: GEGNER-LEISTE (#ei-strip) — Chip-Reihe unter der Detail-Karte.
     ui-235: Strip NACH #el-latest (unterste Kante des Panels).
     Chips: 32×32 px, 6 px border-radius (kanonische Klein-Stufe), 4 px Padding.
     flex-wrap bei vielen Typen (Panel-Breite 264 px ≈ 6 Chips/Zeile).
     pointer-events: #enemy-log ist none; Chips holen sich auto zurück NUR unter
     body.viewer-logged-in (exaktes #hud-queue-list-Muster, DL-6/DL-7).
     ============================================================ */
  .ei-strip {
    display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
    padding-top: 9px; margin-top: 10px;
    border-top: 1px solid var(--border);
  }
  .ei-chip {
    --c: var(--red);                        /* per-Chip = Gegnerfarbe */
    position: relative; width: 32px; height: 32px; flex: 0 0 32px;
    display: flex; align-items: center; justify-content: center;
    background: transparent; border: 1px solid var(--border); border-radius: 6px;
    padding: 4px; cursor: pointer;
    transition: border-color .15s, background .15s, box-shadow .15s, transform .12s;
  }
  .ei-chip svg { width: 100%; height: 100%; display: block; }
  /* aktiv = gerade vom Karussell gezeigt (transient) */
  .ei-chip.is-active {
    border-color: var(--c);
    background: color-mix(in srgb, var(--c) 12%, transparent);
  }
  /* angeheftet = gelockt bis nächste Wave oder 2. Klick (stärker, Pin-Indikator) */
  .ei-chip.is-pinned {
    border-color: var(--c);
    background: color-mix(in srgb, var(--c) 18%, transparent);
    box-shadow: 0 0 10px -2px var(--c);
  }
  .ei-chip.is-pinned::after {
    content: "⟡"; position: absolute; top: -7px; right: -6px;
    font-size: 11px; line-height: 1; color: var(--c);
    text-shadow: 0 0 5px var(--c);
  }
  .ei-chip:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
  /* Ausgeloggt: Chips sind rein dekorativ (pointer-events:none von #enemy-log
     geerbt — kein Hover, keine Interaktion, s. dortige Regel). Eingeloggt:
     Chips bekommen pointer-events:auto zurueck (wie #hud-queue-list, DL-6). */
  body.viewer-logged-in .ei-chip { pointer-events: auto; }

  /* Pin-Hinweiszeile (nur eingeloggt + wenn gepinnt, DL-6) */
  .ei-pinnote {
    display: none; font-size: 11px;
    color: var(--c, var(--cyan));
    margin: -3px 0 9px; letter-spacing: .3px;
  }
  .ei-pinnote.show { display: block; }
  .ei-pinnote b { font-weight: 700; }

  /* Kartenwechsel: reiner Opacity-Fade via .show-Klasse (DL-9, renderCard setzt sie).
     Animation spielt nur wenn die Karte neu gebaut wird (_lastShownKey-Guard in JS).
     Kein zusätzlicher @keyframes — .enemy-intro transition: opacity oben reicht. */

  /* Badge am Gegner-Tab (analog seg-count) — roter Dot */
  .seg-enemy-badge {
    display: none; width: 8px; height: 8px; border-radius: 999px;
    background: var(--red); box-shadow: 0 0 6px -1px var(--red);
    flex-shrink: 0;
  }
  .has-enemy-badge .seg-enemy-badge { display: inline-block; }

  /* ============================================================
     BOSS-FIGHT-KARTE (ui-022 + ui-029, Position seit ui-632)
     Geschwister von #hud/#stage (viewer-shell.js) — lebt NICHT mehr in
     #stage (ui-632, Fifty 13.09. "sie sollen gar nicht ueberlappen",
     Variante E4). Eigene Zone in der linken Spalte: left:12px, Breite
     264px (= #left-info), Oberkante = Oberkante des "Zeit"-Blocks
     (#hud-time inkl. Label) minus 8px. #hud ist eine STATISCHE Saeule
     (fixe Inhalte, zoom:1.3) — die Bildschirm-Position von #hud-time ist
     deshalb bei jeder Fensterhoehe/-breite gleich (empirisch verifiziert
     @1920x1080/1920x950/2560x1440, s. Ticket-Notes) — kein Media-Query,
     kein JS-Anker noetig. --boss-card-top haelt den gemessenen Wert als
     EINZIGE Stelle zum Nachjustieren, falls sich der #hud-Inhalt je
     aendert. Ueberdeckt waehrend der Boss-Welle die HUD-Werte "Zeit"/
     "Gegner" — ausdruecklich erlaubt (Fifty 13.09., Lock 4), "Wave"/
     "DMG done" bleiben frei. z-index:97 liegt ueber #hud (2) und
     #left-info (96), unter Popin/Modals (200). Keine Kollision mit
     Rim-Cards moeglich in DIESER (breiten) Desktop-Komposition (by
     construction, ui-632 Lock 3): rechte Kante bei 12+264=276px liegt
     links von #stages linkem Rand (288px) — gilt NUR fuer die breite
     #hud-Spalten-Variante (zoom:1.3). In der Tablet-Touch-Komposition
     weiter unten (@media max-width:950px and pointer:coarse) schrumpft
     #hud zum Pill-Strip und #stage nimmt die volle Breite (left ~0) —
     die 276<288-Rechnung greift dort NICHT mehr. #boss-card wird dort
     deshalb komplett ausgeblendet statt an --boss-card-top haengen zu
     bleiben (s. dortiger Block "Ausgeblendete Elemente am Handy",
     Review-Fund ui-632 2026-09-13).
     ui-169 ("Boss-Karte ist das einzige In-Stage-Overlay") ist damit
     aufgehoben — die Stage gehoert seither den Rim-Cards.
     Festes DOM-Element — persistent bis 9s nach Boss-Tod
     (DEFEAT_HOLD_MS, /client/elements/boss.js, ui-029/ui-438). Breite
     264px (vormals 322px, s.o.) wegen HP-Leiste + 5-Zeilen-Ranking.
     Boss-Farbe = --magenta.
     ============================================================ */
  :root { --boss-card-top: 226px; }
  #boss-card {
    /* ui-202: --bc-acc = Boss-Art-Akzentfarbe (per JS gesetzt via bossSnap.bossType).
       Fallback auf --magenta = Normal-Boss-Farbe (Design-Lock, auch wenn kein bossType bekannt). */
    --bc-acc: var(--magenta);
    position: absolute; top: var(--boss-card-top); left: 12px;
    width: 264px;
    background: rgba(13, 19, 34, 0.95);
    border: 1.5px solid var(--bc-acc);
    border-radius: 10px; padding: 13px 15px 14px;
    box-shadow: 0 6px 28px rgba(0,0,0,.6), 0 0 22px -5px var(--bc-acc);
    z-index: 97;
    pointer-events: none;
    opacity: 0; transform: translateY(-14px);
    transition: opacity .32s ease, transform .32s cubic-bezier(.2,.9,.3,1.4),
                border-color .4s, box-shadow .4s;
  }
  #boss-card.show { opacity: 1; transform: translateY(0); }

  /* ui-514: Erstkontakt-Erklaerzeile — sitzt zwischen .bc-head und
     .bc-defeated-banner/.bc-hp (Ticket-AK "Boss-Fall an der Boss-Karte").
     Natives hidden-Attribut (mounts.explain.hidden, boss.js) statt einer
     .show/.hidden-Klasse — kein display hier deklariert, das UA-[hidden]
     bleibt daher unschlagbar (s. Kommentar bei der [hidden]-Regel). */
  #boss-card .bc-explain {
    margin-bottom: 11px; padding-bottom: 10px; border-bottom: 1px solid var(--border);
    font-size: 11.5px; line-height: 1.45; color: var(--muted);
  }
  #boss-card .bc-explain b { color: var(--cyan); }
  #boss-card .bc-explain[hidden] { display: none; }

  /* Kopf */
  #boss-card .bc-head { display: flex; align-items: center; gap: 11px; margin-bottom: 9px; }
  #boss-card .bc-glyph {
    /* ui-202: SVG-Icon statt Unicode-Stern — Hintergrund/Rahmen folgen --bc-acc */
    width: 42px; height: 42px; flex: 0 0 42px; border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.18);
    overflow: hidden;
  }
  #boss-card .bc-glyph svg { width: 100%; height: 100%; display: block; }
  #boss-card .bc-titles { min-width: 0; flex: 1; }
  #boss-card .bc-tag {
    font-size: 11px; letter-spacing: 1.6px; font-weight: 700;
    color: var(--bc-acc); text-transform: uppercase; opacity: .92;
  }
  /* ui-632: 264px (vormals 322px) laesst dem Namen weniger Platz neben
     Glyph+Wave — Ellipsis statt Umbruch (.bc-titles hat bereits min-width:0,
     s.o.), Boss-Namen bleiben so IMMER einzeilig (laengster Kanon-Name
     "Schützen-Boss", s. src/boss-arts.js). */
  #boss-card .bc-name { font-size: 18px; font-weight: 800; line-height: 1.1; letter-spacing: .3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #boss-card .bc-wave { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }

  /* Boss-HP-Leiste */
  #boss-card .bc-hp { margin-bottom: 11px; }
  #boss-card .bc-hp-track {
    /* ui-202: HP-Track-Farbe via --bc-acc (Boss-Art-Akzent) */
    height: 11px; border-radius: 6px; overflow: hidden;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid rgba(255, 255, 255, 0.15);
  }
  #boss-card .bc-hp-fill {
    height: 100%; width: 100%;
    background: var(--bc-acc);
    box-shadow: 0 0 10px -1px var(--bc-acc);
    transition: width .25s linear;
  }
  #boss-card .bc-hp-row {
    display: flex; justify-content: space-between; margin-top: 4px;
    font-size: 11px; font-variant-numeric: tabular-nums;
  }
  #boss-card .bc-hp-label { color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
  #boss-card .bc-hp-val { color: var(--bc-acc); font-weight: 700; }

  /* ui-588 (DL-1/DL-7): Kaskaden-Segment-Leiste — vier gleich breite Segmente
     INNERHALB der bestehenden .bc-hp-track-Box, injiziert von boss.js
     createBossElement() (_ensureSegDom(), mounts.hpTrack) NUR wenn
     snap.boss.cascade vorhanden ist (Split-Bosse, aktuell nur Blob-Boss —
     DL-6: die anderen 5 Varianten bleiben bei der einfachen .bc-hp-fill-
     Leiste, position:relative hier ist wirkungslos ohne injizierte Kind-
     Elemente). Reihenfolge rechts->links (row-reverse): Kind 0 (Körper)
     sitzt RECHTS, BBB/BB/B folgen nach links — Fifty-Entscheid Runde 2
     (gespiegelt): folgt derselben Richtung wie .bc-hp-fill selbst (links
     verankert, leert sich von der rechten Kante her). CSS-Vorlage: PROTO-
     Block .pg-seg/.pg-segs im ui-588-Prototyp. */
  #boss-card .bc-hp-track { position: relative; }
  #boss-card .bc-segs {
    position: absolute; inset: 0; display: flex; flex-direction: row-reverse; gap: 2px;
  }
  #boss-card .bc-seg {
    flex: 1 1 0; display: block; height: 100%; overflow: hidden; position: relative;
    background: rgba(255, 255, 255, 0.06);
  }
  #boss-card .bc-seg b {
    display: block; height: 100%; background: var(--bc-acc);
    box-shadow: 0 0 8px -1px var(--bc-acc); transition: width .25s linear;
  }
  #boss-card .bc-seg.is-active { outline: 1px solid rgba(255, 255, 255, .35); }

  /* ui-588 (DL-2): Generations-Zeile — segment-bündig (vier gleiche Spalten,
     dieselbe row-reverse-Spiegelung wie .bc-segs, damit Zähler und Segment
     übereinander liegen). Karte wächst dadurch um GENAU eine Zeile
     (304 -> 328px), NUR während einer Blob-Boss-Kaskade (leer + [hidden]
     sonst — DL-6 byte-gleiche Höhe für die anderen 5 Boss-Varianten). */
  #boss-card .bc-gen-row {
    display: flex; flex-direction: row-reverse; gap: 2px; margin-top: 5px;
    font-size: 11px; font-variant-numeric: tabular-nums; color: var(--muted);
  }
  #boss-card .bc-gen-row[hidden] { display: none; }
  #boss-card .bc-gen {
    flex: 1 1 0; min-width: 0; text-align: center; padding: 1px 2px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    border-radius: 4px; border: 1px solid transparent;
  }
  #boss-card .bc-gen.is-done { color: var(--green); }
  #boss-card .bc-gen.is-active { color: var(--text); border-color: var(--bc-acc); background: var(--mag-a14); }
  #boss-card .bc-gen.is-wait { opacity: .7; }

  /* Ranking-Titel + Legende */
  #boss-card .bc-rank-head {
    display: flex; align-items: baseline; justify-content: space-between;
    margin-bottom: 5px;
  }
  #boss-card .bc-rank-title {
    font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .1em;
    flex-shrink: 0;
  }
  /* ui-632: bei 264px (vormals 322px) reicht der Platz für Titel + Award-
     Projektion nebeneinander nicht mehr immer — min-width:0 erlaubt der
     Legende, unter ihre Content-Breite zu schrumpfen, Ellipsis statt
     Umbruch auf eine zweite Zeile (der Titel links bleibt per flex-shrink:0
     immer vollstaendig sichtbar). */
  #boss-card .bc-legend { font-size: 11px; color: var(--muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #boss-card .bc-legend b { color: var(--yellow); }

  /* Ranking-Tabelle (persistente DOM-Nodes — kein innerHTML per Frame) */
  /* ui-058: .bc-rank Font/tabular-nums kommen aus .ct-rank (additiv vergeben im JS) */
  #boss-card .bc-rank {
    display: table; width: 100%; border-collapse: collapse;
  }
  #boss-card .bc-row { display: table-row; }
  #boss-card .bc-row .c { display: table-cell; padding: 5px 0; vertical-align: middle; }
  /* ui-058: color+weight kommen aus .ct-rank-pos/.ct-rank-name (additiv vergeben im JS) */
  #boss-card .bc-c-rank {
    width: 26px; font-size: 13px; text-align: center;
  }
  #boss-card .bc-c-name {
    font-size: 12.5px; padding-left: 6px !important;
    max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  #boss-card .bc-c-dmg { text-align: right; padding-left: 8px !important; min-width: 64px; }
  #boss-card .bc-c-bonus { text-align: right; padding-left: 8px !important; width: 56px; }

  /* DMG-Zelle: Zahl + dünner Share-Balken darunter */
  #boss-card .bc-dmg-val { font-size: 12px; font-weight: 700; color: var(--text); }
  #boss-card .bc-dmg-bar {
    height: 3px; margin-top: 3px; border-radius: 2px; background: #222b44; overflow: hidden;
  }
  #boss-card .bc-dmg-bar > i {
    display: block; height: 100%; background: var(--magenta);
    box-shadow: 0 0 5px -1px var(--magenta); transition: width .25s linear;
  }

  /* ui-058: Top-3-Highlight kommt aus .ct-rank-1/2/3 (additiv auf cRank im JS vergeben) */

  /* Bonus-Chip */
  #boss-card .bc-chip {
    display: inline-block; padding: 1px 6px; border-radius: 3px;
    font-size: 11px; font-weight: 700; font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-variant-numeric: tabular-nums;
    background: rgba(255, 216, 74, 0.12); color: var(--yellow);
    border: 1px solid rgba(255, 216, 74, 0.4);
  }
  #boss-card .bc-chip.top {
    background: rgba(255, 69, 181, 0.14); color: var(--magenta);
    border-color: rgba(255, 69, 181, 0.5); box-shadow: 0 0 8px -2px var(--magenta);
  }
  #boss-card .bc-chip.none {
    background: var(--muted-a10); color: var(--muted);
    border-color: var(--muted-a30);
  }

  /* Tote Tower (Tower zerstört vor Boss-Tod) */
  #boss-card .bc-row.dead .bc-c-name { color: var(--muted); text-decoration: line-through; }
  #boss-card .bc-row.dead .bc-dmg-val { color: var(--muted); }

  /* Self-Row (eingeloggt, außerhalb Top 5) */
  #boss-card .bc-self-sep .c { padding: 3px 0 1px; display: table-cell; vertical-align: middle; }
  #boss-card .bc-self-sep .dots { color: var(--line-2); font-size: 11px; text-align: center; letter-spacing: 2px; }
  #boss-card .bc-row.self { background: rgba(56, 225, 255, 0.06); }
  #boss-card .bc-row.self .bc-c-name::after {
    content: " · du"; color: var(--muted); font-size: 11px; font-weight: 400;
  }

  /* Footer */
  #boss-card .bc-foot {
    margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border);
    font-size: 11px; line-height: 1.45; color: var(--muted);
  }
  #boss-card .bc-foot b { color: var(--text); }
  #boss-card .bc-foot .k { color: var(--yellow); font-weight: 700; }

  /* „Boss besiegt"-Banner (versteckt bis .defeated aktiv) */
  #boss-card .bc-defeated-banner {
    display: none; margin-bottom: 11px; padding: 7px 0; text-align: center;
    font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
    color: var(--green); border-radius: 6px;
    border: 1px solid rgba(94, 255, 138, 0.4);
    background: rgba(94, 255, 138, 0.08);
    text-shadow: 0 0 10px rgba(94, 255, 138, .5);
  }

  /* „Boss besiegt"-Endzustand */
  #boss-card.defeated {
    border-color: var(--green);
    box-shadow: 0 6px 30px rgba(0,0,0,.6), 0 0 26px -4px var(--green);
    animation: bc-victory .6s ease;
  }
  @keyframes bc-victory {
    0% { transform: scale(1); }
    30% { transform: scale(1.035); }
    100% { transform: scale(1); }
  }
  #boss-card.defeated .bc-glyph {
    /* ui-202: color: var(--green) entfernt — .bc-glyph ist jetzt ein SVG-Container;
       CSS color greift nicht auf SVG-Strokes/-Fills. Das SVG behaelt im defeated-Zustand
       bewusst die Boss-Art-Farbe (via --bc-acc). Nur background/border-color wechseln auf gruen. */
    background: rgba(94, 255, 138, 0.16);
    border-color: rgba(94, 255, 138, 0.5);
  }
  #boss-card.defeated .bc-tag { color: var(--green); }
  #boss-card.defeated .bc-hp { display: none; }
  #boss-card.defeated .bc-defeated-banner { display: block; }
  #boss-card.defeated .bc-rank-title::after { content: " · final"; color: var(--green); }
  #boss-card.defeated .bc-chip { background: rgba(255, 216, 74, 0.2); }
  #boss-card.defeated .bc-chip.top { background: rgba(255, 69, 181, 0.22); }

  /* Awarded-Total (nur Endzustand) */
  #boss-card .bc-awarded-total { display: none; }
  #boss-card.defeated .bc-awarded-total {
    display: block; margin-top: 8px; font-size: 11px;
    color: var(--text); text-align: center;
  }
  #boss-card.defeated .bc-awarded-total b { color: var(--magenta); }

  /* ============================================================
     ui-122: GETEILTES LINKES INFO-PANEL (#left-info)
     Wrapper unten links — Gruppen-Bonus (ui-034) und Warteschlange (ui-037)
     teilen diesen Slot. Immer nur ein Body sichtbar; Toggle-Pille (nur eingeloggt)
     als stabiler Anker unten (ui-189: 2-zeilig, 2×2-Raster). Body wächst nach oben (DL-1/DL-2).
     pointer-events:none — Spectator-Default; nur die Pille (logged-in) + Queue-Scroll
     (logged-in) holen sich pointer-events:auto zurück (DL-6).
     ============================================================ */
  #left-info {
    position: fixed; left: 12px; bottom: 40px; width: 264px; z-index: 96;
    display: flex; flex-direction: column; gap: 8px;
    pointer-events: none;
  }
  #left-info.li-empty { display: none; }        /* Null-Fall: alle Bodies leer */

  /* Gemeinsames Panel-Chrome für beide Bodies (aus ui-034 #group-bonus extrahiert, DL-3) */
  .li-panel {
    background: rgba(13,19,34,.95);
    border: 1.5px solid var(--group-a55);
    border-radius: 11px; padding: 12px 14px;
    box-shadow: 0 6px 28px rgba(0,0,0,.6), 0 0 22px -7px var(--group-a50);
  }
  .li-panel[hidden] { display: none; }

  /* ---- GRUPPEN-BONUS-BODY (ui-034, 1:1 aus altem #group-bonus extrahiert) ---- */
  /* pointer-events:none bleibt (read-only Spectator, ui-034 DL-8). */
  #group-bonus { pointer-events: none; }
  /* Kopf: Herz-Glyph + Titel + Sub (DL-3) */
  .gb-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
  .gb-glyph {
    width: 36px; height: 36px; flex: 0 0 36px; border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    background: var(--group-a12); border: 1px solid var(--group-a42);
  }
  .gb-glyph svg { width: 21px; height: 21px; fill: var(--group); filter: drop-shadow(0 0 6px var(--group-a50)); }
  .gb-titles { min-width: 0; flex: 1; }
  .gb-tag {
    font-size: 11px; letter-spacing: 1.5px; font-weight: 700;
    color: var(--group); text-transform: uppercase; opacity: .92;
  }
  .gb-sub { font-size: 11px; color: var(--muted); }
  .gb-sub b { color: var(--text); }
  /* Hero-Block: Summen pro Effekt (was alle bekommen, DL-4a) */
  .gb-hero {
    background: var(--group-a06); border: 1px solid var(--group-a14);
    border-radius: 8px; padding: 7px 10px 8px;
  }
  .gb-hero-lbl {
    font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
    color: var(--muted); margin-bottom: 3px;
  }
  .gb-totals { display: flex; flex-wrap: wrap; gap: 4px 12px; }
  .gb-tot { display: inline-flex; align-items: baseline; gap: 4px; font-variant-numeric: tabular-nums; }
  .gb-tot .g { font-size: 15px; color: var(--group); opacity: .9; }
  .gb-tot .v { font-size: 15px; font-weight: 700; color: var(--group); }
  /* Geber-Liste: 1 Zeile pro Tower (DL-4b / DL-5) */
  .gb-list-lbl {
    font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
    color: var(--muted); margin: 10px 0 5px;
  }
  .gb-list { display: flex; flex-direction: column; gap: 5px; }
  /* ui-243: cursor:help + Hover-Highlight als Affordanz (DL-6).
     padding + negative margin damit der Highlight die volle Zeilen-Breite ausfüllt.
     Transition leicht damit kein hartes Blinken beim Hover-Enter. */
  .gb-row {
    display: flex; align-items: center; gap: 9px; font-size: 12.5px;
    padding: 3px 5px; margin: 0 -5px; border-radius: 6px;
    cursor: help; transition: background .12s;
  }
  .gb-row:hover { background: rgba(244,247,255,.07); }
  .gb-row.gb-row-active { background: rgba(244,247,255,.10); }
  .gb-row .gb-num {
    width: 28px; flex: 0 0 28px; text-align: center;
    font-weight: 800; font-size: 11px; border-radius: 5px;
    padding: 2px 0; color: var(--text-on-accent);
  }
  .gb-effs { display: flex; gap: 5px; flex: 1; min-width: 0; color: var(--text); }
  .gb-effs span { font-size: 14px; opacity: .92; line-height: 1; }

  /* ---- ui-243: TILE-POPOVER (#gb-tile-pop) ---- */
  /* position:fixed → kein Layout-Shift in #gb-list (DL-4).
     pointer-events:none → reine Vorschau, kein eigener Hover → kein Flacker-Loop (DL-4).
     Standardmäßig display:none; .show schaltet auf block (JS setzt Position vor .show). */
  #gb-tile-pop {
    position: fixed; z-index: 400; display: none;
    pointer-events: none;
    background: rgba(13,19,34,.97); border: 1px solid var(--border);
    border-radius: 12px; padding: 10px;
    box-shadow: 0 10px 40px rgba(0,0,0,.55);
  }
  #gb-tile-pop.show { display: block; }
  .gb-pop-lbl {
    font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
    color: var(--muted); margin-bottom: 8px;
  }
  .gb-pop-lbl b { color: var(--text); }
  .gb-pop-tiles { display: flex; flex-wrap: wrap; gap: 8px; max-width: 320px; }

  /* ---- ui-243: KARTEN-TILE (.gb-tile) — 1:1 .cs-card.is-group, read-only (DL-1) ---- */
  /* Kein Interaktions-Chrome: kein grab-Cursor, kein cs-dragging, kein cs-can-upgrade,
     kein Count-Badge, kein Remove-Button, kein Click-Hint. */
  .gb-tile {
    position: relative; width: 96px; height: 116px;
    background: linear-gradient(155deg, #131a44 0%, #0c1230 100%);
    border: 1px solid var(--card-color, var(--cyan));
    border-radius: 10px; overflow: hidden;
    box-shadow: 0 0 0 1px var(--card-color, var(--cyan)), 0 0 22px rgba(244,247,255,.18);
    flex: 0 0 auto;
  }
  /* Radial-Glow ::before (identisch .cs-card::before) */
  .gb-tile::before {
    content: ''; position: absolute; inset: 0;
    background: radial-gradient(circle at 50% 0%, var(--card-color, var(--cyan)) 0%, transparent 60%);
    opacity: .08; pointer-events: none; z-index: 0;
  }
  /* Herz-Wasserzeichen ::after — 1:1 aus .cs-card.is-group::after (DL-1, DL-3) */
  .gb-tile::after {
    content: ''; position: absolute; top: -4px; left: -16px;
    width: 52px; height: 52px; opacity: .24; z-index: 0; pointer-events: none;
    background-repeat: no-repeat; background-size: contain;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23f4f7ff' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/%3E%3C/svg%3E");
  }
  /* Tier-Farben via data-tier (DL-2) — identisch .cs-card[data-tier="N"] in companion/components.css.
     ui-611: T5-T9 hatten vormals dieselbe (weiße) Bucket-Farbe, jetzt je eigene
     Stufe (Silber..Kosmos) — keine Folie/Shiny-Layer hier (Kacheln sind klein,
     AK6), nur die fünf Farben.
     ui-612 (AK4, verifiziert): T1-T4 nutzen dieselben Token-Referenzen wie
     .cs-card[data-tier] (--cyan/--magenta/--yellow/--green — components.css
     definiert dafür KEINE --card-color-t1..t4-Variablen, der Kanon selbst
     haelt sich an die Basis-Marken-Tokens), T5-T9 dieselben --card-color-t5..
     t9 wie dort — byte-identischer Token-Satz, "eine Quelle" ist bereits
     erfuellt (ui-611 hat dies fuer T5-T9 als Nebeneffekt seines eigenen
     Shiny-Rollouts hier mitgezogen). Keine neue Tabelle noetig. */
  .gb-tile[data-tier="1"] { --card-color: var(--cyan); }
  .gb-tile[data-tier="2"] { --card-color: var(--magenta); }
  .gb-tile[data-tier="3"] { --card-color: var(--yellow); }
  .gb-tile[data-tier="4"] { --card-color: var(--green); }
  .gb-tile[data-tier="5"] { --card-color: var(--card-color-t5); }
  .gb-tile[data-tier="6"] { --card-color: var(--card-color-t6); }
  .gb-tile[data-tier="7"] { --card-color: var(--card-color-t7); }
  .gb-tile[data-tier="8"] { --card-color: var(--card-color-t8); }
  .gb-tile[data-tier="9"] { --card-color: var(--card-color-t9); }
  /* Tile-Surface: Content-Layer über den Pseudo-Elementen */
  .gb-tile-surface {
    position: relative; z-index: 1; height: 100%;
    padding: 8px 8px 6px; display: flex; flex-direction: column;
  }
  /* Glyph: weiß + Glow (wie .cs-card.is-group .cs-card-icon, DL-1) */
  .gb-tile-icon {
    font-size: 30px; line-height: 1; color: var(--white);
    text-shadow: 0 0 12px rgba(244,247,255,.55);
    margin-bottom: 2px;
  }
  /* ui-382: text-shadow wirkt nicht auf Inline-SVG — Glow als drop-shadow-Pendant
     (gleiche Werte) auf das Stat-SVG gespiegelt. */
  .gb-tile-icon svg.stat-icon {
    filter: drop-shadow(0 0 12px rgba(244,247,255,.55));
  }
  .gb-tile-icon .credit-icon {
    width: 1em; height: 1em; vertical-align: -2px;
  }
  .gb-tile-name { font-size: 11px; font-weight: 600; color: var(--text); line-height: 1.15; }
  /* "+X %" in --card-color (DL-3) */
  .gb-tile-bonus {
    margin-top: auto; font-size: 15px; font-weight: 700;
    color: var(--card-color, var(--cyan)); font-variant-numeric: tabular-nums;
  }
  /* „alle Tower" Sublabel: weiß 72% (wie .cs-card.is-group .cs-card-sub, DL-1) */
  .gb-tile-sub {
    font-size: 11px; font-weight: 600; color: var(--white); opacity: .72;
    letter-spacing: .02em; margin-top: 1px;
  }
  /* T-Badge bottom-right (DL-1) */
  .gb-tile-badge {
    position: absolute; bottom: 6px; right: 6px; z-index: 2;
    font-size: 11px; font-weight: 800; padding: 2px 5px; border-radius: 999px;
    background: var(--card-color, var(--cyan)); color: var(--text-on-accent); letter-spacing: .04em;
  }

  /* ---- ui-243: pointer-events:auto nur unter body.viewer-logged-in (DL-7) ---- */
  /* #group-bonus bleibt pointer-events:none für Spectator / OBS.
     Nur eingeloggt (body.viewer-logged-in) bekommt #gb-list Hover/Click zurück. */
  body.viewer-logged-in #gb-list { pointer-events: auto; }

  /* ---- QUEUE-BODY (ui-037 Cards + ui-122 Kopf/Scroll) ---- */
  /* Kopf analog gb-head: Sanduhr-Glyph in --queued-Orange (DL-9) */
  .q-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
  .q-glyph {
    width: 36px; height: 36px; flex: 0 0 36px; border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    background: var(--queued-a10); border: 1px solid var(--queued-a40);
    color: var(--queued); font-size: 19px;
  }
  .q-tag {
    font-size: 11px; letter-spacing: 1.5px; font-weight: 700;
    color: var(--queued); text-transform: uppercase; opacity: .95;
  }
  .q-sub { font-size: 11px; color: var(--muted); }
  .q-sub b { color: var(--text); }
  /* Big-Run-Cap-Hinweis (bleibt erhalten, ui-037) */
  #hud-queue-cap-hint { color: var(--yellow); font-size: 11px; letter-spacing: .02em; margin-bottom: 6px; opacity: .85; }
  /* ui-122: Scroll-Container — max-height auf 1440p-Produktion getuned (DL-8).
     scrollbar-width:thin + dezente Farbe aus Token (Style-Guide-Proposal ui-122-scrollbar). */
  #hud-queue-list {
    display: flex; flex-direction: column; gap: 3px;
    max-height: min(46vh, 520px);
    overflow-y: auto; overflow-x: hidden;
    scrollbar-width: thin; scrollbar-color: var(--group-a42) transparent;
  }
  #hud-queue-list::-webkit-scrollbar { width: 7px; }
  #hud-queue-list::-webkit-scrollbar-thumb { background: var(--group-a42); border-radius: 99px; }
  #hud-queue-list::-webkit-scrollbar-track { background: transparent; }
  /* ui-122: DL-6 — #left-info erbt pointer-events:none (Spectator).
     Die Scroll-Liste holt sich Interaktion zurück, aber NUR eingeloggt.
     OBS-Quelle bleibt vollständig passiv (kein Maus-Input ohnehin). */
  body.viewer-logged-in #hud-queue-list { pointer-events: auto; }
  /* ui-037: flex-Row [Sprite 22x22] [Name], DL-4 */
  /* ui-122: Queue-Cards leicht an Panel-Breite angepasst (Radius 5, etwas mehr Padding, DL-9) */
  .queue-card {
    display: flex; align-items: center; gap: 8px; padding: 4px 8px;
    border: 1px solid var(--border); border-left: 2px solid var(--queued);
    background: rgba(6,8,15,.65); color: var(--text);
    font-size: 11.5px; font-variant-numeric: tabular-nums;
    border-radius: 5px; max-width: 100%; overflow: hidden;
    /* ui-122: DL-8 PFLICHT — flex:0 0 auto verhindert, dass der Flex-Column-Container
       die Cards auf die max-height staucht statt überlaufen zu lassen → kein Scroll sonst. */
    flex: 0 0 auto;
  }
  .queue-card .qc-sprite { flex: 0 0 auto; width: 22px; height: 22px; display: block; }
  .queue-card .qc-name   { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* ---- TOGGLE-PILLE (ui-122/ui-189/ui-195/ui-201, nur body.viewer-logged-in) ---- */
  /* ui-201: 6 Segmente im 2-Spalten-Raster → 3 volle Zeilen (2+2+2, DL-5).
     align-self: stretch + width:100% → Pille bündig mit Graph-Body (DL-4). */
  #li-toggle {
    display: none;                        /* OBS / ausgeloggt: keine Pille */
    pointer-events: auto;                 /* interaktiv — Bodies bleiben pointer-events:none */
    align-self: stretch;                  /* ui-195: war flex-start → stretch = 100% #left-info-Breite (DL-4) */
    width: 100%;                          /* ui-195: explizite 100% (DL-4) */
    background: rgba(13,19,34,.95); border: 1px solid var(--border);
    border-radius: 14px; padding: 3px; gap: 2px;  /* ui-189: 999px→14px (2-zeilig) */
    box-shadow: 0 4px 18px rgba(0,0,0,.5);
  }
  body.viewer-logged-in #li-toggle {
    display: grid;                        /* ui-189: inline-flex→grid (2-Spalten-Raster) */
    grid-template-columns: 1fr 1fr;       /* ui-201: 6 Items → 3 volle Zeilen: Zeile1 DPS·Income, Zeile2 Balance·Bonus, Zeile3 Queue·Gegner */
  }
  /* ui-201: #seg-enemy grid-column:1/-1 aus ui-195-DL-5 gelöscht — 6 Items füllen 3 volle Zeilen ohne Sonderregel (DL-5) */
  .li-seg {
    display: inline-flex; align-items: center; gap: 6px;
    justify-content: center;              /* ui-189: DL-3 — Label in gleich breiten Grid-Zellen mittig */
    border: 0; background: transparent; cursor: pointer;
    color: var(--muted); font: 600 11px/1 inherit; letter-spacing: .02em;
    padding: 7px 12px; border-radius: 999px;
    transition: background .15s, color .15s;
    font-family: inherit;
  }
  .li-seg .seg-glyph { font-size: 13px; line-height: 1; }
  .li-seg .seg-count { font-variant-numeric: tabular-nums; opacity: .8; font-weight: 700; }
  .li-seg:hover:not(.is-active):not(:disabled) { color: var(--text); background: var(--muted-a10); }
  .li-seg.is-active { background: rgba(56,225,255,.14); color: var(--cyan); }
  .li-seg.is-active .seg-count { opacity: 1; }
  .li-seg:disabled { color: var(--muted); opacity: .32; cursor: default; }
  /* ui-136: seg-dps Active-State = Cyan (überschreibt generisches is-active) */
  .li-seg.seg-dps.is-active { background: var(--cyan-a12); color: var(--cyan); }
  /* ui-195: seg-income Active-State = Yellow (Credits-Rolle, DL-6) */
  .li-seg.seg-income.is-active { background: var(--yellow-a12); color: var(--yellow); }
  /* ui-201: seg-balance Active-State = Mint (Credits-Balance/Wallet-Rolle, DL-2/DL-6) */
  .li-seg.seg-balance.is-active { background: var(--mint-a12); color: var(--mint); }
  /* ui-122 existing: seg-group + seg-queue: off-white / orange (unverändert) */
  /* ui-242: seg-pact Active-State = Rot (Penalty-Rolle, DL-3). Volle Breite als 7. Item (DL-3). */
  .li-seg.seg-pact.is-active { background: var(--red-a12); color: var(--red); }
  #seg-pact { grid-column: 1 / -1; }

  /* ---- ui-640 (DL-1=T2/DL-4): GRIFF (#li-handle) + "Stats einklappen"-
     Zeile (.li-collapse-row in #li-toggle) ---- */
  /* GRIFF: eingeklappt der EINZIGE sichtbare Rest des Stapels (Pillen-Chrome
     wie #li-toggle: Hintergrund/Rand/Schatten, DL-4 — radius bewusst 999px
     wie im Fifty-abgenommenen Prototyp .claude/sprint/prototypes/ui-640.html,
     nicht #li-toggles eigene 14px). Sichtbarkeit/Inhalt komplett in
     desktop/main.js updateLeftInfo() (core/left-info-collapse.js
     deriveHandleContent()), hier nur Chrome. IMMER pointer-events:auto
     (Lock 3: Griff bleibt für Eingeloggte UND Zuschauer bedienbar, anders
     als die read-only .li-panel-Bodies). */
  #li-handle {
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px; width: 100%; height: 30px; padding: 0 12px;
    border: 1px solid var(--border); border-radius: 999px;
    background: rgba(13,19,34,.95); box-shadow: 0 4px 18px rgba(0,0,0,.5);
    color: var(--muted); font: 600 11px/1 inherit; letter-spacing: .04em;
    cursor: pointer; pointer-events: auto;
  }
  #li-handle[hidden] { display: none; }
  #li-handle b { color: var(--cyan); font-weight: 700; }
  .li-handle-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .li-handle-hl { color: var(--text); font-variant-numeric: tabular-nums; }
  .li-handle-chev { flex: 0 0 auto; font-size: 12px; color: var(--text-dim); }

  /* "Stats einklappen" — letzte, volle Zeile der Pille (T2, AK1). Abgesetzt
     durch eine Trennlinie statt Segment-Pillen-Optik: keine Tab-Wahl, eine
     Aktion. #li-toggle[hidden] (unten) blendet sie für Eingeloggte im
     eingeklappten Zustand automatisch mit der ganzen Pille aus. */
  .li-collapse-row {
    grid-column: 1 / -1;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    border: 0; border-top: 1px solid var(--border); border-radius: 0;
    margin-top: 2px; padding: 8px 12px 5px; width: 100%;
    background: transparent; color: var(--text-dim); cursor: pointer;
    font: 600 11px/1 inherit; letter-spacing: .02em; font-family: inherit;
  }
  .li-collapse-row:hover { color: var(--text); }

  /* Eingeklappt setzt desktop/main.js .hidden auf #li-toggle (zusätzlich zu
     den .li-panel-Bodies, die ihr bestehendes [hidden]-Gate von oben
     wiederverwenden) — #li-toggle ist sonst NUR über body.viewer-logged-in
     sichtbar (display:grid, s.o.), !important überschreibt das bewusst
     (identisches Muster wie die Tablet-Kompakt-Ausblendung weiter unten,
     `#left-info { display:none!important }`). */
  #li-toggle[hidden] { display: none !important; }

  /* ---- DPS-GRAPH-BODY (ui-136, DL-1/DL-2/DL-3/DL-4/DL-5) ---- */
  /* Panel-Chrome: Cyan-Akzent statt off-white (DL-1). Überschreibt .li-panel Standard-Border. */
  #dps-graph {
    border-color: var(--cyan-a42);
    box-shadow: 0 6px 28px rgba(0,0,0,.6), 0 0 22px -7px var(--cyan-a50);
    pointer-events: none; /* Spectator-Default — logged-in-Hover-Zoom weiter unten setzt es explizit zurück auf auto */
    /* ui-168: Hover-Zoom-Basis — transform-origin + transition immer gesetzt, Hover-Trigger nur eingeloggt */
    transform-origin: bottom left;    /* wächst nach oben+rechts (einzige on-screen-Richtung bei fixed bottom-left) */
    transition: transform .16s ease-out; /* Style-Guide-Motion, kein Bounce */
  }
  /* ui-168: Hover-Zoom nur im eingeloggten Browser (body.viewer-logged-in).
     pointer-events:none bleibt für ausgeloggt → kein Hover → kein Zoom → kein Layout-Shift. */
  body.viewer-logged-in #dps-graph { pointer-events: auto; }
  body.viewer-logged-in #dps-graph:hover {
    transform: scale(2);
    position: relative;  /* Stacking-Context für z-index im Hover; ändert Ruhelage nicht (war schon block) */
    z-index: 97;         /* über Canvas/Nachbarn (left-info ist z-96), unter Endscreen (z-120) */
  }

  /* ---- INCOME-GRAPH-BODY (ui-195, DL-1/DL-2) — struktureller Klon des DPS-Graph-Stacks, Yellow-Akzent ---- */
  /* Panel-Chrome: Yellow-Akzent (Credits-/Income-Rolle, DL-2). Generische .dg-*-Klassen geteilt, nicht dupliziert. */
  #income-graph {
    border-color: var(--yellow-a42);
    box-shadow: 0 6px 28px rgba(0,0,0,.6), 0 0 22px -7px var(--yellow-a50);
    pointer-events: none;
    transform-origin: bottom left;
    transition: transform .16s ease-out;
  }
  /* ui-195: Glyph-Box Yellow (überschreibt .dg-glyph-Default der Cyan-Sektion) */
  #income-graph .dg-glyph {
    background: var(--yellow-a12);
    border-color: var(--yellow-a42);
  }
  /* ui-195: Income-Glyph nutzt das kanonische SVG-Credit-Icon (.credit-icon) statt textuellem Ⓒ.
     Größe an die DPS-Glyph-SVG (21px) angeglichen; Farben sind im Icon-SVG gebacken (kein Akzent-Override). */
  #income-graph .dg-glyph .credit-icon { width: 22px; height: 22px; vertical-align: 0; }
  #income-graph .dg-tag { color: var(--yellow); }
  /* ui-195: Hover-Zoom analog ui-168 — logged-in-only (DL-7: kein Broadcast-Layout-Shift) */
  body.viewer-logged-in #income-graph { pointer-events: auto; }
  body.viewer-logged-in #income-graph:hover {
    transform: scale(2);
    position: relative;
    z-index: 97;
  }

  /* ---- BALANCE-GRAPH-BODY (ui-201, DL-1/DL-2) — struktureller Klon des Income-Graph-Stacks, Mint-Akzent ---- */
  /* Panel-Chrome: Mint-Akzent (Credits-Balance/Wallet-Rolle, DL-2). Generische .dg-*-Klassen geteilt, nicht dupliziert. */
  #balance-graph {
    border-color: var(--mint-a42);
    box-shadow: 0 6px 28px rgba(0,0,0,.6), 0 0 22px -7px var(--mint-a50);
    pointer-events: none;
    transform-origin: bottom left;
    transition: transform .16s ease-out;
  }
  /* ui-201: Glyph-Box Mint (überschreibt .dg-glyph-Default der Cyan-Sektion) */
  #balance-graph .dg-glyph {
    background: var(--mint-a12);
    border-color: var(--mint-a42);
  }
  /* ui-201: Balance-Glyph: credit-icon analog Income; Farbe via CSS-Override auf Mint */
  #balance-graph .dg-glyph .credit-icon { width: 22px; height: 22px; vertical-align: 0; color: var(--mint); }
  #balance-graph .dg-tag { color: var(--mint); }
  /* ui-201: Hover-Zoom analog ui-168 — logged-in-only (DL-7: kein Broadcast-Layout-Shift) */
  body.viewer-logged-in #balance-graph { pointer-events: auto; }
  body.viewer-logged-in #balance-graph:hover {
    transform: scale(2);
    position: relative;
    z-index: 97;
  }

  /* Kopf: Glyph-Box + Titel (analog gb-head, DL-1) — Komposition/statisches
     Markup, bleibt bei ui-436 unverändert (Metrik-Konfig deckt nur
     Wire-Feld/Akzentfarbe/Label ab, kein Icon/Titel-Feld, s.
     /client/elements/graph.js GraphOptions-Kommentar). */
  .dg-head { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
  .dg-glyph {
    width: 36px; height: 36px; flex: 0 0 36px; border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    /* Akzentfarbe per Graph-Scope gesetzt — DPS via #dps-graph .dg-glyph (Cyan), Income via #income-graph .dg-glyph (Yellow) */
    background: var(--cyan-a12); border: 1px solid var(--cyan-a42);
  }
  /* ui-195: #income-graph .dg-glyph überschreibt Background/Border via höhere Spezifität (bereits oben gesetzt) */
  .dg-glyph svg { width: 21px; height: 21px; stroke: var(--cyan); }
  .dg-titles { min-width: 0; flex: 1; }
  .dg-tag {
    font-size: 11px; letter-spacing: 1.5px; font-weight: 700;
    color: var(--cyan); text-transform: uppercase; opacity: .95;
  }
  /* ui-436: .dg-mount ist der leere Mount-Container, den graph.js
     `createGraphElement()` befüllt (Kopfzeile + Canvas + Legende, eigene
     `.ge-*`-Klassen unten) — kein SVG-Innenleben mehr (ui-136/ui-195/ui-201
     Klon-Markup ist entfallen). */
  .dg-mount { position: relative; }
  /* ---- graph.js-Innenleben (.ge-*, ui-436) — geteilt von allen drei
     Instanzen, KEIN Metrik-Override nötig (Akzentfarbe kommt inline aus
     GRAPH_METRICS[id].color, s. graph.js). Analog Mobiles .in-k/.in-legend*,
     hier unter dem gemeinsamen Desktop-Namensraum. ui-456: die byte-
     identischen unter ihnen (.ge-canvas-wrap/.ge-ymax/.ge-legend:empty/
     .ge-legend-chip.own/.ge-legend-num) leben seither EINMAL in
     web/client/css/viewer-base.css; die hier verbliebenen unterscheiden
     sich bewusst von Mobiles Touch-Maßen (Bestand gewinnt, P5-Lock). ---- */
  .ge-sub { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; }
  .ge-sub b { color: var(--text); }
  .ge-canvas { display: block; width: 100%; height: 120px; }
  /* Legende — Chip-Strip unter dem Canvas (ersetzt die vormaligen #n-Gutter-
     Labels, ui-320-Semantik von Mobile übernommen, s. graph.js Datei-
     Kommentar "Architektur-Entscheid"). overflow-x:auto statt Wrap — bei bis
     zu 13 Towern (RUN_CAP_BIG) bliebe ein Wrap mehrzeilig.
     ui-464 R1 (verworfen — empirisch falsch): versuchte den Balken KOMPLETT
     auszublenden (scrollbar-width:none + -ms-overflow-style:none +
     ::-webkit-scrollbar{display:none}), Begründung "Wheel/Trackpad-Swipe/
     Touch scrollen weiterhin". Review-Gegenbeleg (CDP, Chrome 151,
     5×Wheel-Dispatch auf der geshippten Legende): reines vertikales
     Wheel-Delta (deltaY, kein Shift/deltaX) bewegt scrollLeft NICHT —
     Chrome übersetzt hier NICHTS automatisch, anders als behauptet. Nur
     Shift+Wheel und echtes horizontales Delta (Trackpad-Zweifinger-Swipe/
     Tilt-Wheel) funktionieren — beide für einen reinen Maus-Desktop-
     Besucher unentdeckbar UND ohne jedes sichtbare Signal, dass Inhalt
     überläuft. Bei ≥6 Towern war die Legende für Maus-only-Nutzer (die
     Mehrheit hier — dieser Block gilt für `pointer:fine`, s. Media-Query-
     Kommentar weiter unten) faktisch unerreichbar (ui-464-Review, must-fix).
     ui-464 R2 (aktuell): `scrollbar-width:thin` + dezent eingefärbter Thumb
     statt Komplett-Hide — folgt dem bereits etablierten Projekt-Kanon für
     dezente (sichtbare, unaufdringliche) Scrollbars, s. `#hud-queue-list`
     weiter oben (ui-122, Style-Guide-Proposal ui-122-scrollbar) und
     `.gameover .es-body` (Datei-Anfang) — NICHT das Komplett-Hide-Wisch-
     Strip-Muster (Companion pages/dashboard.css .dash-tabs / pages/highscore.css
     .hs-boardbar sind BEIDE @media max-width:600px, also touch-primär; Viewer
     mobile.css #mob-towerbar/#nav>.dash-tabs ebenso). Ein dünner, aber
     SICHTBARER + per Maus-Drag bedienbarer nativer Scrollbar-Thumb gibt
     Maus-only-Nutzern eine Affordance UND einen tatsächlich funktionierenden
     Scroll-Weg zurück (Klick+Drag auf dem Thumb ist Standard-Browser-
     verhalten, unabhängig von jeder Wheel-Übersetzung) — erfüllt Ticket-AK
     #1 ("horizontales Scrollen bleibt MÖGLICH") auf dem direktesten Weg,
     ohne Wheel-Delta-Interpretation nachzubauen. `thin` ist im Sinne der
     AK-Formulierung "Scrollbar dezent/overlay/hover" die DEZENT-Variante —
     R1s Lesart ("thin erfüllt die AK nicht") war zu eng gefasst, von
     Review + Ticket korrigiert. Erwogene Alternative: ein kleiner
     `wheel`-Listener in graph.js (deltaY→scrollLeft, Carousel-Pattern) —
     verworfen, weil (a) kein bestehender Strip-Wheel-Helfer im Projekt zum
     Anschließen existiert (dash-tabs/towerbar sind reines CSS, kein JS),
     (b) `elements/graph.js` damit für ein bislang ungenutztes Interaktions-
     Pattern angefasst würde (Ticket-Out-of-Scope), und (c) das Affordance-
     Problem allein NICHT löst (der Balken bliebe unsichtbar — kein Hinweis
     auf mehr Inhalt). Hover-Reveal bleibt aus R1-Gründen verworfen (OBS/
     reiner-Zuschauer-Modus hovert nie, Leiste bliebe dort dauerhaft
     unsichtbar+unbedienbar-wirkend). Mobile (mobile.css .ge-legend) bleibt
     bewusst beim R1-Komplett-Hide — andere Begründung, kein Widerspruch:
     Touch-Geräte scrollen per Drag-Geste DIREKT auf dem Content (kein
     Scrollbar-Thumb-Grab nötig), die Sichtbarkeit der nativen Leiste ist
     für Touch-Interaktion irrelevant (dortiger Kommentar für Details). */
  .ge-legend {
    display: flex; gap: 6px; margin-top: 8px; padding: 2px 2px 4px;
    overflow-x: auto; overflow-y: hidden;
    scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; /* Firefox */
  }
  .ge-legend::-webkit-scrollbar { height: 6px; }                        /* Chrome/Safari */
  .ge-legend::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; }
  .ge-legend::-webkit-scrollbar-track { background: transparent; }
  .ge-legend-chip {
    display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
    height: 24px; padding: 0 8px; border-radius: 999px;
    border: 1px solid var(--line); background: var(--bg); font-size: 11px;
    color: var(--text);
  }
  .ge-legend-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; }
  /* ui-316-Lektion (Inline-Span ignoriert max-width/Clamp): Truncation braucht
     min-width:0 + explizite max-width + display:inline-block. */
  .ge-legend-name {
    display: inline-block; min-width: 0; max-width: 72px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* ============================================================
     ui-205: MOBILE IN-RUN-VIEWER — LANDSCAPE-FIRST
     Gate: (max-width:950px) and (pointer:coarse) → trifft Handy + Touch-Tablets.
     OBS (2560×1440, fine pointer) + Desktop (fine pointer) bleiben byte-gleich (DL-2/DL-6).
     Alle Overrides Leben AUSSCHLIESSLICH in diesem Block — kein Eingriff ausserhalb.
     ============================================================ */
  @media (max-width: 950px) and (pointer: coarse) {

    /* --- CSS-Var: Panel-Breite (DL-3) --- */
    :root { --m-panel: min(42vw, 280px); }

    /* --- #stage: Canvas-Anker auf volle Breite minus Panel minus Notch-Insets (DL-1/DL-3) ---
       canvas.js liest #stage.getBoundingClientRect() fuer das Spielzentrum.
       Durch diesen Override recentert sich das Spielfeld automatisch —
       kein Eingriff in src/render/* noetig (DL-1).
       ui-231: left-Inset beruecksichtigen, damit Canvas nicht unter die Notch laeuft.
       width: 100vw − Panel − linker Safe-Area-Inset (rechter Inset = Panel-Raum, kein Extra-Abzug). */
    #stage {
      left: env(safe-area-inset-left, 0px);
      width: calc(100vw - var(--m-panel) - env(safe-area-inset-left, 0px));
    }

    /* --- #hud: schwebendes Kompakt-Overlay oben links auf dem Canvas (DL-3) ---
       Im Desktop: linke Spalte (position:absolute, 222px breit, zoom:1.3, background, vertical).
       Im Mobile: horizontaler Pill-Strip oben links, kein Panel-Background, schwebend ueber Canvas.
       ui-231: padding-left/top fuer Notch-/Status-Bar-Abstand (Insets dort 0 auf Geraeten ohne Notch). */
    #hud {
      zoom: 1;
      width: auto;
      height: auto;
      background: transparent;
      border-right: none;
      padding: max(6px, env(safe-area-inset-top, 0px)) 8px 6px max(8px, env(safe-area-inset-left, 0px));
      overflow-y: visible;
      flex-direction: row;
      flex-wrap: wrap;
      align-items: center;
      gap: 5px;
      z-index: 10;
      pointer-events: none;
    }

    /* Label-Divs: im Desktop Beschriftungszeilen, im Mobile ausgeblendet. */
    #hud .label {
      display: none;
    }
    #hud .wave-phase { display: none; }

    /* Value-Elemente werden zu kompakten Stat-Pillen (analog Prototyp .stat). */
    #hud .big,
    #hud .dmg,
    #hud .wave {
      background: rgba(6, 8, 15, 0.74);
      border: 1px solid var(--border);
      border-radius: 7px;
      padding: 4px 8px;
      font-size: 14px;
      font-weight: 700;
      line-height: 1;
      backdrop-filter: blur(3px);
      -webkit-backdrop-filter: blur(3px);
      /* Tabular-Nums fuer stabiles Layout beim Hochzaehlen */
      font-variant-numeric: tabular-nums;
    }

    /* --- #panel: schlanker rechter Streifen, touch-tauglich, zoom:1 (DL-3) ---
       ui-231: right + padding-right auf Safe-Area-Inset, damit Panel-Inhalt nicht
       unter die Notch geschoben wird (Notch rechts in Landscape-Modus = haeufigstes Szenario).
       padding-top fuer iOS-Status-Bar-Inset (Dynamic Island oben). */
    #panel {
      zoom: 1;
      width: var(--m-panel);
      height: 100vh; height: 100svh; /* ui-265 DL-4: svh-Fallback, OBS-CEF bleibt auf vh */
      /* height-override: calc entfaellt, war an zoom:1.3 gebunden */
      right: env(safe-area-inset-right, 0px);
      padding-right: env(safe-area-inset-right, 0px);
      padding-top: env(safe-area-inset-top, 0px);
      padding-bottom: env(safe-area-inset-bottom, 0px);
    }

    /* --- #room-switcher: oben rechts — ui-231: rechter + oberer Inset addiert ---
       top/right bleiben relativ zum Viewport; auf Notch-Geraeten muss der Switcher
       ausserhalb der Kamera-Freizone platziert werden. */
    #room-switcher {
      top: calc(12px + env(safe-area-inset-top, 0px));
      right: calc(12px + env(safe-area-inset-right, 0px));
    }

    /* --- #nav-cluster + #login-indicator: unten links — ui-231: linker + unterer Inset ---
       Home-Indicator-Bereich (unten) und Notch (links in Landscape) nicht ueberlagern.
       ui-413: vormals #setup-btn — der Bereichs-Cluster erbt dieselbe Ecke/denselben Inset. */
    #nav-cluster {
      left: calc(12px + env(safe-area-inset-left, 0px));
      bottom: calc(34px + env(safe-area-inset-bottom, 0px));
    }
    #login-indicator {
      left: calc(14px + env(safe-area-inset-left, 0px));
      bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    }

    /* Touch-Targets: Upgrade-Zellen + Focus-Buttons mindestens 44x44px (DL-3) */
    .ip-strow {
      min-height: 44px;
    }
    .ip-cbz {
      min-width: 44px;
    }
    .ip-cta,
    .vx-cta,
    .ip-tpl-btn,
    .ip-allmode-toggle,
    .ipq-leave {
      min-height: 44px;
    }

    /* web-202: #setup-btn (33px hoch) + #ph-close (30x30px, ui-231-Close-„X")
       waren unter 44px Touch-Target — Geraete-Smoke-Fund. min-height/
       min-width vergroessern die Klickflaeche, ohne die visuelle Glyph-/Label-
       Groesse zu aendern (Desktop/OBS bleiben byte-gleich, dieser Block greift
       nur auf pointer:coarse). justify-content:center haelt Inhalt zentriert
       bei gewachsener Box. ui-413: #setup-btn -> .nc-item (Bereichs-Cluster,
       gleiche 33px-Bauhoehe je Knopf).
       ui-476: Radius/Rand/Farbe/Schatten erben unveraendert von der neuen
       .nc-item-Basisregel oben (Box statt Pille, kein Pillen-Rest noetig) —
       hier bleibt bewusst NUR die 44px-Mindesthoehe stehen, obwohl die neue
       Box-Basis nur min-height:36px verlangt: dieser Block greift NUR auf
       pointer:coarse (echte Touch-Geraete), der 44px-Touch-Target-Fix aus
       web-202 wird durch die reine Stil-Angleichung nicht zurueckgedreht. */
    .nc-item {
      min-height: 44px;
      justify-content: center;
    }
    #ph-close {
      min-width: 44px;
      min-height: 44px;
    }

    /* --- Ausgeblendete Elemente am Handy (DL-4 — Platz + Perf) ---
       #left-info: DPS-/Income-/Balance-Graph, Gruppen-Bonus, Queue (deckte den vormaligen
         drawDpsGraph-Hotspot, 28% Frame-Zeit vor ui-436 + renderRimCards 27%)
       #li-toggle: Toggle-Pille gehoert zu #left-info
       #rim-cards: Spieler-Rim-Cards (renderRimCards 27% Perf-Engpass)
       #settings-bar: Audio/WLAN unten rechts
       #boss-card: ui-632-Review-Fix (2026-09-13, must-fix) — die Karte
         haengt ueber --boss-card-top an der BREITEN #hud-Spalten-Variante
         (zoom:1.3, s. Kommentar oben bei #boss-card); in diesem Kompakt-
         Modus schrumpft #hud zum 36px-Pill-Strip und #stage nimmt die
         volle Breite (left ~0) — ohne diese Regel schwebt die Karte frei
         im Schlachtfeld, genau das Symptom, das ui-632 beheben soll. Ein
         Tablet in Portrait bekommt server-seitig IMMER die Desktop-
         Komposition (isPhoneUserAgent() erkennt es nicht als Phone) und
         landet ueber das ui-205-Gate genau in diesem Block — kein
         Randfall. Kein Funktionsverlust fuer echte Handys: die laufen
         unter der separaten, server-seitig erkannten Mobile-Komposition
         mit eigenem Boss-Streifen (ui-391), nicht unter diesem Block. */
    #left-info  { display: none !important; }
    #li-toggle  { display: none !important; }
    #rim-cards  { display: none !important; }
    #settings-bar { display: none !important; }
    #boss-card  { display: none !important; }

    /* #mobile-rotate-hint: Standard display:none bleibt (Landscape = kein Hinweis).
       Portrait-Block unten schaltet ihn auf display:flex. */
  }

  /* Portrait-Media-Block (DL-5): Dreh-Hinweis sichtbar + Minimal-Fallback-Layout.
     Kein !important — damit der Dismiss (JS setzt .mrh-dismissed) wirkt.
     Nach Dismiss matcht der Selektor #mobile-rotate-hint:not(.mrh-dismissed) einfach
     nicht mehr → display:none (default) bleibt gueltig. Kein Spezifitaets-Konflikt.
     ui-231: #mobile-rotate-hint bekommt Notch-Padding, damit der Inhalt (Animierte Hand +
     Buttons) nicht von Dynamic Island / Status-Bar ueberdeckt wird. */
  @media (orientation: portrait) and (pointer: coarse) and (max-width: 950px) {
    /* Hinweis: nur sichtbar wenn NICHT dismissed */
    #mobile-rotate-hint:not(.mrh-dismissed) { display: flex; }

    /* ui-231: Safe-Area-Padding fuer den Dreh-Hinweis — max() stellt sicher, dass
       das vorhandene padding:24px nie unterschritten wird. */
    #mobile-rotate-hint {
      padding: max(24px, env(safe-area-inset-top, 0px))
               max(24px, env(safe-area-inset-right, 0px))
               max(24px, env(safe-area-inset-bottom, 0px))
               max(24px, env(safe-area-inset-left, 0px));
    }

    /* Portrait-Fallback: Canvas-Strip oben (40vh), Panel-Streifen darunter (60vh). */
    #stage {
      left: 0;
      top: 0;
      width: 100vw;
      height: 40vh;
    }
    #panel {
      zoom: 1;
      position: fixed;
      right: 0; left: 0; top: 40vh;
      width: 100vw;
      height: 60vh;
      /* ui-231: Portrait-Panel braucht keinen rechten/linken Inset (volle Breite),
         aber Bottom-Inset fuer Home-Indicator. */
      padding-bottom: env(safe-area-inset-bottom, 0px);
      padding-right: 0;
    }
    /* #hud bleibt als schwebender Strip auf dem Canvas-Strip (40vh oben) */
  }

  /* ============================================================
     ui-205: #mobile-rotate-hint — Dreh-Hinweis-Overlay (DL-5)
     Immer display:none im Desktop/OBS (keine pointer:coarse → greift nie).
     In Portrait (pointer:coarse + max-width:950px) per Media-Block auf display:flex.
     Dismiss per JS (Trotzdem-Button) → classList.add('mrh-dismissed').
     ============================================================ */
  #mobile-rotate-hint {
    display: none; /* Default: verborgen (Desktop + Landscape) */
    position: fixed; inset: 0;
    background: rgba(2, 3, 8, 0.92);
    flex-direction: column;
    align-items: center; justify-content: center;
    gap: 14px;
    z-index: 500; /* ueber allem */
    text-align: center;
    padding: 24px;
    pointer-events: auto;
  }

  #mobile-rotate-hint .mrh-phone {
    width: 54px; height: 84px;
    border: 2.5px solid var(--cyan);
    border-radius: 10px;
    position: relative;
    animation: mrh-tilt 2.4s ease-in-out infinite;
    transform-origin: center;
  }
  #mobile-rotate-hint .mrh-phone::after {
    content: "";
    position: absolute; left: 50%; bottom: 6px;
    transform: translateX(-50%);
    width: 14px; height: 2px;
    background: var(--cyan); border-radius: 2px;
  }
  @keyframes mrh-tilt {
    0%, 55% { transform: rotate(0deg); }
    75%, 100% { transform: rotate(-90deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    #mobile-rotate-hint .mrh-phone { animation: none; transform: rotate(-45deg); }
  }

  #mobile-rotate-hint h3 {
    margin: 0; font-size: 15px; color: var(--text);
  }
  #mobile-rotate-hint p {
    margin: 0; font-size: 12px; color: var(--muted);
    max-width: 230px; line-height: 1.5;
  }
  #mobile-rotate-hint .mrh-skip {
    margin-top: 6px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--cyan);
    border-radius: 8px;
    padding: 9px 14px;
    font-size: 12px;
    min-height: 44px;
    cursor: pointer;
    font-family: inherit;
  }

  /* ============================================================
     ui-302: #ct-tablet-gate — Tablet-Weiche-Overlay.
     Rein per JS erzeugt (kein statisches Markup, kein DOM-Footprint im
     Normalfall — s. Boot-Script oben). z-index HÖHER als
     #mobile-rotate-hint (500): auf einem Tablet im Portrait ohne
     gespeicherte Wahl matchen beide Trigger-Bereiche gleichzeitig
     (iPad-Portrait 810px liegt sowohl unter der 950px-Rotate-Hint-Schwelle
     als auch über der 700px-Tablet-Gate-Schwelle) — das Tablet-Gate MUSS
     zuerst entschieden werden (Nutzer wählt explizit Hochformat/Querformat,
     bevor der allgemeine Dreh-Hinweis überhaupt relevant wird).
     ============================================================ */
  #ct-tablet-gate {
    position: fixed; inset: 0;
    background: rgba(2, 3, 8, 0.94);
    display: flex;
    align-items: center; justify-content: center;
    z-index: 600; /* > #mobile-rotate-hint (500) */
    padding: 24px;
    padding: max(24px, env(safe-area-inset-top, 0px))
             max(24px, env(safe-area-inset-right, 0px))
             max(24px, env(safe-area-inset-bottom, 0px))
             max(24px, env(safe-area-inset-left, 0px));
  }
  #ct-tablet-gate .ctg-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    text-align: center;
    max-width: 320px;
  }
  #ct-tablet-gate h2 {
    margin: 0; font-size: 17px; color: var(--text);
  }
  #ct-tablet-gate button {
    width: 100%;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--cyan);
    border-radius: 8px;
    padding: 12px 16px;
    font-size: 13px;
    min-height: 44px;
    cursor: pointer;
    font-family: inherit;
  }
  #ct-tablet-gate #ctg-mobile {
    background: var(--cyan);
    color: #06080f;
    border-color: var(--cyan);
    font-weight: 600;
  }