  :root {
    /* Gemeinsame Basis für .item-sub und Buttons (Nutzeranfrage 21.08.2026).
       Nach mehreren Anpassungsrunden (Nutzeranfrage 21.08.2026) liegen
       Buttons wieder auf demselben Wert wie .item-sub.
       .found-btn (Mitarbeiter einstellen/Wartung/Experten/Auto-Verkauf/
       Firma abreißen/Speichern) zieht auf denselben Button-Wert gleich
       (Nutzeranfrage 21.08.2026), statt eigener fester 12px. */
    --text-size-normal: calc(12px + 2pt);
    --text-size-button: var(--text-size-normal);
    /* Feste Breite für Graph + Kurszeile in Waren-/Aktienmarkt-Karten
       (.sparkline-wrap, .price-tag.price-split, .order-toggle-btn-slot,
       siehe styles.css weiter unten) sowie das Farbschema - als eine einzige
       Variable statt drei unabhängiger 250px/400px-Werte, seit sich diese
       Breite bereits mehrfach geändert hat (26.08.2026) und alle drei Stellen
       synchron bleiben müssen, damit Preiszeile/Button weiterhin exakt unter
       dem Graph sitzen. Nutzeranfrage 26.08.2026 ("Bereich muss breiter sein,
       sodass der gesamte Text auch bei einem Kurs von 100.000.000 MC in
       einer Zeile passt"): 400px, live gemessen für "Verkauf 100.000.000,00
       MC" (≈175px) + "Kauf 100.000.000,00 MC" (≈156px, beide 1fr-Spalten des
       Preis-Grids gleich breit wie die längere) + Dreieck + Spaltenabstände -
       ausdrücklich Platzhalter mit Puffer, keine pixelgenaue Untergrenze.
       Nachtrag (26.08.2026, Nutzeranfrage "bei zwei Dritteln Fensterbreite
       sieht es teilweise nicht gut aus" - Ursache: der feste 400px-Wert blieb
       bis exakt 820px unverändert und sprang dort abrupt auf 100%, wodurch
       .market-right (Kaufen/Verkaufen-Buttons) im Bereich ca. 820-1050px
       Fensterbreite keinen Platz mehr in derselben Zeile wie .market-left/
       .market-mid fand, aber noch nicht in den Mobile-Stapel-Modus wechselte
       - es landete dadurch verwaist rechtsbündig mit sichtbarer Lücke
       darunter): clamp() statt fixem Pixelwert lässt die Spalte oberhalb von
       820px kontinuierlich mit der Fensterbreite mitschrumpfen, statt starr
       bei 400px zu bleiben - dadurch bleibt insgesamt mehr Platz für
       .market-right, der oben beschriebene verwaiste Zwischenzustand tritt
       nicht mehr auf. Obergrenze weiterhin 400px (der oben gemessene
       Worst-Case bleibt auf breiten Screens exakt erfüllt), Untergrenze
       220px (kleiner als das - direkt oberhalb von 820px, wo ohnehin bald in
       den Mobile-Stapel-Modus mit width:100% gewechselt wird - dürfte auch
       der Kurstext selbst nicht mehr sinnvoll einspaltig Platz finden; ein
       Umbruch auf zwei Zeilen ist dort wie auf Mobile bereits akzeptiert,
       kein Overflow). 34vw als Mittelwert so gewählt, dass die Kurve bei
       exakt 820px (Breakpoint zum Mobile-Stapel-Modus) nah an der 220px-
       Untergrenze ankommt statt dort ebenfalls abrupt zu springen. */
    --market-price-col-width: clamp(220px, 34vw, 400px);
    /* Analog zu --market-price-col-width: feste Breite der Namensspalte
       (.market-left bei Waren-/Aktienmarkt-Karten, .order-toggle-spacer
       darunter) bislang zweimal unabhängig als 380px hinterlegt (siehe
       Kommentar bei .market-card:has(.market-mid) .market-left weiter unten)
       - beide Stellen jetzt dieselbe Variable, verhindert künftiges
       Auseinanderlaufen. Ebenfalls clamp() statt fixem Wert aus demselben
       Grund wie oben (Nutzeranfrage 26.08.2026, "bei zwei Dritteln
       Fensterbreite sieht es teilweise nicht gut aus").
       24vw (nicht wie bei --market-price-col-width 34vw) - live nachgemessen
       bei 1000px Fensterbreite (dem konkret gemeldeten "verwaisten"
       Zwischenzustand): .market-mid (min-width 220px) + .market-right
       (~406px, von den Kaufen/Verkaufen-Buttons selbst vorgegeben, dort
       nicht angetastet) + zwei 8px-Gaps brauchen zusammen ~642px - erst ab
       einer .market-left-Breite von ca. 274px oder weniger bleibt bei 1000px
       genug Platz, damit .market-right in derselben Zeile bleibt statt
       verwaist umzubrechen. 24vw ergibt bei 1000px 240px (mit Puffer unter
       den 274px), bei 380px-Deckel weiterhin identisch zum bisherigen festen
       Wert auf breiten Screens (>1266px, wo 24vw > 380px). */
    --market-left-col-width: clamp(200px, 24vw, 380px);
    --accent: #2f9e63;
    --accent-dark: #227a4c;
    --accent-light: #e6f5ec;
    --bg: #f5f7f6;
    --card: #ffffff;
    --text: #22292a;
    /* Nutzerreport 04.08.2026: normaler (nicht überschriftartiger) Text in
       dieser gedämpften Farbe wirkte auf vielen Screens zu blass, gerade bei
       kleiner Schrift (12-13px, z. B. .item-sub-Beschriftungen). Kontrast zu
       --card angehoben (4,6:1 → 7,0:1 - deutlich über der WCAG-AA-Mindestgrenze
       von 4,5:1), ohne den Farbton (gedämpftes Grün-Grau, passend zum übrigen
       Farbschema) zu verändern. */
    --muted: #4f5c5a;
    --border: #e1e6e4;
    /* Kontrastaudit (06.08.2026, Nutzeranfrage "sorge dafür, dass der
       Kontrast sowohl im hellen als auch im dunklen Design gut ist") - vorher
       #c94f4f, als Text auf --card (weiß) nur 4,45:1 (unter der WCAG-AA-
       Mindestgrenze 4,5:1 für normal großen Text) - u. a. betroffen: Login-/
       Registrierungs-Fehlermeldungen (#authError etc.), .warning-btn-
       Ruhezustand, .price-down. Minimal auf 4,93:1 nachgedunkelt, gleicher
       Farbton/gleiche Sättigung, kaum wahrnehmbarer optischer Unterschied.
       --down war schon immer identisch zu --danger (zwei Namen für denselben
       Wert, semantische statt farbliche Unterscheidung) und bleibt es. Der
       Dark-Mode-Wert unten ist NICHT betroffen - dort bereits 5,4-8,7:1 als
       Text auf --card, siehe die vielen dortigen Verwendungsstellen. */
    --danger: #c54343;
    /* Vorher identisch zu --accent (#2f9e63) - als Text auf --card (weiß) nur
       3,39:1. Betrifft nur .price-up; --accent selbst bleibt unverändert
       (dutzende andere Verwendungen als Rahmen/Icon-Farbe, dort unkritisch).
       Bewusst entkoppelt statt --accent selbst nachzudunkeln. */
    --up: #257e55;
    --down: #c54343;
    /* Neu (06.08.2026, Kontrastaudit): dedizierte, THEMENUNABHÄNGIGE Hinter-
       grundfarben für Buttons mit weißer Schrift - ersetzen an den
       betroffenen Stellen die bisherige Wiederverwendung von --accent/
       --accent-dark. Grund: --accent-dark ist im Light Mode absichtlich
       dunkler als --accent (für Hover-Verdunklung gedacht), im Dark Mode aber
       absichtlich HELLER als --accent (für Textlesbarkeit auf dunklem
       Hintergrund gedacht) - "weiße Schrift auf --accent/--accent-dark" traf
       dadurch in KEINEM Theme zuverlässig 4,5:1 (Light 3,39/5,31, Dark
       3,00/2,09). Beide neuen Werte sind bewusst so gewählt, dass derselbe
       Hex-Code in beiden Themes funktioniert (weiße Schrift ≥5:1, sichtbar
       gegen --bg/--card in beiden Themes) - deshalb hier in :root definiert,
       ohne Override im Dark-Mode-Block unten. */
    --btn-bg: #257e55;
    --btn-bg-hover: #22774f;
    --btn-danger-bg: #ba4545;
    --btn-danger-bg-hover: #a43d3d;
    /* Lager "fast voll" (12_UI_UX.md, Nutzeranfrage 03.09.2026) - eigene
       Zwischenstufe zwischen normal (--accent) und "voll"/pausiert (--danger).
       Wie --btn-bg oben bewusst themenunabhängig (weiße Schrift auf diesem
       Ton: ~5,0:1, in beiden Themes ausreichend), kein separater Dark-Mode-
       Override nötig. Trägt das weiße "!" auf .storage-warn-badge, deshalb an
       die 4,5:1-Kontrastgrenze für weiße Schrift gebunden - dafür zu dunkel/
       gedämpft, um in der Lagerübersicht klar von --danger unterscheidbar zu
       sein (Nutzerreport 03.09.2026, "sieht fast identisch aus wie das rot").
       --warning-fill (unten) ist die separate, hellere Füllfarbe für genau
       diesen textlosen Fall. */
    --warning: #b45309;
    /* Nur für .progress-bar.near-full (kein Text auf der Leiste, keine
       4,5:1-Bindung wie --warning oben) - deutlich helleres/kräftigeres
       Orange, damit die 90%-Frühwarnung auf der dünnen 8px-Leiste klar als
       Orange statt als weiteres Rot lesbar ist. Bewusst ebenfalls
       themenunabhängig (kein Text, kein Kontrastproblem in keinem Theme). */
    --warning-fill: #f59e0b;
    /* Disabled-Button-Grauton war bisher überall hartkodiert (#cfd6d3/
       #8b9591) ohne Dark-Mode-Gegenstück - im dunklen Design saß dieses helle
       Grau dadurch als einziges "leuchtendes" Element zwischen sonst überall
       nachgedunkelten Farben (Nutzerreport 21.08.2026, "nichts soll blenden").
       Eigene Variablen statt direkter Farbwerte an jeder der betroffenen
       Stellen (button:disabled, button.secondary/.buy/.danger:disabled).
       Nutzeranfrage 02.09.2026 ("wie sähe es aus, wenn wir die WCAG-AA-
       Mindestgrenze einhalten würden") - #8b9591 lieferte auf #cfd6d3 nur
       2,1:1 (WCAG erlaubt das zwar, da 1.4.3 inaktive/disabled Komponenten
       ausdrücklich ausnimmt, siehe Kontrastaudit-Kommentar oben, aber auf
       Wunsch trotzdem angehoben). --btn-disabled-bg bleibt unverändert, der
       Text nutzt jetzt --muted statt eines eigenen Grautons - keine neue
       Farbe, sondern dieselbe bereits etablierte gedämpfte Textfarbe wie
       überall sonst im Spiel, ergibt hier ≈4,7:1. */
    --btn-disabled-bg: #cfd6d3;
    --btn-disabled-text: var(--muted);
    /* Nutzeranfrage 02.09.2026 - button.secondary (ursprünglich transparent
       mit grünem Rahmen/grüner Schrift, zwischenzeitlich ein Sand/Beige-Ton)
       auf ausdrücklichen Wunsch wieder auf denselben Grünton wie die übrigen
       Buttons zurückgesetzt ("so grün wie die anderen Buttons"), weiße
       Schrift statt der vorherigen dunklen. Dieselben Hex-Werte wie
       --btn-bg/--btn-bg-hover (siehe dortiger Kommentar: derselbe Wert
       funktioniert in beiden Themes). Kein separater Dark-Mode-Block mehr
       nötig, aus demselben Grund wie bei --btn-bg.
       (Das Admin-Live-Testfeld, das hier ursprünglich noch einen editierbaren
       Hex-String statt einer var(--btn-bg)-Referenz verlangte, ist seit
       04.09.2026 wieder entfernt, siehe Nutzeranfrage "Secondary-Button-
       Farben (Live-Test) komplett aus Admin-Bereich entfernen".) */
    --btn-secondary-bg: #257e55;
    --btn-secondary-bg-hover: #22774f;
    --btn-secondary-text: #ffffff;
    /* button.buy (Sofortkauf-Rahmenbutton) - Farbwert existierte bisher nur
       als hartkodierter Hex-Wert direkt in der button.buy-Regel, ohne
       Dark-Mode-Gegenstück (siehe dortiger Kommentar). Hier als Variable
       promoted, damit der Dark-Mode-Block unten einen helleren, auf --card
       (dunkel) lesbaren Ton definieren kann. */
    --buy: #1b6fa8;
    --buy-light: #e8f2fa;
    /* MarketBorn-Wortmarke (Header/Login/E-Mail-Bestätigung, Redesign
       09.08.2026): warmer Startton des "Born"-Farbverlaufs. Der Endton
       braucht keine eigene Variable, der Verlauf läuft direkt in --accent
       aus - die Wortmarke endet dadurch tatsächlich im Signalgrün der App
       statt in einem eigenen, unabhängigen Grün. Die Kursverlauf-Linie
       hinter dem Schriftzug braucht ebenfalls keine eigene Variable, die
       nutzt direkt --muted (passender gedämpfter Farbton als eigenständige
       Erfindung). */
    --logo-spark: #f0862e;
  }
  /* Dunkles Farbschema (user request 28.07.2026), per Profil-Toggle wählbar -
     setTheme() setzt data-theme auf <html>, Auswahl wird serverseitig pro
     Account gespeichert (players.theme), nicht nur lokal im Browser. */
  :root[data-theme="dark"] {
    --accent: #34a873;
    --accent-dark: #56c891;
    --accent-light: #1b3a2c;
    --bg: #12171a;
    --card: #1c2226;
    --text: #e6ebe9;
    /* Gegenstück zur Light-Mode-Korrektur oben - dort dunkler, hier heller
       (Kontrast zu --card 5,9:1 → 8,1:1), gleiche Begründung. */
    --muted: #aabcb7;
    --border: #2b3336;
    /* --danger/--up/--down bleiben hier unverändert (bereits 5,4-8,7:1 als
       Text auf --card/--bg, siehe Kontrastaudit-Kommentar oben) - nur der
       Light-Mode-Wert war zu blass. --btn-bg/--btn-bg-hover/--btn-danger-bg/
       --btn-danger-bg-hover werden hier bewusst NICHT überschrieben (derselbe
       Wert funktioniert in beiden Themes, siehe Kommentar oben). */
    --danger: #d97b7b;
    --up: #4cbd84;
    --down: #d97b7b;
    /* Helleres Blau als im Light Mode (dort #1b6fa8) - auf der dunklen Karte
       (--card #1c2226) lieferte der Light-Mode-Ton nur 2,97:1. */
    --buy: #3da1d6;
    --buy-light: #153043;
    /* Deutlich dunkler als der helle Light-Mode-Ton (#cfd6d3 wäre hier die
       hellste Fläche im ganzen dunklen Design gewesen) - Text entsprechend
       umgekehrt heller als die Fläche, gleiches Prinzip wie --muted oben.
       --btn-disabled-text nutzt seit der Kontrast-Anhebung oben (Light Mode-
       Kommentar bei --btn-disabled-text) ebenfalls --muted statt eines
       eigenen Grautons (#7d8a88 lieferte hier nur 3,15:1) - --muted ist im
       Dark Mode bereits deutlich heller als --card/--btn-disabled-bg (siehe
       eigener Kontrastaudit-Kommentar oben), ergibt hier ≈5,7:1. */
    --btn-disabled-bg: #333c40;
    /* Helleres Orange als im Light Mode (dort #f0862e) - Kontrast auf der
       dunklen Karte sonst zu gedämpft. */
    --logo-spark: #ffa24e;
  }
  * { box-sizing: border-box; }
  /* Erzwungene vertikale Scrollbar (Nutzeranfrage 01.08.2026): ohne das
     erscheint/verschwindet die Scrollbar je nach Ansicht (z. B. eine kurze
     Liste vs. die Firmenzentrale mit vielen Firmen), wodurch die verfügbare
     Breite und damit das gesamte Layout leicht hin- und herspringt. Immer
     reserviert/sichtbar hält den Screen unabhängig vom Seiteninhalt identisch.
     Nur vertikal, wie ausdrücklich gewünscht - horizontales Scrollen bleibt
     unverändert (kein overflow-x hier gesetzt). */
  html { overflow-y: scroll; }
  /* Eigene Scrollbar fürs ganze Fenster (Nutzeranfrage 25.08.2026) - dieselbe
     Optik, die zuerst nur für #mailboxList eingeführt wurde (siehe dort für
     die Firefox/WebKit-Doppelspur-Begründung), hier auf html erweitert, dem
     Element mit der oben erzwungenen Scrollbar. scrollbar-width/-color
     vererben sich als normale CSS-Eigenschaften ohnehin schon automatisch an
     jedes Kind-Element (auch #mailboxList) - die ::-webkit-scrollbar-
     Pseudo-Elemente dagegen nicht, die bleiben deshalb dort zusätzlich
     bestehen. */
  html {
    scrollbar-width: thin;
    scrollbar-color: var(--btn-bg) transparent;
  }
  html::-webkit-scrollbar { width: 10px; }
  html::-webkit-scrollbar-track { background: transparent; }
  html::-webkit-scrollbar-thumb { background: var(--btn-bg); border-radius: 999px; }
  html::-webkit-scrollbar-thumb:hover { background: var(--btn-bg-hover); }
  /* Flex-Spalte über die volle Fensterhöhe, damit der Rechts-Footer (siehe
     #legalFooter) bei wenig Inhalt trotzdem am unteren Fensterrand steht und
     bei viel Inhalt normal darunter mitscrollt - ohne position:fixed, das
     sonst mit der mobilen Tab-Leiste und dem Tutorial-Panel um denselben
     Platz konkurrieren würde. */
  body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    background:
      linear-gradient(rgba(245, 247, 246, calc(1 - 0.95)), rgba(245, 247, 246, calc(1 - 0.95))),
      var(--bg) url("/assets/background.jpg") center / cover no-repeat fixed;
    color: var(--text);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }
  /* Sprung-Link (Barrierefreiheits-Audit 22.08.2026, WCAG 2.4.1) - Standard-
     Muster: per absolute Positionierung und clip-artiges 1px-Fenster
     unsichtbar, solange er nicht fokussiert ist, bei Tab-Fokus springt er
     sichtbar oben links ins Bild. z-index 300 liegt über allem Bestehenden
     im Spiel (höchster bisheriger Wert war .modal-overlay mit 200), da der
     Link buchstäblich das allererste fokussierbare Element der Seite ist. */
  .skip-link {
    position: absolute;
    top: -40px;
    left: 8px;
    z-index: 300;
    background: var(--btn-bg);
    color: #fff;
    padding: 10px 16px;
    border-radius: 8px;
    text-decoration: none;
    font-weight: 600;
    transition: top 0.15s ease;
  }
  .skip-link:focus {
    top: 8px;
  }
  /* Bugfix (Nutzerreport 27.08.2026, "seltsamer schwarzer Rahmen um die
     Firmen-Karte, kommt bei Escape") - main hat tabindex="-1" (siehe
     index.html), rein damit der Sprung-Link oben per focus() dorthin
     springen kann und ein per Escape geschlossener Dialog seinen Fokus
     sinnvoll zurückgeben kann (closeModalFocus(), ui-helpers.js - fokussiert
     modalReturnFocusEl zurück; war beim Öffnen des Dialogs zufällig main
     selbst aktiv, z. B. weil noch kein einzelnes Element den Fokus hatte,
     landet der Fokus beim Schließen wieder dort). Ohne eigene Regel zeigte
     der Browser dafür seinen Standard-Fokusrahmen um das komplette main -
     optisch wie ein sinnloser Rahmen um die gesamte Karte, obwohl main selbst
     nie per Tab erreichbar ist (tabindex="-1" schließt es aus dem normalen
     Tab-Fluss aus) und der Rahmen dadurch für sehende Tastatur-Nutzer keinen
     erkennbaren Zweck hat. outline:none entfernt nur diese eine, rein
     technische Fokussierung - der Sprung-Link selbst hat weiterhin seinen
     eigenen, bewusst sichtbaren :focus-Stil (siehe oben), und alle echten
     interaktiven Elemente (Buttons, Links, Eingabefelder) behalten ihren
     normalen Fokusrahmen unverändert. */
  main:focus {
    outline: none;
  }
  :root[data-theme="dark"] body {
    /* War identisch zum Light-Mode-Deckkraft-Wert (0,05) - das Hintergrundbild
       blieb dadurch im dunklen Design praktisch unverändert hell und wirkte
       neben den sonst überall nachgedunkelten Flächen wie beleuchtet
       (Nutzerreport 21.08.2026, "nichts soll blenden"). Deutlich kräftigerer
       dunkler Schleier nur im Dark-Mode-Override, Light Mode bleibt
       unverändert bei 0,05. */
    background:
      linear-gradient(rgba(18, 23, 26, calc(1 - 0.45)), rgba(18, 23, 26, calc(1 - 0.45))),
      var(--bg) url("/assets/background.jpg") center / cover no-repeat fixed;
  }
  /* Bugfix (Nutzeranfrage 20.08.2026, "Hintergrundbild scrollt auf Mobile ein
     bisschen mit"): background-attachment:fixed auf body selbst wird von
     mobilen Browsern nicht zuverlässig eingehalten - beim Scrollen (u.a.
     durch das Ein-/Ausblenden der Adressleiste, das den Viewport laufend neu
     vermisst) rutscht das Bild sichtbar mit. Auf schmalen Screens übernimmt
     stattdessen ein echtes position:fixed-Pseudoelement hinter dem gesamten
     Inhalt - das wird von mobilen Browsern zuverlässig fixiert, anders als
     eine reine background-attachment-Eigenschaft. body selbst bekommt hier
     nur noch die Fallback-Farbe (--bg) als eigenen Hintergrund, das Bild und
     der Deckkraft-Verlauf wandern komplett in dieses Pseudoelement. */
  @media (max-width: 820px) {
    body {
      position: relative;
      background: var(--bg);
    }
    body::before {
      content: "";
      position: fixed;
      inset: 0;
      z-index: -1;
      background:
        linear-gradient(rgba(245, 247, 246, calc(1 - 0.95)), rgba(245, 247, 246, calc(1 - 0.95))),
        var(--bg) url("/assets/background.jpg") center / cover no-repeat;
    }
    :root[data-theme="dark"] body::before {
      /* Gleicher stärkerer Schleier wie beim Desktop-Gegenstück oben. */
      background:
        linear-gradient(rgba(18, 23, 26, calc(1 - 0.45)), rgba(18, 23, 26, calc(1 - 0.45))),
        var(--bg) url("/assets/background.jpg") center / cover no-repeat;
    }
  }
  header {
    background: var(--card);
    /* Gleicher Holzsteg wie section.card oben, hier als einzelne Kante
       (kein border-radius am Header, daher reicht das einfachere
       border-image statt der background-clip-Technik). 3px statt 4px
       (Nutzeranfrage 22.08.2026, "alle Rahmen einen px schmaler") - gilt
       einheitlich für die gesamte Holzsteg-Familie (header, mobile-tab-bar,
       section.card, .kpi je -1px; #legalFooter, bewusst schon vorher 1px
       dünner als die Karten, von 3px auf 2px).
       border-top zusätzlich zu border-bottom (Nutzeranfrage 22.08.2026,
       "auch am oberen Rand des Header") - dasselbe border-image gilt bereits
       für alle Seiten, die eine Breite/Style haben, daher reicht das
       einfache Ergänzen der zweiten Kante ohne die Gradient-Deklaration zu
       verdoppeln. */
    border-top: 3px solid;
    border-bottom: 3px solid;
    border-image: linear-gradient(90deg, #8a6a49, #4c3a28) 1;
    /* Zurück auf 12px/20px (Nutzeranfrage 20.08.2026, "Logo wieder
       reduzieren wie es vorher war") - die kurzzeitige Reduktion auf
       6px/10px (20.08.2026, "Abstand des Logos nach links, oben und unten
       darf gerne etwas geringer sein") diente ausschließlich dazu, die durch
       das inzwischen wieder zurückgenommene 1,5-fach vergrößerte Logo
       gewachsene Kopfzeilenhöhe zu kompensieren - mit dem Logo wieder bei
       42.9px erübrigt sich der Grund dafür von selbst, ohne das reduzierte
       Padding würde die Kopfzeile sonst sogar kleiner als ursprünglich
       ausfallen. padding-right bleibt unverändert der laufzeit-gemessene
       Reserve-Wert weiter unten, hier nicht betroffen. */
    padding: 12px 20px;
    /* Reserviert Platz für die fest positionierten Bedienelemente oben rechts
       (#langSwitch: Theme-Umschalter + Sprachflagge) - die sitzen bewusst
       außerhalb von <header> (siehe Kommentar dort), da der Header vor dem
       Login komplett ausgeblendet ist. Ohne diese Reserve lief der
       Header-Inhalt (allen voran #playerTag, dessen Breite vom Anzeigenamen
       abhängt) darunter und wurde verdeckt (Nutzer-Report 31.07.2026).

       Der Wert wird zur Laufzeit aus der GEMESSENEN Breite gesetzt
       (syncHeaderControlsReserve(), Nutzer-Report 03.08.2026): Vorher stand
       hier fest 74px - passend für die Flagge allein. Mit dem zweiten Button
       reichte das nicht mehr und der Profil-Button wurde überlagert. Ein neu
       geratener Festwert hätte beim nächsten Element wieder gebrochen. */
    padding-right: var(--header-controls-reserve, 74px);
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    position: sticky;
    top: 0;
    z-index: 10;
  }
  .header-left { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  /* Reiner Durchreich-Wrapper auf Desktop (siehe HTML-Kommentar bei
     #headerWarnGroup) - beide Warn-Buttons bleiben dadurch unverändert
     direkte Flex-Kinder von .stats. Erst die Mobile-Media-Query weiter unten
     macht daraus eine echte kleine Flex-Zeile. */
  .header-warn-group { display: contents; }
  header h1 {
    font-size: calc(17px + 1pt);
    margin: 0;
    /* Wortmarke ist seit 13.08.2026 (Nutzeranfrage, "Logo und Text darunter
       zu einer Bilddatei zusammenfügen") das einzige Kind - der Untertitel
       lebt seither direkt im SVG als zweites <text>-Element statt als
       eigenes, separat positioniertes DOM-Element (siehe #logoBtn-Markup),
       daher genügt hier ein einfacher Block statt der vormaligen
       flex-direction:column-Stapelung zweier Kinder. */
    display: block;
    /* Logo springt bei eingeloggten Spielern zum Dashboard (Nutzeranfrage
       06.08.2026) - reines <h1>, kein <button> (der generische Button-Reset
       hätte den fest hellblauen Wortmarken-Hintergrund mit dem grünen
       Standard-Button-Look überschrieben, siehe die immer wieder aufgetretene
       "als button gerendert erbt ungewollt globale Button-Styles"-Falle,
       zuletzt Abschnitt 4j/4p/10). */
    cursor: pointer;
  }
  /* Logo-Höhe jetzt hier statt als Inline-Style auf dem SVG selbst
     (Nutzeranfrage 20.08.2026, "Logo in der Desktop-Version muss wieder
     größer werden, sagen wir mal 1,3fach") - ein Inline-Style lässt sich
     nicht per Media Query überschreiben, Mobile und Desktop sollen hier aber
     unterschiedlich groß sein. 34.856px ist weiterhin die (u.a. mobile)
     Basis-Größe, 45.313px (34.856 * 1,3) gilt nur ab Desktop-Breite - beide
     Werte 22.08.2026 von 42.9px/55.77px auf 52/64 = 0,8125 herunterskaliert,
     als der leere untere Rand aus der viewBox geschnitten wurde (siehe
     Kommentar am SVG in index.html) - derselbe Maßstab wie vorher, nur ohne
     den vorherigen Leerraum. Ändert sich einer der beiden Werte erneut,
     müssen auch die davon abhängigen Ausrichtungswerte neu berechnet werden:
     #logoBtn's margin-left (Mobile, siehe dortiger Kommentar) hängt von der
     Mobile-Höhe ab, .lang-switch's Desktop-top (siehe dortiger Kommentar)
     von der Desktop-Höhe. */
  /* Nutzerreport 04.09.2026 + Rückfrage-Antwort - --header-logo-height wird
     nur auf Mobile per JS gesetzt (syncHeaderControlsReserve(), ui-helpers.js),
     wenn eine aktive Warnung nicht genug Platz für die volle Wortmarkenbreite
     lässt; Fallback bleibt der bisherige feste Basiswert. Der Desktop-Wert
     darunter bleibt unverändert - dort tritt der Platzmangel nie auf
     (.header-warn-group ist dort display:contents, kein position:fixed). */
  #headerLogoSvg { height: var(--header-logo-height, 34.856px); }
  @media (min-width: 821px) {
    #headerLogoSvg { height: 45.313px; }
  }
  /* Every clickable header nav button (Dashboard/Zonen/Rangliste/Zur Karte/
     Profil) shares this one solid-green look, same as the plain <button>
     default - kept as an explicit rule (rather than just relying on the
     bare button style) so it's obvious this is a deliberate, uniform group
     rather than "whatever the default happens to be". Warning badges
     (storageWarningBtn/loanWarningBtn) are intentionally excluded - their
     red/orange color is the point, it signals a problem. #playerTag stays
     display:none until it is revealed once via an inline style set from JS
     once the player profile loads; #zonesBtn/#dashboardBtn/#leaderboardBtn
     have no such toggle and must stay visible by default. */
  #playerTag { display: none; }
  /* #backBtn ist immer sichtbar, statt sich je nach Ansicht ein-/auszublenden
     (Nutzeranfrage 06.08.2026, "Zur Karte soll immer sichtbar sein... Orts-
     Dropdown soll sich immer an derselben Stelle befinden") - dafür reicht
     die reservierte Layout-Box eines normalen, immer aktiven Buttons bereits
     aus (Ortsauswahl und alles danach rückt dadurch nicht je nach Ansicht
     hin und her).
     Nutzeranfrage 06.08.2026 hatte den Button auf der Karte selbst zusätzlich
     per disabled-Attribut ausgegraut/inaktiv geschaltet ("hier bin ich schon"
     -Zustand) - auf erneute Nutzeranfrage (26.08.2026) wieder entfernt: die
     anderen Header-Nav-Buttons (Dashboard/Zonen/Rangliste) verhalten sich auf
     ihrer eigenen Ansicht nicht so, die Ausnahme wirkte irritierend
     inkonsistent. showView() setzt seither kein disabled mehr (siehe dort). */
  #backBtn, #zonesBtn, #dashboardBtn, #leaderboardBtn, #playerTag {
    /* Kontrastaudit 06.08.2026: var(--accent) direkt gab hier nur 3,00:1
       (Dark)/3,39:1 (Light) für die weiße Schrift - siehe --btn-bg-Kommentar
       oben in :root. */
    background: var(--btn-bg);
    color: white;
    border: none;
    padding: 7px 12px;
    border-radius: 8px;
    font-size: calc(13px + 1pt);
    font-weight: 700;
    cursor: pointer;
    font-family: inherit;
    /* sticky-header chrome, not a content action button -- keeps its own
       content-sized width/height instead of the uniform button size below */
    width: auto;
    height: auto;
    display: inline-block;
    /* Nutzeranfrage 21.08.2026 - "alle Buttons/Elemente im Header sollen den
       Schatten der drei rechten Buttons (Theme/Sprache/Logout) bekommen".
       Gleicher Wert wie #themeToggleBtn/#langFlagBtn/#headerLogoutBtn. */
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    /* Nutzeranfrage 21.08.2026 - "Hover-Effekt bei den dunkelgrünen Buttons
       im Header deutlicher machen": macht den Hover-Übergang unten (Schatten
       + Anheben) sichtbar weich statt eines harten Sprungs. */
    transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
  }
  /* Nutzeranfrage 21.08.2026 - "alle Buttons/Elemente im Header sollen den
     Schatten der drei rechten Buttons (Theme/Sprache/Logout) bekommen":
     eigene Regel statt #adminBtn oben mit aufzunehmen, damit dessen
     abweichendes Padding (8px/14px aus der spielweiten "button"-Grundregel,
     siehe Kommentar dort) unverändert bleibt - nur der Schatten soll dazu
     kommen, keine Größenänderung. Gleicher Schattenwert wie
     #themeToggleBtn/#langFlagBtn/#headerLogoutBtn (siehe dortige Regel). */
  #adminBtn { box-shadow: 0 2px 8px rgba(0,0,0,0.12); transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; }
  /* Nutzeranfrage 21.08.2026 - "Hover-Effekt bei den dunkelgrünen Buttons im
     Header deutlicher machen, ich erkenne kaum einen Effekt": die bisherige
     Regel (nur background: var(--btn-bg-hover), #257e55 -> #22774f) ist ein
     sehr naher Grünton - kaum wahrnehmbar. Zusätzlich zur Farbänderung jetzt
     ein spürbares "Anheben" (kräftigerer Schatten + 1px nach oben), dieselbe
     Hover-Sprache wie z. B. .building:hover .tile weiter unten im Stylesheet.
     #adminBtn ergänzt (war zuvor nicht in dieser Liste, bezog seinen Hover
     nur aus der generischen "button:hover:not(:disabled)"-Regel - dieselbe
     Farbe, aber ohne die neue Anhebung). */
  #backBtn:hover, #zonesBtn:hover, #dashboardBtn:hover, #leaderboardBtn:hover, #playerTag:hover, #adminBtn:hover {
    background: var(--btn-bg-hover);
    box-shadow: 0 5px 14px rgba(0,0,0,0.28);
    transform: translateY(-1px);
  }
  /* "Aktuell hier"-Anzeige als Dropdown-Auslöser (Nutzeranfrage 31.07.2026) -
     zeigt weiterhin nur den aktuellen Ort an, ist jetzt aber klickbar und
     öffnet ein Sprungmenü zu allen Kartenzielen der aktuellen Zone
     (Kerngebäude + Firmentypen), alphabetisch sortiert. #locationTag ist
     jetzt ein <button> statt <span> - die Regeln hier setzen die generischen
     button-Stile (Hintergrund/Rahmen/feste Breite) zurück, damit es
     weiterhin wie reiner Text mit Klick-Affordanz aussieht, keine Kachel. */
  /* justify-content:flex-end (Nutzerreport 06.08.2026, siehe #locationDropdownWrap
     in der Mobile-Media-Query für den eigentlichen Grund): Wirkt nur, wenn der
     Container mehr Breite hat als sein Inhalt braucht (mobile: justify-self:
     stretch füllt die zugewiesene Grid-Spur, auch wenn der Inhalt kleiner ist) -
     bei natürlicher, inhaltsgetriebener Breite (Desktop) hat es keinen
     sichtbaren Effekt, da dort keine überschüssige Breite existiert. */
  .location-dropdown { position: relative; display: inline-flex; justify-content: flex-end; }
  .location-tag {
    font-family: inherit;
    font-size: calc(12px + 1pt);
    font-weight: 600;
    color: var(--muted);
    background: none;
    border: none;
    padding: 0;
    width: auto;
    height: auto;
    cursor: pointer;
    /* min-width:0 (Nutzerreport 06.08.2026): Ohne das behält ein Flex-Kind
       standardmäßig eine implizite Mindestbreite in Höhe seines Inhalts, egal
       wie eng sein Elternelement tatsächlich ist - dieselbe "Flexbox-Sprengung"
       wie bei jedem anderen Fall in diesem Projekt, die erst mit min-width:0
       verschwindet und der bereits bestehenden .location-tag-text-Ellipsis
       überhaupt eine Chance gibt zu greifen. */
    min-width: 0;
  }
  /* Kleiner Pfeil als Dropdown-Hinweis (Nutzeranfrage 01.08.2026) - reines
     CSS-generiertes Zeichen statt Markup-Änderung, zieht die Hover-/Theme-
     Farbe der .location-tag automatisch mit. */
  .location-tag::after { content: "▾"; font-size: calc(10px + 1pt); margin-left: 4px; opacity: 0.8; }
  /* Name in einem eigenen inneren Span statt direkt im Button (Nutzerreport
     05.08.2026, "Text im Ortsauswahl-Dropdown verschwindet, nur der Pfeil
     bleibt"): #locationTag ist ein <button> und erbt overflow:hidden/
     text-overflow:ellipsis/white-space:nowrap von der generischen
     button-Grundregel, hat selbst aber width:auto - der Button wuchs also
     bislang immer exakt auf die Breite seines Inhalts, wodurch die Ellipsis
     nie griff. Bei einem langen Firmennamen (z.B. "Seltene-Erden-Mine",
     "Taiga-Holzfällerei") auf einem schmalen Bildschirm sprengte der dadurch
     entstandene, ungebremste Button die verbleibende Grid-Spalte 3 (siehe
     #locationDropdownWrap unten) - das Grid ließ den Button trotzdem auf
     seine volle Breite wachsen und schob ihn dabei teilweise über den
     rechten Bildschirmrand hinaus (position:fixed-Header, kein horizontales
     Scrollen), wodurch der überstehende Teil des Textes schlicht unsichtbar
     wurde, während der `::after`-Pfeil (am Ende des Buttons, also am
     überstehenden Rand) mal sichtbar blieb, mal ebenfalls verschwand - reine
     Zufallssache je nach Namenslänge. Der innere Span trägt jetzt selbst
     overflow:hidden/text-overflow:ellipsis/white-space:nowrap PLUS eine
     harte max-width (nur mobil, siehe Media Query unten) - das begrenzt die
     Breite, auf die der Button überhaupt wachsen kann, wodurch das Grid nie
     mehr über den Bildschirmrand hinausschießt. Der Pfeil sitzt als `::after`
     weiterhin auf dem AUSSEN-Button (unbegrenzt), bleibt also immer
     vollständig sichtbar, unabhängig davon, ob der Name gekürzt wird. */
  .location-tag-text {
    display: inline-block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: top;
  }
  /* #locationTag statt nur .location-tag, da der generische button:hover-Reset
     (siehe unten, "button:hover:not(:disabled)") sonst per Spezifität
     gewinnen und den grünen Hintergrund mit derselben Textfarbe wie den
     Hintergrund kombinieren würde (unlesbar) - Nutzeranfrage 01.08.2026. */
  #locationTag:hover { background: var(--btn-bg); color: #fff; text-decoration: none; border-radius: 6px; padding: 2px 6px; margin: -2px -6px; }
  /* Nebel hinter dem geöffneten Orts-Dropdown auf Mobile (Nutzeranfrage
     22.08.2026, "der Hintergrund zwischen Header und Button-Leiste soll
     etwas verblassen, mit Klick irgendwo rückgängig") - reines
     pointer-events:none-Element, das Klicks zum Schließen (siehe
     closeLocationMenu(), toggelt hier nur die .show-Klasse) unverändert zum
     bestehenden document-weiten Click-Handler durchlässt statt sie selbst
     abzufangen.
     z-index 9 ist bewusst NIEDRIGER als header (10), #mobileTabBar (15) und
     #legalFooter (19) - beim ersten Versuch (selbiger Tag) lag der Nebel
     noch DARÜBER (40) und verdunkelte dadurch auch Header und Tab-Leiste
     mit, obwohl nur der Inhalt dazwischen verblassen sollte. Da header/
     #mobileTabBar/#legalFooter jeweils einen eigenen deckenden
     --card-Hintergrund haben, deckt ihr höherer Stapelkontext den
     darunterliegenden Nebel in ihrem eigenen Bereich vollständig ab - nur
     der dazwischenliegende <main>-Inhalt (kein eigener z-index, daher
     Stapelebene 0 < 9) bleibt sichtbar verdunkelt. .location-menu selbst
     bleibt mit z-index 50 über dem Nebel sichtbar, ebenso
     .lang-switch/.header-warn-group (60), exakt wie beim Tutorial-Schleier.
     Nur die .show-Sichtbarkeit ist auf die Mobile-Media-Query beschränkt
     (siehe dort) - am Desktop bleibt das Dropdown unverändert ohne Nebel. */
  #locationMenuBackdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 9;
    pointer-events: none;
  }
  /* Gleicher Holzsteg wie section.card/.kpi (Nutzeranfrage 22.08.2026) -
     dieselbe background-clip-Technik, siehe dortigen Kommentar. */
  .location-menu {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    border: 4px solid transparent;
    border-radius: 10px;
    background: linear-gradient(var(--card), var(--card)) padding-box, linear-gradient(155deg, #8a6a49, #4c3a28) border-box;
    box-shadow: 0 4px 16px rgba(0,0,0,0.15);
    padding: 6px;
    z-index: 50;
    min-width: 210px;
    max-height: 320px;
    overflow-y: auto;
  }
  .location-menu-item {
    display: flex;
    align-items: center;
    /* Ohne dies gewinnt die generische button-Grundregel (justify-content:
       center) - text-align:left wirkt nicht auf Flex-Kinder, nur auf Inline-
       Inhalt (Nutzeranfrage 01.08.2026: Dropdown-Inhalt war dadurch trotz
       text-align:left optisch zentriert statt linksbündig). */
    justify-content: flex-start;
    gap: 8px;
    width: 100%;
    height: auto;
    background: none;
    border: none;
    text-align: left;
    padding: 8px 10px;
    border-radius: 6px;
    font-size: calc(13px + 1pt);
    font-weight: 400;
    color: var(--text);
    cursor: pointer;
    white-space: nowrap;
  }
  /* background hier ist faktisch tot (Nutzeranfrage 14.08.2026 aufgedeckt):
     ".location-menu-item:hover" ist (0,2,0) spezifisch und verliert gegen die
     generische "button:hover:not(:disabled)"-Regel (0,2,1) - der tatsächlich
     gerenderte Hover-Hintergrund kommt dadurch in BEIDEN Themes aus deren
     --btn-bg-hover (dunkelgrün), nicht aus --accent-light. Die Schriftfarbe
     blieb dabei unverändert bei --text (dunkel im Light-Theme -> schlecht
     lesbar auf dem dunkelgrünen Hover-Hintergrund; im Dark-Theme zufällig
     schon hell genug). Anders als beim Hintergrund gibt es für color keine
     konkurrierende Regel von "button:hover:not(:disabled)" (die setzt nur
     background) - die hier ergänzte Regel gewinnt daher ganz regulär gegen
     die Basisregel ".location-menu-item { color: var(--text); }" (0,1,0). */
  .location-menu-item:hover { background: var(--accent-light); color: #fff; }
  /* Altersgesperrte Firmentypen bleiben anklickbar (die Detailseite erklärt
     den Sperrgrund, genau wie ein Klick auf das gesperrte Kartengebäude
     selbst) - nur optisch als "noch nicht verfügbar" markiert. */
  .location-menu-item.locked { opacity: 0.55; }
  .location-menu-empty { font-size: calc(12px + 1pt); color: var(--muted); padding: 8px 10px; }
  .age-tag {
    font-size: calc(11px + 1pt);
    font-weight: 600;
    /* Gleicher brauner Verlauf wie #locationTag (Nutzeranfrage 22.08.2026,
       "wie auch das Ortsdropdown ist") statt des bisherigen grünen
       var(--accent-light) - dieselben Farbwerte, damit beide Pillen im
       Header optisch zusammengehören. */
    background: linear-gradient(155deg, #8a6a49, #4c3a28);
    /* Nutzeranfrage 21.08.2026 - "alle Buttons/Elemente im Header sollen den
       Schatten der drei rechten Buttons (Theme/Sprache/Logout) bekommen".
       .age-tag wird nur hier im Header verwendet, gleicher Wert wie
       #themeToggleBtn/#langFlagBtn/#headerLogoutBtn. */
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    /* Nutzerreport 14.08.2026 - "Klick auf die Zeitalter-Anzeige macht gar
       nichts": .header-center (der absolut positionierte Desktop-Mittelslot,
       siehe unten) hat bewusst pointer-events:none, damit er die darunter-
       liegenden Nav-Buttons/Kennzahlen-Chips nicht blockiert - das machte
       aber auch #ageTag selbst unklickbar, obwohl es der einzige sichtbare
       Inhalt des Slots ist. pointer-events:auto reaktiviert Klicks gezielt
       nur auf dem Tag selbst, ohne die Durchlässigkeit des restlichen Slots
       aufzuheben. */
    pointer-events: auto;
    color: #f2e8d5;
    padding: 3px 10px;
    border-radius: 999px;
    /* Sicherheitsnetz für den Desktop-Mittelslot (.header-center, siehe unten):
       Reicht dessen per flex:1 zugeteilter Platz nicht für den vollen Text
       (z. B. beim langen Zeitalter-5-Text bei gleichzeitig breitem Nav), soll
       der Text selbst per Ellipsis kürzen statt sichtbar in Nav-Buttons oder
       die Kennzahlen-Chips hineinzulaufen - dieselbe "ehrlich degradieren"-
       Linie wie beim Karten-Layout. min-width:0 erlaubt dem Flex-Kind
       überhaupt erst, unter seine eigene Inhaltsbreite zu schrumpfen. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
    min-width: 0;
    cursor: pointer;
  }
  /* Nutzeranfrage 14.08.2026 - Klick springt zur Zeitalter-Fortschritt-Karte
     in der Firmenzentrale (siehe #ageTag-Klick-Handler weiter unten) - jetzt
     (22.08.2026) auch farblich exakt dasselbe Hover-Muster wie
     #locationTag:hover direkt daneben, statt nur strukturell gleich. */
  .age-tag:hover { background: linear-gradient(155deg, #9c7a52, #5a4530); color: #f2e8d5; }
  /* Zeitalter-Info komplett im Header zentriert (Nutzeranfrage 05.08.2026,
     Desktop, überarbeitet 06.08.2026 - siehe unten). #ageTag stand
     ursprünglich als normales Flex-Kind von .header-left einfach dort, wo es
     nach Logo/Nav-Buttons/Ortsauswahl zufällig hinfiel, nie in der Mitte der
     Kopfzeile. Ein erster Versuch mit position:absolute (Mittelpunkt relativ
     zum ganzen Header) wurde damals verworfen: Bei genug Nav-Buttons
     (Dashboard/Zonen/Ranglisten/Zur Karte) plus Ortsauswahl reichte
     .header-left schon bei 1280px bis über die geometrische Mitte hinaus -
     eine absolute Zentrierung hätte dort sichtbar mit dem Nav überlappt.
     Stattdessen bekam #ageTag einen Flex-"Mittelslot" (flex:1 1 0 zwischen
     .header-left und .stats), zentriert per justify-content:center INNERHALB
     dieses Slots.

     Nutzerreport 06.08.2026 - "die Angabe des Zeitalters soll wirklich exakt
     mittig vom Screen sein": Genau das leistete der Flex-Slot-Ansatz nicht,
     nur zufällig annähernd. Der Slot ist der Platz, der NACH Abzug von
     .header-left und .stats übrig bleibt - seine eigene Mitte liegt exakt
     dann in der Bildschirmmitte, wenn .header-left und .stats GLEICH breit
     sind. Das sind sie in der Praxis nie (unterschiedliche Anzahl/Breite von
     Nav-Buttons vs. Kennzahlen-Chips) - #ageTag lag dadurch immer spürbar
     neben der echten Mitte, zusätzlich schwankend, wenn sich .header-lefts
     Breite je nach Ansicht änderte (siehe #backBtn-Fix oben, der das für
     genau diesen Fall bereits stabilisiert).

     Echte Bildschirmmitte statt einer zufälligen Slot-Mitte: .header-center
     ist jetzt position:absolute, dadurch aus dem Flex-Fluss von <header>
     herausgenommen (die verbleibenden zwei Flex-Kinder .header-left/.stats
     teilen sich seither ganz regulär den Rest per justify-content:
     space-between, unverändert). left:0/right:0 spannen es dabei über die
     GESAMTE Breite von header - wichtig zu wissen: `left`/`right` bei
     position:absolute messen vom PADDING-Rand des Elternelements, nicht von
     der Innenkante des Paddings, header hat aber kein linkes/rechtes
     border (nur border-bottom), wodurch Padding-Rand und Border-Rand
     ohnehin zusammenfallen - header's eigenes, asymmetrisches Padding
     (links 20px, rechts die JS-gemessene --header-controls-reserve) muss
     dadurch NICHT gesondert herausgerechnet werden, left:0/right:0 landen
     bereits exakt an header's äußeren Kanten (ein erster Versuch mit
     zusätzlichem negativem Ausgleich basierte auf einer falschen Annahme
     genau dieses Verhaltens und schoss dadurch beidseitig über header
     hinaus, live nachgemessen: -20px bis +1132px bei einer tatsächlichen
     Header-Breite von nur 946px). Da header selbst kein eigenes max-width
     hat, deckt .header-center dadurch exakt die volle (durch die
     projektweite erzwungene Scrollbar-Reserve, `html{overflow-y:scroll}`,
     bereits um die Scrollbar-Breite bereinigte) sichtbare Bildschirmbreite
     ab - justify-content:center darin trifft dadurch die tatsächliche
     Bildschirmmitte, unabhängig von .header-left/.stats jeweiliger Breite.
     pointer-events:none auf dem jetzt potenziell wieder überlappenden Slot
     lässt Klicks zu darunterliegenden Nav-Buttons durch, falls die
     Zeitalter-Anzeige bei sehr vielen Buttons doch mit ihnen kollidiert -
     die bereits bestehende Ellipsis auf .age-tag (overflow:hidden/
     min-width:0) bleibt als "ehrlich degradieren"-Sicherheitsnetz für genau
     diesen Fall bestehen. Auf Mobile bleibt das bestehende Grid-Layout
     unverändert: .header-center wird dort weiterhin display:contents (wie
     .header-left), wodurch #ageTag von der separaten mobilen #ageTag-Regel
     platziert wird - die Positionsregeln hier greifen dort gar nicht. */
  .header-center {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    pointer-events: none;
  }
  .stats {
    display: flex;
    gap: 10px;
    font-size: calc(14px + 1pt);
    font-weight: 700;
    /* Ohne Umbruch wurde der FP-Chip auf 320px-Geräten rechts abgeschnitten
       (Header-scrollWidth 334 bei 320 clientWidth) - die Chips haben selbst
       nowrap, konnten als Gruppe aber nicht auf eine zweite Zeile ausweichen. */
    flex-wrap: wrap;
  }
  .stat {
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--accent-light);
    padding: 6px 12px;
    border-radius: 10px;
    /* Nutzeranfrage 21.08.2026 - "alle Buttons/Elemente im Header sollen den
       Schatten der drei rechten Buttons (Theme/Sprache/Logout) bekommen".
       Gilt nur im Header (.stat wird sonst nirgends im Spiel verwendet),
       gleicher Wert wie #themeToggleBtn/#langFlagBtn/#headerLogoutBtn. */
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    color: var(--accent-dark);
    white-space: nowrap;
    /* see the dark-mode override further down -- MC/FP switch to the same
       white the Profil button uses, since --accent-dark is a light green
       there and reads washed out on the dark --accent-light chip */
  }
  /* Dark mode: MC/FP chips take the same white text as the Profil button
     (user request 29.07.2026). In light mode --accent-dark is a deep green
     that reads well on the pale chip; in dark mode it's a *light* green
     sitting on an already-dark chip, which looked washed out next to the
     crisp white nav buttons. Excludes .warning-btn, which also carries
     .stat -- its red/orange is a deliberate problem signal (see the
     nav-button comment above) and would otherwise be overridden by this
     more specific selector. */
  :root[data-theme="dark"] .stat:not(.warning-btn) { color: white; }
  /* Sprachumschalter (Nutzeranfrage 31.07.2026) - Flagge fest oben rechts im
     Viewport (siehe HTML-Kommentar bei #langSwitch, außerhalb von <header>,
     dafür da), Klick öffnet ein kleines Dropdown-Menü mit den zwei Sprachen.
     position:relative bleibt als Anker für das absolut positionierte Menü
     darunter nötig, unabhängig vom position:fixed des äußeren Containers. */
  .lang-switch {
    position: fixed;
    /* 21.156px (25.08.2026 neu gemessen, Nutzerreport "sollen oben bündig mit
       den anderen Buttons abschließen" - die drei Buttons saßen 2px zu weit
       oben). Vorheriger Wert 19.157px ging noch von echt gemessenen 31px
       Nav-Button-Höhe aus (siehe Herleitung, die an dieser Stelle stand) -
       zwischenzeitlich (unabhängige spätere Änderung) sind die Nav-Buttons
       (#dashboardBtn etc.) real auf 33px gewachsen, wodurch sie 2px tiefer
       in der 45.313px-hohen .header-left-Zeile zentriert liegen als die
       Rechnung damals annahm. Direkt am echten, gemessenen
       dashboardBtn.getBoundingClientRect().top kalibriert statt erneut
       vorwärts gerechnet, um nicht erneut von einer inzwischen überholten
       Zwischenannahme abhängig zu sein. NUR der Desktop-Wert: Auf Mobile
       (siehe @media max-width:820px weiter unten) sind die Nav-Buttons
       ausgeblendet (eigene Tab-Leiste statt Header-Buttons) - dort ist das
       Logo das einzige Element in Grid-Zeile 1 und top:12 (Headers eigenes
       padding-top) weiterhin exakt richtig, siehe die dortige
       Override-Regel. Ändert sich die Desktop-Nav-Button- oder Logo-Höhe
       erneut, muss dieser Wert neu gemessen werden. */
    top: 21.156px;
    right: 14px;
    /* Nutzeranfrage 25.08.2026 ("Buttons oben rechts auch bei geöffnetem
       Postfach-Popup erreichbar machen") - von 60 auf denselben Wert wie
       #legalFooter (210) angehoben, aus genau demselben, dort bereits
       dokumentierten Grund: schlägt jedes .modal-overlay (200) und liegt
       damit sichtbar UND klickbar über einem geöffneten Popup, statt dass
       dessen Hintergrund-Nebel Klicks hier abfängt (vorher passierte
       genau das: ein Klick auf den Theme-Umschalter schloss stattdessen nur
       das Popup, siehe .modal-overlay-Klick-außerhalb-schließt-Verhalten in
       bootstrap.js). */
    z-index: 210;
    /* Seit dem Theme-Umschalter (Nutzeranfrage 03.08.2026) stehen hier zwei
       Buttons nebeneinander statt nur der Flagge. */
    display: flex;
    align-items: center;
    gap: 8px;
  }
  #themeToggleBtn, #langFlagBtn, #headerLogoutBtn {
    cursor: pointer;
    border: 1px solid var(--border);
    background: var(--card);
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    font-size: calc(18px + 1pt);
    line-height: 1;
    /* Padding oben/unten bewusst asymmetrisch (Nutzeranfrage 21.08.2026,
       "Icons 2px nach oben schieben", danach "wieder 1px nach unten") statt
       der ursprünglich symmetrischen 6px/6px: verschiebt die Icons innerhalb
       der weiterhin festen 31px-Höhe (siehe Kommentar unten) netto 1px nach
       oben, ohne Höhe oder Breite der Buttons zu verändern. */
    padding: 5px 10px 7px;
    width: auto;
    /* Nutzeranfrage 05.08.2026 - "Logo muss auf exakt derselben Höhe sein wie
       die Buttons rechts im Header": Trotz identischer padding/font-size/
       line-height-Werte kamen die drei Buttons bisher auf leicht
       unterschiedliche gemessene Höhen (Theme 32px, Sprache 30px, Logout
       31px) - der Unterschied liegt an ihrem jeweiligen Icon-Inhalt (Emoji
       vs. eingebettetes SVG-Flaggensymbol vs. eingebettetes Logout-SVG),
       deren eigene intrinsische Höhe line-height:1 nicht immer exakt
       überschreibt. Eine explizite Höhe ist robuster als sich auf
       Icon-Metriken zu verlassen - dieselbe Logik, die header h1 bereits auf
       32px gebracht hat (Abschnitt 18).
       Nachtrag (20.08.2026, Nutzeranfrage) - "sollten die selbe Höhe haben wie
       die anderen Buttons im Header": Die übrigen Header-Nav-Buttons
       (#dashboardBtn/#zonesBtn/#leaderboardBtn/#backBtn, height:auto bei
       padding 7px + font-size 13px) rendern echt gemessen auf 31px, nicht
       32px - von 32 auf 31 angeglichen, damit die drei rechten Buttons nicht
       mehr 1px höher wirken als der Rest der Kopfzeile. */
    height: 31px;
    border-radius: 8px;
  }
  /* Nutzeranfrage 05.08.2026 - "Warnhinweis-Button soll gleich breit sein wie
     der Hell/Dunkel-Button": ein fester Wert statt weiterhin "width:auto",
     damit .header-warn-group .warning-btn (siehe Mobile-Regel unten) exakt
     denselben Zahlenwert übernehmen kann, unabhängig vom jeweils
     dargestellten Emoji-Zeichen (☀️/🌙 hier, ⚠️/🚨 dort - einzelne Emoji
     haben leicht unterschiedliche natürliche Breiten). Nur #themeToggleBtn,
     nicht auch Sprache/Logout - die beiden sollen weiterhin ihre eigene,
     inhaltsgetriebene Breite behalten, nur der Warn-Button muss hierzu passen. */
  #themeToggleBtn { width: 44px; }
  /* Logout (Nutzeranfrage 03.08.2026): rot, weil es die einzige Aktion in
     dieser Leiste ist, die einen aus dem Spiel wirft - Sprache und Design
     sind folgenlos umschaltbar. display:flex zentriert das SVG sauber, das
     sonst auf der Textbasislinie säße. Beim Überfahren füllt die Fläche rot
     mit weißem Symbol, statt vom generischen button:hover ein grünes
     (also gegenteiliges) Signal zu erben. */
  #headerLogoutBtn {
    color: var(--danger);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Nutzeranfrage 21.08.2026 - "Icon vom Logout-Button wieder 1px runter":
       überschreibt nur für diesen Button das gemeinsame 5px/7px-Padding oben
       (siehe #themeToggleBtn/#langFlagBtn/#headerLogoutBtn-Regel) zurück auf
       die ursprünglichen symmetrischen 6px/6px - Theme/Sprache bleiben bei
       5px/7px (1px höher). */
    padding-top: 6px;
    padding-bottom: 6px;
  }
  #headerLogoutBtn:hover:not(:disabled) { background: var(--btn-danger-bg-hover); color: #fff; border-color: var(--btn-danger-bg-hover); }
  /* Dark Mode (Nutzeranfrage 21.08.2026): dieselbe Chip-Farbe wie die MC/FP-
     Anzeigefelder (.stat, background: var(--accent-light)) statt des
     generischen --card - im Light Mode bewusst unverändert (dort passt
     --card zum übrigen hellen Kartenlook), nur im Dark Mode wirkte --card
     (dunkles Grau) neben den grünen .stat-Chips uneinheitlich. Nur
     background, nicht color - #headerLogoutBtn behält sein rotes
     Warnsignal (siehe Kommentar oben), Theme/Sprache ihr jeweiliges Icon. */
  :root[data-theme="dark"] #themeToggleBtn,
  :root[data-theme="dark"] #langFlagBtn,
  :root[data-theme="dark"] #headerLogoutBtn {
    background: var(--accent-light);
  }
  /* Nutzeranfrage 03.09.2026 - "Mond auf dem Hell/Dunkel-Button ist nicht
     mittig", zunächst mit exakt vermessenen, PRO ZEICHEN asymmetrischen
     padding-Werten gefixt (☀️/🌙 als Emoji-Glyphen haben unterschiedlich
     viel eigene "Tinte" oberhalb der Schriftgrundlinie, siehe Git-Historie
     für die Herleitung per canvas measureText()). Nutzerreport 04.09.2026,
     live auf einem echten Android-Gerät nachgestellt ("Sonne nicht annähernd
     zentriert", obwohl in dieser Entwicklungsumgebung exakt vermessen) zeigt:
     Emoji-Schriftmetriken sind geräte-/font-abhängig (Windows Segoe UI Emoji
     vs. Android Noto Color Emoji o.ä.) - ein hier fest vermessener Wert kann
     auf einem anderen Gerät danebenliegen, unabhängig davon, wie sorgfältig
     er hergeleitet wurde. Ein erster SVG-Ersatzversuch (dünner grauer
     Umriss) wurde als "richtig hässlich" abgelehnt, eine ausgefüllte/farbige
     Fassung (siehe updateHeaderControls(), avatars.js) auf Nachfrage
     akzeptiert - ein Vektor-Icon hat keine schriftartabhängige Devianz,
     schlicht symmetrisches Padding reicht dadurch aus und bleibt auf jedem
     Gerät exakt zentriert, unabhängig vom gerenderten Font. Eigene, von der
     asymmetrischen Basisregel/@media-Regel unabhängige Werte (höhere
     Spezifität als beide, siehe deren jeweilige Kommentare), gelten für
     beide Breakpoints gleichermaßen. */
  #themeToggleBtn {
    padding-top: 6px;
    padding-bottom: 6px;
  }
  /* Hover für Theme-Umschalter + Sprachflagge (Nutzeranfrage 04.08.2026) -
     bis dahin fehlte hier jeder Effekt: Der generische
     "button:hover:not(:disabled)"-Hintergrund (Zeile ~919) wird von der
     ID-Regel oben ("#themeToggleBtn, #langFlagBtn { background: var(--card) }")
     unabhängig vom Hover-Zustand geschlagen, da ein ID-Selektor auch ohne
     :hover spezifischer ist als "button:hover:not(:disabled)" - dieselbe
     Fehlerklasse wie zuvor schon beim Sprach-Dropdown, den Header-Chips und
     dem Tutorial-Panel. Eigene ID-Hover-Regel statt die generische Regel zu
     verändern, damit andere Buttons nicht mit betroffen sind.
     Bugfix (Nutzerreport 25.08.2026, "auf Mobile bleibt der Button-
     Hintergrund nach dem Klick grün") - @media (hover: hover) begrenzt diese
     Regel auf Geräte, die :hover tatsächlich unterstützen (Maus/Trackpad).
     Auf Touchscreens gibt es kein echtes "Hover" - der Browser simuliert es
     nach einem Tap trotzdem und lässt es bis zum nächsten Tap woanders
     "hängen", wodurch der grüne Hover-Hintergrund optisch wie ein Klebe-
     Zustand wirkte. */
  @media (hover: hover) {
    #themeToggleBtn:hover:not(:disabled), #langFlagBtn:hover:not(:disabled) {
      background: var(--accent);
      border-color: var(--accent);
    }
  }
  /* Nutzeranfrage 25.08.2026 ("in der Desktop-Version die drei Buttons oben
     rechts im Header um 25% in der Größe reduzieren") - eigener,
     min-width:821px-gescopter Block (Umkehrung des bestehenden 820px-Mobile-
     Breakpoints weiter unten) statt die Desktop-Basisregel oben direkt zu
     ändern: die Mobile-Regel überschreibt bisher nur height/padding-top/
     -bottom dieser drei Buttons, nicht font-size/width/border-radius - eine
     Änderung an der Basisregel hätte also ungewollt auch auf Mobile
     durchgeschlagen. Muss NACH der #headerLogoutBtn-Padding-Regel oben
     stehen (gleiche Spezifität, einzelner ID-Selektor je Regel) - sonst
     gewinnt bei gleicher Spezifität die spätere, unbedingte Regel und
     überschreibt das hier gesetzte padding-top/-bottom wieder zurück.
     Alle Werte hier waren ursprünglich die Basiswerte × 0,75 (31px→23.25px,
     44px→33px, 8px→6px, Padding 5/10/7px→3.75/7.5/5.25px), außer der "+1pt"-
     Barrierefreiheits-Mindestgröße im font-size-calc() - die bleibt bewusst
     UNSKALIERT (sie ist ein fester Mindestwert, kein proportionaler Anteil).
     Flagge (#langFlagBtn, Inline-SVG, constants.js FLAG_SVG_*) und Logout-
     Icon (#headerLogoutBtn, Inline-SVG in index.html) tragen ihre Maße als
     feste width/height-Attribute statt em/font-size - font-size allein hätte
     sie unverändert groß gelassen, während der Button drumherum schrumpft.
     Eigene SVG-Regeln unten skalieren beide separat um denselben Faktor.
     Nutzeranfrage 04.09.2026 ("die drei Buttons... auf 1,5-fache Größe
     bringen"): alle Werte hier zunächst nochmal × 1,5 draufgerechnet (macht
     in Summe × 1,125 gegenüber dem ursprünglichen Mobile-Basiswert). Folge-
     Nutzeranfrage 04.09.2026 ("jetzt zu groß, bitte etwa in der Mitte
     zwischen aktueller und letzter Größe"): auf die Mitte zwischen dem
     × 1,5-Wert und dem vorherigen × 0,75-Basiswert zurückgenommen, also
     × 1,125 gegenüber dem × 0,75-Basiswert bzw. in Summe × 0,84375 gegenüber
     dem ursprünglichen Mobile-Basiswert - die Buttons sind auf Desktop damit
     wieder etwas kleiner als auf Mobile, wie schon vor der 1,5×-Anfrage. */
  @media (min-width: 821px) {
    #themeToggleBtn, #langFlagBtn, #headerLogoutBtn {
      font-size: calc(16.875px + 1pt);
      padding: 4.6875px 9.375px 6.5625px;
      height: 29.0625px;
      border-radius: 7.5px;
    }
    #themeToggleBtn { width: 41.25px; }
    /* Nutzerreport 04.09.2026 - dieselbe grouped padding-Regel oben (gleiche
       Spezifität, aber später im Dokument als die #themeToggleBtn-Regel bei
       den Sonne/Mond-Icons) überschrieb dort sonst weiterhin das
       asymmetrische Padding - eigene, symmetrische Override-Regel direkt
       danach, analog zu #headerLogoutBtn direkt darunter. Dieselbe
       Begründung gilt seit der Folgeanfrage auch für #langFlagBtn: die
       Flagge ist ein deterministisches SVG (kein Emoji, siehe
       FLAG_SVG_*-Kommentar), asymmetrisches Padding hatte hier also nie
       einen Font-Metrik-Grund, sondern war schlicht dieselbe ererbte
       "1px nach unten"-Notiz wie einst bei #themeToggleBtn - jetzt ebenfalls
       symmetrisch. Icon-Größen hier mit demselben 1,125-Faktor wie der Rest
       dieses Blocks skaliert, sonst blieben die SVGs (feste width/height-
       Attribute) unverändert groß, während der Button drumherum wächst. */
    #themeToggleBtn { padding-top: 6.09375px; padding-bottom: 6.09375px; }
    #themeToggleBtn svg { width: 16.875px; height: 16.875px; }
    #langFlagBtn { padding-top: 5.625px; padding-bottom: 5.625px; }
    #headerLogoutBtn { padding-top: 5.625px; padding-bottom: 5.625px; }
    #langFlagBtn svg { width: 20.625px; height: 15px; }
    #headerLogoutBtn svg { width: 15.9375px; height: 15.9375px; }
  }
  /* Gleicher Holzsteg wie .location-menu/section.card/.kpi (Nutzeranfrage
     22.08.2026) - dieselbe background-clip-Technik, siehe dortigen
     Kommentar. */
  .lang-menu {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    border: 4px solid transparent;
    border-radius: 10px;
    background: linear-gradient(var(--card), var(--card)) padding-box, linear-gradient(155deg, #8a6a49, #4c3a28) border-box;
    box-shadow: 0 4px 16px rgba(0,0,0,0.15);
    padding: 6px;
    z-index: 50;
    min-width: 150px;
  }
  .lang-option {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 8px;
    width: 100%;
    height: auto;
    background: none;
    border: none;
    text-align: left;
    padding: 8px 10px;
    border-radius: 6px;
    font-size: calc(14px + 1pt);
    color: var(--text);
    cursor: pointer;
  }
  /* svg ist standardmäßig inline mit Basislinien-Lücke darunter - flex
     verhindert das für Flaggen-Icons in Button/Span. */
  #langFlagCurrent, .lang-option-flag { display: flex; border-radius: 2px; overflow: hidden; }
  /* Weiße Schrift beim Überfahren (Nutzeranfrage 01.08.2026). Der Hintergrund
     wechselt dafür von --accent-light (sehr helles Grün) auf das volle
     --accent - auf dem hellen Grün wäre weiße Schrift praktisch unlesbar.
     Gleiches Muster wie bei #locationTag:hover.
     Die :hover-Regeln stehen bewusst NACH .lang-option.active und nennen
     .active zusätzlich explizit: sonst gewönne die gleich spezifische
     .active-Farbregel per Quellreihenfolge und die aktive Sprache bliebe beim
     Überfahren grün statt weiß. */
  .lang-option.active { font-weight: 700; color: var(--accent-dark); }
  :root[data-theme="dark"] .lang-option.active { color: var(--accent); }
  /* #langMenu davor, damit die Regel die generische
     "button:hover:not(:disabled)"-Regel (--accent-dark) schlägt - ohne sie
     bekam die nicht-aktive Sprache einen anderen Grünton als die aktive. */
  #langMenu .lang-option:hover, #langMenu .lang-option.active:hover { background: var(--btn-bg); color: #fff; }
  :root[data-theme="dark"] .lang-option.active:hover { color: #fff; }
  /* Header badges, not content action buttons -- same "keep natural size"
     exception as the nav pills above. Extended (Nutzeranfrage 02.08.2026) to
     the three Kennzahlen-Chips (Coins/FP/Diamanten) - alle drei sind jetzt
     Buttons mit eigenem Sprung-Ziel und sollen wie die MC/FP-<div>s vorher
     schrumpfen/wachsen mit ihrem Inhalt, statt auf die generische
     200x42px-Button-Größe aufgeblasen zu werden. */
  #adminBtn, .warning-btn, #coinsBtn, #fpBtn, #diamondsBtn {
    width: auto;
    height: auto;
  }
  /* Nutzeranfrage 12.08.2026: Die Breite von MC/FP/Diamanten soll sich NICHT
     ändern, nur weil eine Ziffer bei gleicher Zeichenanzahl grafisch etwas
     breiter/schmaler ist als eine andere (z. B. "1" vs. "8" in den meisten
     proportionalen Schriften) - nur eine tatsächliche Änderung der
     Zeichenanzahl (250 → 1.250) soll die Breite ändern. Kein manuelles
     Padding nötig: font-variant-numeric: tabular-nums zwingt jede Ziffer auf
     dieselbe feste Zeichenbreite (OpenType-"tnum"-Feature, von praktisch
     jeder Systemschrift unterstützt) - Nicht-Ziffern (Komma, Punkt,
     " MC"/" FP") bleiben unverändert, wodurch die Gesamtbreite exakt bei
     gleicher Zeichenanzahl konstant bleibt, unabhängig davon, welche
     konkreten Ziffern erscheinen. */
  #coinsVal, #fpVal, #diamondsVal { font-variant-numeric: tabular-nums; }
  .warning-btn {
    border: none;
    cursor: pointer;
    font-family: inherit;
    font-size: calc(14px + 1pt);
    background: #fdecec;
    /* var(--danger) direkt gab hier nur 3,90:1 auf der blassen Fläche (unter
       4,5:1) - --btn-danger-bg-hover ist derselbe Rotton, nur dunkler genug
       (5,55:1). Nur die LIGHT-Ruhefarbe betroffen; der Dark-Mode-Override
       direkt darunter (color: #22292a) gewinnt dort unverändert weiter. */
    color: var(--btn-danger-bg-hover);
  }
  /* Hover-Schriftfarben der Header-Chips (Nutzeranfrage 03.08.2026).
     Vorher hatte keiner der vier Chips eine eigene :hover-Farbregel: MC/FP/
     Diamanten sind <button> und erbten deshalb vom generischen
     "button:hover:not(:disabled)" den grünen Hintergrund, behielten aber ihre
     grüne Schrift (--accent-dark) - grün auf grün, praktisch unsichtbar.
     Dieselbe Klasse Fehler wie beim Sprach-Dropdown und dem Tutorial-Panel.

     Der Warn-Chip brauchte zusätzlich einen neuen Hover-HINTERGRUND: Weiß auf
     dem bisherigen blassen Rosa (#fbdcdc) wäre unlesbar gewesen, deshalb füllt
     er beim Überfahren mit dem vollen --danger. Im Dark Mode andersherum -
     dort bleibt die blasse Fläche und die Schrift wird dunkelrot (ausdrücklich
     so gewünscht); --danger ist dort ein HELLES Rot und käme auf der blassen
     Fläche nicht an. */
  #coinsBtn:hover, #fpBtn:hover, #diamondsBtn:hover { color: #fff; }
  /* :root davor ist nötig, nicht kosmetisch: ".warning-btn:hover" allein ist
     (0,2,0) spezifisch und verliert gegen "button:hover:not(:disabled)"
     (0,2,1) - der Warn-Chip ist ein <button>, sein Hover-Hintergrund kam
     dadurch bislang aus der generischen Regel und war GRÜN statt rot (galt
     schon vor dieser Änderung, fiel nur nie auf, weil die Schrift ebenfalls
     nicht überschrieben wurde). Mit :root sind es (0,3,0) und die Regel greift. */
  /* @media (hover: hover) (Nutzerreport 26.08.2026, "auf Mobile bleibt die
     Hover-Farbe beim Warn-/Aktivitätsbonus-Button nach dem Tap hängen") -
     dieselbe Ursache/derselbe Fix wie schon bei #themeToggleBtn/#langFlagBtn
     weiter oben: Touchscreens haben kein echtes Hover, der Browser simuliert
     es nach einem Tap trotzdem und lässt es bis zum nächsten Tap woanders
     "hängen". Gilt hier für BEIDE Hover-Regeln unten (roter Warn-Zustand UND
     der goldene .bonus-ready-Zustand des Aktivitätsbonus). */
  @media (hover: hover) {
    :root .warning-btn:hover { background: var(--btn-danger-bg-hover); color: #fff; }
    :root[data-theme="dark"] .warning-btn:hover { background: #fbdcdc; color: #7f1d1d; }
  }
  /* @media (hover: none)-Gegenstück: Ohne das würde auf einem Touchscreen
     nicht einfach "kein Hover" passieren, sondern die generische, NICHT
     @media(hover: hover)-gescopte "button:hover:not(:disabled)"-Regel
     (Zeile ~3213, grüner Hintergrund) durchschlagen - genau die Regel, die
     die :root-Präfix-Regel oben eigentlich ausstechen soll (siehe deren
     Kommentar), aber nur wenn diese auch aktiv ist. Statt die generische
     Regel selbst anzufassen (beträfe jeden Button im Spiel, nicht nur diesen)
     wird :hover hier auf Touch einfach explizit wieder auf die Ruhefarben
     zurückgesetzt - optisch identisch zu "kein Hover". */
  @media (hover: none) {
    :root .warning-btn:hover { background: #fdecec; color: var(--btn-danger-bg-hover); }
    :root[data-theme="dark"] .warning-btn:hover { background: #fdecec; color: #22292a; }
  }
  /* Dark mode: the warning chip keeps its fixed pale-red background in both
     themes (it's a problem signal, see the .stat exclusion above), but
     --danger becomes a *light* red there -- light-on-pale-pink read washed
     out. Explicitly dark text instead (user request 29.07.2026), same
     reasoning and same value as the map labels further down. */
  :root[data-theme="dark"] .warning-btn { color: #22292a; }
  /* Aktivitäts-Bonus wieder zurück in .header-warn-group statt eines Punkts
     auf dem Dashboard-Button (Nutzeranfrage 12.08.2026, revidiert den
     Dot-Ansatz aus dem vorigen Durchgang - siehe 12_UI_UX.md Abschnitt 63):
     #storageWarningBtn zeigt jetzt ENTWEDER die Lager-voll-Warnung ODER,
     sobald der Bonus abholbereit ist, ein Geschenk-Icon - beide teilen sich
     dasselbe Element und damit automatisch exakt dieselben Maße wie
     #loanWarningBtn (Padding/Rundung/Schriftgröße/Breite kommen unverändert
     aus .warning-btn bzw. der Mobile-Regel weiter unten). Eigene Gold-Farbe
     statt des roten Warn-Schemas - ein Geschenk ist kein Problem, sondern
     eine Belohnung, dieselbe Semantik-Überlegung wie beim vorigen Dot
     (#e8b923, "Gold"-Avatar-Rahmen). Bewusst dieselbe blasse Fläche in
     beiden Themes (analog zum roten Warn-Chip direkt darüber) statt eines
     satten Gold-Hover mit weißer Schrift - Kontrastrechnung ergab dafür nur
     ~1,8:1 (weiß auf #e8b923), die Hover-Stufe dunkelt die Fläche stattdessen
     nur leicht ab, Text bleibt durchgängig dunkel (~5,6:1 auf der hellsten
     Fläche). :root-Präfix aus demselben Spezifitäts-Grund wie bei
     .warning-btn:hover oben nötig, sonst verliert die Regel im Dark Mode
     gegen die direkt darüberstehende Farbüberschreibung. */
  :root .warning-btn.bonus-ready,
  :root[data-theme="dark"] .warning-btn.bonus-ready {
    background: #fdf1cf;
    color: #7a5c00;
  }
  @media (hover: hover) {
    :root .warning-btn.bonus-ready:hover,
    :root[data-theme="dark"] .warning-btn.bonus-ready:hover {
      background: #f7e3a6;
      color: #7a5c00;
    }
  }
  /* Gleiches @media(hover: none)-Gegenstück wie beim roten Warn-Zustand oben. */
  @media (hover: none) {
    :root .warning-btn.bonus-ready:hover,
    :root[data-theme="dark"] .warning-btn.bonus-ready:hover {
      background: #fdf1cf;
      color: #7a5c00;
    }
  }
  /* Nutzerreport 12.08.2026: Der Geschenk-Zustand zeigte sein Textlabel
     ("Bonus abholbar") bisher nur auf Mobile ausgeblendet - die Regel steckte
     in der @media(max-width:820px)-Sektion, wo sie nur wirkte, weil dort
     zusätzlich eine feste width:44px galt. Am Desktop hat .warning-btn aber
     width:auto (Zeile ~611) und der volle Text blieb sichtbar, wodurch der
     Button "riesig" wirkte. Anders als bei storageWarningCount/loanOverdue
     (dort ist der Text am Desktop eine sinnvolle Zusatzinfo) soll dieser
     Button laut ausdrücklicher Nutzeranfrage NIE Text zeigen, auf keiner
     Breite - die Regel ist deshalb hier global statt mobile-gescoped. Das
     title-Attribut trägt weiterhin den vollen Text als Tooltip. */
  #storageWarningBtn span[data-i18n="hd.activityBonusReady"] { display: none; }

  /* Mobile-Navigation (Nutzeranfrage 30.07.2026, löst 12_UI_UX.md Abschnitt
     2a's "keine echte Tab-Leiste"-Entscheidung gezielt ab): auf schmalen
     Bildschirmen brach die Header-Nav-Zeile (Dashboard/Zonen/Rangliste/
     Zurück/Profil) auf bis zu 3 Zeilen um - ~17% der Viewport-Höhe allein
     für Navigation, bevor überhaupt Content sichtbar war. Ersetzt durch eine
     feste untere Tab-Leiste (daumenfreundlich, gängiges Mobile-App-Muster),
     der Header wird dafür auf eine schlanke Statuszeile reduziert (Logo +
     MC/FP/Warn-Badges). Am Desktop bleibt alles unverändert - #mobileTabBar
     ist dort unsichtbar, die Header-Nav-Buttons bleiben wie gehabt.
     display:none per Default, erst die @media-Regel unten schaltet auf
     display:flex um - kein inline style="display:none", damit dieselbe
     "leeren style.display-String setzen, CSS übernimmt"-Technik wie beim
     Header selbst (siehe enterGame()) funktioniert. */
  #mobileTabBar {
    display: none;
    position: fixed;
    /* Rückt über den fixierten Rechtstexte-Footer (Nutzeranfrage 05.08.2026,
       siehe #legalFooter) statt selbst am true Bildschirmrand zu sitzen -
       gemessen statt geraten (--legal-footer-height, syncHeaderControlsReserve()),
       da die Footer-Höhe von Übersetzungslänge/Schriftgröße abhängt. Safe-Area
       braucht die Tab-Leiste dadurch nicht mehr selbst, das übernimmt jetzt
       der Footer, der als einziges Element noch den true Rand berührt.
       -1px (Nutzeranfrage 23.08.2026): exakt ohne Überlapp (bottom ==
       --legal-footer-height, vorherige Fassung derselben Nutzeranfrage,
       "es soll keinen Overlap geben") ließ eine 1px schmale Lücke zum
       Footer sichtbar werden - Math.ceil() beim Messen der Footer-Höhe
       (siehe syncHeaderControlsReserve()) rundet nur auf volle Pixel, nicht
       auf tatsächlich gerenderte Subpixel. 1px statt der zuvor bis zu 5px
       reicht dafür bereits aus (Folgeanfrage "Button-Leiste 1px nach unten,
       um die Lücke zu schließen") und bleibt klein genug, um keine
       Tab-Button-Fläche mehr unter dem (z-index-höheren) #legalFooter zu
       verdecken - anders als die vorherigen bis zu 5px, die genau das
       verursacht hatten. */
    bottom: calc(var(--legal-footer-height, 34px) - 1px);
    left: 0;
    right: 0;
    background: var(--card);
    /* Gleicher Holzsteg wie header/#legalFooter (Nutzeranfrage 21.08.2026,
       "Rahmen bei den Menü-Buttons unten fehlt noch") - kein border-radius
       am Bildschirmrand, daher reicht border-image statt der
       background-clip-Technik der Karten. */
    /* 3px statt 4px (Nutzeranfrage 22.08.2026, "alle Rahmen einen px
       schmaler") - siehe Kommentar bei header's border-bottom für die ganze
       betroffene Holzsteg-Familie. */
    border-top: 3px solid;
    border-image: linear-gradient(90deg, #8a6a49, #4c3a28) 1;
    /* War 15, jetzt über dem Tutorial-Nebel (#tutorialBackdrop, z-index 18,
       siehe dessen Kommentar) - Nutzerreport 05.09.2026, "der Ausnahmebereich
       um die Buttons wird auf dem Smartphone teilweise mitgescrollt und sieht
       hässlich aus": bis dahin musste updateTutorialSpotlight() (tutorial.js)
       die Nebel-Aussparung bei jedem Schritt-/Scroll-/Resize-Ereignis per JS
       exakt über diese Leiste legen, damit sie nicht mitverdunkelt wird - das
       lief zwangsläufig über den Hauptthread und konnte einer schnellen
       nativen Wisch-Geste sichtbar hinterherhinken. #legalFooter (z-index 210)
       braucht dieselbe Aussparung nie, weil sein eigener, undurchsichtiger
       --card-Hintergrund plus höherer Stapelkontext den Nebel darunter
       automatisch komplett abdeckt (siehe dessen Kommentar) - genau dieser
       Trick jetzt auch hier: #mobileTabBar deckt den Nebel damit permanent
       und rein über CSS ab, keine JS-Positionierung mehr nötig. 19 reicht
       (nur 1 über dem Nebel), bleibt aber unter #tutorialPanel (20) - das
       Panel liegt also weiterhin sichtbar über der Tab-Leiste, falls es sie
       überlappt. */
    z-index: 19;
  }
  /* min-height statt height (Nutzeranfrage 23.08.2026, "Text im Footer muss
     umbrechen statt abgeschnitten zu werden, z.B. spanisches 'Dashboard'"):
     ein fester height-Wert hätte einen umgebrochenen zweizeiligen Label-Text
     (siehe .mobile-tab-label unten) einfach über den Button-Rand hinaus
     überlaufen lassen, statt Platz dafür zu schaffen - min-height lässt den
     Button (und damit die ganze #mobileTabBar-Zeile, deren übrige Tabs per
     Flex-Grundregel align-items:stretch automatisch mitwachsen) bei Bedarf
     höher werden. Die tatsächliche Höhe wird dafür zur Laufzeit gemessen und
     als --mobile-tab-bar-height in body's padding-bottom eingerechnet (siehe
     syncHeaderControlsReserve() in ui-helpers.js) - derselbe "gemessen statt
     geraten"-Ansatz wie bereits bei --legal-footer-height. */
  /* justify-content:flex-start statt :space-between (Nutzeranfrage
     26.08.2026, "falls es mindestens einen Button mit zwei Zeilen gibt,
     sollten einzeilige Texte in der Mitte der zwei Zeilen sein"): Vorher
     (Nutzeranfrage 23./24.08.2026, "Icons sollen alle auf derselben Höhe
     sein"/"Abstand oben/unten soll immer gleich groß sein") landete der
     gesamte Höhenzuwachs (wenn ein Nachbar-Tab mit zweizeiligem Label die
     ganze #mobileTabBar-Reihe per align-items:stretch höher zieht) allein in
     der Lücke ZWISCHEN Icon und Label - ein einzeiliges Label blieb dadurch
     immer unten am padding-bottom kleben, auf Höhe der ZWEITEN Zeile eines
     Nachbar-Labels, statt mittig im dadurch verfügbaren zweizeiligen Raum zu
     stehen. Jetzt übernimmt .mobile-tab-label selbst (flex:1, siehe dort)
     das Wachsen UND Zentrieren seines Inhalts - .mobile-tab-icon bleibt dabei
     unverändert direkt hinter padding-top mit dem festen 2px-Mindestabstand
     verankert (icon hat kein flex-grow, "Icons auf derselben Höhe" bleibt
     dadurch weiterhin erfüllt), padding-top/-bottom bleiben ebenfalls
     unverändert symmetrisch (das "Abstand oben/unten gleich groß"-Ziel bleibt
     dadurch ebenfalls erfüllt - beide Ränder hängen nach wie vor nur an den
     festen paddings, nicht an der Label-Höhe). justify-content ist hier
     bewusst weiterhin gesetzt (statt es ersatzlos zu streichen), auch wenn es
     durch das flex:1-Label faktisch wirkungslos ist (das Label beansprucht
     jeden verbleibenden Platz selbst) - flex-start dokumentiert die Absicht
     klarer als das inzwischen irreführende space-between. */
  .mobile-tab {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 2px;
    background: transparent;
    color: var(--muted);
    border: none;
    border-radius: 0;
    width: auto;
    /* 62px (weitere Nutzeranfrage 23.08.2026, "Box mit den Buttons unten
       wieder um 2px vergrößern"), gewachsen aus ursprünglich 56px.
       #mobileTabBar sitzt seit einer früheren Nutzeranfrage ("es soll keinen
       Overlap geben") ohne Überlapp direkt auf dem Rechtstexte-Footer (siehe
       bottom dort) - der Höhenzuwachs vergrößert dadurch (seit der
       space-between-Umstellung oben) die Lücke zwischen Icon und Label,
       nicht die Ränder. Das ist gewollt, die laufzeit-gemessene
       --mobile-tab-bar-height-Reserve in body zieht den nötigen Platz
       automatisch mit. */
    /* --mobile-tab-content-min-height (gemessen in syncHeaderControlsReserve(),
       ui-helpers.js) überschreibt den 62px-Normalfall, sobald ein
       zweizeilig umgebrochenes Label mehr Platz braucht, als der
       verschachtelte Flex-Container hier sonst zuverlässig für seinen
       eigenen Inhalt reserviert (Nutzeranfrage 24.08.2026) - ohne das würde
       der Text unten über padding-bottom hinaus überlaufen und den oben
       beschriebenen gleich großen Rand-Abstand wieder zunichtemachen. */
    min-height: var(--mobile-tab-content-min-height, 62px);
    /* Oben und unten identisch (10px statt zuvor 4px unten) - genau das
       stellt den in den Kommentaren oben beschriebenen gleich großen
       Abstand zum oberen wie zum unteren Rand her (Nutzeranfrage
       24.08.2026). */
    padding: 10px 4px;
    /* 9px statt 10px (Nutzeranfrage 23.08.2026, "Schriftgröße unter den
       Buttons minimal reduzieren, ohne Barrierefreiheit zu gefährden") -
       betrifft nur .mobile-tab-label, das diese font-size erbt (kein WCAG-
       Kriterium schreibt eine absolute Mindestschriftgröße vor; WCAG 1.4.4
       verlangt Zoombarkeit bis 200%, die unverändert funktioniert, und WCAG
       2.2 SC 2.5.8 (Mindest-Klickfläche 24×24px) hängt an .mobile-tab's
       eigener min-height, nicht an der Textgröße - beides bleibt also
       unberührt). Bewusst nur -1px statt eines größeren Sprungs, das "+1pt"
       (Nutzeranfrage 21.08.2026, "Bump every font-size by 1pt") bleibt
       erhalten, damit die Leiste nicht unter das sonst im Spiel etablierte
       Mindestniveau für kompakte Beschriftungen (z. B. .location-tag::after,
       ebenfalls 10px+1pt) fällt. */
    font-size: calc(9px + 1pt);
    font-weight: 600;
  }
  .mobile-tab:hover:not(:disabled), .mobile-tab:active:not(:disabled) { background: var(--accent-light); }
  .mobile-tab.active { color: var(--accent-dark); }
  /* flex-shrink:0 (Nutzeranfrage 26.08.2026, siehe .mobile-tab-Kommentar
     oben) - rein defensiv: das Icon soll auch auf einem extrem schmalen/
     niedrigen Bildschirm nie das Element sein, das zuerst gestaucht wird,
     unabhängig davon, ob .mobile-tab-label direkt darunter (jetzt flex:1)
     im Normalfall überhaupt je in diese Lage kommt. */
  .mobile-tab-icon { font-size: calc(20px + 1pt); line-height: 1; flex-shrink: 0; }
  /* Profil-Tab zeigt den gewählten Avatar statt eines Emoji-Symbols
     (Nutzeranfrage 05.08.2026, siehe renderHeaderPlayerTag()) - eigene runde
     Kachel statt der reinen font-size-Emoji-Größe darüber. */
  .mobile-tab-avatar {
    width: 22px;
    height: 22px;
    display: inline-block;
    border-radius: 50%;
    overflow: hidden;
  }
  /* white-space:normal statt nowrap+ellipsis (Nutzeranfrage 23.08.2026): ein
     Label, das in der aktuellen Sprache nicht in eine Zeile passt (z. B.
     spanisches "Dashboard" auf schmalen Screens), bricht jetzt in eine zweite
     Zeile um, statt mit "..." abgeschnitten zu werden - der Button selbst
     wächst dafür per min-height mit (siehe .mobile-tab oben). text-align:
     center hält beide Zeilen mittig unter dem Icon statt linksbündig (Default
     für umbrochenen Inline-Text in einer sonst zentrierten Flex-Spalte). */
  /* flex:1 + eigenes display:flex/align-items:center (Nutzeranfrage
     26.08.2026, siehe .mobile-tab-Kommentar oben) - das Label beansprucht
     dadurch jeden Platz, der über Icon+2px-Mindestabstand hinaus im Tab
     verfügbar ist (insbesondere den durch ein zweizeiliges Nachbar-Label
     erzwungenen Höhenzuwachs), und zentriert seinen eigenen Inhalt darin
     vertikal - ein einzeiliges Label steht dadurch mittig im selben Raum,
     den ein zweizeiliges Label an anderer Stelle in der Reihe belegt, statt
     unten am Rand zu kleben. */
  .mobile-tab-label { white-space: normal; overflow-wrap: break-word; max-width: 100%; text-align: center; line-height: 1.15; flex: 1; display: flex; align-items: center; justify-content: center; }

  /* Tutorial-Panel (12_UI_UX.md Abschnitt 8) - bewusst NICHT blockierend:
     es liegt als schmale Karte unten rechts über der Oberfläche, ohne die
     restliche Bedienung zu sperren. Das passt zum "zugänglich statt
     erschlagend"-Grundton der Game Design Bible (Punkt 8) - wer den
     angeleiteten Schritt ignorieren will, kann jederzeit woanders
     weiterklicken; das Panel wartet einfach.
     bottom liegt über der mobilen Tab-Leiste (56px + Safe-Area), damit es
     sie auf dem Telefon nicht verdeckt. */
  /* Grauer Schleier hinter dem zentrierten Panel (Nutzeranfrage 02.08.2026) -
     rein visuell (pointer-events:none), damit er die bestehende Tutorial-
     Sperre nicht durcheinanderbringt: Ein Klick auf die für den aktuellen
     Schritt erlaubte Aktion (.tutorial-allowed, siehe body.tutorial-lock
     weiter unten) muss weiterhin ankommen, statt am Schleier hängen zu
     bleiben. Farbe/Deckkraft wie die bestehenden Bestätigungs-/Info-Dialoge
     (.modal-overlay) - bewusst eine eigene Regel statt .modal-overlay direkt
     zu nutzen, da hier weder die Flex-Zentrierung noch dessen Klick-
     abfangendes Verhalten passen. z-index liegt unter dem Panel (20), aber
     über Header/Main/mobiler Tab-Leiste (10/15) - Rechtstexte-Footer und
     Sprachumschalter bleiben mit ihren eigenen, höheren z-index-Werten
     unbeeinflusst und damit vollständig sichtbar.

     Spotlight-Aussparung (Nutzeranfrage 04.08.2026): Statt einer gleichmäßig
     über den ganzen Bildschirm gelegten Fläche wird das Element jetzt per JS
     (updateTutorialSpotlight()) exakt über die für den aktuellen Schritt
     relevante Box positioniert/skaliert (z. B. "Firma gründen"-Karte in
     Schritt 1, die Marktzeile der eigenen Ware in Schritt 2) - das Element
     selbst bleibt dabei transparent, ein auf 9999px gestreuter box-shadow
     füllt den gesamten Rest des Bildschirms mit dem Nebel. Kein Wechsel des
     Grund-Mechanismus (weiterhin ein einzelnes pointer-events:none-Element),
     nur seine Position/Größe wird jetzt dynamisch statt fix auf inset:0
     gesetzt. Ohne bekannte Zielbox (z. B. während des Zurückblätterns durch
     bereits erledigte Schritte) fällt top/left/width/height auf einen
     entarteten Punkt in der VIEWPORT-MITTE zurück - der box-shadow deckt
     dann wieder gleichmäßig den kompletten Bildschirm ab, exakt wie zuvor.
     Der zweite, engere box-shadow-Ring zieht einen dünnen Akzentfarb-Rahmen
     um die ausgesparte Box, damit sie als "das hier ist gemeint" erkennbar
     bleibt statt nur zufällig hell zu wirken.

     Bugfix (06.08.2026, Nutzerreport "Nebel ist nicht durchgehend da") - der
     entartete Punkt lag ursprünglich in einer Bildschirmecke (top/left:
     -9999px), nicht in der Mitte. Ein 0×0-Element an dieser Position plus
     ein auf 9999px gespreizter box-shadow deckt dann aber NUR den Bereich
     von (-19998,-19998) bis (0,0) ab - der komplette sichtbare Viewport
     (x>0, y>0) blieb dadurch bei jedem "kein Ziel"-Fall (u. a. genau in dem
     kurzen Fenster, in dem der Advance-Request nach einem erledigten Schritt
     noch läuft, siehe checkTutorialProgress()) OHNE jeden Nebel - live per
     Screenshot bestätigt (Backdrop mit .show, Standardposition: Login-Screen
     komplett unverdunkelt). Mit dem Punkt in der Viewport-MITTE (50%/50%,
     eine reine CSS-Prozentangabe relativ zum position:fixed-Containing-Block,
     kein JS-Messen von window.innerWidth/Height nötig) reicht derselbe
     9999px-Spread garantiert über jeden real existierenden Bildschirm hinaus,
     in jede der vier Richtungen. */
  #tutorialBackdrop {
    display: none;
    position: fixed;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 10px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45), 0 0 0 3px var(--accent);
    transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
    z-index: 18;
    pointer-events: none;
  }
  #tutorialBackdrop.show { display: block; }
  /* Kein Akzentring, solange kein konkretes Ziel gespotlightet wird (06.08.2026,
     Nebenbefund beim obigen Bugfix) - der 0×0-Punkt in der Viewport-Mitte
     hätte sonst den zweiten, engeren box-shadow-Ring als kleinen, sinnlosen
     grünen Punkt mitten auf dem Bildschirm sichtbar gemacht (vorher unsichtbar,
     da der Punkt außerhalb des Bildschirms lag). */
  #tutorialBackdrop.full-fog { box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45); }
  #tutorialPanel {
    display: none;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 20;
    width: 480px;
    max-width: calc(100vw - 32px);
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    background: var(--card);
    border: 1px solid var(--accent);
    border-left: 4px solid var(--accent);
    border-radius: 12px;
    box-shadow: 0 6px 24px rgba(0,0,0,0.18);
    padding: 14px 16px;
  }
  #tutorialPanel.show { display: block; }
  /* Wackel-Effekt (Nutzeranfrage 02.08.2026) - lenkt den Fokus zurück aufs
     zentrierte Panel, wenn außerhalb geklickt wird, statt das Panel z. B.
     stumm zu ignorieren. Jede Keyframe-Stufe behält die Zentrierungs-
     Transformation (translate(-50%,-50%)) bei und hängt nur den seitlichen
     Ausschlag dahinter an - sonst würde die Animation das Panel aus der Mitte
     reißen statt es nur wackeln zu lassen. */
  @keyframes tutorial-shake {
    0%, 100% { transform: translate(-50%, -50%) translateX(0); }
    20% { transform: translate(-50%, -50%) translateX(-10px); }
    40% { transform: translate(-50%, -50%) translateX(9px); }
    60% { transform: translate(-50%, -50%) translateX(-6px); }
    80% { transform: translate(-50%, -50%) translateX(6px); }
  }
  #tutorialPanel.shake { animation: tutorial-shake 0.4s ease-in-out; }
  /* Minimieren (Nutzeranfrage 02.08.2026) - das zentrierte Panel kann genau
     das Eingabefeld/den Button verdecken, auf den sich der aktuelle Schritt
     bezieht (z. B. "Verkaufen" im Marktplatz). Im minimierten Zustand
     schrumpft das Panel auf eine kleine Ecken-Kachel (dieselbe Position, an
     der es vor der Zentrierung permanent stand) und lässt dadurch die ganze
     restliche Ansicht frei - inkl. des grauen Schleiers, der ebenfalls
     ausgeblendet wird (siehe applyTutorialLock() im Script). Ein Klick auf
     die Kachel stellt sie wieder her; sobald der Schritt erledigt ist, holt
     renderTutorial() sie ohnehin automatisch zurück (siehe dort). */
  #tutorialPanel.minimized {
    top: auto;
    left: auto;
    right: 16px;
    bottom: 16px;
    transform: none;
    width: auto;
    max-width: 220px;
    max-height: none;
    overflow: visible;
    padding: 10px 14px;
    cursor: pointer;
  }
  #tutorialPanel.minimized .tutorial-body { display: none; }
  #tutorialPanel.minimized .tutorial-skip { display: none !important; }
  .tutorial-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
  }
  #tutorialPanel.minimized .tutorial-head { margin-bottom: 0; }
  .tutorial-head-actions { display: flex; align-items: center; gap: 10px; }
  /* Quadratisch statt eines an den Glyph angepassten Rechtecks, und als
     letztes Element in .tutorial-head-actions (siehe HTML) ganz rechts im
     Kopf des Panels (Nutzeranfrage 04.08.2026) - vorher stand der
     Abbrechen-Link ("Tutorial beenden") noch rechts daneben. */
  .tutorial-minimize-btn {
    width: 24px;
    height: 24px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(13px + 1pt);
    line-height: 1;
    background: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--muted);
    cursor: pointer;
  }
  /* Beide Tutorial-Buttons erben vom generischen "button:hover:not(:disabled)"
     den grünen Hintergrund, behielten aber ihre graue Textfarbe - grau auf
     Grün war kaum lesbar (Nutzeranfrage 03.08.2026). Weiß auf --accent-dark
     ist ohnehin schon der Hover-Zustand jedes regulären Buttons, in beiden
     Themes; die beiden hier fehlten nur in dieser Linie. Der graue Rahmen des
     Minimieren-Buttons wird dabei transparent, sonst stünde er als heller
     Umriss auf der grünen Fläche. */
  .tutorial-minimize-btn:hover { color: #fff; border-color: transparent; }
  /* Tippbares Info-Icon (05.08.2026, Nutzeranfrage) - siehe infoIconHtml().
     Gleiche 24x24px-Kachelgröße wie der Minimieren-Button oben, damit es auf
     einem Touchscreen sicher treffbar bleibt (kein winziges Symbol). Rund
     statt eckig, damit es sich von normalen Buttons klar als reines
     Info-Element unterscheidet. War bislang tatsächlich nur 22x22px, also
     knapp unter der eigenen 24px-Zielgröße (auch unter WCAG 2.2s neuem
     2.5.8-Mindestmaß) - per Barrierefreiheits-Audit 22.08.2026 aufgefallen
     und auf die im Kommentar schon immer beschriebenen 24px korrigiert. */
  .info-icon {
    width: 24px;
    height: 24px;
    min-width: 24px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: calc(13px + 1pt);
    line-height: 1;
    background: none;
    border: 1px solid var(--border);
    border-radius: 50%;
    color: var(--muted);
    cursor: pointer;
    vertical-align: middle;
    margin-left: 6px;
  }
  .info-icon:hover { color: #fff; background: var(--btn-bg-hover); border-color: transparent; }
  /* Blätter-Pfeile (Nutzeranfrage 03.08.2026) - bewusst so unauffällig wie
     der Minimieren-Button daneben, sie sind eine Nebenfunktion. Weiße Schrift
     beim Überfahren aus demselben Grund wie dort: der generische
     button:hover-Hintergrund ist grün, graue Schrift darauf wäre kaum lesbar. */
  .tutorial-nav { display: flex; align-items: center; gap: 4px; }
  .tutorial-nav-btn {
    width: auto;
    height: auto;
    padding: 0 5px;
    font-size: calc(15px + 1pt);
    line-height: 1.1;
    background: none;
    border: none;
    color: var(--muted);
    border-radius: 6px;
    cursor: pointer;
  }
  .tutorial-nav-btn:hover:not(:disabled) { color: #fff; }
  .tutorial-nav-btn:disabled { background: none; opacity: 0.25; cursor: default; }
  /* Im minimierten Zustand ist der Textkörper ausgeblendet - Blättern würde
     dort nichts zu lesen geben. Die Schritt-Zählung selbst bleibt. */
  #tutorialPanel.minimized .tutorial-nav-btn { display: none; }
  .tutorial-past-note {
    font-size: calc(11px + 1pt);
    font-weight: 700;
    color: var(--accent-dark);
    margin-bottom: 8px;
  }
  .tutorial-step-count {
    font-size: calc(11px + 1pt);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-dark);
  }
  /* margin-bottom (Nutzeranfrage 18.09.2026, "auch zwischen Überschrift und
     erstem Satz eine Leerzeile") bewusst nicht mehr 4px, sondern dieselbe
     Höhe wie die per \n\n erzeugte Leerzeile INNERHALB von .tutorial-text
     (line-height 1.45 bei dessen font-size, siehe dortiger Kommentar zu
     white-space:pre-line) - eine echte Leerzeile, kein bloß etwas größerer
     Abstand. */
  .tutorial-title { font-size: calc(15px + 1pt); font-weight: 700; margin-bottom: calc((13px + 1pt) * 1.45); }
  /* white-space: pre-line (Nutzeranfrage 30.08.2026) - lässt einen \n\n in den
     tut.*.text-Strings (i18n.js) als echte Leerzeile vor dem Belohnungssatz
     durchschlagen; textContent (tutorial.js) schreibt \n weiterhin als reinen
     Text, kein innerHTML/XSS-Risiko nötig dafür. */
  .tutorial-text { font-size: calc(13px + 1pt); color: var(--muted); line-height: 1.45; margin-bottom: 10px; white-space: pre-line; }
  .tutorial-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
  .tutorial-actions button { width: auto; }
  /* Abbrechen bewusst als unauffälliger Textlink statt als Button - gleiche
     Abstufung wie bei "Account löschen" im Profil (08_MULTIPLAYER.md 7a):
     die alltägliche Aktion ist ein Button, die Ausnahme ein kleiner Link. */
  .tutorial-skip {
    font-size: calc(11px + 1pt);
    color: var(--muted);
    text-decoration: underline;
    cursor: pointer;
    background: none;
    border: none;
    /* Etwas Innenabstand statt padding:0, damit der grüne Hover-Hintergrund
       nicht bündig an den Buchstaben klebt. */
    padding: 2px 6px;
    border-radius: 6px;
    width: auto;
  }
  /* Siehe die Begründung bei .tutorial-minimize-btn:hover. */
  .tutorial-skip:hover { color: #fff; }
  .tutorial-progress {
    height: 4px;
    border-radius: 2px;
    background: var(--border);
    overflow: hidden;
    margin-top: 10px;
  }
  .tutorial-progress > div { height: 100%; background: var(--up); transition: width 0.3s; }
  /* Zentriert (Nutzeranfrage 02.08.2026) statt vormals unten rechts/über der
     mobilen Tab-Leiste - die Basisregel oben deckt beide Breiten bereits über
     max-width/max-height ab, kein eigener Mobile-Sonderfall mehr nötig. */
  /* Tutorial-Sperre (Nutzeranfrage 01.08.2026, siehe applyTutorialLock() im
     Script) - header/mobile Tab-Leiste/main komplett inert, außer für
     Elemente, die applyTutorialLock() gezielt mit .tutorial-allowed markiert
     (die eine für den aktuellen Schritt nötige Aktion). Sprachumschalter,
     Tutorial-Panel selbst, Rechtstexte-Footer und Bestätigungs-/Info-Dialoge
     bleiben absichtlich außerhalb dieser drei Container (siehe DOM-Struktur)
     und damit immer bedienbar. */
  body.tutorial-lock header,
  body.tutorial-lock #mobileTabBar,
  body.tutorial-lock main { pointer-events: none; }
  body.tutorial-lock .tutorial-allowed { pointer-events: auto; }
  /* Nutzeranfrage 03.09.2026 (12_UI_UX.md Abschnitt 8, CTA-Umbau): die
     Tutorial-Schritte navigieren nicht mehr selbst zum Zielort (goto()),
     der Text verweist stattdessen auf den Header-Button "Karte" - der
     Spieler muss ihn also während der Sperre tatsächlich anklicken können.
     #backBtn (Desktop) bzw. #tabMap (dessen mobiles Äquivalent, siehe
     Kommentar bei der @media-Regel unten) sowie das Orts-Dropdown waren
     dafür bislang eine UNBEDINGTE Ausnahme vom obigen header-/
     #mobileTabBar-Lock, unabhängig davon, ob "Karte" für den aktuellen
     Schritt gerade überhaupt der richtige nächste Klick ist.
     Bugfix (05.09.2026, Nutzeranfrage "wenn ich mich schon auf der Karte
     befinde oder gerade in der Firmenzentrale, darf der Button Karte weder
     bei Hover noch bei Klick reagieren") - diese drei bekommen jetzt
     stattdessen wie jedes andere erlaubte Element .tutorial-allowed
     (applyTutorialLock(), tutorial.js), und zwar NUR dann, wenn "Karte" für
     den aktuellen Schritt tatsächlich die Durchgangsstation ist (die
     aktuelle Ansicht weder die Zielansicht des Schritts noch bereits "map"
     ist - exakt dieselbe Bedingung wie beim isHeaderNavTarget-Fallback in
     updateTutorialSpotlight()) - die allgemeine
     ".tutorial-allowed{pointer-events:auto}"-Regel oben deckt sie dann mit
     ab, kein eigener Ausnahme-Selektor mehr nötig. Ist der Spieler bereits
     auf der Zielansicht oder schon auf der Karte selbst, bleibt "Karte"
     jetzt ebenso gesperrt wie jeder andere nicht erlaubte Header-Button. */
  /* Nur Buttons/Eingaben optisch abdunkeln, nicht den gesamten Inhalt (Text,
     Icons, Karten) - applyTutorialLock() vergibt .tutorial-allowed ohnehin
     nur an button/input/select. */
  body.tutorial-lock main :is(button, input, select):not(.tutorial-allowed) { opacity: 0.5; }
  /* Nutzeranfrage 05.09.2026 - Ersatz für die frühere, per JS (tutorial.js,
     updateTutorialSpotlight()) über #mobileTabBar gelegte Nebel-Aussparung
     (bis dahin nötig, um die Leiste trotz ihres damals niedrigeren z-index
     unverdunkelt zu halten - jetzt hinfällig, siehe deren eigener Kommentar
     in styles.css). Markiert stattdessen direkt den gerade erlaubten Tab
     selbst (".tutorial-allowed" sitzt bereits auf #tabMap, applyTutorialLock()
     in tutorial.js) mit demselben Akzentfarb-Rahmen wie zuvor die Aussparung -
     rein deklaratives CSS auf einem ohnehin schon fest positionierten
     Element, kann beim Scrollen also nichts mehr "hinterherhinken". */
  body.tutorial-lock #mobileTabBar .tutorial-allowed {
    box-shadow: inset 0 0 0 3px var(--accent);
  }

  @media (max-width: 820px) {
    #mobileTabBar { display: flex; }
    /* !important, weil dieselben Elemente an mehreren Stellen im JS aktiv
       ein-/ausgeblendet werden (z. B. #adminBtn je nach isAdmin,
       #playerTag in renderHeaderPlayerTag) - ohne !important würde jede
       dieser inline style.display-Zuweisungen die Regel hier wieder
       überschreiben. Funktional komplett redundant zur neuen Tab-Leiste
       (#backBtn "Karte" entspricht z. B. exakt dem "Karte"-Tab). */
    #dashboardBtn, #zonesBtn, #leaderboardBtn, #adminBtn, #backBtn, #playerTag {
      display: none !important;
    }
    /* --mobile-tab-bar-height statt eines festen Werts (65px = 62px
       .mobile-tab-Höhe, weitere Nutzeranfrage 23.08.2026, "nochmal 2px
       größer" + 3px Holzsteg-border-top) - seit .mobile-tab auf min-height
       umgestellt ist (Nutzeranfrage 23.08.2026, umbrechende Footer-Labels),
       kann die Leiste bei einem umgebrochenen zweizeiligen Label höher als
       65px werden. Gemessen statt geraten (syncHeaderControlsReserve() in
       ui-helpers.js, derselbe Ansatz wie --legal-footer-height direkt
       darunter) statt weiterhin blind draufzuaddieren - 65px bleibt nur als
       Fallback, bevor das erste Mal gemessen wurde. + die gemessene
       Footer-Höhe (--legal-footer-height, enthält bereits ihre eigene
       Safe-Area, siehe #legalFooter) - beide fixierten Streifen zusammen,
       seit der Footer nicht mehr Teil des Dokumentflusses ist (Nutzeranfrage
       05.08.2026). */
    body { padding-bottom: calc(var(--mobile-tab-bar-height, 65px) + var(--legal-footer-height, 34px)); }
  }
  /* Mobile-Header-Umbau, zweiter Durchgang (Nutzeranfrage 05.08.2026): Der
     erste Durchgang (04.08.2026, per flex-direction:column + zwei erzwungenen
     Zeilenumbrüchen) verteilte die Elemente zwar auf mehrere Zeilen, aber ohne
     gezielte Ausrichtung innerhalb jeder Zeile (Zeile 2 z. B. rein linksbündig,
     da .header-left seinen Flex-Default behielt). Auf konkreten Wunsch jetzt
     durch ein einziges 3-Spalten-CSS-Grid ersetzt (auto/1fr/auto):
       Zeile 1: Logo (Spalte 1, linksbündig)
       Zeile 2: Zeitalter-Chip (Spalte 1, linksbündig - dieselbe Kante wie das
                Logo darüber) · Ortsauswahl-Dropdown (Spalte 3, rechtsbündig)
       Zeile 3: MC (Spalte 1, linksbündig) · FP (spannt alle 3 Spalten, echt
                zentriert) · Diamanten (Spalte 3, rechtsbündig - dieselbe
                Kante wie die Ortsauswahl darüber)
     .header-left/.stats werden dafür zu reinen Durchreich-Wrappern
     (display:contents) - ihre Kinder werden dadurch layouttechnisch zu
     direkten Grid-Kindern von <header> und lassen sich einzeln per
     grid-row/grid-column platzieren, ohne die zwei HTML-Container aufzulösen
     (die auf Desktop weiterhin unverändert als normale Flex-Zeilen dienen).

     Dritter Durchgang (Nutzeranfrage 05.08.2026, mehrere Nutzerreports):
     - Die Warn-Button-Gruppe lag als in Spalte 2 zentriertes Grid-Element
       direkt UNTER dem fest positionierten Theme-Button (Spalte 2 war durch
       das feste header-padding-right:20px viel breiter bemessen, als der
       tatsächlich freie Platz neben den Theme-/Sprache-/Logout-Buttons
       erlaubte). Sie ist jetzt selbst position:fixed (wie .lang-switch) und
       steht mit demselben 8px-Abstand direkt links davon - keine Grid-
       Platzierung mehr nötig, siehe #headerWarnGroup unten.
     - FP war zwar "in Spalte 2 zentriert", aber Spalte 1 (Logo, ~148px) und
       Spalte 3 (Ortsauswahl/Diamanten, ~91px) sind UNTERSCHIEDLICH breit -
       "zentriert in Spalte 2" ist dadurch nicht dasselbe wie "zentriert im
       Header". Behoben, indem FP jetzt über alle 3 Spalten spannt
       (grid-column: 1/-1) und darin zentriert wird - die Spannweite ist
       dadurch die VOLLE Kopfzeilenbreite, unabhängig davon, wie breit Spalte
       1/3 tatsächlich sind.
     - row-gap verdoppelt (6px -> 12px).
     - Header ist jetzt position:fixed statt position:sticky (verlässt damit
       den Dokumentfluss und scrollt nie mit) - main bekommt die tatsächlich
       gemessene Header-Höhe als zusätzlichen oberen Abstand
       (--mobile-header-height, siehe syncHeaderControlsReserve()). */
  @media (max-width: 820px) {
    header {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      width: 100%;
      padding-right: 20px;
      /* Nutzeranfrage 04.09.2026 - Folge der wiederholten Logo-Abstand-
         Verkleinerung: der Rest des sichtbaren Abstands zum linken
         Bildschirmrand war das feste header-padding (20px, Basisregel oben,
         gilt sonst identisch für Mobile UND Desktop), das bislang nie Teil
         der bisherigen Halbierungen war. Nur hier, mobil-gescoped, auf 10px
         reduziert - Desktop bleibt bei 20px. headerPaddingLeft in
         syncHeaderControlsReserve() (ui-helpers.js) MUSS bei einer erneuten
         Änderung dieses Werts synchron angepasst werden, sonst zentriert die
         dortige Formel gegen den falschen Wert. */
      padding-left: 10px;
      display: grid;
      /* Spalte 3 bewusst "auto": Diamanten (Zeile 4) ist ein festes, kurzes
         Navigationselement, das nie gequetscht werden soll. Spalte 1 war
         ursprünglich ebenfalls reines "auto" (fürs Logo, Zeile 1, ~148px
         Platzbedarf) - seit Zeile 2 sich #ageTag und #locationDropdownWrap
         teilen (Nutzerreport 06.08.2026), reicht das nicht mehr: Ein reines
         "auto"-Grid-Track hat keine erzwingbare Ober-/Untergrenze und wächst
         bei langem Zeitalter-Text ungebremst auf dessen volle Inhaltsbreite,
         auch wenn #ageTag selbst schon overflow:hidden/ellipsis besitzt - die
         Ellipsis kann erst greifen, wenn die umgebende Spalte schon schmaler
         als der Inhalt ist, eine sich selbst aufblasende "auto"-Spalte wird
         aber nie schmaler. minmax(0, auto) erzwingt stattdessen eine
         Mindestbreite von 0 (max weiterhin "auto" = Inhaltsbreite als
         Wachstumsgrenze) - dasselbe bereits etablierte "Grid Blowout"-Muster
         wie bei .grid2 (siehe dortiger Kommentar), nur mit "auto" statt "1fr"
         als Wachstumsgrenze, da Spalte 1 (anders als .grid2s Spalten) nicht
         gleichmäßig mit ihren Nachbarn wachsen, sondern nur ihren tatsächlichen
         Bedarf bekommen soll. Spalte 2 (nur noch FP, siehe oben) bekommt den
         Rest über minmax(0,1fr) - kann bis auf 0 schrumpfen. */
      grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto;
      row-gap: 12px;
      column-gap: 10px;
    }
    .header-left { display: contents; }
    /* .header-center (der Desktop-Mittelslot für #ageTag, siehe Basis-Regel)
       wird auf Mobile ebenfalls zum reinen Durchreich-Wrapper - #ageTag bleibt
       dadurch weiterhin direktes Grid-Kind von <header> und landet unverändert
       in der eigenen #ageTag-Regel weiter unten, nicht in einer zusätzlichen
       Grid-Zelle für den jetzt leeren Wrapper selbst. */
    .header-center { display: contents; }
    /* .stats bleibt (anders als .header-left) ein ECHTER Flex-Container statt
       display:contents. justify-content:space-between statt center
       (Nutzeranfrage 20.08.2026, "MC linksbündig mit dem Zeitalter, Diamanten
       rechtsbündig mit dem Orts-Dropdown"): #coinsBtn (erstes Kind) landet
       dadurch am linken Rand von .stats, #diamondsBtn (letztes sichtbares
       Kind - #playerTag ist auf Mobile display:none, .header-warn-group ist
       position:fixed und nimmt am Flex-Layout gar nicht erst teil) am rechten
       Rand. Da .stats dieselbe volle Grid-Breite (Spalte 1/-1) einnimmt wie
       #ageTag (Spalte 1, justify-self:start) und #locationDropdownWrap
       (Spalte 2/-1, justify-self:stretch, dessen intern rechtsbündiger
       Inhalt siehe .location-dropdown), fallen linker Rand von #coinsBtn und
       rechter Rand von #diamondsBtn exakt mit deren Rändern zusammen, ohne
       dass es einer eigenen Breitenberechnung bedarf. #fpBtn (mittleres Kind)
       landet als Nebeneffekt zwischen beiden, mit demselben Abstand zu
       #coinsBtn wie zu #diamondsBtn - vorher (Nutzeranfrage 05.08.2026,
       "MC↔FP-Abstand soll genauso groß sein wie FP↔Diamanten-Abstand") war
       genau das mit der zentrierten Gruppe + gleichmäßigem `gap` der Zweck
       der Regel; bleibt bei drei Elementen mit space-between weiterhin
       erfüllt, ist hier aber nicht mehr der Grund für die Anordnung selbst. */
    /* grid-row:3 statt vormals 4 (Nutzerreport 06.08.2026, "Abstand zwischen
       Logo und Zeitalter ist wesentlich kleiner als zwischen Zeitalter und
       MC"): Reihe 4 lag um eine LEERE Reihe 3 von #ageTag/#locationDropdownWrap
       (beide Reihe 2) entfernt - niemand belegt Reihe 3 (siehe die
       Grep-Historie: kein einziges "grid-row: 3" mehr im Code, seit
       #locationDropdownWrap auf Reihe 2 zog). Eine per Zeilennummer
       übersprungene, aber referenzierte Grid-Reihe wird trotzdem implizit
       erzeugt (Reihe 4 setzt voraus, dass Reihen 1-3 existieren) - mit 0px
       Eigenhöhe, aber MIT row-gap auf beiden Seiten, wodurch der Abstand
       zwischen Reihe 2 und 4 effektiv doppelt so groß wirkte (zwei row-gaps
       statt einem) wie zwischen den direkt aufeinanderfolgenden Reihen 1/2.
       Reihe 3 direkt zu belegen schließt die Lücke - alle drei sichtbaren
       Abstände (Logo↔Zeitalter, Zeitalter↔MC) nutzen jetzt genau einen
       row-gap. */
    .stats {
      grid-row: 3;
      grid-column: 1 / -1;
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 10px;
      /* margin-bottom statt eines transform (Nutzeranfrage 22.08.2026, "MC/FP/
         Diamanten etwas höher, sodass Abstand zum Ortsdropdown da ist"): Die
         Nase (#locationTag, siehe dortige Regel) hängt unverändert exakt zur
         Hälfte über die untere Headerkante - dieser margin vergrößert nur den
         Freiraum ZWISCHEN den Chips und dieser Kante (Zeile wächst nach oben
         weg von der Nase), die Kante selbst und damit die Nase bleiben an
         ihrer Stelle. Ein transform:translateY wäre hier die naheliegendere
         Wahl gewesen, hätte aber #headerWarnGroup (position:fixed, direktes
         Kind von .stats) seine Fixierung zum Viewport gekostet - jedes
         Transform auf einem Vorfahren macht diesen zum neuen containing block
         für fixed-positionierte Nachfahren.
         10px -> 12.5px (Nutzerreport 22.08.2026, "Abstand von MC/FP-Zeile zur
         Ortsdropdown-Nase an die übrigen 12px-Abstände angleichen"): Die Nase
         hängt bei bottom:0 + translate(50%) exakt zur Hälfte ihrer eigenen
         Höhe (gemessen 25px, also 12.5px) über die untere Headerkante - ihre
         sichtbare OBERKANTE liegt dadurch 12.5px VOR dieser Kante, nicht
         direkt darauf. Der sichtbare Abstand von der Chip-Zeile bis zur
         Nasen-Oberkante ist deshalb margin-bottom MINUS diese 12.5px, nicht
         margin-bottom selbst - bei 10px kamen dadurch nur 9.5px sichtbarer
         Abstand heraus statt den angestrebten 10, und v. a. weniger als der
         12px-row-gap der übrigen Zeilen. 12.5px trifft genau den vollen
         12px-Rhythmus (12.5 - 12.5 Halbhöhe = 12). Ändert sich die Höhe der
         Nase erneut, muss dieser Wert entsprechend neu berechnet werden. */
      margin-bottom: 12.5px;
    }
    /* Ursprünglich (05.08.2026) eine feste height:32px, damit das Logo exakt
       dieselbe Höhe wie Warnung/Hell-Dunkel/Sprache/Logout hat (Nutzeranfrage
       "Logo muss dieselbe Höhe haben wie..."). Mit dem neuen zweizeiligen
       Logo+Untertitel-Layout (Nutzeranfrage 13.08.2026, siehe .header-logo-
       tagline oben) passt eine starre 32px-Box nicht mehr - height:auto lässt
       die Zeile stattdessen genau so hoch werden, wie h1 tatsächlich braucht;
       da h1 als einziges Element in Grid-Zeile 1 steht (kein Geschwister,
       das kollidieren könnte), wächst dadurch nur diese eine Zeile. Die
       Kopfzeilen-Gesamthöhe wird ohnehin schon live gemessen statt geraten
       (--mobile-header-height, siehe syncHeaderControlsReserve()) - eine
       höhere Zeile 1 wird von dort automatisch mit kompensiert. */
    /* margin-left:-18.43px (Nutzeranfrage 20.08.2026, "Logo im Header links
       bündig mit dem Feld für die Angabe des Zeitalters machen"; Wert war
       zwischenzeitlich -27.65px, als das Logo 1,5-fach vergrößert war -
       mit der Rücknahme der Vergrößerung ["Logo wieder reduzieren wie es
       vorher war, Abstand nach links kann aber gerne so bleiben"] hier neu
       berechnet): Obwohl h1 und #ageTag dieselbe Grid-Spalte 1 mit
       justify-self:start belegen, ihre Boxen sich also bereits exakt an
       derselben Kante berühren, klafft zwischen dem sichtbaren "M" von
       "MarketBorn" und der Zeitalter-Pille eine Lücke: Die Logo-SVG hat eine
       viewBox von "-23.5 0 210 52" (Platz für den breiteren, zentrierten
       Untertitel "plant a seed, grow your economy", siehe Kommentar beim
       SVG-Markup; Höhe 22.08.2026 von 64 auf 52 zugeschnitten, siehe dortiger
       Kommentar zum unteren Leerraum - ändert an dieser X-Rechnung nichts) -
       links vom sichtbaren Wortmarken-Text (beginnt bei x=4) liegt dadurch
       unsichtbarer Leerraum in der SVG-Box selbst, den justify-self:start
       nicht "wegrechnen" kann, weil es nur die BOX ausrichtet, nicht deren
       sichtbaren Inhalt. Gemessen: (4-(-23.5)) Viewbox-Einheiten * aktueller
       Skalierungsfaktor 34.856/52 (= unverändert 42.9/64, siehe
       #headerLogoSvg-Kommentar) ≈ 18.43px sichtbarer Versatz nach rechts.
       Der negative Margin schiebt die komplette (breitere) SVG-Box exakt um
       diesen Betrag nach links, sodass ihr unsichtbarer linker Leerraum vor
       die Kopfzeile hinausragt (unschädlich, dort ist nichts zu sehen) und
       das sichtbare "M" stattdessen an derselben Stelle wie die
       Zeitalter-Pille beginnt. Ändert sich die Logo-Höhe erneut, muss dieser
       Wert proportional neu berechnet werden (Skalierungsfaktor = neue
       Höhe/52). NUR Mobile: Am
       Desktop läuft #ageTag in einem eigenen, unabhängig positionierten
       Mittelslot (.header-center), eine analoge Ausrichtungsanforderung
       besteht dort nicht.
       Nachtrag (03.09.2026, zwei Nutzeranfragen direkt hintereinander) - die
       obige Bündig-mit-Zeitalter-Pille-Herleitung gilt seither nicht mehr,
       jetzt gilt eine simple, feste Vorgabe statt einer Berechnung: "25px vom
       linken Rand weg, mögliche Überlappungen erstmal ignorieren". Erster
       Versuch war "+25px auf den alten Wert", das kollidierte aber sichtbar
       mit #headerWarnGroup (eigene Regel weiter unten, position:fixed,
       unabhängig vom Logo) - seit der Header-Wortmarke der Untertitel
       entfernt wurde (viewBox "0 0 168 35" statt "-23.5 0 210 52", siehe
       Kommentar am SVG-Markup), ist die gerenderte Box ~26,5px breiter als
       vorher, was den bis dahin unbemerkten Sicherheitsabstand zur
       Warn-Gruppe weitgehend aufgebraucht hatte. Der zurückhaltend
       nachgebesserte Wert (-24.25px, kollisionsfrei) landete dadurch aber
       sichtbar WEITER LINKS als zuvor - genau das bemängelt dieser Nachtrag
       ("jetzt ist das Logo ja noch weiter nach links gewandert"). Deshalb
       jetzt bewusst eine absolute statt einer relativen Vorgabe: 25px
       Abstand von der linken Kopfzeilenkante (h1 liegt in Grid-Spalte 1, die
       selbst bei x=20px beginnt, siehe Header-padding-left - margin-left
       muss also 25-20 = 5px betragen, live per getBoundingClientRect()
       gegengeprüft). Eine erneute Überlappung mit #headerWarnGroup ist damit
       laut Nutzeranfrage ausdrücklich in Kauf genommen, nicht übersehen.
       Nachtrag (03.09.2026, Nutzeranfrage "das ist doch etwas zu weit"): 5px
       direkt auf 2px reduziert (= 22px Abstand von der linken
       Kopfzeilenkante statt 25px) - erneut ein fester, vom Nutzer direkt
       vorgegebener margin-left-Wert, keine Neuberechnung.
       Nachtrag (03.09.2026, dritte Nutzeranfrage direkt hintereinander):
       "zentriere es zwischen linkem Rand und dem linken Rand des
       Warnung-Buttons" - löst die feste 2px-Vorgabe wieder durch eine
       berechnete ab. #headerWarnGroup ist auf Mobile ohne aktive Warnung
       (Lager voll/kritische Wartung/Kredit überfällig, siehe dortige Regeln) 0px breit
       und sitzt per position:fixed/right an x=279px (bei 375px
       Referenzbreite, aus --warn-group-right berechnet) - linker Bildschirmrand
       ist x=0. Ziel-Mittelpunkt der Logo-Box also (0+279)/2=139.5px. Die
       Logo-Box selbst ist (bei margin-left:2px) 167.25px breit und beginnt
       bei x=22px (20px Header-padding-left + 2px), ihr Mittelpunkt liegt
       also bei 105.625px - fehlen 33.875px, macht 2+33.875=35.875px
       margin-left (live per getBoundingClientRect() nachgemessen und
       gegengeprüft, exaktes Zentrieren bestätigt).
       Nachtrag (Nutzerreport 04.09.2026, "Logo total verschoben"): Sind
       Warnungen aktiv, wird #headerWarnGroup breiter und beginnt weiter
       links, wodurch sich der Zielmittelpunkt verschiebt - das wurde hier
       ursprünglich bewusst nicht live nachgerechnet und blieb bei diesem
       einen, nur für den warnungsfreien Fall stimmenden Wert stehen. Seit
       "kritische Wartung" (04.09.2026) dieselbe Warn-Gruppe mitbenutzt,
       tritt der Fall so häufig auf, dass das Logo dabei sichtbar abgeschnitten
       wirkte ("MarketBo" statt "MarketBorn") - jetzt per
       syncHeaderControlsReserve() (ui-helpers.js) bei jedem Aufruf live
       nachgerechnet (dieselbe Zentrierungsformel wie oben hergeleitet, nur
       mit der tatsächlich gemessenen #headerWarnGroup-Position statt der
       angenommenen 0px-Breite). 35.875px bleibt als Fallback für den
       warnungsfreien Fall bzw. bis zum ersten JS-Durchlauf stehen. */
    /* Fallback 11.46875px, neu berechnet nachdem header-padding-left auf
       Mobile von 20px auf 10px reduziert wurde (Nutzeranfrage "Ja, bitte
       verkleinern!") - dieselbe Zentrierungsformel wie zuvor (35.875-Basis,
       zweimal halbiert), nur mit dem neuen 10px-Wert statt 20px. Nur bis zum
       ersten JS-Durchlauf bzw. falls dieser fehlschlägt relevant, siehe
       Kommentar bei syncHeaderControlsReserve() (ui-helpers.js) für die
       eigentliche Berechnung. */
    header h1 { grid-row: 1; grid-column: 1; justify-self: start; height: auto; margin-left: var(--header-logo-margin-left, 11.46875px); }
    /* Der Fließtext-Zusatz "Kredit überfällig" (~140px) ist auf Mobile der
       eigentliche Grund, warum die Warn-Gruppe sonst nicht schmal genug wäre
       - das title-Attribut erklärt den Button weiterhin vollständig, das
       Icon allein bleibt klickbar und springt unverändert zur Bank. */
    #loanWarningBtn span[data-i18n="hd.loanOverdue"] { display: none; }
    /* Zurück auf top:12 (Headers eigenes padding-top) - Nutzeranfrage
       22.08.2026, "Buttons oben bündig mit der oberen Kante des Logos
       abschließen": Die zwischenzeitliche Umstellung auf 23.89px (selbiger
       Tag, siehe Git-Historie) richtete stattdessen die UNTERKANTEN von Logo
       und Buttons aneinander aus - genau das soll jetzt wieder rückgängig
       sein. .header-warn-group (weiter unten) übernimmt denselben Wert, da
       beide Leisten auf derselben Höhe stehen sollen. */
    .lang-switch { top: 12px; }
    /* Nutzeranfrage 21.08.2026 - "auf Mobile Theme/Sprache-Icons 1px nach
       unten, Desktop soll so bleiben wie es ist": überschreibt hier gezielt
       nur die beiden, NICHT #headerLogoutBtn (eigene, unveränderte
       Padding-Regel weiter oben) und NICHT die Desktop-Basisregel (5px/7px)
       selbst. Nachtrag (21.08.2026, "noch 1px runter") - von 6px/6px auf
       7px/5px, macht die Icons ein weiteres Pixel tiefer, Summe bleibt bei
       12px (Button-Höhe unverändert).
       #themeToggleBtn seit 04.09.2026 hier entfernt - dessen eigene, gleich
       spezifische Regel (siehe #themeToggleBtn oben, "Symbole viel zu weit
       oben") wurde von dieser gleich spezifischen, aber später im Dokument
       stehenden Regel überschrieben, wodurch weiterhin die alte 7px/5px-
       Asymmetrie statt der neuen symmetrischen 6px/6px griff.
       #langFlagBtn (Folge-Nutzeranfrage, selber Tag, "Länderflaggen sehen
       auch nicht ganz zentriert aus") aus demselben Grund komplett entfernt
       und durch eine eigene symmetrische Regel direkt darunter ersetzt - die
       Flagge ist ein deterministisches SVG (FLAG_SVG_*, constants.js), die
       asymmetrische 1px-Notiz von damals hatte für sie nie einen echten
       Font-Metrik-Grund. */
    /* Symmetrisches Pendant zur oben entfernten #langFlagBtn-Zeile - Summe
       weiterhin 12px wie zuvor (6px/6px statt 7px/5px). */
    #langFlagBtn { padding-top: 6px; padding-bottom: 6px; }
    /* Nutzeranfrage 22.08.2026, "Restdifferenz noch schließen": Die
       Desktop-Basisregel setzt height:31px, passend zu den übrigen
       Header-Nav-Buttons (Dashboard/Zonen/...), die dort ebenfalls 31px
       hoch sind - das bleibt für Desktop unverändert richtig. Auf Mobile
       gibt es diese Nav-Buttons nicht (eigene Tab-Leiste, siehe
       #mobileTabBar), stattdessen sollen die drei Buttons hier exakt so
       hoch wie das (ebenfalls mobile, auf 34.856px zugeschnittene, siehe
       #headerLogoSvg-Kommentar) Logo sein - beide sind top:12 bündig zur
       Logo-Oberkante ausgerichtet, bei gleicher Höhe fallen dadurch auch
       ihre Unterkanten zusammen, wodurch der Abstand zur Zeitalter-Zeile
       darunter exakt dem row-gap (12px) entspricht statt wie zuvor größer
       zu wirken. .header-warn-group .warning-btn (weiter unten) bekommt
       denselben Wert, aus demselben Grund. */
    #themeToggleBtn, #langFlagBtn, #headerLogoutBtn { height: 34.856px; }
    /* Warn-Button-Gruppe: eigene fest positionierte Leiste, exakt wie
       .lang-switch, direkt links davon mit demselben 8px-Abstand
       (--warn-group-right, per JS aus der gemessenen Position von
       .lang-switch berechnet - siehe syncHeaderControlsReserve()). Kein
       Grid-Platz mehr nötig, dadurch auch kein Überlappungsrisiko mehr. */
    .header-warn-group {
      position: fixed;
      /* Siehe .lang-switch (Nutzerreport 05.08.2026 zum Grundwert, 22.08.2026
         zurück auf top:12) - beide Leisten sollen auf derselben Höhe stehen. */
      top: 12px;
      right: var(--warn-group-right, 180px);
      z-index: 60;
      display: inline-flex;
      flex-wrap: wrap;
      justify-content: flex-end;
      gap: 8px;
    }
    /* Gleiche Größe wie #themeToggleBtn/#langFlagBtn/#headerLogoutBtn
       (Nutzeranfrage 05.08.2026) - eigene Warnfarbe (Hintergrund/Text) bleibt
       unverändert, nur Padding/Rundung/Schriftgröße/Rahmen/Schatten werden
       angeglichen, damit die Gruppe optisch zur selben Button-Reihe gehört.
       Nachtrag (05.08.2026, Nutzerreport "Warn-Button zu groß"): Der Button
       war trotz identischer Werte oben spürbar höher (38px statt 32px) - die
       drei Vorbild-Buttons setzen zusätzlich line-height:1 (Zeile ~276), was
       hier fehlte. Ohne das fällt line-height auf den Browser-Standard
       "normal" zurück (~1.15-1.2× font-size bei einem Emoji-Zeichen mit
       eigenen, oft höheren Schriftmetriken als lateinische Buchstaben) - genau
       die fehlenden paar Pixel, die den Button größer wirken ließen. */
    /* Nutzeranfrage 05.08.2026 - Icon/Schrift kleiner: 18px matchte zwar
       rechnerisch #themeToggleBtn/#langFlagBtn/#headerLogoutBtn, die Emoji-
       Glyphen (⚠️/🚨) wirkten dabei aber optisch deutlich wuchtiger als deren
       SVG-/Text-Icons gleicher Schriftgröße. Font-size auf 14px reduziert;
       eine explizite height:32px hält den Button trotz kleinerem Inhalt exakt
       auf derselben Höhe wie die drei Vorbild-Buttons (button-Grundregel
       zentriert den jetzt kleineren Inhalt automatisch, display:inline-flex/
       align-items:center/justify-content:center kommt von dort). */
    .header-warn-group .warning-btn {
      padding: 6px 10px;
      border-radius: 8px;
      font-size: calc(14px + 1pt);
      line-height: 1;
      /* 34.856px seit 22.08.2026 (Nutzeranfrage) - mitgezogen mit
         #themeToggleBtn/#langFlagBtn/#headerLogoutBtn (siehe deren
         Mobile-Kommentar), sonst wäre diese Gruppe niedriger als die drei
         Vorbild-Buttons. */
      height: 34.856px;
      /* Nutzeranfrage 05.08.2026 - derselbe feste Wert wie #themeToggleBtn
         (siehe dortiger Kommentar), damit beide exakt gleich breit sind,
         unabhängig vom jeweiligen Emoji. */
      width: 44px;
      border: 1px solid var(--border);
      box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    }
    /* Bei fester Breite muss der Inhalt dazu passen (Nutzeranfrage: "ggf. muss
       die Größe der Inhalte angepasst werden") - der Bestandszähler neben dem
       Lager-Warnsymbol wird auf Mobile ausgeblendet, exakt wie der
       Fließtext-Zusatz beim Kredit-Warnsymbol direkt darüber. Das title-
       Attribut erklärt den Button weiterhin vollständig, das Icon allein
       bleibt klickbar und springt unverändert zum ersten vollen Lager. */
    #storageWarningBtn span#storageWarningCount { display: none; }
    /* Nutzeranfrage 05.08.2026 - "Orts-Dropdown soll auf exakt derselben Höhe
       sein wie die Zeitalter-Info": beide bekommen dieselbe explizite Höhe
       statt sich weiterhin auf zufällig unterschiedliche, content-getriebene
       Werte zu verlassen (damals 21px vs. 16px) - dieselbe "explizit statt
       zufällig passend"-Logik wie bei header h1 (Abschnitt 18). display:flex
       + align-items:center zentriert den jeweiligen Inhalt (Badge-Text bzw.
       Icon+Name+Pfeil) exakt in der festen Box.
       24px statt der ursprünglichen 21px (Barrierefreiheits-Audit
       22.08.2026, WCAG 2.2 SC 2.5.8 - Mindest-Klickfläche 24×24px, damals
       bewusst zurückgestellt, um das bestehende Header-Feinabstimmungs-
       Raster nicht zu riskieren, auf Nutzerwunsch jetzt trotzdem
       nachgezogen): Die umliegende Positionierung hängt NICHT an diesem
       Festwert, sondern entweder an zur Laufzeit gemessenen Werten
       (--mobile-header-height, siehe syncHeaderControlsReserve()) oder an
       header-relativen Ankern (#locationTag: bottom:0 + translate(-50%,50%)
       relativ zu <header>, siehe dessen Kommentar; .location-menu:
       top:calc(100% + 16px)) - die 3px zusätzliche Zeilenhöhe propagieren
       sich dadurch von selbst, ohne dass an anderer Stelle etwas
       nachgerechnet werden muss. */
    /* grid-column jetzt 1/-1 statt nur Spalte 1, justify-self:center statt
       start (Nutzeranfrage 22.08.2026, "Zeitalter jetzt zentrieren") - vormals
       teilte sich #ageTag Zeile 2 optisch mit #locationDropdownWrap (Spalte
       1 vs. 2/-1), seit dessen sichtbarer Inhalt (#locationTag) selbst
       position:absolute als "Nase" am Headerrand hängt (siehe dortige Regel
       weiter unten), ist #locationDropdownWrap in Zeile 2 nur noch eine leere,
       unsichtbare Box - #ageTag kann die volle Zeilenbreite deshalb gefahrlos
       für sich beanspruchen und darin echt zentriert werden, statt weiterhin
       künstlich auf Spalte 1 (linksbündig unter dem Logo) beschränkt zu
       bleiben. */
    /* width:75% (Nutzeranfrage 22.08.2026, "etwa 75% der verfügbaren Breite")
       statt der bisherigen reinen Inhaltsbreite - löst sich vom vorherigen
       "so breit wie der Text" und füllt stattdessen einen festen Anteil der
       Zeile 2 (Spalte 1/-1, siehe grid-column). justify-content:center hält
       den Text (kein eigener innerer Span, siehe Kommentar oben) mittig in
       der jetzt breiteren Pille statt am linken Rand (Flex-Grundregel ohne
       das würde ihn dort verankern). */
    #ageTag { grid-row: 2; grid-column: 1 / -1; justify-self: center; width: 75%; min-width: 0; height: 24px; display: flex; align-items: center; justify-content: center; }
    /* Nutzerreport 06.08.2026 - "Orts-Dropdown soll rechtsbündig in derselben
       Zeile wie die Zeitalter-Info stehen, nicht darunter": Die volle eigene
       Zeile (vormals grid-row:3, grid-column:1/-1, siehe Historie oben - erst
       zur Vermeidung abgeschnittener Namen, dann auf 05.08.2026 diagonal
       ausgerichtet) ist damit hinfällig. #locationDropdownWrap teilt sich jetzt
       Zeile 2 mit #ageTag (Spalte 1), Spannweite 2/-1 nutzt die schrumpfbare
       mittlere Spalte (minmax(0,1fr)) plus die feste dritte Spalte.

       justify-self:stretch statt naheliegendem "end" - der eigentliche Grund,
       warum das beim Live-Test zunächst trotzdem überlappte: "end" (wie jeder
       nicht-stretch-Wert) nimmt das Grid-Item aus der Flächen-Größenberechnung
       heraus und lässt es stattdessen auf seine eigene Inhaltsbreite wachsen,
       min-width:0 hin oder her - die zugewiesene Spur bestimmt dann nur noch
       die ANKER-Position, nicht mehr die tatsächliche Breite. Bei einem langen
       Zeitalter-Text (z. B. Zeitalter 5, ~245px) blieb dadurch kein Platz mehr
       übrig, das Element wuchs trotzdem auf volle Namensbreite und überlappte
       #ageTag sichtbar. "stretch" zwingt das Element stattdessen auf GENAU die
       Breite, die die Spur nach Abzug von Spalte 1 tatsächlich noch hat (und
       kann dabei bis auf 0 schrumpfen, siehe min-width:0) - das eigentliche
       Rechtsbündig-Wirken kommt jetzt von der internen Flex-Ausrichtung
       (justify-content:flex-end auf .location-dropdown, siehe Basis-Regel)
       innerhalb dieser jetzt korrekt begrenzten Box, nicht mehr von
       justify-self selbst. Erst dadurch kann die bereits bestehende
       .location-tag-text-Ellipsis (Kommentar unten) auch tatsächlich greifen -
       vorher lag der Text ja nie in einem wirklich zu schmalen Container. */
    #locationDropdownWrap { grid-row: 2; grid-column: 2 / -1; justify-self: stretch; min-width: 0; height: 24px; align-items: center; }
    /* Ortsauswahl als mittig überlappende "Nase" am unteren Headerrand
       (Nutzeranfrage 21.08.2026, "Form von 1 und Farbe von 2") statt normaler
       Grid-Teilnehmer - #locationTag wird dazu selbst position:absolute
       relativ zu <header> (position:fixed auf Mobile, siehe dortige Regel -
       .location-dropdown bleibt bewusst position:static, siehe deren
       Kommentar weiter oben, sonst würde .location-menu wieder relativ zu
       diesem jetzt sehr schmalen Button statt zu <header> positioniert und
       rechts über den Bildschirmrand hinauslaufen). bottom:0 +
       translate(-50%, 50%) statt eines festen top-Werts - hängt dadurch
       IMMER exakt zur Hälfte über die untere Headerkante, unabhängig von der
       tatsächlichen Header-Höhe (robuster als ein weiterer fest verdrahteter
       Pixelwert wie bei .location-menu unten). #locationDropdownWrap selbst
       bleibt unverändert im Grid (Zeile 2) stehen, kollabiert dort aber
       praktisch auf 0, da sein einziges Kind jetzt aus dem Fluss genommen
       ist - #ageTag braucht dadurch keine eigene Anpassung. */
    #locationTag {
      position: absolute;
      left: 50%;
      bottom: 0;
      transform: translate(-50%, 50%);
      background: linear-gradient(155deg, #8a6a49, #4c3a28);
      color: #f2e8d5;
      border-radius: 999px;
      padding: 4px 14px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
      z-index: 2;
    }
    #locationTag:hover { background: linear-gradient(155deg, #9c7a52, #5a4530); color: #f2e8d5; padding: 4px 14px; margin: 0; border-radius: 999px; }
    /* Deutlich enger als die frühere calc(100vw - 60px)-Grenze (Nutzeranfrage
       21.08.2026, "Nase"-Umbau oben): #locationTag sitzt jetzt als kleine
       zentrierte Pille statt als volle Grid-Spalten-Breite - eine fast
       bildschirmbreite Textgrenze würde die Pille bei einem langen Namen
       unproportional breit werden lassen. min(), damit sie auf sehr schmalen
       Screens trotzdem nicht über den Rand hinauswächst. */
    .location-tag-text { max-width: min(140px, 50vw); }
    /* Der Ortsauswahl-Dropdown ragte ursprünglich rechts über den
       Bildschirmrand hinaus (left:0 relativ zum nahe am rechten Rand
       liegenden Wrapper, plus min-width:210px) - das erzeugte horizontales
       Dokument-Overflow, wodurch alle position:fixed-Elemente mit right:14px
       (Theme/Sprache/Logout/Warn-Gruppe) sichtbar nach rechts aus dem
       Bildschirm rutschten, sobald das Dropdown geöffnet wurde (Nutzerreport
       05.08.2026). position:static auf .location-dropdown (nur mobil) nimmt
       dem Wrapper seine Rolle als Bezugsrahmen weg - das Menü bezieht sich
       dadurch stattdessen auf <header> (position:fixed, siehe dortige Regel).
       Nutzerreport 22.08.2026, "Dropdown überlappt nach oben, muss schmaler
       werden": Der vorherige feste Wert "top:93.9px" war exakt aus der
       damaligen Kopfzeilenhöhe zurückgerechnet (Headers padding-top + Logo-
       Höhe + row-gap + Zeile-2-Höhe) - jede spätere Änderung an dieser Höhe
       (Nase, Holzsteg-Rahmen, der Stats-Abstand weiter unten) machte den Wert
       stillschweigend falsch, ohne dass eine der Änderungen selbst hier
       etwas anfassen musste. Zuletzt lag er dadurch weit VOR dem tatsächlichen
       Headerende (93.9px bei einer inzwischen gewachsenen Kopfzeile von
       ~154px) - das Menü öffnete sich mitten im Header und überlappte sowohl
       die MC/FP/Diamanten-Zeile als auch die Nase selbst.
       "top: calc(100% + 16px)" ersetzt den Festwert durch denselben robusten,
       sich selbst nachführenden Ansatz wie am Desktop (siehe
       @media(min-width:821px) unten): 100% ist hier die tatsächliche,
       jederzeit aktuelle Höhe von <header> (dem positionierten Bezugsrahmen),
       +16px schafft denselben Abstand zur halb überstehenden Nase wie am
       Desktop. Ändert sich die Kopfzeilenhöhe künftig erneut, muss dieser
       Wert nicht mehr von Hand nachgerechnet werden.
       left:50%/transform statt links/rechts auf die volle Headerbreite
       gespannt: Das Menü zentriert sich jetzt unter der Nase statt fast
       bildschirmbreit zu sein, width per min() schmaler als zuvor (335px),
       aber nie schmaler als der Bildschirm abzüglich desselben 20px-Randes
       wie zuvor. */
    .location-dropdown { position: static; }
    .location-menu {
      top: calc(100% + 16px);
      left: 50%;
      right: auto;
      transform: translateX(-50%);
      width: min(240px, calc(100vw - 40px));
    }
    #locationMenuBackdrop.show { display: block; }
  }
  /* Ortsauswahl auch am Desktop als mittig überlappende "Nase" am unteren
     Headerrand (Nutzeranfrage 22.08.2026, "Ortsdropdown auch in der
     Desktopversion so platzieren"), exakt dieselbe Technik wie die mobile
     Fassung oben (nicht das .header-center-Muster, siehe dazu vorheriger,
     verworfener Versuch): #locationDropdownWrap ist HTML-seitig ein und
     dasselbe Element wie .location-dropdown (`<span class="location-dropdown"
     id="locationDropdownWrap">`) - #locationTag UND .location-menu sind beide
     direkte Kinder DAVON, nicht eines separaten äußeren Wrappers. Ein
     .header-center-artiger Ansatz (äußere Box vollbreit + #locationTag als
     normales, zentriertes Flex-Kind darin) hätte .location-dropdown selbst
     zur vollbreiten Box gemacht - .location-menus "left:0" (Basisregel) wäre
     dadurch auf deren linke Kante bei x=0 gefallen statt auf die Position des
     (visuell zentrierten, aber layouttechnisch irgendwo im Flex sitzenden)
     Buttons. Live getestet, genau das ist zunächst passiert.
     Stattdessen wie mobil: .location-dropdown wird position:static (nimmt
     sich selbst als Bezugsrahmen raus), wodurch sowohl #locationTag als auch
     .location-menu sich stattdessen auf <header> beziehen (position:sticky,
     volle Headerbreite). #locationTag zentriert sich per position:absolute +
     left:50%/translateX(-50%) exakt wie mobil; bottom:0 + translateY(50%)
     hängt es immer zur Hälfte über die untere Headerkante, unabhängig von der
     tatsächlichen (auf Desktop bei schmalen Fenstern variablen, siehe
     .header-left{flex-wrap:wrap}) Header-Höhe.
     .location-menu zentriert sich ebenso per left:50%/translateX(-50%) -
     "top:calc(100% + 16px)" ist jetzt relativ zu <header> (100% = dessen volle
     Höhe) statt zuvor zum eng um den Button sitzenden .location-dropdown,
     landet dadurch bereits unterhalb des Headers; +16px schafft zusätzlich
     Abstand zur halb überstehenden Nase selbst (~10-12px sichtbare Höhe unter
     der Kante), robust gegenüber jeder Desktop-Header-Höhe - kein
     hartcodierter Pixel-Top-Wert wie das mobile 93.9px nötig, der bei
     umbrechenden Nav-Buttons ohnehin nie für alle Fensterbreiten gleichzeitig
     gestimmt hätte. */
  @media (min-width: 821px) {
    .location-dropdown { position: static; }
    #locationTag {
      position: absolute;
      left: 50%;
      bottom: 0;
      transform: translate(-50%, 50%);
      background: linear-gradient(155deg, #8a6a49, #4c3a28);
      color: #f2e8d5;
      border-radius: 999px;
      padding: 4px 14px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
      z-index: 2;
    }
    #locationTag:hover { background: linear-gradient(155deg, #9c7a52, #5a4530); color: #f2e8d5; padding: 4px 14px; margin: 0; border-radius: 999px; }
    .location-tag-text { max-width: 240px; }
    .location-menu { top: calc(100% + 16px); left: 50%; right: auto; transform: translateX(-50%); }
    /* text-align:center (Nutzeranfrage 22.08.2026) - die eigentliche Breite
       kommt per JS aus syncAgeTagWidth() (ui-helpers.js, siehe dortiger
       Kommentar): auf den breitesten aller möglichen Zeitalter-Texte
       gemessen plus Puffer, damit nie ein Text abgeschnitten wird und die
       Box beim Zeitalterwechsel nicht springt - das braucht eine
       Laufzeitmessung, reines CSS kann sich nicht auf "die breiteste von
       mehreren möglichen Textvarianten" beziehen. */
    #ageTag { text-align: center; }
  }
  /* Every screen shares this exact same width (user request 27.07.2026) -
     no per-view narrower container, regardless of how few elements a screen
     has (e.g. the login form is just as wide as the market list). Capped
     well above typical desktop widths so it reads as "near-fullscreen" on
     any normal monitor, while still not stretching to an absurd line length
     on an ultra-wide display. */
  main {
    max-width: 1600px;
    margin: 20px auto 18px;
    /* Unteres Padding zunächst von 60px auf 30px halbiert (Nutzeranfrage
       09.08.2026), auf erneute Nutzeranfrage direkt danach ("Abstand Karte zu
       Footer genau so groß wie der zwischen Header und Karte") ganz auf 0
       gesetzt: main hat oben padding-top:0 und margin:20px auto (wirkt auf
       beide Seiten gleich) - der sichtbare Freiraum zwischen Header und der
       jeweils aktiven Ansicht besteht dadurch ausschließlich aus diesem
       20px-margin. #legalFooter steuert seinerseits weiterhin sein eigenes,
       unverändertes Padding/border-top bei - das ist sein eigenes
       Innenpolster, kein Teil dieses Freiraums, genau wie header
       {padding:12px 20px} auch nicht mitgezählt wird. Gilt für main
       insgesamt, nicht nur für die Kartenansicht - es gibt keinen separaten
       Container pro View (siehe Kommentar oben), eine Sonderregel nur fürs
       Karten-View hätte diesem Grundsatz widersprochen.
       Bugfix (Nutzeranfrage 20.08.2026, "Abstand unterste Box zu Footer nicht
       identisch zu den übrigen Boxenabständen"): margin-bottom von 20px auf
       18px angeglichen (identisch zum Abstand zwischen allen anderen
       Boxen/Grids) und ist jetzt DIE alleinige Quelle für den Abstand zur
       Fußzeile - main ist als Flex-Item selbst formatierungskontext-bildend
       (wie display:flow-root), sein margin-bottom kollabiert deshalb NICHT
       mit dem margin-bottom des letzten Kindelements einer View, sondern
       würde sich sonst ungewollt dazu addieren. Damit trotzdem nur EINE
       18px-Lücke entsteht statt zwei gestapelter, neutralisiert die Regel
       ".view > *:last-child" (siehe dort) das margin-bottom des jeweils
       letzten View-Kindelements - unabhängig davon, ob das eine section.card,
       ein .grid2/.grid4 oder (Forschungslabor-Firmenansicht) ein einfaches
       div ohne eigenes margin-bottom ist. */
    padding: 0 16px;
    width: 100%;
    box-sizing: border-box;
    /* Schiebt #legalFooter ans Fensterende, solange der Inhalt kürzer ist. */
    flex: 1 0 auto;
  }
  @media (max-width: 820px) {
    /* Kompensiert den durch position:fixed entfallenen Dokumentfluss-Platz
       des Headers (Nutzeranfrage 05.08.2026) - 20px ist derselbe Abstand,
       den main auf Desktop ohnehin oben hat (siehe margin:20px auto oben).
       Muss NACH der unconditional main-Regel stehen, sonst gewinnt diese bei
       gleicher Spezifität rein über die Quellreihenfolge und die Reserve
       hätte nie gewirkt (genau das war ein Bug in einer früheren Fassung
       dieser Änderung - hier war die Regel zunächst innerhalb des Header-
       Mobile-Blocks weiter oben platziert). */
    main { margin-top: calc(var(--mobile-header-height, 130px) + 20px); }
    /* Vertikale Zentrierung kurzer Views (Nutzeranfrage 27.08.2026, "Abstand
       vom Button zum unteren Rand wie bei anderen Screens") läuft NICHT über
       reines CSS (erster Versuch: main{display:flex}+".view.active"
       margin:auto) - ein echtes Gerät zeigte dabei eine stark asymmetrische
       Verteilung (fast der gesamte Freiraum oben statt gleichmäßig verteilt),
       vermutlich wegen der auf Mobile bekanntermaßen unzuverlässigen
       vh-Berechnung (Adressleiste blendet sich ein/aus, `100vh` bezieht sich
       je nach Browser auf die größte statt die gerade sichtbare Höhe -
       body/main hängen indirekt über `min-height:100vh` daran). Stattdessen
       misst `syncMobileViewCentering()` (prototype/js/ui-helpers.js) die
       tatsächliche, aktuelle Geometrie per getBoundingClientRect()/
       window.innerHeight (immun gegen die vh-Eigenart) und setzt
       margin-top/-bottom direkt in Pixeln - siehe dort für die Rechnung. */
  }
  /* Impressum/Datenschutz (Nutzeranfrage 01.08.2026): rechtlich verpflichtende
     Links, die auf JEDEM Screen erreichbar sein müssen - deshalb bewusst
     außerhalb von <main> und außerhalb jeder Karte/jedes Dialogs, statt wie
     zuvor nur im Auth-Screen und im Profil versteckt. */
  #legalFooter {
    flex: 0 0 auto;
    align-self: center;
    width: fit-content;
    text-align: center;
    font-size: calc(12px + 1pt);
    color: var(--muted);
    /* Nutzerreport 25.08.2026 (Desktop) - obwohl padding-top/-bottom im Code
       gleich waren (je 8px), wirkte der Abstand über dem Text sichtbar
       größer als darunter: der verwendete Font reserviert laut
       canvas.measureText() für diesen Text ca. 14px Ascent, aber nur 3px
       Descent (kein Zeichen mit Unterlänge in "Impressum · Datenschutz ·
       AGB") - der Text sitzt dadurch von Natur aus im oberen Teil seiner
       Zeilenbox, gleiches padding oben/unten reicht deshalb optisch nicht.
       2px von oben nach unten verschoben, um das auszugleichen - nur hier,
       nicht im separaten Mobile-Padding weiter unten (dort ohnehin schon
       eigenständig 4px/8px+, siehe @media(max-width:820px)). */
    padding: 6px 16px 10px;
    /* margin-top bewusst 0 (Nutzeranfrage 20.08.2026, siehe Kommentar bei
       main oben): main/#legalFooter sind Flex-Geschwister und kollabieren
       ihre Margins deshalb NICHT wie normale Blockelemente - main allein
       bestimmt den 18px-Abstand zur Karte darüber, ein zusätzliches
       margin-top hier würde sich einfach dazu addieren statt sich damit zu
       vereinheitlichen. */
    margin: 0 16px 24px;
    /* Nutzeranfrage 20.08.2026: seit dem Foto-Seitenhintergrund
       (background.jpg auf <body>) saß der Footer-Text ohne eigenen
       Hintergrund direkt auf dem Bild und war dadurch stellenweise schlecht
       lesbar - eine eigene Box (wie die Header-Logo-Pille) statt der
       bisherigen reinen Trennlinie löst das für jedes Motiv. */
    /* Gleicher Holzsteg wie section.card/.kpi (Nutzeranfrage 21.08.2026),
       nur 2px statt 3px - die Footer-Pille ist deutlich kompakter als eine
       Karte, ein dickerer Rand hätte hier unproportional dick gewirkt (1px
       dünner als section.card/.kpi bleibt dabei bestehen, siehe deren
       Kommentar - Nutzeranfrage 22.08.2026, "alle Rahmen einen px
       schmaler"). */
    border: 2px solid transparent;
    border-radius: 8px;
    background: linear-gradient(var(--card), var(--card)) padding-box, linear-gradient(155deg, #8a6a49, #4c3a28) border-box;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
    /* Über JEDEM Nebel/Overlay der Seite (Nutzeranfrage 22.08.2026,
       "Impressum/Datenschutz/AGB müssen immer direkt erreichbar sein, auch
       wenn ein Popup den Nebel hervorruft") - 210 schlägt den bisher
       höchsten Wert im gesamten Stylesheet, .modal-overlay (z-index 200,
       Bestätigungs-/Info-Dialoge, Diamanten-Shop). Ursprünglich (Nutzer-
       anfrage 02.08.2026) nur für den Tutorial-Schleier (#tutorialBackdrop,
       z-index 18) gedacht - bleibt dadurch weiterhin ebenso vollständig
       sichtbar und unverdunkelt, statt vom jeweiligen Overlay grau
       überdeckt zu werden. Ändert sich künftig ein noch höherer z-index
       irgendwo im Stylesheet, muss dieser Wert entsprechend mitwachsen. */
    position: relative;
    z-index: 210;
  }
  #legalFooter a { color: var(--muted); }
  @media (max-width: 820px) {
    /* Fest direkt am unteren Bildschirmrand statt Teil des normalen
       Dokumentflusses (Nutzeranfrage 05.08.2026) - die Tab-Leiste
       (#mobileTabBar) rückt dafür etwas nach oben (siehe dort), damit beide
       fixierten Streifen übereinander Platz haben, ohne dass der Footer je
       unter der Tab-Leiste verschwindet oder umgekehrt. Kompakter als die
       Desktop-Fassung (kleinere Schrift/Padding), da hier nur noch ein
       schmaler Streifen zur Verfügung steht. */
    #legalFooter {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      width: auto;
      margin: 0;
      background: var(--card);
      /* Dünner Holzsteg-Trennstrich zu #mobileTabBar darüber (Nutzeranfrage
         23.08.2026, "dünner brauner Strich zwischen die Bereiche") - löst die
         frühere "kein eigener Rand"-Entscheidung ab (21.08.2026), die noch
         von der damaligen 1px-Überlappung mit #mobileTabBar ausging (dessen
         eigener border-top wäre sonst als Doppellinie direkt daneben
         erschienen). Seit dort kein Überlapp mehr existiert (23.08.2026, "es
         soll keinen Overlap geben"), sitzen beide Kanten nicht mehr
         übereinander - 1px statt der sonst 2-3px der übrigen Holzsteg-Familie
         (siehe Kommentar bei header), damit die Linie hier bewusst dünn
         bleibt. */
      border: none;
      border-top: 1px solid;
      border-image: linear-gradient(90deg, #8a6a49, #4c3a28) 1;
      border-radius: 0;
      box-shadow: none;
      font-size: calc(11px + 1pt);
      /* padding-top 4px statt zuletzt 6px (weitere Nutzeranfrage
         23.08.2026, "Bereich mit den Links um 2px reduzieren, oben
         abschneiden") - bottom:0 verankert die Box am true Bildschirmrand,
         der Text-Abstand zum unteren Rand hängt daher nur an padding-bottom
         (unverändert) - nur padding-top zu kürzen entfernt die 2px Höhe
         exakt oben, ohne den Text zu verschieben. #mobileTabBar's
         bottom:calc(var(--legal-footer-height) - 1px) hängt an der
         laufzeit-gemessenen Höhe dieser Box (siehe syncHeaderControlsReserve()
         in ui-helpers.js) - die Buttons darüber rutschen dadurch automatisch
         um dieselben 2px mit nach unten, ganz ohne eigene Änderung an
         #mobileTabBar (Folgeteil derselben Nutzeranfrage, "Bereich mit den
         Buttons darüber dann auch 2px nach unten schieben"). */
      padding: 4px 16px calc(8px + env(safe-area-inset-bottom, 0px));
    }
  }
  .view { display: none; }
  .view.active { display: block; }
  /* margin-top/-bottom:auto auf Mobile (Nutzeranfrage 27.08.2026, siehe
     Kommentar bei main/@media(max-width:820px) oben) - zentriert eine View
     dort vertikal in main, aber NUR solange sie kürzer als der verfügbare
     Platz ist: auto-Margins in einem Flex-Container lösen sich auf 0 auf,
     sobald das Element selbst mehr Platz braucht, als übrig ist (Standard-
     CSS-Verhalten) - eine lange View (Dashboard, Bank etc.) beginnt dadurch
     unverändert ganz oben, exakt wie showView()s window.scrollTo(0,0) es
     erwartet. */
  /* Neutralisiert das margin-bottom des jeweils letzten direkten
     View-Kindelements (Nutzeranfrage 20.08.2026, siehe ausführlicher
     Kommentar bei main oben) - main allein bestimmt danach den Abstand zur
     Fußzeile, unabhängig vom Elementtyp am Ende einer View. */
  .view > *:last-child { margin-bottom: 0; }
  .grid2 {
    display: grid;
    /* minmax(0, 1fr) statt reinem 1fr (Nutzeranfrage 03.08.2026, gefunden bei
       der Firmenzentrale-Spaltenbreite): ein reines "1fr" darf laut Grid-Spec
       nie unter den min-content der Zellen schrumpfen ("Grid Blowout") - eine
       Spalte mit breiterem Inhalt (z.B. viele Buttons in einer Reihe) hätte
       die andere Spalte dadurch schmaler als 50% gedrückt, obwohl beide
       gleich breit sein sollen. minmax(0, 1fr) erlaubt das nötige Schrumpfen. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* align-items bleibt beim Grid-Standard "stretch" (Nutzeranfrage
       02.09.2026, Nachtrag zu den auf-/zuklappbaren Dashboard-/
       Firmenzentrale-Karten): sind beide Karten einer Zeile aufgeklappt,
       sollen sie weiterhin gleich hoch erscheinen, genau das liefert
       "stretch". Nur eine zugeklappte Karte soll NICHT auf die Höhe ihres
       Zeilen-Partners gezogen werden - das regelt stattdessen
       align-self:start direkt an der jeweils zugeklappten Karte
       (wireDashboardSectionToggles()/applyDashboardSectionToggle() in
       dashboard.js bzw. das analoge Muster in firms.js setzen das inline,
       synchron zum Auf-/Zuklappen). */
    gap: 18px;
    margin-bottom: 18px;
  }
  @media (max-width: 820px) {
    /* Auch hier minmax(0, 1fr) statt reinem 1fr - aus demselben Grund wie
       oben, nur hier zunächst vergessen (Nutzer-Report 04.08.2026: "Boxen im
       Profil gehen über den sichtbaren Bereich hinaus"). Eine einzelne Spalte
       aus reinem 1fr kann nicht unter die min-content-Breite ihres Inhalts
       schrumpfen: Im Profil erzwang das eine 446px breite Karte in einem
       375px-Fenster samt seitlichem Scrollen. Betraf potenziell JEDEN
       .grid2-Screen auf schmalen Geräten, im Profil fiel es nur zuerst auf,
       weil dessen Inhalt die breiteste min-content-Breite hat. */
    .grid2 { grid-template-columns: minmax(0, 1fr); gap: 12px; margin-bottom: 12px; }
  }
  /* Ranglisten: ursprünglich (01.08.2026) alle 4 nebeneinander, eigene
     Klasse statt .grid2 wegen der dafür schmaleren Spalten. Auf Nutzeranfrage
     26.08.2026 ("in der Desktopversion bitte in 2x2 aufteilen, also nicht
     alle 4 nebeneinander") ist die 4-Spalten-Stufe entfallen - 2 Spalten
     gelten jetzt durchgängig von der schmalsten bis zur breitesten Desktop-
     Ansicht, der vormalige 1100px-Umbruch (dort ohnehin schon auf 2 Spalten)
     ist dadurch die neue Grundregel statt einer Zwischenstufe. */
  .grid4 {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 18px;
    margin-bottom: 18px;
  }
  /* Gleicher Box-Abstand wie .grid2/.kpi-grid auf schmalen Screens (Nutzer-
     anfrage 20.08.2026: der Abstand zwischen Boxen soll überall identisch
     sein), unabhängig vom eigenen 560px-Spaltenumbruch unten. */
  @media (max-width: 820px) {
    .grid4 { gap: 12px; margin-bottom: 12px; }
  }
  @media (max-width: 560px) {
    /* minmax(0, 1fr) aus demselben Grund wie bei .grid2 - hier vorsorglich,
       die Ranglisten-Karten haben aktuell keine breiten Inhalte. */
    .grid4 { grid-template-columns: minmax(0, 1fr); }
  }
  /* Symmetrische Kachel-Raster für Profil-Picker (Avatar/Rahmen/Banner,
     Nutzeranfrage 03.08.2026) - ersetzt display:flex;flex-wrap, das je nach
     Label-Textbreite unterschiedlich viele Elemente pro Zeile zuließ und
     keine bündige Spaltenausrichtung garantierte. CSS Grid teilt dieselben
     Spalten-Tracks über alle Zeilen (garantiert bündige Spalten), --cols legt
     je Picker eine FESTE Spaltenzahl fest, die die tatsächliche Elementanzahl
     exakt teilt (4 Avatare-Spalten für 12 Elemente - Nutzeranfrage 04.08.2026,
     max. 4 nebeneinander, ergibt 3 Zeilen -, 5 für 5 Rahmen, 7 für 7 Banner) -
     bewusst kein repeat(auto-fit, minmax(...)), das bei einer Elementanzahl,
     die nicht durch die auto-berechnete Spaltenzahl teilbar ist, eine
     ungleich kurze letzte Zeile erzeugt hätte. minmax(0,1fr) verhindert
     Grid-Blowout (siehe .grid2-Kommentar), justify-items:center zentriert
     jedes Element (samt seiner bereits text-align:center'ten Beschriftung
     darunter) auf seine eigene Breite in der Spalte - reicht für die Rahmen-/
     Banner-Kacheln, die zusätzlich noch margin:0 auto tragen, aber NICHT für
     die Avatar-Kachel: ihr 64px-Kreis ist ein block-level Element mit fester
     Breite, das text-align:center vom Elternelement ignoriert (das wirkt nur
     auf Inline-Inhalt wie den Text darunter) - ohne eigenes margin:0 auto
     hing der Kreis links, sobald der Zellen-Container durch ein längeres
     Label breiter wurde als 64px (Bugfix 04.08.2026, Nutzeranfrage). */
  /* Drei-Wege-Umschalter über dem Avatar-Raster (Nutzeranfrage 04.08.2026) -
     die aktive Option ist ein voller Button, die beiden anderen sind
     secondary, gleiches Muster wie der Hell/Dunkel-Umschalter im Profil. */
  .gender-switch { display: flex; gap: 8px; flex-wrap: wrap; }
  .gender-switch button { width: auto; height: auto; padding: 6px 14px; font-size: calc(13px + 1pt); }
  .symmetric-grid {
    display: grid;
    grid-template-columns: repeat(var(--cols, 6), minmax(0, 1fr));
    gap: 14px;
    justify-items: center;
  }
  /* Bugfix (28.08.2026, Nutzerreport "Texte überlappen sich" bei langen
     zusammengesetzten Wörtern wie "Logistikunternehmen"/"Bankbetreiber"):
     justify-items:center schrumpft jedes Element auf seine bevorzugte
     (Inhalts-)Breite - bei einem einzelnen, nicht trennbaren Wort ist das
     breiter als die Spalte, wodurch es sichtbar in die Nachbarspalte
     hineinragt (Grid clippt Overflow standardmäßig nicht). stretch zwingt
     jedes Element stattdessen auf die tatsächliche Spaltenbreite (die
     Kreis-Kacheln bleiben davon unberührt, die zentrieren sich weiterhin
     selbst per margin:0 auto), erst DANN kann overflow-wrap das lange Wort
     innerhalb dieser Breite umbrechen statt es zu clippen.
     .symmetric-grid > * statt einer generischeren Auswahl, weil nur die
     direkten Rasterkinder (nicht z. B. verschachtelte .item-sub in anderen
     Kontexten) betroffen sein sollen. */
  .symmetric-grid > * { justify-self: stretch; }
  .symmetric-grid .item-sub { overflow-wrap: break-word; word-break: break-word; }
  /* Bugfix (29.08.2026, Nutzerreport "Text unter den Icons darf maximal aus
     zwei Zeilen bestehen") - die Desktop-Spaltenzahlen (--cols, inline in
     profile.js gesetzt) sind auf Mobile zu schmal: einzelne Labels wie
     "Bankier (Burgunder)" oder "Logistikunternehmen" liefen auf bis zu 6
     Zeilen, statt nur an Leerzeichen umzubrechen. !important überschreibt
     die inline gesetzte --cols (reguläre Kaskade kann ein Stylesheet ein
     Inline-Style sonst nicht schlagen), gleiches Muster wie schon bei
     #dashboardBtn u.a. weiter oben. Selbe 820px-Grenze wie sonst überall im
     Spiel für "Mobile" (siehe #mobileTabBar-Media-Query).
     Avatar-Raster (12 Einträge) auf 2 Spalten statt 4: bei 3 Spalten passt
     "(Burgunder)"/"(Karmesin)" immer noch nicht auf eine Zeile, UND der fest
     64px breite Avatar-Kreis (avatarSvg-Kachel) ist dann breiter als die
     Spalte selbst, wodurch sein Auswahl-Rahmen sichtbar in die Nachbarspalte
     hineinragt (derselbe Kreis passt bei 2 Spalten mit deutlichem Abstand in
     die Spalte). Profil-/Popup-Banner (11 Einträge) sowie Abzeichen
     (7 Einträge) auf 3 statt 4 Spalten - deren Kacheln haben keinen so
     breiten festen Inhalt, drei Spalten reichen dort für zwei Zeilen.
     Avatar-Rahmen/Popup-Rahmen (je 5 Einträge, --cols:5) auf 3 statt 5
     Spalten (Nutzeranfrage 29.08.2026): ergibt bewusst zwei Zeilen (3 oben,
     2 unten) statt einer einzigen breiten Zeile. */
  @media (max-width: 820px) {
    .avatar-grid { --cols: 2 !important; }
    .profile-banner-grid, .popup-banner-grid, .badge-grid { --cols: 3 !important; }
    .avatar-frame-grid, .popup-frame-grid { --cols: 3 !important; }
  }
  /* Einzelner Aktions-Button am Ende einer Profil-Karte rechtsbündig auf
     Desktop (Nutzeranfrage 13.09.2026, "wie im übrigen Spiel auch") - gleiches
     Muster wie .bank-terms-submit/.offer-post-submit (siehe dort): auf Mobile
     stattdessen zentriert statt rechtsbündig, da .item-row:has(button) diese
     Reihen nicht erfasst (kein .item-row). */
  .profile-action-row { display: flex; justify-content: flex-end; }
  @media (max-width: 820px) {
    .profile-action-row { justify-content: center; }
  }
  /* Eigene Zeile für Rangliste-Einträge statt .item-row (Nutzeranfrage
     01.08.2026): kein flex-wrap, damit ein langer Anzeigename nie in eine
     zweite Zeile umbricht und dadurch die bündige Werte-Spalte zerstört -
     er kürzt stattdessen per Ellipsis, der Wert bleibt fest rechts mit
     garantiertem Mindestabstand zum Namen. */
  .lb-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
  }
  .lb-row:last-child { border-bottom: none; }
  /* .lb-name ist jetzt selbst eine Flex-Zeile (Rang + Avatar + Name, siehe
     05.08.2026 Nutzeranfrage) statt eines einzelnen Text-Elements - die
     Ellipsis-Kürzung sitzt deshalb auf dem inneren .lb-name-text statt auf
     .lb-name selbst, sonst würde ein Flex-Container den Umbruch/die Kürzung
     seiner Kind-Elemente nicht mehr wie reinen Text behandeln. */
  .lb-name { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 6px; }
  .lb-rank { flex-shrink: 0; }
  .lb-avatar { width: 22px; height: 22px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; overflow: hidden; }
  /* Regenbogen-Rahmen (Avatar- UND Popup-Rahmen, Nutzeranfrage 13.09.2026,
     "so wie wir es auch bei den Rahmen für die Firmen haben") - box-shadow
     kann keinen Farbverlauf rendern, deshalb ein eigener Ring über ::before
     statt eines box-shadow-Werts. inset:-3px legt den Ring EINEN Ring-Schritt
     außerhalb des Elements an (gleiche Grundidee wie die bisherigen
     box-shadow-Ringe, nur als Verlauf statt Volltonfarbe), border-radius:
     inherit übernimmt automatisch die Form des jeweiligen Elements (Kreis
     bei Avataren, abgerundetes Rechteck beim Kurzprofil-Popup) statt fest
     50% anzunehmen. z-index:-1 hält den Ring hinter dem eigentlichen Inhalt
     (Avatar-SVG bzw. Popup-Karteninhalt), position:relative auf der Klasse
     selbst macht ein zusätzliches Markup-Attribut dafür unnötig. */
  .frame-rainbow { position: relative; }
  .frame-rainbow::before {
    content: "";
    position: absolute;
    inset: -3px;
    border-radius: inherit;
    background: conic-gradient(#ef4444, #f97316, #eab308, #22c55e, #3b82f6, #8b5cf6, #ef4444);
    box-shadow: 0 0 10px rgba(139, 92, 246, 0.45);
    z-index: -1;
  }
  /* Nutzeranfrage 26.08.2026: mehr Abstand zwischen Profilbild und Name -
     nur auf Desktop, wo die Rangliste seither in 2 statt 4 Spalten steht
     (siehe .grid4) und dadurch pro Karte mehr Breite zur Verfügung hat.
     Gezielt auf .lb-avatar statt das gemeinsame .lb-name-gap zu erhöhen,
     damit nur die Avatar-Name-Lücke wächst, nicht auch die Rang-Avatar-Lücke
     davor. */
  @media (min-width: 821px) {
    .lb-avatar { margin-right: 4px; }
  }
  .lb-name-text { min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Nutzeranfrage 10.08.2026 - Hover-Unterstreichung als Hinweis, dass die
     Zeile klickbar ist (Kurzprofil-Popup, siehe openPlayerProfile() weiter
     unten). Nur der Name unterstreicht, nicht die ganze Zeile - der Rang
     und der Avatar-Kreis sind kein Text, den man "unterstrichen" lesen
     würde. */
  .lb-row:hover .lb-name-text { text-decoration: underline; }

  /* Eröffnungs-Effekt fürs Kurzprofil-Popup (10_MONETARISIERUNG.md
     Abschnitt 2o/2p, Nutzeranfrage 10.08.2026) - ein einmaliger Glanz-
     Sweep über die Popup-Karte beim Öffnen. Die Klasse wird bei jedem
     Öffnen neu gesetzt (siehe openPlayerProfile()), damit die Animation
     bei wiederholtem Öffnen jedes Mal erneut abläuft, statt nur beim
     allerersten Mal. */
  .profile-intro-shimmer { position: relative; overflow: hidden; }
  .profile-intro-shimmer::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.55) 48%, transparent 66%);
    background-size: 250% 100%;
    background-position: 150% 0;
    animation: profile-intro-shimmer-sweep 1.1s ease-out;
    pointer-events: none;
  }
  @keyframes profile-intro-shimmer-sweep { from { background-position: 150% 0; } to { background-position: -50% 0; } }
  @media (prefers-reduced-motion: reduce) {
    .profile-intro-shimmer::after { animation: none; }
    /* Barrierefreiheits-Audit 22.08.2026 - tutorial-shake und storage-flash
       (siehe deren Definitionen weiter unten) waren bisher nicht an
       prefers-reduced-motion angebunden, anders als diese und die drei
       Wetter-Animationen weiter unten. Beide sind zwar kurze Einmal-
       Effekte (0,4s/2s), aber ein Shake-Effekt ist genau die Art von
       Bewegung, die reduced-motion ausdrücklich abschalten soll. */
    #tutorialPanel.shake { animation: none; }
    .storage-highlight { animation: none; }
    /* Postfach-Wackeln (Nutzeranfrage 25.08.2026) - dauerhaft wiederkehrend,
       anders als die beiden Einmal-Effekte oben, deshalb hier genauso
       zwingend abzuschalten. */
    .mailbox-btn.has-unread { animation: none; }
  }
  .lb-value { flex-shrink: 0; text-align: right; }
  /* Name und Wert sind der eigentliche Inhalt einer Rangliste-Zeile, keine
     sekundäre Zusatzinfo - color:var(--text) überschreibt hier bewusst die
     geerbte .item-sub-Farbe (Nutzerreport 04.08.2026: beide Spalten lasen
     sich in der gedämpften Sekundärfarbe wie Fließtext-Beiwerk statt wie
     die Kernaussage der Zeile). Zwei Klassen (.lb-row .lb-name) statt nur
     .lb-name, damit die Regel .item-sub (dieselbe Spezifität, aber später im
     Stylesheet) sicher schlägt, unabhängig von der Reihenfolge im Quelltext. */
  .lb-row .lb-name, .lb-row .lb-value { color: var(--text); }
  /* Report-Button (11_BACKEND_ARCHITEKTUR.md Abschnitt 7f) - dezent, erst bei
     Hover auf der Zeile deutlich sichtbar, damit die Rangliste im Normalfall
     nicht mit Icons überladen wirkt. */
  .lb-report { flex-shrink: 0; cursor: pointer; opacity: 0.35; margin-left: 4px; font-size: calc(13px + 1pt); }
  .lb-row:hover .lb-report { opacity: 0.9; }
  /* Erst testhalber nur für die Unternehmensbewertung (03.09.2026), dann
     (04.09.2026, Nutzeranfrage "gefällt mir total gut, baue es für die
     anderen Ranglisten genau so ein") auf alle vier ausgeweitet: jede zeigt
     jetzt die komplette Spielerliste statt nur Top 10 (siehe
     GET /game/leaderboard, routes.ts) in einem eigenen Scroll-Container.
     max-height auf 10 sichtbare Zeilen bemessen (Zeile: 22px Avatar + 2*6px
     Padding + 1px Rahmen = 35px, 10x minus dem fehlenden Rahmen der letzten
     Zeile = 349px). padding-right 10px (Nutzeranfrage 04.09.2026, vorher
     25px) hält den Zeileninhalt auf Abstand zur Scrollbar - die native
     Scrollbar liegt bei overflow:auto AUSSERHALB der Padding-Box (zwischen
     Rahmen- und Padding-Kante), das Padding bleibt dadurch zuverlässig als
     Lücke zwischen Text und Scrollbar sichtbar statt von ihr überdeckt zu
     werden. */
  #leaderboardValuation,
  #leaderboardRevenue,
  #leaderboardProfit,
  #leaderboardResearch {
    max-height: 349px;
    overflow-y: auto;
    padding-right: 10px;
  }
  /* Joint-Venture-Rangliste (UI-Überarbeitung, Nutzeranfrage 09.09.2026) -
     eigene, kleinere Scroll-Höhe (statt der 349px der vier Haupt-Ranglisten
     oben): die Teilnehmerzahl eines einzelnen Events ist deutlich kleiner als
     die komplette Spielerliste, und die Liste sitzt hier innerhalb einer
     ohnehin schon langen Detailansicht statt eine eigene volle Karte für sich
     zu haben. Nutzt dieselben .lb-*-Klassen wie die vier Haupt-Ranglisten
     (siehe deren Regeln oben), damit sich die Zeilen optisch nicht wie ein
     Fremdkörper anfühlen. */
  .jv-leaderboard {
    max-height: 245px;
    overflow-y: auto;
    padding-right: 10px;
  }
  .jv-leaderboard-title {
    margin-top: 10px;
    margin-bottom: 4px;
    font-weight: 600;
  }
  /* Spaltenüberschriften über der Rangliste (Nutzeranfrage 09.09.2026) - sitzt
     bewusst AUSSERHALB des scrollenden .jv-leaderboard-Containers, damit sie
     beim Scrollen durch viele Teilnehmer sichtbar bleibt statt mitzuwandern
     (kein sticky nötig, die Liste ist kurz genug, dass "einmal oben fix"
     reicht). Nutzt .lb-row/.lb-name/.lb-value für identische Spaltenbreiten
     wie die tatsächlichen Zeilen darunter, aber kleiner/gedämpft/mit
     Versalien, damit sie sich klar als Kopfzeile statt als weitere
     Rangliste-Zeile lesen - derselbe Kontrast wie zwischen Tabellenkopf und
     -inhalt an anderer Stelle im Spiel (z. B. .admin-table th). */
  .jv-leaderboard-header {
    padding: 0 0 4px;
    border-bottom: 1px solid var(--border);
  }
  .jv-leaderboard-header .lb-name,
  .jv-leaderboard-header .lb-value {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* Fortschritts-Ring (Nutzeranfrage 09.09.2026, "der Ring sollte die
     gelieferte Menge abbilden und die Restzeit kann daneben angezeigt
     werden") - SVG-Kreis, dessen stroke-dashoffset (inline von
     renderJointVentureView() gesetzt) den Liefer-Fortschritt zur Zielmenge
     zeigt. Restzeit steht bewusst prominent (größere/fettere Schrift als der
     übrige Fließtext) daneben, statt wie zuvor als schlichte .item-sub-Zeile
     - macht das Event insgesamt "wichtiger" wirken, wie ursprünglich
     gewünscht. */
  /* Historie: Der Ring saß zwischenzeitlich als zweites Kind direkt in der h2
     (auf Höhe der Überschrift, Nutzeranfrage 09.09.2026), dadurch aber
     rechtsbündig an der Kartenkante statt über dem darunterstehenden
     Liefertext. Nutzeranfrage 10.09.2026 verschob den Ring+Text-Block dann
     neben die Zielmengen-Zeile ("nach rechts oben, neben 'Benötigt:
     [Ware]'"). Nutzeranfrage 12.09.2026 ("den Ring mit Text als gemeinsamen
     Block bitte zentriert anzeigen") löst diese Nebeneinander-Anordnung
     wieder auf - der Block steht jetzt für sich, über die volle Kartenbreite
     zentriert (.jv-progress-block, siehe unten), die Zielmengen-Zeile
     wieder als eigene linksbündige Zeile darüber. */
  .jv-ring-box { position: relative; width: 84px; height: 84px; flex-shrink: 0; }
  .jv-ring-box svg { transform: rotate(-90deg); }
  .jv-ring-track { fill: none; stroke: var(--border); stroke-width: 8; }
  .jv-ring-fill {
    fill: none;
    stroke: var(--accent);
    stroke-width: 8;
    stroke-linecap: round;
    transition: stroke-dashoffset 0.4s ease;
  }
  .jv-ring-center {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .jv-ring-pct { font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }
  /* Fortschritts-Beschriftung + Countdown, zentriert unter dem Ring - bricht
     lange Warennamen/große Zielmengen bei Bedarf mehrzeilig um, ohne dass
     eine Zeile unter den Ring geraten könnte: der Ring steht als eigenes
     Kind VOR dem Text im normalen Zeilenfluss, nicht absolut positioniert
     darüber (Nutzertest mit "Landwirtschaftsmaschinen"/8-stelliger
     Zielmenge). Nutzeranfrage 12.09.2026 ("den Ring mit Text als
     gemeinsamen Block bitte zentriert anzeigen") - vormals .jv-progress-side,
     Kind einer eigenen Flex-Reihe neben der Zielmengen-Zeile
     (.jv-progress-row, seit dieser Anfrage entfallen); steht jetzt für sich
     als eigener Block über die volle Kartenbreite zentriert, statt an einer
     Seite auszurichten - margin:0 auto zentriert den (nur so breit wie sein
     Inhalt werdenden, dank width:fit-content) Block selbst horizontal,
     align-items:center zentriert Ring/Text zusätzlich untereinander. */
  .jv-progress-block { display: flex; flex-direction: column; align-items: center; text-align: center; width: fit-content; margin: 4px auto 10px; }
  /* Eigene, deutlich größere/fettere Fassung von buildCountdownHtml()'s
     Standard-.item-sub-Optik, gezielt via Klassenname (statt eines
     Inline-Styles) an buildCountdownHtml() übergeben, damit
     tickBuildCountdowns() (ui-helpers.js) den Text weiterhin automatisch
     live nachzieht - nur die CSS-Klasse selbst ist hier neu, keine eigene
     Ticking-Logik. */
  .jv-ring-countdown {
    font-size: 16px;
    font-weight: 700;
    color: var(--accent-dark);
    margin-top: 2px;
  }
  .jv-not-on-track { color: var(--danger); }
  /* "Bauchbinde" (Nutzeranfrage 12.09.2026) - durchlaufendes Laufband der
     letzten 10 Einzel-Lieferungen (jv.recentContributions), zusätzlich zur
     nach Gesamtmenge sortierten Rangliste weiter unten. .jv-ticker-track
     wird in jointVenture.js mit ihrem eigenen Inhalt VERDOPPELT gerendert
     (items+items) - die Animation verschiebt exakt um -50% der Track-Breite
     (= genau einmal den nicht-verdoppelten Inhalt), wodurch der Übergang vom
     Ende zurück zum Anfang nahtlos wirkt, ohne sichtbaren Sprung. Pausiert
     bei Hover, damit ein einzelner Eintrag bei Interesse in Ruhe lesbar
     bleibt, exakt wie ein klassischer TV-Nachrichtenticker. */
  .jv-ticker-title { margin-top: 4px; }
  .jv-ticker { overflow: hidden; background: var(--bg); border-radius: 10px; padding: 8px 0; margin: 4px 0 10px; }
  .jv-ticker-track { display: inline-flex; white-space: nowrap; animation: jv-ticker-scroll 22s linear infinite; }
  .jv-ticker:hover .jv-ticker-track { animation-play-state: paused; }
  .jv-ticker-item { display: inline-flex; align-items: center; gap: 6px; padding: 0 22px; font-size: 13px; }
  .jv-ticker-avatar { width: 22px; height: 22px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; overflow: hidden; }
  @keyframes jv-ticker-scroll {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
  }
  /* "Mein Stand"-Karte (Rang + Live-Belohnungsvorschau) - dieselbe
     item-row/item-sub/item-name-Struktur wie das Kurzprofil-Popup
     (renderPlayerProfileContent(), leaderboard.js), nur mit einem leichten
     Kartenhintergrund, damit sie sich von der übrigen Fließtext-Liste
     absetzt, ohne eine eigene section.card (mit vollem Holzsteg-Rahmen)
     dafür zu brauchen. */
  .jv-standing {
    background: var(--bg);
    border-radius: 10px;
    padding: 8px 10px;
    margin: 10px 0;
  }
  /* Trennzeile innerhalb der Rangliste, die den Diamanten-Cutoff (nur die
     besten X liefern zusätzlich Diamanten, siehe 03_WIRTSCHAFT.md Abschnitt
     "Joint Venture Phase 2") sichtbar macht, statt ihn nur in Textform in der
     eigenen Vorschau zu erwähnen. */
  .jv-cutoff-line {
    text-align: center;
    border-top: 1px dashed var(--border);
    padding-top: 4px;
    margin-top: 2px;
  }
  /* Diamanten-Badge pro Rangliste-Zeile (Bugfix 09.09.2026, Nutzerreport: das
     Badge saß vorher am Ende von .lb-name und rückte dadurch optisch bis an
     die danebenstehende gelieferte Menge heran, "💎 25" sah wie 25 Diamanten
     aus statt der tatsächlichen Schätzung) - flex-shrink:0 wie .lb-rank/
     .lb-avatar, damit es bei einem langen, per Ellipsis kürzenden Namen
     (.lb-name-text) nicht selbst mit gequetscht wird. */
  .jv-diamond-badge {
    flex-shrink: 0;
    font-size: 12px;
    color: var(--muted);
  }
  /* Prestige-Rang-Icon (08_MULTIPLAYER.md Abschnitt 7d, Nutzeranfrage
     10.09.2026) - wiederverwendet exakt denselben Slot/flex-shrink:0-Trick
     wie .jv-diamond-badge oben, in den vier Hauptranglisten (leaderboard.js)
     UND hier in der Joint-Venture-Beitragsliste (beide teilen sich .lb-name),
     damit es bei einem langen, per Ellipsis kürzenden Namen nicht selbst mit
     gequetscht wird. */
  .prestige-badge { flex-shrink: 0; font-size: 14px; }
  /* Melde-Icon für die drei neuen meldbaren Texte außerhalb der Rangliste
     (Börsenkürzel/Bank-Name/Speditionsname, Nutzeranfrage 06.08.2026, siehe
     11_BACKEND_ARCHITEKTUR.md Abschnitt 7m) - container-unabhängig (anders
     als .lb-report, das gezielt auf .lb-row:hover reagiert), da diese drei
     Stellen unterschiedliche Eltern-Strukturen haben. */
  .report-flag { cursor: pointer; opacity: 0.35; margin-left: 6px; font-size: calc(13px + 1pt); }
  .report-flag:hover { opacity: 0.9; }
  /* Schmaler Holzsteg statt der schlichten 1px-Grau-Linie (Nutzeranfrage
     21.08.2026, "Bilderrahmen-ähnlicher Rahmen", dann "schmaler Holzsteg") -
     dieselbe background-clip-Technik und dieselben Braun-Töne wie der
     bestehende Fensterrahmen um die Firmenfotos (siehe
     .firm-detail-illustration weiter unten) - border-image würde hier
     ebenso wie dort den border-radius ignorieren (siehe dessen Kommentar),
     die zwei übereinandergelegten Hintergründe (padding-box/border-box)
     respektieren ihn dagegen. Bewusst themenunabhängig (kein --card-Bezug
     im Holzton wie bei der Illustration nötig, da hier direkt background
     statt nur border ersetzt wird) - funktioniert dadurch identisch in
     Light/Dark, da nur die INNERE Fläche (padding-box) noch var(--card)
     nutzt. */
  section.card {
    /* 3px statt 4px (Nutzeranfrage 22.08.2026, "alle Rahmen einen px
       schmaler") - gilt für die ganze Holzsteg-Familie, siehe Kommentar bei
       header's border-bottom. */
    border: 3px solid transparent;
    border-radius: 14px;
    background: linear-gradient(var(--card), var(--card)) padding-box, linear-gradient(155deg, #8a6a49, #4c3a28) border-box;
    padding: 16px 18px;
    margin-bottom: 18px;
  }
  /* Gleicher Box-Abstand wie .grid2/.grid4/.kpi-grid auf schmalen Screens
     (Nutzeranfrage 20.08.2026: der Abstand zwischen Boxen soll überall
     identisch sein - vorher blieb dieser Abstand auf Mobile bei 18px,
     während .kpi-grid dort schon auf 12px umschaltete). */
  @media (max-width: 820px) {
    section.card { margin-bottom: 12px; }
  }
  /* Bugfix (Nutzeranfrage 20.08.2026, "Abstände nicht identisch"): section.card
     ist als Grid-Item von .grid2/.grid4 KEIN eigener Nachbar-Block, dessen
     margin-bottom mit dem margin-top des nächsten Geschwisters kollabiert -
     Grid-Item-Margins bleiben immer voll erhalten und addierten sich bisher
     zum grid-eigenen "gap" dazu. Zwischen zwei Grid-Zeilen (z.B. Dashboard
     "Unternehmenswert"/"Firmen & Produktion" zu "Zeitalter & Forschung"/
     "Offene Vorgänge") ergab das 18px gap + 18px margin-bottom = 36px, obwohl
     derselbe Abstand direkt über der Grid (kpi-grid zu Grid) nur den "reinen"
     18px-margin-bottom von .kpi-grid hatte - sichtbar als doppelt so großer
     Abstand. .grid2/.grid4 tragen jetzt selbst ein margin-bottom (für den
     Abstand NACH der Grid, z.B. zum Footer) - die einzelnen Karten darin
     brauchen ihr eigenes margin-bottom deshalb nicht mehr. */
  .grid2 > section.card, .grid4 > section.card { margin-bottom: 0; }
  section.card h2 {
    font-size: calc(15px + 1pt);
    margin: 0 0 12px;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .progress-wrap {
    background: var(--accent-light);
    border-radius: 999px;
    height: 8px;
    overflow: hidden;
    margin-top: 6px;
  }
  .progress-bar {
    background: var(--accent);
    height: 100%;
    transition: width .3s ease;
  }
  .progress-bar.full { background: var(--danger); }
  /* Frühwarnung ab 90% Füllstand (12_UI_UX.md, Nutzeranfrage 03.09.2026) -
     eigene Zwischenstufe VOR "voll"/pausiert (.full oben bleibt unverändert
     bei tatsächlich erreichter Kapazität), damit der Spieler reagieren kann,
     bevor die Produktion tatsächlich pausiert. */
  .progress-bar.near-full { background: var(--warning-fill); }
  /* Nutzeranfrage 04.09.2026: Zeitalter-Freischalten-Button neben statt unter
     der Fortschrittsleiste - die Leiste wird dafür verkürzt (flex:1), statt
     wie sonst die volle Kartenbreite einzunehmen.
     Nutzeranfrage (Folgeanfrage, selbe Änderung): die übrigen ("nicht
     erfüllten") Leisten in der Zeitalter-Fortschritt-Karte sollen exakt so
     lang sein wie die mit Button daneben - dashboard.js rendert dafür bei
     ihnen einen unsichtbaren .age-unlock-spacer statt eines echten Buttons
     (progressBarRow(..., {forceRow:true}), nur in renderAgeProgress/
     #ageProgress verwendet - im Dashboard (#dashAge) bleibt eine Leiste ohne
     Button dagegen bewusst voll breit, siehe dortiger Aufruf ohne
     forceRow). Button UND Spacer bekommen dieselbe FESTE Breite statt auto,
     damit beide Leistentypen (mit/ohne echtem Button) exakt gleich lang
     werden, unabhängig vom tatsächlichen Button-Label - 180px deckt die
     längste der 4 Übersetzungen (DE "Zeitalter 5 freischalten" ~176px) mit
     etwas Puffer ab. Der Klassen-Selektor gewinnt dank höherer Spezifität
     automatisch gegen button{width:100%} aus der 480px-Media-Query weiter
     unten, auch auf Mobile - keine eigene Mobile-Sonderregel nötig. */
  .age-unlock-row { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
  .age-unlock-row .progress-wrap { flex: 1; min-width: 24px; margin-top: 0; }
  /* Folge-Nutzeranfrage 04.09.2026 - der FP-Zähler ("50 / 150 FP") stand in
     der Kopfzeile über der Leiste, jetzt auf derselben Höhe wie die Leiste
     selbst: .age-unlock-fp reiht sich hier als dritte mögliche Belegung
     dieser Spalte neben Button/Spacer ein, gleiche feste Breite wie diese. */
  .age-unlock-row button, .age-unlock-row .age-unlock-spacer, .age-unlock-row .age-unlock-fp { width: 180px; flex-shrink: 0; margin: 0; white-space: nowrap; }
  .age-unlock-row .age-unlock-spacer { height: 0; visibility: hidden; }
  .age-unlock-row .age-unlock-fp { text-align: center; }
  /* Nutzeranfrage 02.09.2026 - "Füllstands-Leiste bei den Lagern um 25px
     schmaler" - eigene Klasse statt der geteilten .progress-wrap-Grundregel
     zu ändern: Diese wird auch für Zeitalter- und Zonen-Freischalt-
     Fortschrittsbalken verwendet (dashboard.js/zones.js), die von dieser
     Anfrage nicht betroffen waren. Seit der Folgeanfrage 04.09.2026 unten
     (.storage-next-cap-row) sitzt dieser Balken in einer flex:1-Spalte -
     dort greift width nicht mehr (flex-basis 0% ignoriert es), diese Regel
     bleibt aber als Fallback erhalten, falls die Klasse je wieder isoliert
     verwendet wird. */
  .storage-progress-wrap { width: calc(100% - 25px); }
  /* Nutzeranfrage 04.09.2026 - Fortschrittsbalken für die nächste Lager-
     Ausbaustufe soll auf Höhe der "Nächste Ausbaustufe: X Einheiten"-Zeile
     stehen statt als eigene volle Zeile darunter. Label bekommt eine feste
     Breite (320px, deckt die längste der 4 Übersetzungen bei 100.000
     Einheiten mit Puffer ab - Spanisch braucht davon am meisten, ca. 310px),
     damit der Balken nie mit dem Text überlappt, egal wie groß die Zahl
     wird, und immer an derselben Stelle beginnt. */
  .storage-next-cap-row { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
  .storage-next-cap-row .storage-next-cap-label { flex: 0 0 320px; width: 320px; }
  .storage-next-cap-row .progress-wrap { flex: 1; min-width: 24px; margin-top: 0; }
  /* Unterhalb von 480px ist selbst 320px oft breiter als die ganze Zeile -
     zurück auf die ursprüngliche gestapelte Darstellung (Label über dem
     Balken), statt Überlauf/Zusammenquetschen zu riskieren. */
  @media (max-width: 480px) {
    .storage-next-cap-row { flex-direction: column; align-items: stretch; gap: 4px; }
    .storage-next-cap-row .storage-next-cap-label { width: 100%; flex-basis: auto; }
    /* flex:1 von oben setzt flex-basis:0% - in flex-direction:column ist die
       Hauptachse aber die Höhe statt der Breite, wodurch die Leiste ohne
       dieses Zurücksetzen auf 0px Höhe kollabiert (live beobachtet). flex:
       none kombiniert mit width:100% verhält sich hier wie ganz ohne
       Flex-Item - genau die ursprüngliche, volle Breite/feste 8px-Höhe aus
       der geteilten .progress-wrap-Grundregel. */
    .storage-next-cap-row .progress-wrap { flex: none; width: 100%; }
  }
  /* Kurzes Aufblitzen, wenn von "Lager voll" o.ä. zu einer bestimmten Zeile
     in der konsolidierten Lager-Übersicht gesprungen wird (29.07.2026). */
  .storage-highlight { animation: storage-flash 2s ease-out; border-radius: 8px; }
  @keyframes storage-flash {
    0% { box-shadow: inset 0 0 0 999px var(--accent-light); }
    100% { box-shadow: inset 0 0 0 999px transparent; }
  }
  .fp-note {
    font-size: calc(12px + 1pt);
    color: var(--muted);
    margin-top: 4px;
  }
  .item-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
    gap: 10px;
    flex-wrap: wrap;
  }
  /* Dashboard-Firmenzeile springt seit Nutzeranfrage 21.08.2026 per Klick zur
     Firma (siehe renderDashboardFirms() in dashboard.js) - dezente
     Hover-Fläche als Klick-Affordanz, gleicher Farbton wie die MC/FP-Chips. */
  .dash-firm-row:hover { background: var(--accent-light); border-radius: 6px; }
  /* Nutzeranfrage 02.09.2026, "angezeigte Firmen auf 6 begrenzen und dafür
     eine Scrollbar einfügen" (11.09.2026 auf 5 reduziert) - .dash-firm-row
     (padding:4px 0 im Markup + .item-sub-Zeilenhöhe) misst live nachgemessen
     durchgehend 29px pro Zeile, 5 × 29px als max-height lässt genau 5 Zeilen
     unbeschnitten stehen, die 6. wird zur Hälfte sichtbar angeschnitten -
     dasselbe "eine angeschnittene Zeile deutet den Rest an"-Muster wie z.B.
     bei .location-dropdown (max-height:320px, siehe dort). Betrifft nur die
     Firmenzeilen selbst - die "X Firmen"-Kopfzeile und der "N pausiert"-
     Hinweis (beide außerhalb von .dash-firms-list, siehe
     renderDashboardFirms() in dashboard.js) bleiben immer sichtbar, scrollen
     nicht mit. */
  /* scrollbar-gutter:stable (Nutzerreport 02.09.2026, "Text soll an derselben
     Stelle sein, auch wenn keine Scrollbar angezeigt wird") - ohne das
     reserviert overflow-y:auto den Scrollbar-Platz NUR, wenn tatsächlich
     mehr Firmen als die max-height zulässt zum Scrollen führen; darunter
     reicht die Zeile bis zur echten Kartenkante, wodurch die rechtsbündige
     Stufenzahl (justify-content:space-between) je nach Firmenanzahl an
     unterschiedlichen Stellen landete. stable reserviert die Gutter-Breite
     immer, unabhängig davon, ob gerade gescrollt werden kann. */
  .dash-firms-list { max-height: 145px; overflow-y: auto; scrollbar-gutter: stable; }
  /* Nutzerreport 02.09.2026, "vor allem auf mobil verschwindet der Text unter
     der Scrollbar" (11.09.2026 präzisiert: "10px von der Scrollbar weg") -
     die Scrollbar von .dash-firms-list liegt (v.a. auf mobilen Browsern, die
     sie über statt neben dem Inhalt einblenden) direkt über der
     rechtsbündigen Stufenzahl (justify-content:space-between, siehe
     .item-row-Basisregel) - padding-right rückt die ganze Zeile (Name UND
     Stufenzahl) von der Kante ab, statt nur die Stufenzahl separat zu
     verschieben. Dank scrollbar-gutter:stable oben ist diese Kante jetzt
     immer dieselbe (der reservierte Gutter), unabhängig von der
     tatsächlichen Firmenanzahl. */
  .dash-firms-list .dash-firm-row { padding-right: 10px; }
  /* Nutzerreport 02.09.2026, "Tagesaufgabe und die zugehörige Bedingung
     sollen nie umgebrochen werden" - die geerbte .item-row-Basisregel
     (flex-wrap:wrap, kein flex-Wachstum auf .item-name) ließ bei langem
     Aufgabentext das GANZE zweite Kind (die Fortschrittsanzeige, z.B.
     "50 / 50 MC") als eigenständiges Flex-Item auf eine neue Zeile
     UNTER den Text rutschen, statt dass stattdessen der Text selbst
     intern umbricht. flex:1/min-width:0 auf .item-name erlaubt genau das
     (Zeilenumbruch INNERHALB des Labels statt Verdrängung des
     Geschwisters), flex-shrink:0/white-space:nowrap auf .item-sub hält die
     Fortschrittsanzeige als festen, nie umbrechenden Block rechts;
     align-items:flex-start (statt des geerbten center) hält sie dabei auf
     Höhe der ERSTEN Zeile eines mehrzeilig gewordenen Labels, nicht mittig
     über die ganze (dann höhere) Zeile verteilt. */
  .dash-quest-row { align-items: flex-start; flex-wrap: nowrap; }
  .dash-quest-row .item-name { flex: 1; min-width: 0; }
  .dash-quest-row .item-sub { flex-shrink: 0; white-space: nowrap; }
  /* Bugfix (Nutzerreport 04.09.2026, "erledigte nicht linksbündig zu
     unerledigten") - "✅" (breiteres Emoji) und "☐" (schmaleres Textzeichen,
     siehe Kommentar bei renderDashboardQuests() in dashboard.js) hatten ohne
     eigene Breite unterschiedlich viel Platz belegt, wodurch das Label
     danach je nach Erledigt-Status an einer anderen X-Position begann.
     Feste Breite + zentriert reserviert für beide Icon-Varianten denselben
     Platz, das Label beginnt dadurch in jeder Zeile an derselben Stelle. */
  .dash-quest-check { display: inline-block; width: 1.3em; text-align: center; }
  /* Buttons auf Mobile horizontal zentrieren (Nutzeranfrage 20.08.2026,
     "prinzipiell alle Buttons außer die im Header") - betrifft bewusst auch
     .item-row-Zeilen mit Label+Button (z.B. Marktplatz-Einträge), die dadurch
     ihr bisheriges Label-links/Button-rechts-Muster verlieren; das war
     ausdrücklich gewünscht. :has(button) statt einer pauschalen Regel auf
     .item-row selbst, da diese Klasse auch für reine Text-/Infozeilen ohne
     jeden Button verwendet wird (z.B. "Wartungszustand: 100%") - dort bleibt
     die bisherige Ausrichtung unverändert. .item-actions/.cosmetic-row/
     .tutorial-actions/.gender-switch/.offer-zone-grid bestehen dagegen
     immer ausschließlich aus Buttons, brauchen die Prüfung daher nicht.
     .modal-actions (Bestätigen/Abbrechen-Dialoge) ist mit aufgenommen, auch
     wenn nicht in .item-row - ebenfalls eine reine Button-Zeile.
     Bewusst NICHT angefasst: .sell-controls (der Mengen-Stepper im Markt) -
     dessen eigene mobile Regel (justify-content:flex-start) geht auf eine
     frühere, gezielte Nutzeranfrage zurück (gleich große Lücken zwischen den
     Buttons über mehrere umgebrochene Zeilen hinweg) und ist zudem
     spezifischer (.market-right .sell-controls), eine :has()-Regel hier
     hätte sie ohnehin nicht überstimmt. Ebenso nicht angefasst: Admin-Tabellen
     und -Pagination (.admin-table, .admin-pager-nav) - kein Spieler-Kontext,
     eine zentrierte Vor/Zurück-Navigation wäre dort unüblich/verwirrend. */
  @media (max-width: 820px) {
    .item-row:has(button) { justify-content: center; }
    .item-actions, .cosmetic-row, .tutorial-actions, .gender-switch, .modal-actions {
      justify-content: center;
    }
  }
  /* Admin-Tabellen ("Alle Spieler", seit 06.08.2026 auch das Audit-Log,
     Nutzeranfrage) - die einzigen echten <table>-Elemente im ganzen Client,
     alles andere nutzt div-basierte .item-row-Listen. Bewusst als echte
     Tabelle, weil so gefragt und weil dieser Screen ohnehin nur für Admins
     (nicht die mobile-first-Spielerbasis) gedacht ist - der
     overflow-x:auto-Wrapper um die jeweilige Liste (Markup) fängt trotzdem
     ein zu schmales Gerät ab, statt Inhalt abzuschneiden. Eine gemeinsame
     Klasse statt einer pro Tabelle, da beide identisch gestaltet sind. */
  .admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: calc(13px + 1pt);
    white-space: nowrap;
  }
  .admin-table th, .admin-table td {
    padding: 6px 10px;
    text-align: center;
    border-bottom: 1px solid var(--border);
  }
  .admin-table th {
    color: var(--muted);
    font-weight: 600;
  }
  .admin-table td.num { text-align: center; font-variant-numeric: tabular-nums; }
  .admin-table button { width: auto; height: auto; padding: 4px 10px; font-size: calc(12px + 1pt); margin-right: 6px; }
  .admin-table button:last-child { margin-right: 0; }
  /* Pagination-Leiste unter/über den Admin-Tabellen (Audit-Log, 06.08.2026,
     Nutzeranfrage) - Seitengröße links, Vor/Zurück + Seitenanzeige rechts. */
  .admin-pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 10px;
  }
  .admin-pager-nav { display: flex; align-items: center; gap: 8px; }
  .admin-pager-nav button { width: auto; height: auto; padding: 4px 10px; font-size: calc(12px + 1pt); }
  /* Nutzeranfrage 11.09.2026 - bislang ohne eigenen Rahmen/border-radius
     (nackter Browser-Default), dadurch optisch inkonsistent zu den anderen
     Dropdowns im Spiel (.qty-input, z. B. die Joint-Venture-Auswahlfelder). */
  .admin-pager select { width: auto; height: auto; padding: 4px 8px; font-size: calc(12px + 1pt); border: 1px solid var(--border); border-radius: 8px; font-family: inherit; }
  .item-row:last-child { border-bottom: none; }
  .item-info { display: flex; align-items: flex-start; gap: 10px; flex: 1 1 220px; min-width: 220px; }
  .item-row > button { flex-shrink: 0; }
  /* Ausbauen + Firma abreißen (Nutzeranfrage 21.08.2026, "sollen so breit
     sein wie die übrigen Buttons oben drüber") - die beiden sitzen seit
     Kurzem gemeinsam in einem Wrapper-Div statt direkt als .item-row-Kinder,
     die obige Regel griff dadurch nicht mehr: ohne flex-shrink:0 quetschten
     sich beide Buttons gemeinsam in die verfügbare Zeilenbreite, statt
     jeweils ihre normale responsive Breite zu behalten und bei Bedarf
     umzubrechen (Text wurde dadurch per Ellipsis abgeschnitten). */
  .firm-action-row {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    /* Erzwingt eine eigene volle Zeile statt neben dem "Aktuell: X -> Y"-Text
       zu landen (gleiches Muster wie .item-actions) - sonst blieb der Zeile
       oft weniger als die 242px (2x116px + gap) übrig, die beide Buttons
       nebeneinander brauchen, und flex-shrink:0 ließ sie dadurch über den
       Kartenrand hinauslaufen statt zu schrumpfen. */
    flex-basis: 100%;
    /* War früher fest 66px hoch (Nutzeranfrage), damit der Abstand vom
       Ausbauen-Button zum Kartenrand IMMER gleich bleibt, ob gerade ein Bau
       läuft (dann steckt unter dem Button noch .firm-rush-progress'
       Countdown-Zeile: 42px-Button + 4px Gap + 20px Textzeile = 66px) oder
       nicht (Button allein, 42px). Auf ausdrücklichen Wunsch (Nutzerreport
       28.08.2026, "auch bei gegründeten Firmen den unteren Abstand so klein
       wie bei ungegründeten machen, aber bei Countdown-Text automatisch
       wieder verlängern") jetzt umgekehrt: keine eigene min-height mehr,
       die Zeile schrumpft/wächst mit ihrem tatsächlichen Inhalt (42px ohne,
       66px mit Countdown) - der Kartenrand rückt dadurch im Normalfall
       näher an den Button heran (identisch zum unbesessenen Gründen-Fall,
       siehe .firm-action-row-unfounded) und weicht nur noch zurück, wenn
       die Countdown-Zeile tatsächlich angezeigt wird. */
  }
  /* .firm-action-row-unfounded gab es schon einmal (Nutzeranfrage
     27.08.2026, mehr Abstand zum Text) und war zwischenzeitlich wieder
     entfallen, als der "Gründen"-Button neben Icon/Titel statt in
     .firm-action-row zog (Nutzeranfrage 28.08.2026). Jetzt wiederbelebt,
     aber für einen ANDEREN Zweck: seit dem Nachtrag desselben Tages
     ("Button soll unten rechts unterm Bild sein, wie Ausbauen/Abreißen",
     siehe renderFirmDetail() in firms.js) sitzt Gründen bei Firmen mit
     großem Bild wieder in .firm-action-row - die ist von sich aus aber
     linksbündig (kein justify-content unten). Ausbauen/Abreißen wirken nur
     ab 1350px rechtsbündig, weil syncFirmActionButtonsDesktopPosition()
     (firms.js) sie dort in die .action-grid der bereits gegründeten Firma
     umzieht - ein Mechanismus, der ownedSection/.action-grid braucht und
     bei einer noch unbesessenen Firma nie greift. Ohne eigene Regel wäre
     Gründen dadurch bei jeder Breite unter 1350px linksbündig, obwohl
     Ausbauen/Abreißen dort ebenfalls linksbündig sind (kein Widerspruch)
     - der Nutzerwunsch war aber ausdrücklich "rechtsbündig", nicht nur
     "an derselben Stelle wie Ausbauen unterhalb 1350px". justify-content
     nur auf dieser eigenen Klasse lässt .firm-action-row selbst (Ausbauen/
     Abreißen) unangetastet. */
  .firm-action-row-unfounded { justify-content: flex-end; }
  .firm-action-row button { flex-shrink: 0; }
  .icon { font-size: calc(22px + 1pt); width: 30px; text-align: center; }
  /* Firmen-/Waren-/Fahrzeug-Icons sind seit 24.08.2026 (Nutzeranfrage) SVG-
     Markup statt eines einzelnen Emoji-Zeichens (icon-Feld in
     server/src/db/seed.ts) - anders als ein Text-Emoji reagiert ein <svg>
     ohne eigene width/height nicht auf font-size und rendert sonst bei
     0x0px (live beobachtet: alle betroffenen Karten-/Listen-Icons blieben
     unsichtbar). 1em bindet die SVG-Größe an genau den font-size-Wert, den
     bisher das Emoji-Zeichen selbst genutzt hätte - jede Stelle, die den
     Emoji-Platzhalter bereits über font-size skaliert hat, muss dafür nicht
     angefasst werden. Deckt alle bekannten Icon-Container ab (siehe deren
     jeweilige render*()-Funktion in firms.js/market.js/transport.js/map.js/
     navigation.js/dashboard.js/cosmetics.js). */
  .icon svg, .livery-chip svg, .item-sub svg, .location-menu-item svg, .cosmetic-swatch-icon-preview svg {
    width: 1em;
    height: 1em;
    display: inline-block;
    vertical-align: -0.15em;
  }
  /* Nutzeranfrage 04.09.2026 - Icon+Name standen in der Dashboard-
     Firmenliste (renderDashboardFirms(), dashboard.js) unterschiedlich weit
     eingerückt: SVG-Icons rendern exakt 1em breit (Regel oben), ein reines
     Emoji-Zeichen wie beim Forschungslabor ("🔬") aber je nach Font oft
     breiter - beide standen direkt als Text vor dem Namen statt in einer
     festen Spalte. Feste Breite unabhängig vom Icon-Inhalt behebt das für
     jede bestehende UND künftige Firma in dieser Liste. */
  .dash-firm-icon { display: inline-block; width: 1.4em; margin-right: 4px; text-align: center; }
  /* Nachtrag (24.08.2026, Nutzeranfrage) - Firmen-Icon oben auf der
     Detailseite (renderFirmDetail(), firms.js) sitzt auf einer
     gebürsteten-Stahl-Plakette (Design "A" aus einer Vorschlagsrunde mit
     mehreren Varianten, samt Nachbesserungen an Schrauben-Deutlichkeit und
     -Größe) statt des ursprünglichen farbigen Cosmetic-Rahmens. War
     zwischenzeitlich auf Nutzeranfrage ganz entfernt, auf erneute
     Nutzeranfrage aber wieder eingebaut - diesmal ausdrücklich NUR auf
     Desktop ("in der mobilen Version soll es weiterhin nicht angezeigt
     werden"), siehe die @media-Regel weiter unten, die .firm-icon-plate
     dort ausblendet. Bewusst nicht themenabhängig (kein dark-mode-
     Gegenstück) - Metall bleibt Metall, wie auch die Kartenkachel ihren
     Rand-Akzent unabhängig vom Theme behält. Das Rahmen-Cosmetic
     (Rahmenfarbe-Auswahl weiter unten auf dieser Seite) ist von alldem
     unberührt und wirkt weiterhin auf der Kartenkachel, siehe
     applyFirmFrameBorder() in map.js.
     Größe/Zentrierung standen ursprünglich als Inline-Style direkt am
     <div> (firms.js) - hierher verschoben, weil ein Inline-`style` jede
     externe Regel schlägt, auch innerhalb einer @media-Query: Die
     Mobile-Ausblendregel unten (display:none) konnte ein inline gesetztes
     display:flex sonst nicht überschreiben (live per getComputedStyle()
     bemerkt - display blieb "flex" trotz @media-Treffer). */
  .firm-icon-plate {
    position: relative;
    font-size: 44px;
    width: 56px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    border-radius: 6px;
    background:
      linear-gradient(125deg, rgba(255,255,255,.35) 0%, rgba(255,255,255,0) 30%, rgba(0,0,0,.12) 55%, rgba(255,255,255,.22) 78%, rgba(0,0,0,.06) 100%),
      repeating-linear-gradient(100deg, #c7cbcd 0px, #b9bdbf 1px, #d3d6d8 2px, #c1c5c7 3px);
    box-shadow: 0 3px 8px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.35), inset 0 -2px 3px rgba(0,0,0,.18);
  }
  .firm-icon-plate svg { filter: drop-shadow(0 1px 1px rgba(0,0,0,.4)); }
  .firm-icon-plate .plate-screw {
    position: absolute; width: 6px; height: 6px; border-radius: 50%;
    background: linear-gradient(135deg, #f4f5f6 0%, #cfd3d5 35%, #8d9295 65%, #eef0f1 100%);
    box-shadow: 0 1px 1px rgba(0,0,0,.5), inset 0 0 0 1px rgba(0,0,0,.25);
  }
  .firm-icon-plate .plate-screw::after {
    content: ""; position: absolute; left: 50%; top: 50%; width: 4px; height: 0.9px;
    background: rgba(40,40,40,.65); transform: translate(-50%,-50%) rotate(35deg); border-radius: 1px;
  }
  .firm-icon-plate .plate-screw.tl { top: 4px; left: 4px; }
  .firm-icon-plate .plate-screw.tr { top: 4px; right: 4px; }
  .firm-icon-plate .plate-screw.bl { bottom: 4px; left: 4px; }
  .firm-icon-plate .plate-screw.br { bottom: 4px; right: 4px; }
  /* Nachtrag (24.08.2026, Nutzerreport) - "1em" allein ließ auf der
     Kartenkachel (.tile, 62px Box, aber nur calc(28px+1pt) Schriftgröße -
     ein Wert, der ursprünglich für ein einzelnes Emoji-Zeichen kalibriert
     war) einen großen, ungenutzten Rand zum grünen Rahmen: Das SVG selbst
     füllt sein eigenes viewBox längst zu 90 %+ (siehe die beiden vorigen
     Nachträge oben zur Rand-Halbierung), aber die Kachel gab ihm nur ~28px
     von 62px verfügbarer Fläche - ein ganz anderer Hebel als die
     viewBox-Füllung selbst. .tile-icon kommt ausschließlich auf der Karte
     vor (Kern-Gebäude wie Firmentyp-Kacheln, siehe map.js/index.html) -
     bewusst eigene, größere Multiplikator-Regel statt den gemeinsamen
     1em-Wert für alle Icon-Container zu erhöhen, sonst liefe z. B. das
     30px-Waren-Icon im Marktplatz über seine Zeile hinaus. */
  .tile-icon svg {
    width: 1.85em;
    height: 1.85em;
    display: inline-block;
  }
  .item-name { font-weight: 600; font-size: calc(14px + 1pt); }
  /* +2pt testweise (Nutzeranfrage 21.08.2026) - .item-sub ist die
     meistgenutzte Hilfstext-Klasse im Spiel (Firmenbeschreibungen, Hinweise,
     Dashboard-Kennzahlen wie "Firmenwert"/"Coins", Firmennamen in der
     Dashboard-Liste usw.), betrifft also bewusst breit alles davon auf
     einmal statt nur einer einzelnen Stelle. */
  .item-sub { font-size: var(--text-size-normal); color: var(--muted); }
  /* Trennt den Info-Teil einer Karte (Name/Status) sichtbar von den
     Aktions-Buttons darunter (user request 31.07.2026, Design-Durchgang
     Firmenzentrale) - vorher lagen z.B. bei einer Firma mit Mitarbeiter/
     Wartung/3x Experte/Auto-Verkauf/Produktionsfaktor alle Controls
     unstrukturiert im selben Flex-Wrap wie der Info-Block. */
  .item-actions {
    flex-basis: 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 6px;
    padding-top: 8px;
    border-top: 1px dashed var(--border);
  }
  /* Nutzeranfrage: der Produktionsfaktor-Speichern-Button soll bündig unter
     demselben Button landen, den die Zeile darüber an dieser Stelle abschliesst
     (z.B. "Wartung durchführen") statt irgendwo links darunter. Flexbox richtet
     umgebrochene Zeilen NICHT aneinander aus - jede Zeile wird unabhängig
     gepackt. CSS Grid dagegen teilt dieselben Spalten-Tracks über alle Zeilen
     hinweg: mit --action-cols (in JS auf die tatsächliche Button-Anzahl der
     ersten Zeile gesetzt) landet Element Nr. cols+1 (Produktionsfaktor-Label)
     automatisch in Spalte 1 der zweiten Zeile und Element Nr. cols+2
     (Speichern) in Spalte 2 - exakt unter dem zweiten Button der ersten Zeile.
     Nur ab einer Breite, die die 6 Spalten auch wirklich nebeneinander
     zulässt - NICHT die sonst übliche 820px-Schwelle (Nutzerreport
     20.08.2026, "Buttons überlappen nach rechts und werden teils
     unsichtbar, Umbruch passiert zu spät"): Jeder Button ist global fix
     200px breit (siehe button{width:200px}), macht bei 6 Stück + 5×10px
     Gap 1250px reine Inhaltsbreite. main{padding:0 16px} + section.card
     {padding:16px 18px} ziehen davon nochmal 68px ab -> ab ca. 1318px
     Fensterbreite passt das gerade so. Bei 821-1317px (ein sehr üblicher,
     nicht maximierter Fensterbereich) wurden die Spalten dagegen als
     starres 6er-Grid erzwungen, das (anders als Flexbox) nicht umbricht,
     sondern schlicht über den Kartenrand hinaus überläuft. 1350px statt
     1318px als Puffer gegen Rundungsdifferenzen. Unterhalb bleibt das
     bisherige flex-wrap (siehe .item-row) unverändert - dort bricht die
     Reihe korrekt um, nur eben ohne die Spaltenausrichtung. */
  /* Auto-Verkauf-Platzhalter (Nutzeranfrage 28.08.2026, siehe autoSellBtnHtml
     in firms.js) - reserviert seine Spalte NUR im festen 6-Spalten-Grid ab
     1350px (unten), wo mehrere Firmenkarten dieselben Spalten-Tracks teilen
     müssen. Unterhalb dessen ist .action-grid ein normales umbrechendes
     .item-row ohne geteilte Spalten - dort komplett aus dem Layout nehmen
     (display:none statt nur unsichtbar), damit der nächste echte Button
     direkt an dieser Stelle weitermacht statt eine Lücke offenzulassen. */
  .auto-sell-placeholder { display: none; }
  /* Wartungs-Platzhalter (Nutzeranfrage 11.09.2026, siehe ownedSection() in
     firms.js) - "Wartung durchführen" ist neben die Wartungszustand-Anzeige
     im Text darüber gewandert, die dadurch im .action-grid frei werdende
     Spalte sollte auf ausdrücklichen Wunsch ("die dadurch entstehende Lücke
     bewusst lassen") als Lücke stehen bleiben - der nächste Button
     (Auto-Verkauf) soll nicht in die freie Spalte rutschen. visibility:hidden
     nimmt immer Platz im Layout ein, unabhängig vom umgebenden
     Grid/Flexbox-Modus.
     Nachtrag (11.09.2026, Nutzeranfrage) - auf Mobile soll die Lücke
     entgegen der ursprünglichen Anfrage doch schließen ("Desktop bleibt so"),
     Auto-Verkauf rutscht dort also direkt neben Mitarbeiter einstellen.
     Unterhalb 820px jetzt genau wie .auto-sell-placeholder oben komplett aus
     dem Layout genommen (display:none) - ab 821px (Desktop, unverändert)
     bleibt visibility:hidden aktiv.
     Nachtrag (13.09.2026) - ein kurzer Zwischenversuch entfernte diesen
     Platzhalter zwecks gleichmäßiger Button-Abstände wieder, wurde aber auf
     erneute Nutzeranfrage ("erstmal wieder rückgängig machen") rückgängig
     gemacht. */
  .refill-placeholder { visibility: hidden; }
  @media (max-width: 820px) {
    .refill-placeholder { display: none; }
  }
  @media (min-width: 1350px) {
    .auto-sell-placeholder { display: inline-flex; visibility: hidden; }
    .action-grid {
      display: grid;
      grid-template-columns: repeat(var(--action-cols, 1), max-content);
      /* Nutzeranfrage - etwas mehr Luft zwischen der oberen Button-Zeile und
         der zweiten Zeile (Ausbauen/Abreißen + Info-Text, siehe
         syncFirmActionButtonsDesktopPosition() in firms.js) als der normale
         10px-Zeilenabstand. Nur row-gap, nicht column-gap - die Lücken
         INNERHALB der oberen Zeile kommen ohnehin vom geerbten
         justify-content:space-between der .item-row-Basisregel, nicht von
         gap, bleiben also unberührt. */
      row-gap: 20px;
    }
  }
  /* Nutzeranfrage - Produktionsfaktor + Speichern-Button wandern vor den
     "Aktionen"-Abschnitt (syncProdFactorSlotPosition() in firms.js
     verschiebt die bestehenden DOM-Knoten physisch dorthin - ursprünglich
     nur ab 1350px gedacht, siehe dessen Kommentar zum "Warum" bzgl. der
     action-grid-Spaltenbreite, auf erneute Nutzeranfrage aber auf JEDE
     Breite ausgeweitet, siehe dortiger Kommentar). .prodfactor-slot ist ein
     eigener Wrapper UM [data-prodfactor-row] UND den Speichern-Button (kein
     bloßes Klassen-Tag auf der Zeile selbst). margin-bottom gilt bei jeder
     Breite (Nutzerreport "Abstand dazwischen lassen") - auf Mobile/Tablet
     bleibt die Zeile selbst dabei unverändert (Label links, Regler rechts,
     siehe deren eigenes Inline-Style im Markup), nur eben an dieser neuen
     Stelle.
     Nachtrag (13.09.2026, Nutzeranfrage "Abstand zwischen Bild und den
     Buttons darunter um etwa die Hälfte reduzieren") - das große Firmenfoto
     schließt seit syncFirmIllustrationHeight() (firms.js) unten bündig mit
     dem Speichern-Button dieses Slots ab, wodurch genau dieser
     margin-bottom-Wert zum sichtbaren Abstand zwischen Bildunterkante und
     der darunter folgenden "Aktionen"-Zeile wurde - von 24px auf 12px
     halbiert. */
  .prodfactor-slot { margin-bottom: 12px; }
  /* Nutzeranfrage 11.09.2026 - der unsichtbare Produktionsfaktor-Platzhalter
     des Forschungslabors (prodFactorPlaceholderHtml, siehe renderFirmDetail()
     in firms.js, Abschnitt 151) reserviert dort dieselbe Höhe wie der echte
     Regler bei jeder anderen Firma, damit "Aktionen" firmentypübergreifend
     an derselben Stelle beginnt. Auf Mobile soll diese Lücke aber schließen
     ("Wartung durchführen" direkt gefolgt von "Aktionen"), Desktop bleibt
     unverändert - [aria-hidden="true"] trifft NUR den Platzhalter, nicht den
     echten .prodfactor-slot (der bei jeder anderen Firma tatsächliche
     Bedienelemente enthält und auf Mobile weiterhin sichtbar bleiben muss). */
  @media (max-width: 820px) {
    .prodfactor-slot[aria-hidden="true"] { display: none; }
  }
  /* Nutzeranfrage 28.08.2026 - Abstand zwischen der Produktionsfaktor-Zeile
     (Label+Regler) und ihrem darunterstehenden "Speichern"-Button auf Mobile
     vergrößert, die beiden standen bislang ohne jeden eigenen Abstand
     zueinander (weder [data-prodfactor-row] noch der Button selbst brachten
     einen mit). Nur unterhalb 1350px - ab dort übernimmt die eigene
     Grid-Regel weiter unten (row-gap:6px) dieselbe Aufgabe bereits. */
  @media (max-width: 1349px) {
    .prodfactor-slot [data-save-prodfactor] { margin-top: 12px; }
  }
  /* Nutzeranfrage (zum ersten Layout-Versuch, "Label soll über BEIDEN
     Elementen stehen, Slider UND Button, der Button muss also weiter runter
     wandern") - nur ab 1350px: eine reine flex-column auf der Zeile plus
     align-items:flex-end auf dem Wrapper reichte dafür allein nicht - der
     Button (42px) ist höher als nur die Reglerzeile allein (~20px), an der
     Unterkante ausgerichtet ragte er dadurch weiterhin bis auf Höhe des
     Labels nach oben. [data-prodfactor-row] hat aber nur zwei Kinder (Label,
     Regler-Gruppe) und keine dritte Ebene, die eigens fürs Grid-Layout UND
     den Button gemeinsam zuständig sein könnte - display:contents auf der
     Zeile lässt ihre beiden Kind-<span>s stattdessen direkt als
     Grid-Elemente des WRAPPERS auftreten (die Zeile selbst erzeugt keine
     eigene Box mehr), wodurch Label, Reglergruppe UND Speichern-Button alle
     im selben 2-Spalten-Grid des Wrappers landen: Label in Zeile 1 über
     beide Spalten, Regler in Zeile 2 Spalte 1, Button in Zeile 2 Spalte 2 -
     exakt auf Höhe des Reglers, nicht mehr des Labels. Bewusst nur ab
     1350px - auf schmaleren Screens bleibt die ursprüngliche
     Label-links/Regler-rechts-Zeile mit dem Button darunter (genug Platz
     fehlt dort ohnehin für zwei nebeneinanderstehende Spalten). */
  /* Nutzeranfrage 26.08.2026, "%-Angabe bündig mit dem Mitarbeiter
     einstellen-Button abschließen und den Speichern-Button bündig an
     Wartung durchführen angleichen": max-content max-content oben sah zwar
     hübsch kompakt aus, saß aber NICHT wirklich unter Spalte 1/2 der
     action-grid - eigene, unabhängig vom Inhalt berechnete Spaltenbreiten
     (~180px statt 200px) plus ein fester 16px-column-gap statt des dortigen
     10px + dynamisch verteiltem Rest (justify-content:space-between auf
     6 Spalten, geerbt von .item-row) ließen den Speichern-Button je nach
     Fensterbreite ein paar Pixel neben "Wartung durchführen" landen, nie
     exakt darunter. .prodfactor-slot bekommt deshalb jetzt dieselben
     sechs 200px-Spalten (jeder Button im Spiel ist global fix 200px breit,
     siehe button{width:200px}), denselben 10px-column-gap und dasselbe
     justify-content:space-between wie .action-grid - bei identischer
     Container-Breite (beide sind volle Block-Kinder derselben Karte)
     ergibt exakt dieselbe Grid-Rechnung exakt dieselbe Spalte-2-Position,
     unabhängig von der tatsächlichen Fensterbreite, ganz ohne eigene
     Breiten-/Lückenmessung. */
  @media (min-width: 1350px) {
    .prodfactor-slot {
      display: grid;
      grid-template-columns: repeat(6, 200px);
      column-gap: 10px;
      row-gap: 6px;
      justify-content: space-between;
      align-items: center;
    }
    .prodfactor-slot [data-prodfactor-row] { display: contents !important; }
    .prodfactor-slot [data-prodfactor-row] > span:first-child { grid-column: 1 / -1; grid-row: 1; }
    .prodfactor-slot [data-prodfactor-row] > span:last-child { grid-column: 1; grid-row: 2; width: 100%; }
    .prodfactor-slot [data-save-prodfactor] { grid-column: 2; grid-row: 2; }
  }
  /* Nutzeranfrage 02.09.2026, "Speichern-Button zum Firmenschild bündig mit
     dem Speichern-Button zur Produktionsrate ausrichten" - der naheliegende
     erste Versuch (dieselben sechs 200px-Spalten + justify-content:
     space-between wie .prodfactor-slot direkt oberhalb, exakt derselbe
     Kniff wie dort) griff hier NICHT: .plaque-save-row sitzt (anders als
     .prodfactor-slot/.action-grid, beide unterhalb des Illustrationsbilds)
     noch in der allerersten Zeile von .firm-content-with-illustration, die
     laut deren eigener Regel weiter unten pauschal padding-right:69% fürs
     Bild reserviert - ihr Container ist dadurch tatsächlich nur ~31% der
     Kartenbreite breit, nicht wie bei .prodfactor-slot die volle Breite.
     Dieselbe Spaltenrechnung über zwei unterschiedlich breite Container
     ergibt zwangsläufig unterschiedliche Pixel-Positionen.
     Lösung: Der Button wird stattdessen relativ zu #firmDetail (bereits
     position:relative - selbe Bezugsbox, die auch .firm-detail-illustration
     rechtsbündig positioniert, siehe dort) absolut positioniert, mit derselben
     Spalte-2-Formel wie das CSS-Grid sie für 6×200px-Spalten + 10px-Gap +
     space-between errechnen würde: Spalte2-Position = 200px Breite + 10px
     Basis-Gap + Rest-Freiraum/5 = W/5 - 40px, W = Breite von #firmDetail
     (=100%, da #firmDetail selbst die volle, ungepolsterte Kartenbreite hat -
     dieselbe Breite, die auch .action-grid/.prodfactor-slot als Bezugsgröße
     nutzen). Kein top nötig - ohne eigenen top/bottom-Wert behält eine
     absolut positionierte Kachel laut Spezifikation ihre "static position"
     (die Stelle im normalen Textfluss), die vertikale Position bleibt also
     unverändert neben dem Eingabefeld. */
  @media (min-width: 1350px) {
    .plaque-save-row [data-save-plaque] {
      position: absolute;
      left: calc(100% / 5 - 40px);
    }
  }
  /* Nutzeranfrage 11.09.2026 ("stell sicher, dass der 'Wartung durchführen'-
     Button bündig ist mit den 'Speichern'-Button") - "Wartung durchführen"
     sitzt seit Abschnitt 150 in derselben Zeile wie der Zustands-Text
     (.firm-maintenance-row, ehemals ein reines Inline-Style statt einer
     eigenen Klasse - siehe unten, warum das für diese Regel wichtig ist).
     Ohne diese Regel landete der Button bei längerem Zustandstext (z. B. mit
     Produktivitätsverlust) nicht an der "Spalte 2"-Position, an der auch die
     beiden Speichern-Buttons (Firmenschild/Produktionsfaktor, siehe
     .plaque-save-row [data-save-plaque] oben und .prodfactor-slot weiter
     unten) sitzen.
     Zwei vorige Ansätze verworfen: (1) dasselbe echte CSS-Grid-Muster wie
     .prodfactor-slot lief hier auf einen Container-Überlauf - anders als
     .prodfactor-slot (volle #firmDetail-Breite) sitzt diese Zeile innerhalb
     von .firm-production-info, das per padding-right:69% nur ~31% der
     Kartenbreite freigibt, viel zu wenig für repeat(6,200px) (braucht
     >1200px). (2) reines position:absolute+calc() wie bei .plaque-save-row
     oben, ohne eigenen top-Wert: Ein aus dem Fluss genommenes Flex-Kind
     nimmt an der flex-wrap-Zeilenberechnung nicht mehr teil, die "static
     position" landete dadurch auf derselben Zeile wie der Text und
     überdeckte dessen Ende bei längerem Zustandstext.
     Lösung: dieselbe position:absolute+calc()-Formel wie bei
     .plaque-save-row (Bezugsbox #firmDetail, siehe dortiger Kommentar),
     aber mit einem von syncFirmMaintenanceButtonPosition() (firms.js) live
     berechneten und gesetzten top-Wert statt der unzuverlässigen "static
     position". Nachtrag (Nutzerreport, "weiter oben, auf derselben Höhe
     wie die Zustand-Angabe") - reicht der Text neben dem Button Platz
     (Normalfall, kurzer Zustandstext), sitzt der Button auf derselben
     Zeile wie der Text, vertikal auf dessen Zeile zentriert; reicht der
     Platz nicht (längerer Text mit Produktivitätsverlust-Hinweis, s.o.),
     bleibt es beim Versatz unterhalb der gerenderten Textunterkante - die
     Funktion prüft das bei jedem Aufruf live nach Textbreite, kein fester
     Schwellenwert.
     .firm-maintenance-row braucht dafür eine eigene Klasse statt weiterhin
     eines Inline-Styles (wie noch in Abschnitt 150) - ein Inline-Style hätte
     diese Media-Query-Regel NIE schlagen können (Inline-Styles gewinnen
     immer gegen externe Regeln, unabhängig von deren Spezifität), das
     position:absolute dort wäre sonst wirkungslos geblieben (live genau so
     gefunden). Die Basisregel (unterhalb 1350px, identisch zum vorigen
     Inline-Style) steht deshalb jetzt hier als echte CSS-Klasse. */
  .firm-maintenance-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
  @media (min-width: 1350px) {
    .firm-maintenance-row [data-refill] {
      position: absolute;
      left: calc(100% / 5 - 40px);
    }
  }
  /* Nutzeranfrage ("stell sicher, dass die Buttons bündig sind") -
     Ausbauen/Abreißen sollen auf Desktop pixelgenau unter Experte-3-Tage/
     Experte-7-Tage landen (die letzten beiden Spalten der 6-Spalten-
     action-grid oben), nicht nur ungefähr.
     Zwei vorige, rein CSS-basierte Anläufe (Breite der äußeren Zeile fix auf
     1250px zwingen; .firm-action-row auf seine natürliche Breite zurücksetzen
     und auf das geerbte justify-content:space-between der äußeren .item-row
     vertrauen) trafen beide nur ungefähr: Abreißen landete zufällig richtig
     (space-between richtet erste/letzte Spalte IMMER an den Rändern des
     jeweiligen Containers aus, unabhängig von dessen Breite), Ausbauen aber
     nicht - der Lückenabstand ZWISCHEN den 6 Grid-Spalten (space-between,
     schwankt mit der Fensterbreite) und der feste 10px-Gap innerhalb von
     .firm-action-row sind zwei unabhängige Werte, die nur zufällig
     übereinstimmen können.
     Robuste Lösung: syncFirmActionButtonsDesktopPosition() (firms.js)
     verschiebt Ausbauen/Abreißen bei >=1350px physisch AUS .firm-action-row
     HERAUS und HINEIN in dieselbe .action-grid, mit grid-column:-3 (Ausbauen)
     bzw. grid-column:-2 (Abreißen, "letzte Spalte" via negativem
     CSS-Grid-Linienindex - funktioniert unabhängig von --action-cols).
     Beide sitzen dadurch in DENSELBEN Spalten-Tracks wie Experte 3/7 Tage -
     pixelgenau bündig, ganz ohne eigene Breiten-/Lücken-Berechnung. Ohne
     eigene grid-row lässt die Grid-Auto-Platzierung sie automatisch in die
     nächste Zeile fallen, in der Spalte 5/6 frei ist (meist Zeile 2, direkt
     unter Experte 3/7 Tage) - dieselbe Technik, die --action-cols weiter oben
     bereits für den Produktionsfaktor-Speichern-Button nutzt.
     .firm-action-row bleibt dabei als leerer Wrapper zurück (siehe
     :empty-Regel oben) - unterhalb von 1350px verschiebt dieselbe Funktion
     beide Buttons wieder zurück, das bisherige (unangetastete) Mobile-/
     Tablet-Layout ist von alldem nicht betroffen.
     Nachtrag (Nutzerreport) - mit den Buttons jetzt oben in der Grid-Zeile
     hing der "Aktuell: X -> Y"-Text (outputPreviewHtml(), vormals mit ihnen
     zusammen in der eigenen .firm-expand-row-Zeile ganz unten) allein und
     spürbar tiefer als die Buttons zurück, mit viel Leerraum dazwischen und
     zum Kartenrand. syncFirmActionButtonsDesktopPosition() verschiebt das
     Info-Div (data-firm-expand-info) deshalb ebenfalls in dieselbe Grid-
     Zeile - grid-column:1/-3 (alles links der beiden letzten, von
     Ausbauen/Abreißen belegten Spalten) lässt es automatisch in dieselbe
     Zeile wie die beiden Buttons fallen, auf gleicher Höhe. Der Ausbauen-
     Slot bekommt zusätzlich einen eigenen Wrapper mit min-height:66px
     (dieselbe Herleitung wie zuvor bei .firm-action-row: 42px-Button +
     4px Gap + 20px Countdown-Zeile) - dadurch wächst NUR dieser eine
     Zellenbereich beim Ausbauen nicht sichtbar, statt wie zuvor die ganze
     Zeile/Karte. .firm-expand-row selbst bleibt als leere Hülle zurück,
     sobald BEIDE Kinder (Text und Button-Zeile) verschoben sind -
     syncFirmActionButtonsDesktopPosition() blendet sie dafür explizit per
     style.display="none" aus (kein CSS ":empty", siehe Kommentar bei
     .firm-action-row weiter oben zum selben Leerzeichen-/Textknoten-
     Problem), sonst bliebe ein Leerraum bis zum Kartenrand übrig. */
  .action-grid > [data-firm-expand-info] { grid-column: 1 / -3; align-self: center; }
  /* Nutzerreport 28.08.2026, "Abstand vom Ausbauen-Button zum unteren Rand
     immer noch viel zu groß, soll nur bei laufendem Bau (Countdown-Zeile
     drunter) kurzzeitig größer werden" - .firm-action-slot-desktop trug
     dieselbe feste min-height:66px wie einst .firm-action-row (siehe
     dessen Fix weiter oben in dieser Datei) - DAS war der eigentliche
     Übeltäter auf breiten Desktop-Screens (>=1350px): dort verschiebt
     syncFirmActionButtonsDesktopPosition() (firms.js) Ausbauen/Abreißen
     aus dem längst schrumpfenden .firm-action-row HERAUS in genau diesen
     Slot innerhalb der .action-grid - der frühere .firm-action-row-Fix
     wirkte auf dieser Bildschirmbreite dadurch gar nicht, weil
     .firm-action-row dort ohnehin display:none ist (leere Hülle, siehe
     Kommentar oben). Entfernt, aus demselben Grund wie dort: die Zeile
     schrumpft jetzt auf ihre echte Inhaltshöhe (42px ohne, 66px mit
     Countdown) statt permanent den Countdown-Freiraum vorzuhalten. */
  /* Nutzeranfrage - Ausbauen und Abreißen sollen auf gleicher Höhe stehen.
     Abreißen ist ein einfacher 42px-Button OHNE eigenen Wrapper und würde
     von der Grid-Standardausrichtung (stretch: vertikal über die volle,
     vom höheren Ausbauen-Slot vorgegebene Zeilenhöhe) mittig statt oben
     platziert, sobald die Countdown-Zeile den Ausbauen-Slot höher als
     42px macht - dadurch ein Höhenversatz. align-self:start richtet ihn
     stattdessen an derselben oberen Kante aus wie den Ausbauen-Button. */
  .action-grid > [data-demolish] { align-self: start; }
  /* Mobile (09.08.2026, Nutzeranfrage): Der Mindestpreis fürs Auto-Verkauf-
     Modul soll direkt beim Auto-Verkauf-Button stehen, nicht erst nach den
     Experten-Buttons. .action-grid ist hier weiterhin ein einfaches
     flex-wrap (siehe .item-row), das ungeordnete Kind-Elemente einfach in
     DOM-Reihenfolge packt - eine Umsortierung im Markup selbst würde aber
     die oben dokumentierte Desktop-Spaltenausrichtung (--action-cols)
     durcheinanderbringen. Die Experten-Buttons (das einzige noch verbliebene
     dazwischenliegende Element - der Produktionsfaktor sitzt seit
     syncProdFactorSlotPosition() in firms.js gar nicht mehr im Grid, siehe
     .prodfactor-slot weiter oben) bekommen deshalb stattdessen auf Mobile
     einen höheren flex-order - alles andere behält den Default-Wert 0 und
     damit seine gegenseitige DOM-Reihenfolge (Mitarbeiter, Wartung,
     Auto-Verkauf, dann direkt der Mindestpreis), die Experten rutschen
     dadurch ans Ende. */
  @media (max-width: 820px) {
    .action-grid [data-hire-expert] {
      order: 1;
    }
  }
  /* Bank-Konditionen-Eingabefelder (08.08.2026, Nutzeranfrage - ersetzt die
     ursprüngliche Fassung vom 05.08.2026 mit fest geschätzten 90/90/110/90px-
     Spaltenbreiten): Die Eingabefelder sollen so breit wie ihr eigenes Label
     sein, UND "Kredit-Konditionen" (4 Felder) sowie "Festgeld-Konditionen"
     (3 Felder + Button) sollen weiterhin spaltenweise aneinander ausgerichtet
     bleiben - beides zusammen braucht EINEN gemeinsamen Grid-Container über
     BEIDE Zeilen hinweg statt zwei unabhängiger: CSS Grids max-content-
     Spaltenbreite bemisst sich am breitesten Element JEDER Spalte über ALLE
     Zeilen DESSELBEN Grids (zwei separate Grids würden dagegen unabhängig
     rechnen und z.B. bei "Kredit-Zins %..." vs. "Festgeld-Zins %..." leicht
     unterschiedlich breit ausfallen). Beide "Zeilen" sind daher direkte
     Kind-Elemente EINES .bank-terms-grid-Containers (Kredit-Felder zuerst,
     Festgeld-Felder inkl. Button danach) statt zwei eigener .item-row-Divs.
     Damit das Label (nicht das Eingabefeld selbst) die Spaltenbreite bestimmt,
     bekommen die Inputs ein kleines size-Attribut im Markup (hält ihren
     eigenen intrinsischen Beitrag zur max-content-Berechnung klein) und
     füllen die vom Label bestimmte Spalte erst über width:100% aus.
     Mobil (Nutzeranfrage 22.08.2026, "alle Eingabefelder in eine einzelne
     Zeile schieben und die Breite wie das Feld für 'Name Deiner Bank'
     nehmen" - vormals 2 gleich breite Spalten) wird aus demselben Container
     nur noch 1 Spalte (1fr) - jedes Feld bekommt dadurch eine eigene Zeile
     in voller Breite, genau wie .bank-brand-name (dort ebenfalls width:100%
     auf Mobile, siehe dessen Kommentar). In der "Bank eröffnen"-Ansicht
     (noch kein eigenes Institut) trennen zusätzlich zwei Überschriften
     ("Kredit-Konditionen"/"Festgeld-Konditionen") die beiden Gruppen - als
     eigene Grid-Kinder mit grid-column:1/-1 (volle Spaltenbreite), wodurch
     CSS Grid sie automatisch auf eine eigene Zeile setzt, ohne die
     Spaltenausrichtung der Felder davor/danach zu stören. */
  /* Nutzeranfrage 26.08.2026, "Eingabefelder alle gleich breit machen (am
     breitesten orientieren)": Die obige max-content-Idee (jede Spalte so
     breit wie ihr eigenes, längstes Label) sah pro Spalte unterschiedlich
     breite Felder vor - wirkte uneinheitlich, obwohl inhaltlich nichts
     zusammengehört (Spalte 1 ist bei "Kredit"/"Festgeld" jeweils "Zins %",
     Spalte 4 gibt es nur bei "Kredit"). Jetzt bekommen alle vier Spalten
     stattdessen dieselbe feste Breite, bemessen am breitesten Label über
     ALLE Spalten hinweg ("Strafgebühr %/Tag (ab Tag 3)", live gemessen
     218px) statt nur innerhalb der eigenen Spalte - macht die vorherige
     max-content-Spaltenbreite obsolet, siehe @media (min-width: 821px)
     unten. Ausdrücklich ein fester Platzhalter-Wert wie an dieser Stelle
     schon vor der max-content-Fassung (siehe Kommentar oben) - kein
     pixelgenaues Tuning je Sprache. */
  .bank-terms-grid { display: grid; gap: 12px; align-items: end; }
  .bank-terms-grid > div { min-width: 0; }
  .bank-terms-grid label { display: block; }
  .bank-terms-grid input { width: 100%; box-sizing: border-box; }
  /* Nutzeranfrage 22.08.2026, "mehr Abstand zwischen Eingabefeld Strafgebühr
     und Text 'Festgeld-Konditionen', alternativ Trennerlinie - dann aber
     auch zwischen 'Leer lassen für den Username' und 'Kredit-Konditionen'":
     Trennerlinie statt reinem Extra-Abstand, deckt beide genannten Stellen
     ab, da .bank-terms-heading auf BEIDE Überschriften zutrifft ("Kredit-
     Konditionen" direkt nach der Bankname-Zeile außerhalb des Grids, UND
     "Festgeld-Konditionen" direkt nach dem Strafgebühr-Feld). border-top
     statt margin-top allein, damit der zusätzliche Abstand sichtbar als
     eigene Trennung erkennbar ist, nicht nur als etwas größere Lücke.
     Ursprünglich nur mobil (auf Desktop schien die 4-Spalten-Aufteilung
     bereits ausreichend Trennung zu bieten) - auf Nutzeranfrage 23.08.2026
     ("hätte ich gerne auch in der Desktop-Version die Trennlinie") jetzt
     durchgängig in .bank-terms-heading selbst statt nur im Mobile-Media-
     Query. Farbe von --border auf --muted angehoben (ebenfalls Nutzeranfrage
     23.08.2026, "Kontrast zwischen Linie und Hintergrund größer") - --border
     ist bewusst sehr blass gehalten (siehe Kontrastaudit-Kommentar oben bei
     den Root-Variablen) und dafür bei dezenten Card-Rahmen gedacht, nicht bei
     einer bewusst wahrnehmbaren Trennlinie wie hier. */
  .bank-terms-heading {
    grid-column: 1 / -1;
    border-top: 1px solid var(--muted);
    padding-top: 14px;
    margin-top: 2px;
  }
  @media (max-width: 820px) {
    .bank-terms-grid { grid-template-columns: 1fr; }
  }
  @media (min-width: 821px) {
    /* minmax(0, 220px) statt fixem 220px (Nutzeranfrage 26.08.2026, "bei
       zwei Dritteln Fensterbreite sieht es teilweise nicht gut aus", hier
       konkret: horizontaler Overflow bei ca. 850-1000px Fensterbreite -
       repeat(4, 220px) ergibt 4×220px + 3×12px Gap = 916px, unabhängig von
       der tatsächlich verfügbaren Breite; die einzelnen Karten-Container
       sind aber gerade knapp oberhalb von 821px oft schmaler als das, bevor
       der Mobile-Fallback bei genau 820px greift - derselbe Bug-Typ wie bei
       den Waren-/Aktienmarkt-Karten weiter oben. minmax(0, 220px) lässt die
       vier Spalten bis zu 220px breit werden, aber gemeinsam enger, wenn
       weniger Platz da ist, statt zu überlaufen - auf breiten Screens ändert
       sich dadurch nichts (dort ist ohnehin genug Platz für volle 220px). */
    .bank-terms-grid { grid-template-columns: repeat(4, minmax(0, 220px)); justify-content: start; }
    /* white-space:nowrap entfernt (Nutzeranfrage 26.08.2026, siehe Kommentar
       bei grid-template-columns direkt oben): war ursprünglich exakt auf die
       feste 220px-Spaltenbreite abgestimmt ("Strafgebühr %/Tag (ab Tag 3)"
       passt bei 220px knapp einzeilig) - sobald die Spalte oben schrumpfen
       darf, würde ein erzwungenes nowrap das Label stattdessen über die
       eigene (jetzt schmalere) Spalte hinaus überlaufen lassen. Ohne nowrap
       bleibt das Label einzeilig, solange genug Platz ist (unverändertes
       Verhalten auf breiten Screens), und bricht nur im Engpass sauber auf
       eine zweite Zeile um statt zu überlaufen. */
  }
  /* "Bank eröffnen"/"Konditionen ändern" absenden (09.08.2026, Nutzeranfrage) -
     der Button liegt bewusst AUSSERHALB von .bank-terms-grid statt als
     letztes Grid-Kind, damit sein (oft breiterer) Button-Text nicht in die
     Spaltenbreiten-Berechnung einfließt.
     Nutzeranfrage 26.08.2026, "Button auf gleiche Höhe wie untere
     Eingabefelder": Grid und Button sind jetzt gemeinsame Kinder eines
     neuen .bank-terms-row-Wrappers (statt zweier unabhängiger, vertikal
     gestapelter Blöcke) - align-items:flex-end richtet den Button an der
     Unterkante der letzten Feld-Zeile aus (Festgeld-Zeile), justify-content:
     space-between schiebt ihn dabei weiterhin an den rechten Kartenrand,
     genau wie zuvor mit margin-top+justify-content:flex-end auf der
     eigenständigen Zeile. */
  /* Kein zusätzliches gap hier (Nutzeranfrage 26.08.2026 zunächst mit 12px
     umgesetzt, dann korrigiert): justify-content:space-between verteilt den
     Abstand zwischen Grid und Button ohnehin über den gesamten verfügbaren
     Rest-Platz - ein zusätzliches, erzwungenes Mindest-gap führte dazu, dass
     .bank-terms-row bereits bei ~1200px Fensterbreite (Grid + Button passen
     dann nur noch knapp, ohne jeden Puffer) unnötig in den Mobile-Fallback
     (siehe unten) umbrach, obwohl beide nebeneinander gepasst hätten. */
  .bank-terms-row { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; }
  .bank-terms-submit { display: flex; justify-content: flex-end; }
  /* Auf Mobile wirklich zentriert statt weiterhin rechtsbündig (Nutzerreport
     20.08.2026, "Bank eröffnen ist nicht wirklich zentriert") - diese Zeile
     lief bislang unter dem Radar der generellen Mobile-Button-Zentrierung
     (.item-row:has(button) etc., siehe dort), weil .bank-terms-submit keine
     dieser Klassen trägt. Rechtsbündig auf Desktop bleibt unverändert, siehe
     Kommentar oben. .bank-terms-row wechselt auf Mobile zurück auf eine
     einfache Spalte (Grid dann volle Breite, Button darunter zentriert) -
     dieselbe gestapelte Reihenfolge wie vor dem 26.08.2026-Wrapper. */
  @media (max-width: 820px) {
    .bank-terms-row { flex-direction: column; align-items: stretch; }
    .bank-terms-submit { justify-content: center; margin-top: 10px; }
  }
  /* Preisfelder je Zielzone in "Transportangebot veröffentlichen" (Nutzeranfrage
     12.08.2026) - vorher ein reines flex-wrap ohne Spaltenobergrenze, wirkte
     auf breiten Bildschirmen mit vielen aktiven Zonen unübersichtlich. Auf
     Mobile bleibt es beim einfachen Umbruch (kaum Platz für mehr als eine
     Spalte ohnehin); ab Desktop-Breite maximal 3 Zonen nebeneinander. Die
     inline gesetzten flex-Styles im Markup wurden dafür entfernt - eine
     inline style hätte diese Klasse unabhängig von der Media Query immer
     geschlagen.
     Mobil (Nutzeranfrage 22.08.2026, "Eingabefelder auf volle Breite, für
     Mediterran links ausrichten wie die anderen auch") wird daraus ein
     echtes einspaltiges Grid statt des bisherigen flex-wrap: Der eigentliche
     Grund für die uneinheitliche Ausrichtung war die vormalige
     justify-content:center (siehe @media max-width:820px weiter oben,
     .offer-zone-grid stand dort in der gemeinsamen Button-Zentrierungs-
     Liste) - bei ungerader Zonen-Anzahl landete das letzte, allein
     übrigbleibende Feld dadurch zentriert statt wie die übrigen (paarweise
     nebeneinander) am linken Rand. Ein einspaltiges Grid macht jedes Feld
     gleichermaßen volle Breite, die Zentrierungsfrage stellt sich dadurch
     gar nicht mehr - dieselbe Lösung wie schon bei .bank-terms-grid. */
  .offer-zone-grid { display: flex; flex-wrap: wrap; gap: 10px; }
  @media (max-width: 820px) {
    .offer-zone-grid { display: grid; grid-template-columns: 1fr; gap: 16px; }
  }
  @media (min-width: 821px) {
    .offer-zone-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
  }
  /* Nutzerreport 02.09.2026, "Button nach rechts verschieben, da wo in den
     anderen Abschnitten der Button ja auch ist" - "Angebot veröffentlichen"
     stand bisher als einzelnes Element ohne umgebende Flex-/Grid-Zeile direkt
     unter dem Zielzonen-Grid, dadurch links (Standard-Blockfluss) statt wie
     jeder andere Formular-Button rechtsbündig zur Kartenkante. Gleiches
     Muster wie .bank-terms-submit (siehe dort): justify-content:flex-end auf
     Desktop, echte Zentrierung on Mobile (auch hier greift die generelle
     .item-row:has(button)-Zentrierung nicht, da kein .item-row). */
  .offer-post-submit { display: flex; justify-content: flex-end; margin-top: 12px; }
  @media (max-width: 820px) {
    .offer-post-submit { justify-content: center; }
  }
  /* Einklappbare Erklärtexte (user request 31.07.2026) - für Hinweisblöcke,
     die mehrere Sätze lang sind und nicht bei jedem Besuch erneut gelesen
     werden müssen (siehe 12_UI_UX.md Abschnitt 6, "Übersicht zuerst, Details
     auf Wunsch"). Kurze Ein-Satz-Hinweise (z.B. im Marktplatz/Transportamt)
     bleiben bewusst unverändert sichtbar - nur mehrzeilige Blöcke profitieren
     vom Einklappen. */
  .withdrawal-consent {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-size: calc(12px + 1pt);
    color: var(--muted);
    line-height: 1.4;
    margin-bottom: 14px;
    cursor: pointer;
  }
  .withdrawal-consent input { margin-top: 2px; flex-shrink: 0; }
  /* Widerrufsbelehrung-Link (Nutzeranfrage 31.08.2026) - direkt im Fließtext
     der Checkbox-Beschriftung statt einer eigenen Zeile darunter, damit er
     erkennbar zu genau DIESER Erklärung gehört. white-space: nowrap hält den
     kurzen Linktext beim Umbruch zusammen. */
  .withdrawal-info-link { white-space: nowrap; }
  .hint-details { margin-bottom: 12px; }
  /* Die Zusammenfassungszeile war grau wie eine gewöhnliche Bildunterschrift
     (--muted) und dadurch nicht als Bedienelement erkennbar - der Mechanismus
     funktionierte, aber niemand klickte darauf, weshalb die Erklärung faktisch
     unsichtbar blieb (Nutzer-Report 03.08.2026: "sollte aufklappbar sein, ist
     aktuell nicht so"). Jetzt in Akzentfarbe, halbfett und beim Überfahren
     unterstrichen, damit sichtbar ist, dass hier etwas aufgeht. */
  .hint-details summary {
    cursor: pointer;
    font-size: calc(13px + 1pt);
    color: var(--accent-dark);
    font-weight: 600;
    list-style: none;
    display: inline-block;
    padding: 2px 0;
  }
  .hint-details summary:hover { text-decoration: underline; }
  :root[data-theme="dark"] .hint-details summary { color: var(--accent); }
  .hint-details summary::-webkit-details-marker { display: none; }
  .hint-details summary::before { content: "▸ "; }
  .hint-details[open] summary::before { content: "▾ "; }
  .hint-details .item-sub { margin-top: 8px; }
  /* Uniform size for every button in the game (user request 28.07.2026),
     except the quantity-stepper cluster (.step-btn, .max-btn) and the sticky
     header chrome (nav pills, warning badges) -- both keep their own
     narrower width rules below, which win via higher selector specificity.
     Long labels ellipsize instead of growing the button; most already carry
     a title attribute with the full text as a tooltip fallback. */
  button {
    font-family: inherit;
    border: none;
    /* Kontrastaudit 06.08.2026 (Nutzeranfrage): war var(--accent), gab
       weißer Schrift nur 3,00:1 (Dark)/3,39:1 (Light) - siehe --btn-bg-
       Kommentar in :root oben. Betraf praktisch jeden Primär-Button im
       gesamten Spiel. */
    background: var(--btn-bg);
    color: white;
    padding: 8px 14px;
    border-radius: 8px;
    font-size: var(--text-size-button);
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    width: 200px;
    height: 42px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Nutzeranfrage 25.08.2026 ("diesen orangenen Rahmen um alle Buttons
       legen") - der Nutzer meinte damit den nativen Browser/OS-Fokusring
       (rgb(229,151,0), nur bei Tastatur-Fokus sichtbar, keine eigene
       Formatierung dieser App), wollte ihn aber ausdrücklich dauerhaft auf
       JEDEM Button sehen, nicht nur beim Fokus. Bewusst outline statt border:
       outline nimmt am Box-Modell nicht teil (kein zusätzlicher
       Platzbedarf, kein box-sizing-Effekt) - ein border hätte auf jedem
       der zahllosen eng bemessenen Buttons im ganzen Spiel dasselbe
       Zusammenquetschen riskiert, das erst kürzlich beim Postfach-Icon
       gefunden wurde (siehe .mailbox-btn-Kommentar), nur diesmal überall
       statt an einer einzelnen Stelle. */
    outline: 2px solid #e59700;
    /* Nutzeranfrage 25.08.2026 ("etwas enger, damit es keine Lücke gibt") -
       0 statt 1px, Ring liegt jetzt direkt am Button-Rand an. */
    outline-offset: 0;
  }
  /* Nutzeranfrage 25.08.2026 ("beim Postfach-Öffnen-Button und beim
     Orts-Dropdown den orangenen Rand wieder weg, aussehen wie vorher",
     erweitert um "auch die Buttons INNERHALB der Orts-Auswahl") - drei
     gezielte Ausnahmen vom generischen Rahmen oben: #mailboxBtn hat bereits
     ein eigenes, mehrstufiges Ring-System (siehe dort), der globale Rahmen
     kollidierte optisch damit; #locationTag ist die schmale "Ort ▾"-
     Dropdown-Nase im Header; #locationMenu die Liste der Sprungziele, die
     sich beim Öffnen dieser Nase aufklappt - beide zusammen die komplette
     Orts-Auswahl. */
  #mailboxBtn, #locationTag, #locationMenu button { outline: none; }
  /* Nutzeranfrage 25.08.2026 ("in der mobilen Version dürfen die Buttons
     unten in der Leiste keinen Rahmen haben") - die Tab-Leiste (#mobileTabBar,
     nur auf Mobile sichtbar) bekommt dieselbe Ausnahme wie #mailboxBtn/
     #locationTag oben. */
  #mobileTabBar button { outline: none; }
  /* Nutzeranfrage 25.08.2026 ("Rahmen um die 'Info'-Buttons auch überall
     entfernen") - die kleinen (i)-Tooltip-Buttons (.info-icon) sind bewusst
     unauffällig gehalten (siehe dortiger Kommentar), der globale orangene
     Rahmen widerspricht dem. Klasse statt einzelner ID/Ort, wirkt dadurch
     überall im Spiel gleichzeitig. */
  .info-icon { outline: none; }
  /* Nutzeranfrage 25.08.2026 ("im Nachrichten-Popup sollen nur die drei
     Filter-Buttons oben und Schließen den orangenen Rand haben") - Rahmen
     für JEDEN Button im Popup zunächst zurückgesetzt, dann gezielt für genau
     diese beiden Stellen wieder angeschaltet. #mailboxOverlay #mailboxCloseBtn
     statt nur der ID allein: eine einzelne ID-Regel (1,0,0) hätte sonst
     gegen den Reset direkt darüber (1,0,1, ID + Element-Selektor) verloren -
     #mailboxFilterRow button trifft das exakt (1,0,1) und gewinnt bereits
     durch die spätere Position im Stylesheet.
     Nachtrag (02.09.2026, Nutzeranfrage) - die beiden Sammel-Aktionen ("Alle
     als ungelesen markieren"/"Alle löschen", #mailboxBulkActions) sollen den
     Rahmen jetzt ebenfalls bekommen, dieselbe (1,0,1)-Spezifität wie
     #mailboxFilterRow button, aus demselben Grund mit aufgenommen.
     Nachtrag (03.09.2026, Nutzeranfrage) - #mailboxBulkActions wieder aus
     dieser Liste entfernt: die beiden Buttons sind jetzt unterstrichene
     Text-Links statt echter Buttons (siehe #mailboxBulkActions button weiter
     unten), ein umlaufender Rahmen passt zu diesem Link-Look nicht mehr -
     sie fallen damit zurück auf #mailboxOverlay button { outline: none }
     direkt darunter. */
  #mailboxOverlay button { outline: none; }
  #mailboxOverlay #mailboxCloseBtn,
  #mailboxFilterRow button {
    outline: 2px solid #e59700;
    outline-offset: 0;
  }
  /* Nutzeranfrage 25.08.2026 (löst den vorherigen Admin-Ein-/Ausschalter
     wieder ab, "Checkboxen bzgl. orangenen Rahmen komplett aus dem
     Admin-Bereich entfernen, Rahmen sollen bei Buttons und im Header bleiben
     wie sie sind - Ausnahme: Sprachauswahl") - Rahmen ist jetzt wieder
     überall dauerhaft an (keine Schalter-Logik mehr).
     Korrektur (25.08.2026, direkte Folgeanfrage) - "Rahmen um den Button für
     die Sprachauswahl muss wieder hergestellt werden, aber um die Sprachen
     selbst soll keiner sein": #langFlagBtn (der Flaggen-Button selbst) hat
     den Rahmen jetzt wieder wie jeder andere Button (keine Ausnahme mehr
     nötig, fällt einfach unter die generische button-Regel), nur die vier
     Sprachoptionen im aufklappenden Menü darunter (#langMenu button) bleiben
     ausgenommen. */
  #langMenu button { outline: none; }
  /* Nutzeranfrage 26.08.2026 ("im Tutorial dürfen die orangenen Rahmen bis
     auf den Sprung-Button zur jeweiligen Aufgabe, z.B. 'Zum Lager', nicht da
     sein") - gleiches Muster wie beim Nachrichten-Popup oben: Rahmen für
     JEDEN Button im Panel zunächst zurückgesetzt (Vor-/Zurück-Blättern,
     Tutorial-abbrechen-Link, Minimieren), dann gezielt nur für
     #tutorialGotoBtn (die Sprung-Schaltfläche, "Zum Lager"/"Zum
     Marktplatz"/"Tutorial abschließen" je nach Schritt) wieder angeschaltet. */
  #tutorialPanel button { outline: none; }
  /* #tutorialPanel #tutorialGotoBtn statt nur #tutorialGotoBtn allein - exakt
     dieselbe Spezifitätsfalle wie beim Nachrichten-Popup oben
     (#mailboxOverlay #mailboxCloseBtn): eine einzelne ID-Regel (1,0,0) hätte
     gegen den Reset direkt darüber (1,0,1, ID + Element-Selektor) verloren,
     zwei ID-Selektoren (2,0,0) gewinnen dagegen sicher. */
  #tutorialPanel #tutorialGotoBtn {
    outline: 2px solid #e59700;
    outline-offset: 0;
  }
  /* Breite auf Mobile an das Aktienmengen-Eingabefeld angeglichen
     (Nutzeranfrage 20.08.2026, Nachtrag - vorher pauschal 1,5x/300px, siehe
     Historie unten) - .qty-input.market-qty (Börse, Marktübersicht, "Menge"
     bei Aktien) hat selbst KEINE einzelne feste Mobile-Breite, sondern zwei
     verschiedene, je nach Fensterbreite: 116px zwischen 481-820px (eigene
     Basisregel), volle verfügbare Zeilenbreite unter 480px (dort per
     order:99/flex-basis:100% auf eine eigene Zeile gezwungen, siehe
     .market-right .sell-controls .market-qty weiter unten). Damit Buttons
     bei JEDER Mobile-Breite exakt gleich breit wie dieses Feld sind, statt
     nur zufällig bei einer davon, bekommen sie hier denselben zweistufigen
     Mechanismus nachgebildet, statt eines einzelnen festen Werts.
     Ein einzelner Basis-Selektor reicht jeweils: die schon oben
     dokumentierten Ausnahmen (.step-btn/.max-btn, Header-Chrome) haben
     eigene, spezifischere width-Regeln, die unverändert gewinnen -
     .found-btn/.trade-btn/.expert-btn-2line/etc. setzen dagegen nie eine
     eigene width, übernehmen also automatisch den jeweiligen neuen Wert.
     Historie: 20.08.2026 zunächst 200px -> 300px (pauschal 1,5x, Nutzerzitat
     "falls Buttons danach zu breit sein sollten, melde ich mich nochmal") -
     auf erneute Nutzeranfrage direkt danach durch diese Angleichung an das
     Mengenfeld ersetzt. */
  @media (max-width: 820px) {
    button { width: 116px; }
  }
  @media (max-width: 480px) {
    button { width: 100%; }
  }
  /* Bugfix (Nutzerreport 25.08.2026, "Button verschiebt sich etwas nach
     links, wenn ich gerade eine Firma ausbaue"): der Fertigstellen-Button
     selbst ist über die obige Regel längst fest breit (200px/116px/100%) -
     der darunterstehende Countdown-Text ("Wird ausgebaut - fertig in {time}")
     ist aber ein normaler Block ohne eigene Breite und damit deutlich breiter
     als der Button. Da beide zusammen in einer align-items:center-Spalte
     stecken, bestimmte bislang der breiteste der beiden - also der Text -
     die Spaltenbreite; schrumpfte der Text mit jeder tickenden Sekunde
     (zweistellig -> einstellig, sinkender Diamanten-Preis), schrumpfte die
     Spalte mit und der zentrierte Button rutschte sichtbar mit. Der Text
     bekommt hier dieselbe, bereits etablierte Button-Breite je Breakpoint -
     er bricht dadurch bei Bedarf mehrzeilig um, statt die Spalte zu
     verbreitern, und der Button bleibt an Ort und Stelle stehen.
     Nachtrag (Nutzerreport, "Button wird auf Mobile schmaler, wenn dort
     'Fertigstellen...' steht"): die Spalte selbst (.firm-rush-progress)
     hatte dabei keine eigene Breite, blieb also auto/inhaltsbestimmt. Unter
     480px greift button{width:100%} (siehe oben) - 100% von einem auto-
     breiten Elternelement löst sich aber NICHT gegen die volle Zeilenbreite
     auf, sondern fällt auf die eigene Inhaltsbreite zurück (CSS-Spec: ein
     Prozentwert gegen ein selbst noch unbestimmtes Elternelement zählt als
     auto) - der Button war dadurch schmaler als jeder normale, direkt in
     .item-row sitzende Button. Die Spalte bekommt deshalb hier dieselbe
     Breite wie Button/Text, macht die 100%-Bezugsgröße dadurch eindeutig
     und behebt beides in einem Zug. */
  .firm-rush-progress { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 200px; }
  /* Nutzeranfrage - der Countdown-Text soll auf Desktop auch bei maximaler
     Bauzeit vollständig in einer Zeile passen, statt (wie im Bugfix oben) auf
     die 200px-Buttonbreite begrenzt zu sein und dafür bei Bedarf umzubrechen.
     300px deckt mit Puffer selbst den längsten realistisch auftretenden Satz
     ab (gemessen: "Wird ausgebaut - fertig in 999 Tage, 23 Std." ~280px bei
     der hier verwendeten Schriftgröße). Der Text ist damit breiter als sein
     200px breiter Elternflex (.firm-rush-progress, s.o.) und der Button
     daneben (Ausbauen/Abreißen, siehe .firm-expand-row) - align-items:center
     zentriert ein Kind, das breiter als sein Container ist, aber weiterhin
     symmetrisch um dessen Mitte, der Text ragt dadurch gleichmäßig auf
     beiden Seiten über den Button hinaus, genau wie gewünscht. Nur auf
     Desktop (Basisregel, keine Media Query) - die beiden spezifischeren
     Mobile-Regeln darunter (Tablet/Phone) bleiben unverändert, siehe deren
     eigene Bugfix-Historie oben. */
  .firm-rush-progress .item-sub { width: 300px; text-align: center; }
  /* Nutzeranfrage 28.08.2026 - springt jetzt wie die übrigen Firmen-Buttons
     (siehe .plaque-input/button[data-save-plaque] weiter oben sowie die
     #firmDetail-Sammelregel unten) schon ab 820px auf volle Breite statt
     erst ab 480px, die beiden vorigen getrennten Tiers (116px/100%) sind
     dadurch zu einem zusammengefallen. */
  @media (max-width: 820px) {
    .firm-rush-progress { width: 100%; }
    .firm-rush-progress .item-sub { width: 100%; }
  }
  /* Bugfix (Nutzerreport 25.08.2026, "Fertigstellen-Button soll gleich breit
     wie Ausbauen sein") - Pendant zu .firm-rush-progress oben, für den
     eigenständigen Lagerausbau-Countdown (renderStorageRow() in market.js,
     nicht dieselbe Stelle wie bei Firmen). Genau derselbe Fehler und
     dieselbe Lösung: der Wrapper hatte keine eigene Breite, wodurch
     button{width:100%} auf Mobile gegen ein noch unbestimmtes Elternelement
     auflöste und auf die eigene, schmalere Inhaltsbreite zurückfiel statt
     die volle Zeilenbreite wie der "Ausbauen"-Button (ein normaler, direkt
     in .item-row sitzender Button ohne diesen Wrapper) zu bekommen.
     Zentriert statt rechtsbündig, Button vor statt nach dem Countdown-Text
     (Nutzeranfrage 27.08.2026, "wird ausgebaut - fertig in... unterhalb des
     Buttons und zentriert, so wie beim Ausbau einer Firma") - identisch zu
     .firm-rush-progress oben, obwohl die ursprüngliche rechtsbündige Variante
     hier bewusst an die breite Lagerliste angepasst war (der "Ausbauen"-
     Button selbst landet dort über die .item-row-Basisregel ebenfalls
     rechtsbündig) - auf ausdrücklichen Wunsch trotzdem vereinheitlicht. */
  .storage-rush-progress { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 200px; }
  /* Gleiche Begründung wie bei .firm-rush-progress .item-sub oben - der
     Countdown-Text ("Wird ausgebaut - fertig in {time}") ist breiter als der
     200px-Button und würde ohne feste Breite die zentrierte Spalte beim
     Ticken der Restzeit sichtbar mitschrumpfen lassen. */
  .storage-rush-progress .item-sub { width: 300px; text-align: center; }
  /* Bugfix (29.08.2026, Nutzerreport "Text läuft im Lager zweizeilig um,
     obwohl er in der Firmenzentrale einzeilig bleibt") - die vormalige
     116px-Zwischenstufe unterhalb 820px (an den dortigen Button-Breiten
     ausgerichtet, siehe Kommentar bei .firm-rush-progress oben) war deutlich
     schmaler als der oben gemessene 300px-Bedarf für den längsten Countdown-
     Text und ließ ihn dort zwangsläufig umbrechen - ein Zwischenschritt, den
     .firm-rush-progress nie hatte (dort direkt 200px -> 100% ab 820px). Auf
     dieselbe zweistufige Struktur vereinheitlicht statt die 116px-Stufe nur
     zu verbreitern, damit beide Countdown-Anzeigen wieder identisch reagieren. */
  @media (max-width: 820px) {
    .storage-rush-progress { width: 100%; }
    .storage-rush-progress .item-sub { width: 100%; }
  }
  /* Pendant zu .firm-rush-progress/.storage-rush-progress oben, für den
     Zonen-Freischalten-Countdown (renderZonesList() in zones.js) -
     Nutzeranfrage 31.08.2026, "Restdauer wie beim Ausbau einer Firma unter
     dem Button". Identisch aufgebaut (Countdown zentriert unterhalb des
     Buttons statt daneben), eigene Klasse statt Wiederverwendung, weil der
     Rush-Button hier "storage-link-btn" statt "found-btn" als Basisklasse
     nutzt. */
  .zone-unlock-progress { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 200px; }
  .zone-unlock-progress .item-sub { width: 300px; text-align: center; }
  @media (max-width: 820px) {
    .zone-unlock-progress { width: 100%; }
    .zone-unlock-progress .item-sub { width: 100%; }
  }
  /* Bugfix (Nutzerreport 20.08.2026, "Diamanten-Bereich soll wirklich
     rechtsbündig mit dem Orts-Dropdown sein"): .stat (MC/FP/Diamanten-Chips
     im Header, siehe .stat-Basisregel weiter unten) ist ebenfalls ein
     <button> ohne eigene width - der Kommentar oben ("Header... ohnehin
     ausgeblendet") galt nur für die Nav-Buttons (#dashboardBtn etc., echt
     display:none auf Mobile), nicht für diese Chips, die auf Mobile
     weiterhin sichtbar in .stats stehen. Sie erbten dadurch ungewollt
     116px/100%-Breite statt ihrer natürlichen Inhaltsbreite - .stat selbst
     zentriert seinen Inhalt nicht horizontal (kein justify-content), der
     sichtbare Icon+Zahl-Inhalt hing dadurch links in einer künstlich
     verbreiterten Box, während nur die (unsichtbare) Box-Außenkante über
     .stats' space-between zufällig noch bündig mit dem Orts-Dropdown war -
     "der Bereich, in dem die Diamanten angezeigt werden" (der sichtbare
     Inhalt) war es also gerade NICHT. width:auto stellt die ursprüngliche,
     inhaltsbestimmte Breite wieder her. */
  @media (max-width: 820px) {
    .stat { width: auto; }
  }
  /* War var(--accent-dark) - im Dark Mode absichtlich HELLER als --accent
     (fürs Lesen von Text auf dunklem Grund gedacht), dadurch beim Hover
     jedes Buttons nur 2,09:1 für die weiße Schrift statt einer Verdunklung. */
  button:hover:not(:disabled) { background: var(--btn-bg-hover); }
  button:disabled {
    background: var(--btn-disabled-bg);
    cursor: not-allowed;
    color: var(--btn-disabled-text);
    /* Nutzeranfrage 25.08.2026 ("orangenen Rahmen um inaktive Buttons etwas
       weniger präsent machen") - dünner und per Transparenz gedämpft statt
       der vollen 2px/#e59700 aus der generischen button-Regel oben; gewinnt
       dort durch die Pseudoklasse (höhere Spezifität + spätere Position),
       ohne dass diese Regel selbst etwas an :disabled/inaktiven Buttons
       ändern muss. */
    outline: 1px solid rgba(229, 151, 0, 0.45);
  }
  button.secondary {
    background: var(--btn-secondary-bg);
    color: var(--btn-secondary-text);
    border: none;
  }
  button.secondary:hover:not(:disabled) { background: var(--btn-secondary-bg-hover); }
  /* Auswahl-Buttongruppen im Profil (Geschlecht, Theme, Kartenfilter,
     Wetter-Overlay, Ambiente-Effekt, Eröffnungs-Effekt) - Nutzerreport
     03.09.2026, "nicht mehr ersichtlich, welcher Button aktiv ist". Bis
     02.09.2026 markierte button.secondary den inaktiven Zustand über eine
     eigene, deutlich hellere Farbe - seit button.secondary dort auf
     ausdrücklichen Wunsch ("so grün wie die anderen Buttons") denselben
     Grünton wie normale Buttons bekam, sehen aktiv/inaktiv in genau diesen
     Auswahlgruppen identisch aus. Statt button.secondary selbst (das jetzt
     überall sonst im Spiel bewusst grün bleiben soll) erneut umzustellen,
     bekommen nur diese Auswahl-Buttons eine eigene, von .secondary
     unabhängige Kennzeichnung: inaktiv gedimmt ("ausgegraut"), aktiv normal
     hell - eine der beiden vom Nutzer vorgeschlagenen Varianten. */
  button.choice-btn { opacity: 0.55; }
  button.choice-btn.active { opacity: 1; }
  button.choice-btn:hover:not(:disabled):not(.active) { opacity: 0.8; }
  /* Postfach-Filterzeile (Alle/Ungelesen/Gelesen, Nutzeranfrage 04.09.2026) -
     eigene, andere Kennzeichnung als .choice-btn oben (Opazität): hier
     explizit invertierte Farben statt Dimmen, wie vom Nutzer beschrieben -
     inaktiv weiß mit grüner Schrift, aktiv grün mit weißer Schrift. */
  #mailboxFilterRow button { background: var(--card); color: var(--btn-bg); border: 1px solid var(--btn-bg); }
  #mailboxFilterRow button.active { background: var(--btn-bg); color: #fff; border-color: transparent; }
  #mailboxFilterRow button:hover:not(.active):not(:disabled) { background: var(--btn-secondary-bg-hover); color: #fff; }
  button.buy {
    background: transparent;
    /* War hartkodiert #1b6fa8 ohne Dark-Mode-Gegenstück - auf der dunklen
       Karte (--card) nur 2,97:1. --buy liefert im Dark Mode einen helleren,
       auf --card lesbaren Ton (siehe :root[data-theme="dark"]). */
    color: var(--buy);
    border: 1px solid var(--buy);
  }
  button.buy:hover:not(:disabled) { background: var(--buy-light); }
  /* War var(--danger) - im Dark Mode (#d97b7b, absichtlich hell für Text auf
     dunklem Grund) nur 2,97:1 für weiße Schrift; im Light Mode mit 4,45:1
     ebenfalls knapp unter der Mindestgrenze. --btn-danger-bg ist derselbe
     Rotton, nur dunkel genug für ≥5:1 in beiden Themes. */
  button.danger { background: var(--btn-danger-bg); }
  button.danger:hover { background: var(--btn-danger-bg-hover); }
  /* Nutzeranfrage 11.09.2026 - Abbrechen-Buttons sollen rot sein (Logout-
     Bestätigung, Order-Änderung abbrechen), statt im normalen grünen
     .secondary-Ton. Gezielt per ID/eigener Zweitklasse statt button.secondary
     selbst umzustellen, da .secondary auch für andere, nicht abbrechende
     Buttons (Auswahlgruppen im Profil, Postfach-Filter/-Bulk-Aktionen)
     verwendet wird, die weiterhin grün bleiben sollen. */
  #confirmCancelBtn, button.secondary.stock-order-cancel {
    background: var(--btn-danger-bg);
    color: #fff;
  }
  #confirmCancelBtn:hover:not(:disabled), button.secondary.stock-order-cancel:hover:not(:disabled) {
    background: var(--btn-danger-bg-hover);
  }
  /* Nutzeranfrage 02.09.2026 - Kostenanzeige (MC/FP/Diamanten) innerhalb
     eines Buttons, wenn der Spieler sie sich gerade nicht leisten kann,
     siehe costText() in ui-helpers.js für die ausführliche Kontrast-
     Begründung ("reines Rot direkt auf dem grünen Button-Hintergrund wäre
     praktisch unlesbar"). Selber Rotton wie button.danger, aber als kleiner
     Chip statt des ganzen Buttons - inline-block statt inline, damit
     padding/border-radius greifen, ohne den umgebenden Flex-Textfluss des
     Buttons zu stören. */
  .cost-unaffordable {
    display: inline-block;
    background: var(--btn-danger-bg);
    color: #fff;
    padding: 1px 6px;
    border-radius: 999px;
    font-weight: 700;
  }
  /* Higher specificity than button.secondary/.buy/.danger alone (which would
     otherwise win the cascade over the plain button:disabled rule above,
     since same-specificity rules resolve by source order and the variant
     classes are declared after it) - without this, a disabled secondary/buy/
     danger button kept looking fully "enabled" even though clicks did nothing. */
  button.secondary:disabled, button.buy:disabled, button.danger:disabled {
    background: var(--btn-disabled-bg);
    color: var(--btn-disabled-text);
    border-color: var(--btn-disabled-bg);
    cursor: not-allowed;
  }
  /* Aktiver Experten-Vertrag (Nutzeranfrage 21.08.2026, "Button muss
     hervorstechen, damit klar ist, dass der Experte aktiv ist") - ohne das
     sah die laufende, bereits bezahlte Stufe optisch identisch zu den beiden
     ANDEREN, durch sie blockierten Stufen aus (beide nur button:disabled-Grau).
     Zwei Klassen (.secondary.expert-btn-active) für höhere Spezifität als die
     button.secondary:disabled-Regel oben, unabhängig von der Regel-Reihenfolge.
     cursor:default statt not-allowed - ein aktiver Vertrag ist kein
     verbotener/fehlgeschlagener Klick, nur aktuell nichts zu tun. */
  button.secondary.expert-btn-active:disabled {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    cursor: default;
  }
  .locked { opacity: .55; }
  .qty-input {
    width: 58px;
    padding: 6px 8px;
    border: 1px solid var(--border);
    border-radius: 8px;
    font-size: calc(13px + 1pt);
    font-family: inherit;
  }
  /* Zahlen-/Mengenwerte zentriert statt linksbündig (Nutzeranfrage
     21.08.2026, "Werte aller Eingabefelder sollten zentriert sein") -
     bisher galt das nur für .qty-input.market-qty. Bewusst input.qty-input
     statt der bloßen Klasse: <select class="qty-input"> (Fahrzeugtyp,
     Sortierung, Order-Seite etc.) bleibt dadurch unangetastet, Browser
     rendern zentrierten Select-Text uneinheitlich. Freitext-Felder (Name
     statt Wert - Firmenschild/.plaque-input, Bankname/.bank-brand-name, die
     beiden Suchfelder, der Freunde-werben-Link) bleiben bewusst linksbündig,
     siehe deren eigene, spezifischere Regeln. */
  input.qty-input { text-align: center; }
  input.qty-input.plaque-input,
  input.qty-input.bank-brand-name,
  #adminPlayersSearchInput,
  #stocksSearchInput,
  #referralLinkInput,
  #authEmail, #authDisplayName, #authPassword, #authPasswordConfirm,
  #forgotEmail, #resetPassword, #resetPasswordConfirm, #chooseNameInput {
    text-align: left;
  }
  /* Firmenschild-Eingabefeld (Rahmenfarbe/Bauform/Ambiente-Kosmetikkarte,
     firms.js) - war fest 220px inline, blieb dadurch auf Mobile viel
     schmaler als der direkt danebenstehende "Speichern"-Button (der über die
     allgemeine button-Regel responsiv mitschrumpft/-wächst, siehe dort).
     Nutzerreport 28.08.2026, "Feld genau so breit wie der Button darunter,
     nicht breiter" - die 220px waren nie am Button ausgerichtet, sondern nur
     "breit genug" gegen die viel schmalere .qty-input-Basisbreite (58px)
     gemeint; auf Desktop-Breiten (>820px, wo button{width:200px} statt
     button[data-save-plaque]{width:100%} greift, siehe Media Query unten)
     stand das Feld dadurch sichtbar 20px breiter als der Speichern-Button
     darunter. 200px exakt auf die Basis-Buttonbreite abgestimmt. */
  .plaque-input { width: 200px; }
  /* Nutzeranfrage 28.08.2026, "Eingabefeld und Button auf die volle Breite
     bringen" - sprangen bisher erst unter 480px auf 100% (siehe die
     ursprüngliche, zweistufige 116px/100%-Fassung dieser Regel in der
     Historie), dazwischen (481-820px) griff derselbe 116px-Zwischenschritt
     wie button{width:116px} (siehe dessen Basisregel oben) - auf
     ausdrücklichen Wunsch für dieses Feld+Button-Paar übersprungen, beide
     gehen jetzt schon ab 820px auf volle Breite. button[data-save-plaque]
     (Attribut-Selektor, keine eigene Klasse nötig) gewinnt dank höherer
     Spezifität zuverlässig gegen die allgemeine button{width:116px}-Regel,
     unabhängig von der Position im Stylesheet. */
  @media (max-width: 820px) {
    .plaque-input, button[data-save-plaque] { width: 100%; }
  }
  /* Nutzerreport 28.08.2026, "Eingabefelder und Buttons bei den Firmen auf
     die volle Breite bringen, so wie die übrigen Buttons etc. auch schon" -
     dieselbe 820px-Ausnahme wie bei .plaque-input/button[data-save-plaque]
     oben galt bislang nur für das Firmenschild-Paar, nicht für die übrigen
     Buttons der Firmen-Detailseite (Mitarbeiter einstellen/Wartung/
     Auto-Verkauf/Experten in .action-grid, Ausbauen/Abreißen in
     .firm-action-row, die beiden "Speichern"-Buttons für Produktionsfaktor/
     Auto-Verkauf-Mindestpreis) - die blieben bis 480px beim allgemeinen
     116px-Zwischenschritt (button{width:116px} oben) stehen. Jetzt
     einheitlich: alle springen gemeinsam mit dem Firmenschild-Paar bei
     derselben Breite auf 100%. Attribut-/Klassen-Selektoren gewinnen dank
     höherer Spezifität zuverlässig gegen die allgemeine Regel, unabhängig
     von der Position im Stylesheet - .cosmetic-swatch-Buttons (Rahmenfarbe/
     Bauform) sind bewusst NICHT Teil dieser Liste, die haben ihr eigenes,
     bereits auf Wunsch gestaltetes Grid-Layout (siehe dortige Regeln) und
     sollen kein einspaltiges 100%-Layout bekommen. */
  @media (max-width: 820px) {
    #firmDetail .action-grid > button,
    #firmDetail .firm-action-row > button,
    #firmDetail [data-save-prodfactor],
    #firmDetail [data-save-autosell-minprice] { width: 100%; }
  }
  /* Rush-Eingabefelder (Extra-FP/Extra-MC) einer laufenden Forschung -
     dieselbe Schieflage wie beim Firmenschild oben: fest 80px inline, der
     "Rush jetzt"-Button daneben responsiv. justify-content:flex-start war
     ebenfalls inline gesetzt und überstimmte dadurch IMMER die allgemeine
     Mobile-Zentrierung (.item-row:has(button), siehe dort) - jetzt als
     normale Klassenregel, die die spezifischere :has()-Regel auf Mobile wie
     vorgesehen gewinnen lässt.
     Von 80px auf 130px verbreitert (Nutzeranfrage 27.08.2026, "beide
     Eingabefelder genug Platz haben, Platzhalter darin haben") - beide Felder
     tragen seither einen Platzhalter ("Menge (FP)"/"Menge (MC)", siehe
     research.js), der bei 80px abgeschnitten wurde. 130px ist auch für den
     längeren Platzhalter ("Importe (PI)"/"Importo (PR)", Spanisch/
     Italienisch) ausreichend Platz. */
  .rush-row { flex-wrap: wrap; gap: 8px; align-items: flex-end; justify-content: flex-start; }
  .rush-input { width: 130px; }
  @media (max-width: 820px) {
    /* War 116px, schmaler als die 130px-Basisregel oben - lag noch aus der
       Zeit vor den Platzhaltern (siehe deren Kommentar oben) und hätte diese
       hier wieder abgeschnitten. Jetzt durchgehend mindestens 130px bis zum
       Sprung auf volle Breite (Media-Query darunter). */
    .rush-input { width: 140px; }
  }
  @media (max-width: 480px) {
    .rush-input { width: 100%; }
    .rush-field { flex-basis: 100%; }
  }
  /* Speditionsname und Preisfelder je Zielzone im "Transportangebot
     veröffentlichen"-Formular (Nutzeranfrage 22.08.2026, "auf volle
     Breite") - waren bisher inline auf 220px/120px fest verdrahtet. Bare
     Klassenregeln statt input.qty-input.XXX-Verbund, exakt wie
     .plaque-input direkt oberhalb - müssen dafür nach der .qty-input-
     Basisregel (Zeile ~2614) stehen, sonst gewinnt bei gleicher Spezifität
     die Quellreihenfolge zugunsten von .qty-input (genau dieser Fehler
     ist beim ersten Versuch mit einer zu früh im Stylesheet stehenden
     Regel tatsächlich passiert - Eingabefeld blieb auf 58px). */
  .shipping-name-input { width: 220px; }
  .offer-price-input { width: 120px; }
  @media (max-width: 820px) {
    .shipping-name-input, .offer-price-input { width: 100%; }
  }
  /* Fahrzeugtyp-Dropdown und Anzahl-Fahrzeuge-Feld im selben Formular
     (Nutzeranfrage 24.08.2026, "betrifft nur mobile version" - Dropdown auf
     volle Breite, Anzahl-Fahrzeuge-Feld in eine neue Zeile und ebenfalls auf
     die maximale Breite der anderen Eingabefelder hier, siehe
     .offer-price-input direkt oberhalb). Auf Desktop unverändert: Dropdown
     width:auto (Options-Textbreite), Zahlenfeld 80px, beide nebeneinander in
     derselben Zeile wie bisher (vorher als Inline-Styles gesetzt). */
  .offer-vehicle-select { width: auto; }
  .offer-vehicle-count-input { width: 80px; }
  @media (max-width: 820px) {
    .offer-vehicle-select-field, .offer-vehicle-count-field { flex-basis: 100%; }
    .offer-vehicle-select, .offer-vehicle-count-input { width: 100%; }
  }
  /* Eigentransport-Formular (Transportamt) - Ware/Menge/Zielzone/Fahrzeugtyp
     waren auf Mobile ein unregelmäßiges flex-wrap aus content-breiten
     Selects (width:auto, je nach Options-Text unterschiedlich breit) und
     einem 100px-Eingabefeld, das den verfügbaren Platz nicht ausnutzte
     (Nutzerreport 21.08.2026, "Elemente so ausrichten, dass der Platz
     bestmöglich genutzt wird"). self-transport-input hält auf Desktop die
     bisherige width:auto-Breite (vorher inline gesetzt). Auf Mobile ein
     2-Spalten-Grid statt freiem Flex-Wrap - nutzt die Zeilenbreite
     symmetrisch aus, statt Felder unterschiedlicher Breite unvorhersehbar
     umbrechen zu lassen; der Button spannt beide Spalten und behält seine
     übliche responsive Breite (allgemeine button-Regel). */
  .self-transport-input { width: auto; }
  /* Mengenfeld hatte als einziges der vier Felder einen eigenen, engeren
     Desktop-Wert (100px statt width:auto) - eigene Klasse statt Inline-Style,
     damit die Mobile-Regel unten (.self-transport-input, gleiche
     Spezifität, aber später in der Datei) es trotzdem wie die übrigen drei
     Felder auf 100% im Grid strecken kann - ein Inline-Style hätte das immer
     verhindert. */
  .self-transport-amount { width: 100px; }
  /* Nutzerreport 02.09.2026, Nachtrag zu Abschnitt 100/101 (12_UI_UX.md) -
     "mit deiner letzten Änderung bin ich nicht zufrieden": Das Annehmen-
     Formular saß im generischen, unbenannten zweiten Kind von .item-info
     (Icon + dieser Div, Muster an etlichen Stellen im Projekt), das OHNE
     eigenes flex-Wachstum nur seine eigene Inhaltsbreite beansprucht statt
     die volle verfügbare Kartenbreite - die geerbte justify-content:
     space-between der .item-row-Basisregel (Grund für das gewünschte
     "über die volle Breite verteilte Felder, Button rechtsbündig"-Aussehen
     bei Eigentransport) hatte dadurch keinen Spielraum, etwas zu verteilen:
     Inhalt + Lücken füllten den ohnehin schon eng bemessenen Container fast
     exakt aus. Gleiches Muster wie .mailbox-item-body (siehe dort) - flex:1
     lässt diesen einen Container (nicht den generischen .item-info-Zweitkind-
     Fall an anderer Stelle im Projekt) auf die tatsächlich verfügbare Breite
     wachsen, wodurch .self-transport-row darin dieselbe space-between-
     Verteilung wie bei Eigentransport bekommt. */
  .offer-item-body { flex: 1; min-width: 0; }
  /* Transportangebote annehmen (Nutzeranfrage 02.09.2026, "so wie bei
     Eigentransport anordnen") - dasselbe .self-transport-*-Layout, aber ohne
     Fahrzeugtyp-Feld (das Angebot legt den Fahrzeugtyp bereits fest). Der
     dafür zwischenzeitlich eingefügte leere vierte Platzhalter
     (.offer-accept-vehicle-gap) ist auf Nutzeranfrage (13.09.2026, "Abstände
     sollen alle gleich sein") wieder entfernt - siehe Kommentar bei
     renderTransportOffers() (transport.js) zum "Warum". */
  @media (max-width: 820px) {
    .self-transport-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: end; }
    .self-transport-field { min-width: 0; }
    .self-transport-input { width: 100%; }
    .self-transport-row button { grid-column: 1 / -1; justify-self: center; }
  }
  @media (max-width: 480px) {
    .self-transport-row { grid-template-columns: minmax(0, 1fr); }
  }
  /* Joint Venture Admin-Formular (Nutzeranfrage 07.09.2026, "mobil soll es
     auch ansprechend aussehen, so wie bei den anderen Screens, Label über dem
     Element, beides auf volle Breite") - identisches Muster wie das
     Eigentransport-Formular direkt oberhalb (.self-transport-row/-field/
     -input): Desktop nutzt bereits .item-row's geerbtes justify-content:
     space-between (Label+Select/Input-Paare gleichmäßig verteilt, Button
     rechtsbündig als fünftes Element), Mobile wechselt auf ein 2-Spalten-,
     dann 1-Spalten-Grid mit Feldern auf voller Breite und zentriertem Button
     über beide Spalten. */
  .jv-admin-field { min-width: 0; }
  .jv-admin-input { width: auto; }
  /* Eigene Klasse statt Inline-Style für die beiden datetime-local-Felder,
     genau aus demselben Grund wie bei .self-transport-amount oben (Kommentar
     dort): ein Inline-Style hätte die Mobile-Regel (.jv-admin-input,
     gleiche Spezifität, aber später in der Datei) unabhängig von der Media
     Query immer geschlagen. */
  .jv-admin-date { width: 190px; }
  @media (max-width: 820px) {
    .jv-admin-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: end; }
    .jv-admin-input { width: 100%; }
    .jv-admin-row button { grid-column: 1 / -1; justify-self: center; }
  }
  @media (max-width: 480px) {
    .jv-admin-row { grid-template-columns: minmax(0, 1fr); }
  }
  /* Joint-Venture-Beitragsformular (Mengenfeld + "Liefern"-Button, Spieler-
     Ansicht, renderJointVentureView() in jointVenture.js) - Nutzerreport
     09.09.2026: beide Elemente trugen bisher feste Inline-Breiten
     (120px/auto) und blieben dadurch auf Mobile schmal statt wie überall
     sonst im Spiel auf volle Breite zu wachsen. Der Nutzer nennt das
     ausdrücklich den allgemein geltenden Standard für JEDE Eingabefeld+
     Button-Zeile im Spiel, nicht nur für dieses eine Formular - dasselbe
     Grundprinzip wie .self-transport-row/.jv-admin-row oben, hier ohne deren
     2-Spalten-Zwischenstufe, da nur ein einzelnes Feld (statt mehrerer)
     vorhanden ist: unter dem Breakpoint stapeln sich Feld und Button direkt
     auf jeweils volle Breite. Eigene Klassen statt der bisherigen
     Inline-Styles, aus demselben Grund wie bei .self-transport-amount/
     .jv-admin-date oben - ein Inline-Style würde die Mobile-Regel unten
     (gleiche Spezifität, aber später in der Datei) immer schlagen. 200px
     Desktop-Breite wie das bereits etablierte .qty-input.market-qty, statt
     hier einen eigenen, abweichenden Wert einzuführen.

     Bugfix (Nutzerreport 09.09.2026): justify-content:flex-start zog den
     "Liefern"-Button auf Desktop direkt neben das Eingabefeld an den linken
     Rand - der Nutzer wollte den Button stattdessen rechtsbündig, "das sollte
     eigentlich der Standard in der Desktop-Version sein". Das ist es auch
     bereits: .item-row's geerbtes justify-content:space-between (siehe
     .self-transport-row/.jv-admin-row oben, deren Buttons genau dadurch schon
     immer rechtsbündig landen) reicht dafür allein aus - die eigene
     flex-start-Regel hier war die einzige Abweichung von diesem Standard und
     entfällt jetzt ersatzlos. */
  .jv-contribute-row { border: none; padding: 4px 0 0; gap: 8px; }
  .jv-contribute-input { width: 200px; }
  @media (max-width: 820px) {
    .jv-contribute-row { flex-direction: column; align-items: stretch; }
    .jv-contribute-input, .jv-contribute-row button { width: 100%; }
  }
  /* "Alle Accounts"-Filterzeile (Suche + Status-Dropdown, admin.js
     renderAdminPlayersTable()) - Nutzeranfrage 07.09.2026, dasselbe
     Label-über-Feld-auf-voller-Breite-Muster wie .jv-admin-row direkt
     oberhalb, hier ohne Button also ohne dessen grid-column:1/-1-Sonderfall. */
  .adm-search-field { min-width: 0; }
  .adm-search-input { width: auto; }
  /* Eigene Klasse statt Inline-Style, gleicher Grund wie bei .jv-admin-date
     oben - ein Inline-Style würde die Mobile-Regel (.adm-search-input,
     gleiche Spezifität, aber später in der Datei) immer schlagen. */
  .adm-search-text { width: 220px; }
  @media (max-width: 820px) {
    .adm-search-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: end; }
    .adm-search-input { width: 100%; }
  }
  @media (max-width: 480px) {
    .adm-search-row { grid-template-columns: minmax(0, 1fr); }
  }
  /* Bugfix (Nutzerreport 02.09.2026, "alles unterhalb des Icons und
     Transporteur-Namens links einrücken, wie bei den anderen Boxen auch") -
     plain 1fr statt minmax(0, 1fr) blieb bislang auf Eigentransport (volle
     Kartenbreite, keine Icon-Einrückung) folgenlos, ließ das Grid aber im
     schmaleren .offer-item-body-Kontext (Transportangebote annehmen, siehe
     oben) auf den Inhalt breiter werden als der verfügbare Platz neben dem
     Icon ("Grid Blowout", dasselbe bereits an .grid2 dokumentierte Muster,
     siehe dortiger Kommentar). Die von .item-row:has(button) (siehe oben)
     geerbte justify-content:center zentrierte dieses zu breite Grid dann
     symmetrisch um seine Mitte - der linke Rand rutschte dadurch bis vor das
     Icon, an der Karten-Padding-Kante vorbei, statt bündig mit dem
     Transporteur-Namen zu bleiben. minmax(0, 1fr) erlaubt den Spalten zu
     schrumpfen, wodurch KEIN Überschuss mehr entsteht, den justify-content
     zentrieren könnte - Eigentransport bleibt dadurch unverändert (die
     Zentrierung greift dort ohnehin nie, das Grid füllt schon vorher exakt
     die Kartenbreite). */
  /* Desktop-Basisbreite an die übrigen Buttons angeglichen (Nutzeranfrage
     21.08.2026, "so breit wie der Button 'An die Börse gehen'") - die
     allgemeine button-Regel setzt dort 200px (siehe deren Kommentar "Uniform
     size for every button in the game"), dieses Eingabefeld hatte bisher
     durchgehend (auch auf Desktop) die feste Mobile-Breite 116px. Die beiden
     Mobile-Stufen (481-820px: 116px, unter 480px: 100%) bleiben unverändert
     bestehen - nur der Desktop-Basiswert wechselt von 116px auf 200px. */
  .qty-input.market-qty { width: 200px; text-align: center; }
  @media (max-width: 820px) {
    .qty-input.market-qty { width: 116px; }
  }
  /* Volle Zeilenbreite für JEDES .market-qty-Feld unter 480px, nicht nur
     innerhalb von .market-right .sell-controls (siehe die dortige
     order/flex-basis-Regel weiter unten, die zusätzlich die DOM-Reihenfolge
     ändert - hier nicht nötig). Direkter Nutzen: die Börsen-Ordereingabe
     (Nutzeranfrage 20.08.2026, "Elemente symmetrisch verteilen") nutzt
     .market-qty jetzt auch für Seite/Preis/Menge (siehe stockOrderFormHtml())
     - werden auf schmalen Screens dadurch (wie der Absenden-Button, der
     bereits über die allgemeine Mobile-Breite denselben Wert erreicht)
     gleich breit UND brechen einzeln, symmetrisch untereinander um, statt
     mit ungleichen 90/100/110px-Breiten nebeneinander zu hängen. Für das
     bereits bestehende .market-right .sell-controls .market-qty (Menge bei
     Firmen/Aktien-Kauf) ändert das nichts Sichtbares - dort gewinnt ohnehin
     die explizitere flex-basis:100% für die Größenberechnung, width:100%
     hier ist für diesen Fall nur ein redundanter, aber harmloser Fallback. */
  @media (max-width: 480px) {
    .qty-input.market-qty { width: 100%; }
    /* Wie .stocks-filter-field oben: Seite/Preis/Menge in der Ordereingabe
       stecken jeweils in einem eigenen Wrapper-Div (fürs Label darüber),
       width:100% auf dem Feld allein bezöge sich sonst nur auf die eng am
       Inhalt sitzende Wrapper-Breite statt auf die Zeile. */
    .stock-order-field { flex-basis: 100%; }
  }
  /* Ordereingabe-Layout (Nutzeranfrage 21.08.2026, "Seite und Menge in einer
     Zeile, darunter Preis, darunter Button") - überschreibt gezielt die
     beiden Regeln oben nur für diese drei Felder: Seite/Menge bleiben auf
     JEDER Mobile-Breite paarweise in einer Zeile (~50/50 statt jeweils
     eigener Zeile), Preis bekommt schon ab 820px (nicht erst ab 480px wie
     bisher pauschal für .stock-order-field) eine eigene volle Zeile - der
     Button danach hat dadurch selbst keine eigene Breitenregel nötig: Die
     Preis-Zeile ist bereits voll, der Button startet automatisch in einer
     neuen, eigenen Zeile mit seiner üblichen responsiven Button-Breite.
     Höhere Spezifität (3 statt 2 Klassen) als .qty-input.market-qty oben,
     gewinnt daher unabhängig von der Regel-Reihenfolge. */
  @media (max-width: 820px) {
    .stock-order-field-side, .stock-order-field-amount {
      flex: 1 1 calc(50% - 4px);
    }
    .stock-order-field-side .qty-input.market-qty,
    .stock-order-field-amount .qty-input.market-qty,
    .stock-order-field-price .qty-input.market-qty {
      width: 100%;
    }
    .stock-order-field-price { flex-basis: 100%; }
  }
  /* Dividenden-Eingabefeld (Börse, eigenes Unternehmen) - dieselbe Schieflage
     wie Firmenschild/Rush oben: fest 100px inline, "Speichern"-Button
     daneben responsiv. */
  .dividend-input { width: 100px; }
  @media (max-width: 820px) {
    .dividend-input { width: 116px; }
  }
  @media (max-width: 480px) {
    .dividend-input { width: 100%; }
  }
  /* Suche/Sortieren in der Börsen-Marktübersicht (Nutzeranfrage 20.08.2026,
     "so breit wie das Mengenfeld bei Aktien"; Desktop-Breite von Suche am
     21.08.2026 von 180px auf 200px angeglichen, siehe Kommentar bei
     .qty-input.market-qty oben) - beide 200px auf Desktop, auf Mobile
     aber derselbe zweistufige Mechanismus wie bei .qty-input.market-qty
     weiter oben: 116px zwischen 481-820px, volle Zeilenbreite darunter.
     Erfordert (anders als .market-qty, das direktes Flex-Kind seiner Zeile
     ist) zusätzlich flex-basis:100% auf dem umgebenden .stocks-filter-field
     -Wrapper-Div (siehe stockOrderFormHtml()-Nachbarfunktion renderStocksList()
     in stocks.js) - Feld und Label stecken hier in einem eigenen Div statt
     direkt in der Flex-Zeile, width:100% auf dem Feld allein würde sich
     sonst nur auf die (eng am Inhalt sitzende) Wrapper-Breite beziehen,
     nicht auf die tatsächliche Zeilenbreite. */
  #stocksSearchInput { width: 200px; }
  #stocksSortSelect { width: 200px; }
  @media (max-width: 820px) {
    #stocksSearchInput, #stocksSortSelect { width: 116px; }
  }
  @media (max-width: 480px) {
    .stocks-filter-field { flex-basis: 100%; }
    #stocksSearchInput, #stocksSortSelect { width: 100%; }
  }
  /* "Name Deiner Bank" (Bank eröffnen/Konditionen ändern) - gleiches Muster
     wie Börsen-Suche/Sortieren oben (Nutzeranfrage 20.08.2026, "so breit wie
     die Buttons, die wir vorhin geändert haben"): Desktop-Wert (100%,
     gedeckelt auf 280px, vormals Inline-Style) bleibt Basis, auf Mobile
     derselbe zweistufige Mechanismus wie bei .qty-input.market-qty/den
     Buttons: 116px zwischen 481-820px, volle Zeilenbreite darunter. Der
     umgebende Wrapper-Div ist hier schon selbst width:100% (siehe
     renderBankView() in bank.js), keine zusätzliche flex-basis:100%-Regel
     nötig wie bei den Börsenfeldern. max-width:none unter 480px, damit der
     280px-Deckel die volle Breite auf einem knapp unter 480px breiten Gerät
     nicht doch wieder kappt. */
  .bank-brand-name { width: 100%; max-width: 280px; }
  @media (max-width: 820px) {
    .bank-brand-name { width: 116px; }
  }
  @media (max-width: 480px) {
    .bank-brand-name { width: 100%; max-width: none; }
  }
  /* Produktionsfaktor > 100% (Nutzeranfrage 01.08.2026): roter Rahmen statt
     nur eines gesperrten Buttons, damit sofort klar ist, WELCHES Feld das
     Problem ist. Dasselbe Muster nutzt seit 10.08.2026 auch die Bank-
     Konditionen-Validierung (siehe validateBankTermsInputs()). */
  .qty-input.invalid { border-color: var(--danger); }
  .required-mark { color: var(--danger); margin-left: 2px; }
  .sell-controls { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
  /* Kaufen/Verkaufen in der Börsen-Marktübersicht teilen sich auf Mobile eine
     Zeile statt untereinander zu stehen (Nutzeranfrage 21.08.2026) - die
     allgemeine button-Mobile-Breite (116px bzw. 100% unter 480px) ließ hier
     immer nur einen der beiden Buttons pro Zeile zu, der zweite brach um.
     flex:1 1 0 lässt beide Buttons gleichmäßig eine gemeinsame Zeile teilen. */
  @media (max-width: 820px) {
    .stock-trade-controls { flex-wrap: nowrap; }
    .stock-trade-controls button { width: auto; min-width: 0; flex: 1 1 0; }
  }
  /* Basis-Layout für Eingabefeld+Speichern-Button (Desktop) - vormals als
     Inline-style direkt im generierten <div> (siehe renderMyStockCard() in
     stocks.js), hierher verschoben (Nutzeranfrage 21.08.2026), da ein
     Inline-style jede CSS-Regel schlägt und die Mobile-Regel darunter
     dadurch nie zum Zug kam. */
  .dividend-row { flex-wrap: wrap; gap: 14px; align-items: flex-end; margin-top: 4px; }
  /* Label, Eingabefeld und Speichern-Button auf Mobile jeweils in einer
     eigenen Zeile untereinander (Nutzeranfrage 21.08.2026) - löst die
     vorherige Regel ab, die Eingabefeld+Button noch eine gemeinsame Zeile
     teilen ließ. flex-direction:column statt des Basis-flex-wrap:wrap oben,
     align-items:stretch überschreibt das dortige flex-end (bezieht sich in
     Spaltenrichtung sonst auf die Querachse, also horizontal) - beide Kinder
     (Feld-Block, Button) füllen dadurch die volle Breite. */
  @media (max-width: 820px) {
    .dividend-row { flex-direction: column; align-items: stretch; }
    .dividend-input { width: 100%; }
    .dividend-save-btn { width: 100%; }
  }
  .empty-hint { font-size: calc(13px + 1pt); color: var(--muted); padding: 8px 0; }
  /* Die frühere generische footer-Regel (max-width, space-between) stammte
     noch aus der Zeit, als der Logout-Button im Footer saß - er ist seit dem
     25.07.2026 im Profil (08_MULTIPLAYER.md Abschnitt 7a), das Element war
     danach ersatzlos verschwunden. Die Regel blieb als tote CSS stehen und
     hat den neuen #legalFooter beim Einbau prompt falsch ausgerichtet
     (1100px Breite statt voller Breite, 30px Abstand nach unten). Entfernt;
     die Gestaltung des einzigen Footers steht bei #legalFooter selbst. */
  /* No longer auto-dismisses (user request 28.07.2026) -- stays until the
     player clicks the close "x", so pointer-events must be enabled while
     shown (disabled while hidden, so the invisible bubble never blocks
     clicks on whatever's underneath it). */
  /* Nutzerreport - der Toast hing auf Höhe von #legalFooter (Impressum etc.)
     und wurde von dessen Inhalt verdeckt: #legalFooter trägt bewusst
     z-index:210 (siehe dessen eigener Kommentar - "über JEDEM Nebel/Overlay
     der Seite"), weit über dem bisherigen z-index:100 des Toasts. Auf
     schmalen Screens sitzt #legalFooter zudem fest am unteren Bildschirmrand
     (position:fixed, siehe eigene @media-Regel), auf breiten Screens landet
     er bei wenig Bildschirminhalt ebenfalls am unteren Fensterrand (main ist
     Flex-Geschwister von #legalFooter, siehe dessen Kommentar) - in beiden
     Fällen genau dort, wo der Toast fix positioniert ist. z-index:211 (nur 1
     höher als #legalFooter, dieselbe "knapp über dem bisher höchsten Wert"-
     Logik) macht den Toast unabhängig von jeder Positions-Überlappung immer
     vollständig sichtbar. Zusätzlich sitzt er jetzt spürbar höher (statt nur
     knapp über dem Fensterrand direkt auf/neben dem Footer), damit beide
     Elemente sich optisch nicht überlappen: auf Mobile anhand derselben
     laufzeit-gemessenen Höhen wie #mobileTabBar (--mobile-tab-bar-height/
     --legal-footer-height, syncHeaderControlsReserve() in ui-helpers.js -
     "gemessen statt geraten", da beide Höhen von Sprache/Zeilenumbruch
     abhängen), auf Desktop mit einem festen, für die dort deutlich
     kompaktere Footer-Pille (siehe #legalFooter) ausreichenden Wert. */
  .toast {
    position: fixed;
    bottom: 90px;
    left: 50%;
    transform: translateX(-50%);
    /* Kontrastaudit 06.08.2026: war var(--accent-dark), im Dark Mode nur
       2,09:1 für die weiße Schrift - jede Toast-Meldung war dort praktisch
       unlesbar. --btn-bg-hover bewahrt den "etwas dunkler/kräftiger als ein
       normaler Button"-Charakter, den --accent-dark im Light Mode ursprünglich
       hatte, funktioniert aber jetzt in beiden Themes. */
    background: var(--btn-bg-hover);
    color: white;
    padding: 10px 34px 10px 18px;
    border-radius: 10px;
    font-size: calc(13px + 1pt);
    opacity: 0;
    pointer-events: none;
    transition: opacity .25s ease, transform .25s ease;
    z-index: 211;
    /* Gleiche Breite wie das Sicherheitsabfrage-Popup (.modal-box, Nutzer-
       anfrage 05.08.2026) - vorher schrumpfte der Toast auf den Inhalt, was
       bei kurzen Meldungen viel schmaler als der Bestätigungsdialog wirkte.
       box-sizing nötig, damit das bestehende Padding nicht zusätzlich zur
       max-width addiert wird.
       Nachtrag (05.08.2026, Nutzeranfrage) - "genau so breit wie das Popup"
       stimmte trotzdem noch nicht exakt: .modal-box sitzt in .modal-overlay,
       das selbst padding:20px hat (Flex-zentriert), wodurch .modal-box'
       width:100% effektiv gegen "Viewport minus 40px" auflöst, nicht gegen
       den vollen Viewport wie beim Toast (kein umgebender Container mit
       eigenem Padding). Bei 375px Breite ergab das reale 375px (Toast) vs.
       335px (.modal-box) - ein waschechter, gemessener 40px-Unterschied statt
       nur einer theoretischen Abweichung. calc(100% - 40px) zieht denselben
       Rand ab, den .modal-overlay's Padding erzeugt - macht beide bei jeder
       Bildschirmbreite identisch, nicht nur zufällig bei einer bestimmten. */
    max-width: 380px;
    width: calc(100% - 40px);
    box-sizing: border-box;
    text-align: center;
  }
  /* Auf Mobile sitzen #mobileTabBar UND #legalFooter beide fest übereinander
     am unteren Bildschirmrand (siehe deren jeweilige Kommentare) - der
     pauschale Desktop-Wert oben reicht hier nicht, der Toast braucht die
     tatsächliche Höhe BEIDER gestapelten Leisten als Fundament. Dieselben
     laufzeit-gemessenen Variablen wie bei body's padding-bottom (siehe dort,
     "gemessen statt geraten"), + 12px sichtbarer Luft zur Tab-Leiste. */
  @media (max-width: 820px) {
    .toast { bottom: calc(var(--mobile-tab-bar-height, 65px) + var(--legal-footer-height, 34px) + 12px); }
  }
  .toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); pointer-events: auto; }
  .toast.clickable { cursor: pointer; }
  .toast.clickable:hover { background: var(--btn-bg); }
  .toast #toastCloseBtn {
    position: absolute;
    top: 2px;
    right: 6px;
    background: transparent;
    border: none;
    color: white;
    font-size: calc(15px + 1pt);
    line-height: 1;
    padding: 4px;
    width: auto;
    height: auto;
    cursor: pointer;
    /* Nutzeranfrage 26.08.2026, "orangener Rahmen darf nicht um das
       Schließen-X auf dem Message-Toast sein" - Ausnahme vom globalen
       button-Rahmen, gleiches Muster wie die übrigen dort dokumentierten
       Ausnahmen (Postfach, Orts-Dropdown). */
    outline: none;
  }
  .toast #toastCloseBtn:hover { color: var(--accent-light); }
  /* System-Postfach-Button (08_MULTIPLAYER.md Abschnitt 7b, Nutzeranfrage
     25.08.2026) - fixed unten rechts, wie vom Nutzer gewünscht ("Fancy-
     Overlay-Button"). z-index 60 wie .lang-switch/.header-warn-group (beides
     ebenfalls dauerhaft sichtbare, fixed-positionierte Header-Bedienelemente),
     bewusst UNTER .modal-overlay (200)/.toast (211), damit ein geöffneter
     Dialog oder Toast ihn unverändert überdeckt. Lebt als DOM-Kind von
     <header> (siehe index.html-Kommentar dort) - position:fixed hier löst ihn
     trotzdem optisch aus dem Header-Fluss. */
  .mailbox-btn {
    /* Bugfix (Nutzerreport 25.08.2026, "Icon ist auf einmal total klein") -
       der generische button-Reset setzt padding:8px 14px; das war hier nie
       überschrieben. Bei einem 48px-Kreis mit box-sizing:border-box blieb
       dadurch kaum Innenfläche für das 22px-Icon übrig - schrumpfte
       (flex-shrink:1 Default) still und leise mit, bis der breitere Rahmen
       (border 3px->5px, siehe unten) die verbleibende Fläche unter die
       Icon-Breite drückte und es sichtbar zusammenquetschte. */
    padding: 0;
    position: fixed;
    /* Nutzeranfrage 25.08.2026 ("5px nach links verschieben", danach
       "Desktop-Version nochmal 5px nach links") - von 14px auf 19px, dann auf
       24px. Dies ist inzwischen der DESKTOP-Wert - Mobile hat unten (siehe
       @media max-width:820px) einen eigenen, separat angeforderten Wert
       ("mobil 5px nach rechts", von 19px auf 14px). */
    right: 24px;
    bottom: 16px;
    z-index: 60;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    /* Rahmen-Verlauf (Nutzeranfragen 25.08.2026): erst 3px dunkelgrüner
       border ("sehe ich fast gar nicht"), dann auf drei konzentrische Ringe
       erweitert (5px dunkelgrün als border, 2px weiß + 3px dunkelgrün als
       box-shadow-Ringe darum), zuletzt der innere border-Ring ganz entfernt
       ("ersatzlos löschen, den Rest entsprechend einrücken") - kein border
       mehr, die verbleibenden box-shadow-Ringe rücken dadurch von selbst an
       die Füllung heran (ihr spread misst sich immer vom aktuellen
       border-box-Rand, der jetzt direkt die Füllung ist). */
    border: none;
    cursor: pointer;
    font-size: 22px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Nutzerreport 25.08.2026 ("Info-Icon abgeschnitten") - derselbe Fehler
       wie beim Diamanten-Freischalt-Abzeichen auf .cosmetic-swatch (siehe dort):
       der globale button-Reset setzt overflow:hidden, das schneidet
       .mailbox-badge exakt am kreisrunden Rand ab, da sie bewusst mit
       negativem top/right über den Button-Rand hinausragt. */
    overflow: visible;
    background: var(--btn-bg);
    color: #fff;
    /* Reihenfolge wichtig: kleinerer spread (weißer 2px-Ring) zuerst, dann
       der größere (dunkelgrüner Ring, davon nur der Unterschied zum weißen
       Ring sichtbar, da dieser die inneren 2px davon überdeckt) - äußerer
       Ring auf Nutzerwunsch (25.08.2026) von 5px auf 4px spread verschmälert
       (sichtbare Ringbreite dadurch 3px->2px), zuletzt der ursprüngliche
       weiche Schlagschatten. */
    box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--btn-bg-hover), 0 2px 8px rgba(0, 0, 0, 0.25);
  }
  /* Nutzeranfrage 25.08.2026 ("Hintergrund bei Hover dunkler machen") -
     --btn-bg-hover war bereits dunkler als die Ruhefarbe --btn-bg, aber
     offenbar nicht deutlich genug. Eigener, spürbar dunklerer Grünton statt
     filter:brightness() - eine Filter-Abdunklung träfe auch das weiße Icon
     und das rote Badge im Inneren mit, gewünscht war ausdrücklich nur der
     Hintergrund. :root-Präfix nötig (dieselbe Spezifitätsfalle wie
     mehrfach in dieser Datei): ohne ihn verliert die Regel (0,2,0) gegen die
     generische "button:hover:not(:disabled)"-Regel (0,2,1) - fiel bislang nie
     auf, weil beide zufällig denselben Wert (--btn-bg-hover) gesetzt hatten,
     erst der jetzt bewusst ABWEICHENDE Wert macht sichtbar, welche Regel
     tatsächlich gewinnt. */
  :root .mailbox-btn:hover { background: #1b5c3d; }
  /* Gleiche "über Tab-Leiste + Rechts-Footer" Positionierung wie .toast oben
     (gemessen statt geraten, --mobile-tab-bar-height/--legal-footer-height). */
  @media (max-width: 820px) {
    .mailbox-btn {
      bottom: calc(var(--mobile-tab-bar-height, 65px) + var(--legal-footer-height, 34px) + 12px);
      /* Nutzeranfrage 25.08.2026 ("Inbox-Button mobil 5px nach rechts
         schieben") - eigener Mobile-Wert, unabhängig vom Desktop-Wert oben
         (right: 24px). */
      right: 14px;
    }
  }
  /* Bugfix (Nutzerreport 14.09.2026, "Weiter-Button im Admin-Dashboard ist
     abgeschnitten"): #view-admin ist die einzige View mit mehreren langen,
     eigenständig paginierten Listen (Multi-Accounting-Cluster, Audit-Log,
     "Alle Spieler") - je nachdem, welche Karte gerade als letztes Element am
     Ende des sichtbaren Bereichs landet, geriet deren "Weiter"-Button exakt
     in den fest positionierten (position:fixed) Kreis des Postfach-Buttons
     oben (48px Durchmesser + bis zu 4px Box-Shadow-Ring, bottom:16px auf
     Desktop bzw. bottom:calc(...+12px) auf Mobile) - und blieb dahinter
     hängen, da unterhalb schlicht kein weiterer Seiteninhalt mehr folgte, um
     ihn per Scroll darunter hervorzuholen. Andere Views sind davon nicht
     betroffen (ihr jeweils letztes Element ist kein interaktiver Button, der
     exakt in dieser Ecke sitzen könnte) - deshalb hier gezielt nur für
     #view-admin gelöst, statt pauschal main/body-Innenabstand für das ganze
     Spiel zu verändern (dortiger 18px/20px-Abstand ist bereits an anderer
     Stelle bewusst exakt festgelegt, siehe Kommentar bei main oben). 90px
     reichen mit Sicherheitsabstand für Button + Ring plus etwas Luft, auf
     Mobile zusätzlich zur ohnehin schon über body reservierten
     Tab-Leisten-/Footer-Höhe. */
  #view-admin { padding-bottom: 90px; }
  /* "Wackeln" bei ungelesenen Nachrichten (Nutzeranfrage 25.08.2026, seither
     mehrfach nachgeschärft: erst "das ganze Icon soll wackeln, nicht nur der
     Umschlag" - Ziel jetzt der ganze Button statt nur .mailbox-btn-icon,
     damit das Badge sichtbar mitwackelt statt regungslos danebenzustehen -
     dann "doppelt so häufig" (4s->2s), "halb so schnell" (2s->4s), "1
     Sekunde länger" (4s->5s), "die Bewegung selbst 1s länger im selben
     Tempo, nicht der Abstand dazwischen" (drei abklingende Ausschläge statt
     einem), zuletzt korrigiert: "total ungleichmäßig, am Anfang schnell und
     dann langsamer" - das Abklingen (drei Ausschläge mit sinkender
     Amplitude + Pausen dazwischen) WAR genau diese Ungleichmäßigkeit.
     Ersetzt durch 7 identische, gleich schnelle Durchläufe desselben
     einzelnen Ausschlags OHNE Amplitudenabnahme und OHNE Pausen dazwischen -
     exakt "die Geschwindigkeit der ersten Bewegung", nur wiederholt statt
     einmalig, macht dieselbe ~1,2s Gesamtdauer der Wackelphase (Dauer bleibt
     unverändert bei 5s) durchgehend gleichmäßig. Je Rotationswinkel EINE
     Regel mit allen sieben Wiederholungen als kommagetrennte Prozentliste,
     statt 49 einzelner Blöcke - liest sich als "welcher Winkel wird wann
     erreicht", nicht als 7x derselbe Textblock. */
  @keyframes mailbox-wiggle {
    0%, 74%, 77.5%, 81%, 84.5%, 88%, 91.5%, 95%, 98.5%, 100% { transform: rotate(0deg); }
    74.5%, 78%, 81.5%, 85%, 88.5%, 92%, 95.5% { transform: rotate(-14deg); }
    75%, 78.5%, 82%, 85.5%, 89%, 92.5%, 96% { transform: rotate(12deg); }
    75.5%, 79%, 82.5%, 86%, 89.5%, 93%, 96.5% { transform: rotate(-9deg); }
    76%, 79.5%, 83%, 86.5%, 90%, 93.5%, 97% { transform: rotate(7deg); }
    76.5%, 80%, 83.5%, 87%, 90.5%, 94%, 97.5% { transform: rotate(-4deg); }
    77%, 80.5%, 84%, 87.5%, 91%, 94.5%, 98% { transform: rotate(2deg); }
  }
  .mailbox-btn.has-unread {
    /* Dauer bleibt 5s (siehe Verlauf im Kommentar über @keyframes oben) -
       die zuletzt gewünschte zusätzliche Sekunde steckt in den Keyframes
       selbst (drei Ausschläge statt einem), nicht in dieser Zahl. */
    animation: mailbox-wiggle 5s ease-in-out infinite;
    transform-origin: 50% 50%;
  }
  .mailbox-badge {
    position: absolute;
    /* Positions-Verlauf (Nutzeranfragen 25.08.2026): -4px ursprünglich, dann
       -10px ("weiter nach außen"), als der Button noch einen 5px-Rahmen +
       Doppelring hatte - beide sind inzwischen wieder entfernt (siehe
       .mailbox-btn oben), der Button ist wieder eine schlichte 48px-Kreis-
       fläche. -10px ließ das Badge dadurch zu weit außen schweben, kaum noch
       auf dem Button liegend. Zurück auf -6px ("weiter auf den Button drauf,
       soll mehr überlappen") - näher am Kreisrand als ursprünglich, aber
       spürbar mehr Überlappung mit der Kreisfläche als bei -10px. */
    top: -6px;
    right: -6px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--btn-danger-bg-hover);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* Nutzerreport 28.08.2026, "Buttons in der obersten Zeile sollen den
     gesamten Platz nutzen" (Mobile) - #mailboxFilterRow (Alle/Ungelesen/
     Gelesen) ist ein .gender-switch (Basisregel: display:flex, Buttons
     width:auto, also nur so breit wie ihr Text plus Padding) - eine
     eigene, auf diese ID beschränkte Regel statt die geteilte
     .gender-switch-Basis anzufassen, die auch Profil-/Admin-Filterzeilen
     mit anderer Button-Anzahl/-Breite nutzen (siehe profile.js/admin.js),
     wo eine volle Breitenverteilung nicht gewünscht wäre. width:100% auf
     die Zeile plus flex:1 auf jeden Button verteilt die drei Filter
     gleichmäßig über die komplette Kartenbreite statt wie bisher
     linksbündig mit ungenutztem Rest rechts.
     Zweite Regel direkt darunter (Nutzerreport 28.08.2026, "Alle als
     gelesen markieren"/"Alle löschen" in eine Zeile packen", ebenfalls
     Mobile): beide Buttons zusammen sind bei ihrer .gender-switch-
     Basisgröße (Padding 6px 14px, Schrift calc(13px+1pt)) zu breit für
     die Kartenbreite (287px auf 375px-Mobile): "Alle als ungelesen
     markieren" (der längere der beiden Umschalt-Zustände, siehe
     markAllButtonLabel() in mailbox.js) allein schon ~216px, macht mit
     "Alle löschen" + Gap zusammen ~332px. Kleineres Padding/Gap/Schrift
     NUR für diese Gruppe (eigene ID statt der geteilten .gender-switch-
     Basis, aus demselben Grund wie bei #mailboxFilterRow oben) drückt
     beide zusammen auf ~260px - passt mit Puffer für andere Sprachen
     (Spanisch/Italienisch ähnlich lang, siehe i18n.js). Beide Regeln
     bewusst auf Mobile beschränkt (dieser Teil des Stylesheets liegt
     anders als die Firmen-Detailseite NICHT bereits in einer
     Mobile-Media-Query) - auf Desktop ist reichlich Platz, dort teilen
     sich Filter- und Toggle-Paar-Gruppe ohnehin eine Zeile statt je
     eigener Breite zu brauchen (siehe #mailboxTopRow-Desktop-Regel weiter
     unten), die Buttons bleiben dort in normaler Größe. */
  /* Nutzeranfrage 03.09.2026 - "Alle als gelesen markieren"/"Alle löschen"
     sollen keine vollwertigen Buttons mehr sein, sondern unterstrichene
     Text-Links: im Light Mode dieselbe dunkelgrüne Schriftfarbe wie die
     übrigen Buttons des Spiels (--accent-dark - im Light Mode ein sattes
     Dunkelgrün, siehe Kommentar bei .stat weiter oben), im Dark Mode aber
     bewusst Weiß statt --accent-dark (das dort ein HELLES Grün ist, siehe
     button:hover-Kommentar oben) - ausdrücklicher Nutzerwunsch, kein
     automatisches Theme-Pärchen wie sonst üblich. (1,0,1)-Spezifität (ID +
     Element) überschreibt zuverlässig sowohl die generische button-Regel
     oben als auch die .gender-switch-Basis-Polsterung. */
  #mailboxBulkActions button {
    background: none;
    border: none;
    color: var(--accent-dark);
    text-decoration: underline;
    padding: 6px 4px;
    width: auto;
  }
  #mailboxBulkActions button:hover:not(:disabled) { background: none; }
  :root[data-theme="dark"] #mailboxBulkActions button { color: #fff; }
  /* "Finde mich" auf den vier Ranglisten (Nutzeranfrage 03.09.2026) - derselbe
     unterstrichene Text-Link-Look wie #mailboxBulkActions button direkt
     darüber, hier aber mit var(--up) statt --accent-dark: --up hat (anders
     als dort bewusst gewählt) bereits ein automatisches, kontrastgeprüftes
     Theme-Pärchen (helles Grün im Dark Mode statt Weiß), kein Grund, davon
     abzuweichen. */
  .lb-find-me-btn {
    background: none;
    border: none;
    color: var(--up);
    text-decoration: underline;
    padding: 6px 4px;
    width: auto;
    font-size: calc(13px + 1pt);
  }
  .lb-find-me-btn:hover:not(:disabled) { background: none; }
  @media (max-width: 820px) {
    #mailboxFilterRow { width: 100%; }
    #mailboxFilterRow button { flex: 1; }
    #mailboxBulkActions { gap: 4px; }
    #mailboxBulkActions button { padding: 6px 8px; font-size: 12px; }
  }
  /* #mailboxTopRow (Filterreihe + Toggle-Paar) war ursprünglich per
     style="" direkt im HTML auf display:flex/flex-direction:column
     gesetzt - eine Inline-Style gewinnt aber IMMER gegen jede externe
     Stylesheet-Regel, unabhängig von deren Spezifität, weshalb die
     folgende Desktop-Regel (flex-direction:row) zunächst wirkungslos
     blieb. Basis-Layout deshalb hierher verschoben, index.html setzt nur
     noch die ID. */
  #mailboxTopRow { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
  /* Nutzerreport 28.08.2026, "Desktop: die beiden Buttons doch bitte
     wieder rechts in derselben Zeile mit den anderen drei Buttons" -
     löst die vorherige Fassung ab (die stapelte Filter/Toggle-Paar auf
     JEDER Breite in zwei Zeilen, Toggle-Paar mittig darunter). #mailboxTopRow
     ist auf Mobile weiterhin eine Spalte (siehe Basisregel direkt oben) -
     erst ab 821px zurück auf eine Zeile mit space-between, exakt das
     ursprüngliche Vor-28.08.2026-Layout (Filter links, Toggle-Paar
     rechts). Eigene ID statt .gender-switch-Basis anzufassen, aus
     demselben Grund wie bei den Mobile-Regeln oben. */
  @media (min-width: 821px) {
    #mailboxTopRow { flex-direction: row; justify-content: space-between; align-items: center; }
  }
  /* overscroll-behavior:contain (Nutzeranfrage 05.09.2026) - ein bis ans
     eigene Ende gescrollter Postfach-Liste soll den Hintergrund nicht per
     Scroll-Chaining mitscrollen, siehe Kommentar bei .modal-overlay oben. */
  #mailboxList { max-height: 50vh; overflow-y: auto; overscroll-behavior: contain; }
  /* Eigene Scrollbar statt System-Standard (Nutzeranfrage 25.08.2026) -
     erstes Vorkommen im Projekt, bisher gab es dafür keine Konvention.
     scrollbar-width/-color deckt Firefox ab, die ::-webkit-scrollbar-
     Pseudo-Elemente Chrome/Edge/Safari - kein gemeinsamer Standard, beide
     Pfade sind nötig. Track transparent, damit er sich in den Popup-
     Hintergrund einfügt statt eine eigene Fläche zu zeigen; Thumb in
     --btn-bg (derselbe Grünton wie Buttons/Badges im ganzen Spiel), damit es
     nicht wie ein neues, unabhängig erfundenes Farbschema wirkt. */
  #mailboxList {
    scrollbar-width: thin;
    scrollbar-color: var(--btn-bg) transparent;
  }
  #mailboxList::-webkit-scrollbar { width: 8px; }
  #mailboxList::-webkit-scrollbar-track { background: transparent; }
  #mailboxList::-webkit-scrollbar-thumb { background: var(--btn-bg); border-radius: 999px; }
  #mailboxList::-webkit-scrollbar-thumb:hover { background: var(--btn-bg-hover); }
  /* Äußerer Zeilen-Container ist seit dem Lese-Umschalter (Nutzeranfrage
     25.08.2026) bewusst KEIN <button> mehr, sondern enthält zwei eigene
     Buttons nebeneinander (.mailbox-item-main zum Öffnen/Alslesenmarkieren,
     .mailbox-read-toggle zum expliziten Umschalten) - verschachtelte
     interaktive Elemente sind ungültiges HTML, ein <button> im <button> hätte
     der Browser stillschweigend "repariert" und den Toggle unbedienbar
     gemacht. */
  .mailbox-item {
    display: flex;
    align-items: center;
    /* 14px statt zuvor 6px (Nutzeranfrage 26.08.2026, "Mindestabstand
       zwischen Text und Briefumschlag") - dieser gap gilt jetzt nur noch
       zwischen .mailbox-item-main (Text) und .mailbox-item-actions (den
       beiden Icon-Buttons als EINE Gruppe, siehe dort) statt vorher
       gleichermaßen auch zwischen den beiden Icons selbst, die stattdessen
       enger zusammenrücken sollten - deren eigener, viel kleinerer Abstand
       sitzt jetzt separat in .mailbox-item-actions. */
    gap: 14px;
    width: 100%;
    border-bottom: 1px solid var(--border);
  }
  .mailbox-item:last-child { border-bottom: none; }
  /* Wrapper um Lese-Umschalter + Löschen-Button (Nutzeranfrage 26.08.2026,
     "die beiden Icons können wesentlich enger zusammenrücken") - eigener
     kleiner gap statt des viel größeren .mailbox-item-gaps oben, der jetzt
     nur noch die Nachricht vom Icon-Paar trennt. Kein negativer Abstand/
     kleinerer Button nötig: beide Buttons behalten ihre bereits mehrfach
     abgestimmte 56px-Klickfläche (siehe .mailbox-read-toggle-Kommentar),
     nur die Lücke ZWISCHEN ihnen wird entfernt. */
  .mailbox-item-actions { flex-shrink: 0; display: flex; align-items: center; gap: 0; }
  .mailbox-item-main {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    flex: 1;
    min-width: 0;
    text-align: left;
    padding: 10px 8px;
    border: none;
    border-radius: 0;
    background: none;
    cursor: pointer;
    font: inherit;
    color: inherit;
    /* Nutzerreport 25.08.2026 ("Datum/Uhrzeit nicht komplett lesbar") - der
       globale button-Reset legt hier unbemerkt height:42px, overflow:hidden,
       text-overflow:ellipsis UND white-space:nowrap fest (für den normalen
       Ein-Zeilen-Button gedacht). white-space:nowrap vererbt sich dabei sogar
       auf .mailbox-item-time weiter (kein eigenes white-space dort), wodurch
       die Zeitzeile bei zweizeilig umgebrochenem Nachrichtentext zusammen mit
       dem Rest der festen 42px-Höhe abgeschnitten wurde. Alle vier Werte hier
       für diese mehrzeilige Zeile explizit zurückgesetzt. */
    height: auto;
    overflow: visible;
    white-space: normal;
  }
  /* Gelesen/ungelesen (Lesestatus pro Nachricht einzeln, Nutzerentscheidung
     25.08.2026) - dezente Hervorhebung statt eines zusätzlichen Punkts/Icons,
     ändert sich direkt beim Klick (siehe markMailboxItemRead()/
     toggleMailboxItemRead() in mailbox.js). Sitzt am äußeren .mailbox-item,
     nicht an .mailbox-item-main, damit sie auch hinter dem
     Lese-Umschalter-Button durchläuft statt an dessen Rand abzuschneiden.
     Kartenhintergrund für gelesene Zeilen (Nutzeranfrage 25.08.2026, "Gewicht/
     Hintergrund vertauscht"). Fett-Gewicht dagegen zuletzt zurückgedreht
     (erneute Nutzeranfrage 25.08.2026: "ungelesen fett, gelesen nicht mehr
     fett") - beide Eigenschaften sitzen deshalb bewusst auf getrennten
     Selektoren, nicht mehr in einer gemeinsamen Regel. Der Lese-Umschalter
     (Umschlag-Symbol, siehe .mailbox-read-toggle unten) bleibt davon
     unberührt und zeigt weiterhin unverändert den tatsächlichen Lesestatus
     an. */
  .mailbox-item:not(.unread) { background: var(--card); }
  .mailbox-item.unread { font-weight: 600; }
  .mailbox-item-icon { flex-shrink: 0; }
  .mailbox-item-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
  /* Bugfix (Nutzerreport während der Verifizierung 25.08.2026) - ohne
     min-width:0 verhindert der Flexbox-Default (min-width:auto) den
     Zeilenumbruch längerer Nachrichtentexte und schneidet sie stattdessen am
     Popup-Rand ab. */
  /* overflow-wrap:break-word (Nutzerreport 26.08.2026, "Text geht über die
     Icons hinaus/berührt sie, die grüne Antipp-Markierung auf Mobile umfasst
     nicht den kompletten Text") - min-width:0 allein lässt den Text nur
     zwischen WÖRTERN umbrechen; ein einzelnes langes, zusammengesetztes Wort
     (im Deutschen häufig) hatte keine Umbruchstelle und wuchs dadurch über
     den eigenen Rand von .mailbox-item-main hinaus in die benachbarten
     Icon-Buttons hinein - die :active-Hervorhebung (siehe .mailbox-item
     weiter oben) folgt aber der tatsächlichen Button-Box, nicht dem
     überlaufenden Text, wodurch beide optisch auseinanderfielen. */
  .mailbox-item-text { min-width: 0; white-space: normal; overflow-wrap: break-word; }
  /* Datum/Uhrzeit (Nutzeranfrage 25.08.2026) - Datum über
     toLocaleDateString(), Uhrzeit ohne Sekunden + sprachabhängigem Suffix
     ("Uhr" im Deutschen, siehe formatMessageTime()/mailbox.timeSuffix in
     mailbox.js/i18n.js), hier ohne umgebenden Satz, da die Zeile für sich
     selbst spricht (reine Metadaten-Zeile unter dem Nachrichtentext). Bewusst
     nie fett (font-weight:normal überschreibt das geerbte 600 der
     ungelesenen Zeilen, siehe .mailbox-item.unread oben) - sonst wirkt die
     Zeitangabe wie ein Teil der eigentlichen Meldung. */
  .mailbox-item-time { font-size: calc(11px + 1pt); color: var(--muted); font-weight: normal; }
  /* Nutzeranfrage 25.08.2026 - die gedämpfte --muted-Farbe war auf dem
     grünen Hover-Hintergrund von .mailbox-item-main (button:hover:not
     (:disabled), siehe generische Button-Regel) zu dunkel/schwer lesbar,
     deutlich heller als --muted auf Hover. */
  .mailbox-item-main:hover .mailbox-item-time { color: #fff; }
  /* Nutzeranfrage 25.08.2026 ("im hellen Design den Nachrichtentext beim
     Hovern auch so hell machen wie das Datum darunter") - .mailbox-item-text
     erbt sonst color:inherit von .mailbox-item-main (im Light Theme also das
     dunkle --text) und blieb auf demselben grünen Hover-Hintergrund
     dunkel/schwer lesbar, während die Zeitzeile direkt darunter (siehe oben)
     bereits weiß wurde - beide jetzt einheitlich. */
  .mailbox-item-main:hover .mailbox-item-text { color: #fff; }
  /* Lese-Umschalter (Nutzeranfrage 25.08.2026, Symbol seither auf
     Nutzerwunsch von ●/○ auf einen Umschlag zu/auf umgestellt, siehe
     envelopeToggleIconHtml() in mailbox.js) - eigener Button neben
     .mailbox-item-main statt eines zweiten Verhaltens auf demselben Klick:
     ein Klick auf die Nachricht selbst markiert sie (wie gehabt) als
     gelesen, dieser Button hier schaltet EXPLIZIT in beide Richtungen um,
     unabhängig vom Lesen-Klick - dasselbe "getrennte Aktion neben der
     Hauptzeile" Muster wie schon die "Lager voll"-Warnung im Dashboard
     (dash-storage-link, dashboard.js). Geschlossener Umschlag = ungelesen,
     offener = gelesen - dieselbe Farbe wie der Ungelesen-Hintergrund oben
     (currentColor im SVG folgt dieser color-Regel), damit beide Signale
     zusammengehören. display:flex/centering kommt vom generischen
     button-Reset (inline-flex, align-items/justify-content:center). */
  .mailbox-read-toggle {
    flex-shrink: 0;
    /* Größenverlauf (Nutzerreports 25.08.2026): 32px/16px -> 40px/24px
       ("super winzig") -> 80px/48px ("nochmal doppelt so groß") -> 56px/32px
       ("viel zu groß", "größer als vorher, aber nicht so groß wie jetzt") -
       SVG-Maße direkt in envelopeToggleIconHtml(), mailbox.js.
       Nutzerreport 28.08.2026, "Buttons enger zusammen und weiter nach
       rechts, Nachrichtentext breiter" - height (Tapfläche, an der sich die
       obige Rundenreihe orientierte) bleibt deshalb unverändert bei 56px,
       nur width auf 40px verengt: das eigentliche Icon (32px) bleibt exakt
       gleich groß und wirkt dadurch nicht "wieder kleiner" wie die bereits
       verworfene 40px/24px-Runde - nur sein leerer seitlicher Rand
       schrumpft. Ein negativer margin-left (Buttons überlappend statt
       schmaler) wurde bewusst verworfen: der Löschen-Button läge dann
       teils ÜBER dem Lese-Umschalter und würde dessen Klicks im
       Überlappungsbereich stehlen - bei einer destruktiven Aktion
       (Nachricht löschen) ein zu hohes Fehlklick-Risiko. border-radius von
       50% auf 20px (halbe Höhe) angepasst, da 50% bei width != height eine
       Ellipse statt der bisherigen Kreisoptik ergäbe. padding:0 nötig, da
       die globale button-Basisregel 8px 14px setzt (border-box) - bei den
       alten 56px blieben davon noch 28px Content-Breite für das 32px-Icon
       übrig (leicht gequetscht, kaum auffällig), bei den neuen 40px wären
       es nur noch 12px gewesen: das SVG ist als Flex-Kind (display:flex
       oben) ohne eigenes flex-shrink:0 schrumpfbar und wäre dadurch massiv
       zusammengestaucht worden statt nur sein Rand. */
    width: 40px;
    height: 56px;
    padding: 0;
    border: none;
    border-radius: 20px;
    background: none;
    cursor: pointer;
    color: var(--muted);
  }
  .mailbox-item.unread .mailbox-read-toggle { color: var(--btn-bg); }
  /* Nutzeranfrage 25.08.2026 ("im dunklen Design die Farben der Umschläge im
     normalen Stil und im Hover gleich machen, beides normal hell und bei
     Hover grün") - NUR im Dark Theme: gelesen/ungelesen sehen in der Ruhe
     beide gleich hell aus (--text statt der bisherigen Aufteilung --muted/
     --btn-bg), Hover bleibt unverändert grün (--accent, siehe unten - gilt
     bereits themeunabhängig für beide Zustände, keine Änderung nötig). Das
     Light Theme behält bewusst die bisherige grün/grau-Unterscheidung, war
     nicht Teil der Anfrage. ".unread" statt ".mailbox-item.unread" hier
     bewusst gekürzt (Spezifität (0,4,0) statt (0,5,0)) - genau eine Stufe
     unter der Hover-Regel für den Ungelesen-Fall weiter unten (0,5,0), damit
     Hover in JEDEM Fall gewinnt, unabhängig von der Reihenfolge im
     Stylesheet (bei exakt gleicher Spezifität hätte die Reihenfolge
     entschieden - unnötiges Risiko für eine Eigenschaft, die ohnehin
     identisch sein soll). */
  :root[data-theme="dark"] .mailbox-read-toggle,
  :root[data-theme="dark"] .unread .mailbox-read-toggle {
    color: var(--text);
  }
  /* Nutzeranfrage 25.08.2026 ("Kreis beim Hovern komplett entfernen") - kein
     Hover-Hintergrund mehr, nur noch die Farbaufhellung unten. :root-Präfix
     trotzdem nötig (bleibt bewusst stehen): ohne ihn würde nicht MEIN
     background:none gelten, sondern die generische
     "button:hover:not(:disabled)"-Regel (0,2,1, höher spezifisch als ein
     einfaches ".mailbox-read-toggle:hover" mit 0,2,0) weiterhin ihren
     eigenen dunkelgrünen Kreis zeichnen - dieselbe Spezifitätsfalle wie an
     mehreren anderen Stellen in dieser Datei (Header-Chips, Tutorial-Panel,
     Sprach-Dropdown, Warn-Chip). */
  :root .mailbox-read-toggle:hover { background: none; }
  /* Nutzeranfrage 25.08.2026 ("Farbe bei Hover heller machen") - --accent ist
     in beiden Themes messbar heller/gesättigter als sowohl --muted (gelesen)
     als auch --btn-bg (ungelesen), siehe :root-Definitionen - bleibt dabei
     im selben Grünton statt auf ein fremdes Hover-Schema (z.B. weiß)
     umzuspringen, das auf dem hellen --accent-light-Hintergrund oben im
     Light-Theme zu kontrastarm wäre. Ebenfalls mit :root-Präfix (s.o.), plus
     eine zusätzliche, noch spezifischere Fassung für den Ungelesen-Fall -
     ohne sie würde dort weiterhin die Regel ".mailbox-item.unread
     .mailbox-read-toggle" (0,3,0) gewinnen und die Hover-Farbe unterdrücken,
     da :hover allein (0,2,0 selbst mit :root-Aufwertung auf 0,3,0) exakt
     gleichauf läge und je nach Reihenfolge verlieren könnte. */
  :root .mailbox-read-toggle:hover { color: var(--accent); }
  :root .mailbox-item.unread .mailbox-read-toggle:hover { color: var(--accent); }
  /* Löschen-Button (Nutzeranfrage 25.08.2026) - gleiche Button-Maße/
     Kreis-freie Hover-Optik wie .mailbox-read-toggle oben, aber in Rot statt
     Grün (--btn-danger-bg-hover), da es sich um eine destruktive statt einer
     Lesestatus-Aktion handelt. Ruhefarbe bewusst gedämpft (--muted, nicht
     schon rot) - erst der Hover/die Sicherheitsabfrage macht die
     Zerstörungs-Absicht deutlich, ein dauerhaft roter Mülleimer neben jeder
     Zeile wäre unnötig alarmierend. */
  .mailbox-delete-btn {
    flex-shrink: 0;
    width: 40px;
    height: 56px;
    padding: 0;
    border: none;
    border-radius: 20px;
    background: none;
    cursor: pointer;
    color: var(--muted);
  }
  /* :root-Präfix aus demselben Spezifitätsgrund wie bei .mailbox-read-toggle
     oben (generische "button:hover:not(:disabled)"-Regel). */
  :root .mailbox-delete-btn:hover { background: none; color: var(--btn-danger-bg-hover); }
  /* "Neu"-Badge (Nutzeranfrage 25.08.2026, zusätzlich zum Umschlag-Symbol) -
     dieselbe Pillen-Optik wie die Shop-Badges (diamonds.js), eigene Farbe
     (--btn-bg statt des dortigen Orange) - "neu/ungelesen" ist kein
     Angebot, sondern derselbe Grünton wie der Lese-Umschalter oben. */
  .mailbox-new-badge {
    display: inline-block;
    background: var(--btn-bg);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    border-radius: 999px;
    padding: 1px 6px;
    margin-right: 6px;
    vertical-align: middle;
  }
  .price-tag { font-size: calc(12px + 1pt); color: var(--muted); }
  .price-up { color: var(--up); }
  .price-down { color: var(--down); }

  /* In-game confirm dialog (27.07.2026) - see showConfirmDialog(). Same card
     look as the rest of the app instead of the browser's own confirm() popup. */
  .modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    z-index: 200;
  }
  /* Bugfix (Nutzerreport 25.08.2026, Postfach-Löschen-Sicherheitsabfrage
     "erschien" laut display:flex, war aber unsichtbar) - Bestätigungs-/
     Info-Dialog sind die einzigen beiden .modal-overlay-Popups, die
     ausdrücklich aus JEDEM anderen Kontext heraus aufgerufen werden können
     (showConfirmDialog()/showInfoDialog(), von überall im Spiel genutzt,
     jetzt erstmals auch von INNERHALB eines bereits offenen Popups wie
     #mailboxOverlay) - alle .modal-overlay teilen sich denselben z-index
     (200), bei Gleichstand gewinnt die DOM-Reihenfolge, und #confirmOverlay/
     #infoOverlay stehen im Markup VOR #mailboxOverlay & Co., verschwanden
     dadurch optisch dahinter. Knapp über 200 statt einer pauschalen Anhebung
     von .modal-overlay selbst, die jedes Popup gleichermaßen beträfe und das
     eigentliche Problem (Rangordnung UNTEREINANDER) nicht löste.
     #mailboxDetailOverlay (Nutzeranfrage 04.09.2026) hier mit aufgenommen -
     öffnet sich ebenfalls von INNERHALB des bereits offenen #mailboxOverlay
     aus, exakt derselbe Fall. */
  #infoOverlay, #mailboxDetailOverlay { z-index: 202; }
  /* Bugfix (Nutzerreport 05.09.2026, "Logout-Popup soll... auf jeden Fall
     auch immer im Vordergrund sein") - #confirmOverlay/#infoOverlay teilten
     sich bislang denselben z-index (202), #infoOverlay steht im Markup NACH
     #confirmOverlay und gewann die Rangordnung deshalb bei Gleichstand
     (siehe Kommentar oben). Löste sich der Logout-Bestätigungsdialog
     (#confirmOverlay) z. B. mit einem erneuten Aufruf von showInfoDialog()
     überlappt (beobachtet mit dem Erst-Login-Begrüßungsdialog, vom Nutzer
     per Screenshot belegt), lag Letzterer sichtbar darüber statt der
     Bestätigung, der Spieler gerade tatsächlich zugewandt war. Eine echte,
     unbedingte Vorrang-Garantie ("Bestätigungsdialog ist NIE ein anderes
     Popup verdeckt") bräuchte eine eigene Modal-Warteschlange - dieser Fix
     deckt den konkret gemeldeten Fall knapp und gezielt ab, ohne diesen
     größeren Umbau. */
  #confirmOverlay { z-index: 203; }
  /* Nutzerreport 21.08.2026 ("Diamanten-Kauf-Popup hängt auf Mobile unter dem
     Header, kein Scrollen möglich, sehe nicht das ganze Popup") - align-
     items:center + kein overflow zusammen mit dem fest positionierten
     Mobile-Header (siehe header/main-Regel oben) schnitt ein hohes .modal-box
     (z. B. der mehrere Pakete lange Diamanten-Shop) oben ab, ohne dass der
     abgeschnittene Teil je erreichbar war. Jetzt oben ausgerichtet statt
     zentriert, mit demselben --mobile-header-height-Abstand wie main, und der
     Overlay selbst scrollt, wenn die Box höher als der verbleibende Platz ist. */
  @media (max-width: 820px) {
    .modal-overlay {
      align-items: flex-start;
      overflow-y: auto;
      /* Nutzeranfrage 05.09.2026, "Scrollen bei offenem Popup komplett
         verhindern" - ist die Box (siehe Kommentar oben) höher als der
         verfügbare Platz, scrollt der Overlay hier selbst; ohne containment
         würde ein am eigenen Ende angekommener Scroll-Versuch per Scroll-
         Chaining trotzdem noch die Seite dahinter mitscrollen. */
      overscroll-behavior: contain;
      padding-top: calc(var(--mobile-header-height, 130px) + 20px);
      /* Bugfix (Nutzerreport 25.08.2026, "Popup ist abgeschnitten und ich
         kann auch nicht weiter runterscrollen") - position:fixed + inset:0
         verlässt sich sonst auf die große (Adressleiste eingeklappt)
         Viewport-Höhe, die mobile Browser für "100%"/"0" bei fixed-
         Elementen ansetzen. Fährt die Adressleiste beim Scrollen wieder aus
         (üblich, sobald man versucht, IN diesem Overlay zu scrollen), bleibt
         der untere Streifen des Overlays dadurch dauerhaft unterhalb der
         tatsächlich sichtbaren, kleineren Viewport-Höhe - der Rest des
         Inhalts (inkl. Schließen-Button) war dann da, aber nie erreichbar.
         100dvh (dynamic viewport height) bindet die Höhe stattdessen an die
         JEWEILS aktuell sichtbare Höhe; 100vh direkt davor als Fallback für
         Browser ohne dvh-Unterstützung (dort bleibt exakt das alte, teils
         fehlerhafte Verhalten bestehen, aber immerhin kein CSS-Fehler). */
      height: 100vh;
      height: 100dvh;
      /* Bugfix (Nutzerreport 26.08.2026, "Postfach-Popup verschwindet
         teilweise unter dem Footer, kann nicht weiter scrollen") -
         #mobileTabBar UND #legalFooter sitzen auf Mobile beide fest am
         unteren Bildschirmrand mit z-index 210 (bzw. #legalFooter allein auf
         Screens ohne Tab-Leiste), ÜBER jedem .modal-overlay (200) - bewusst
         so (Nutzeranfrage 22.08.2026, Impressum/Datenschutz müssen auch bei
         offenem Popup erreichbar bleiben, siehe #legalFooter-Kommentar) und
         NICHT rückgängig gemacht. Ohne eigene Reserve am unteren Rand endete
         der scrollbare Bereich des Overlays trotzdem exakt an dessen eigener
         Höhe - der letzte Teil eines hohen Popups (z.B. das lange Postfach)
         lag dadurch dauerhaft UNTER diesem fest positionierten Streifen,
         ohne dass Scrollen dort je hinkam. Dieselbe "gemessen statt
         geraten"-Reserve wie schon bei .toast/.mailbox-btn (--mobile-tab-
         bar-height/--legal-footer-height, syncHeaderControlsReserve() in
         ui-helpers.js) schafft jetzt genug zusätzlichen Scroll-Weg, damit
         auch das untere Ende jedes Popups oberhalb des Streifens ankommt. */
      padding-bottom: calc(var(--mobile-tab-bar-height, 65px) + var(--legal-footer-height, 34px) + 20px);
    }
    /* Ausnahme von der Oben-Ausrichtung oben (Nutzeranfrage 26.08.2026,
       "Willkommen bei MarketBorn"-Begrüßung soll nicht auf Kopfzeilen-Höhe,
       sondern zentriert stehen) - siehe showInfoDialog()-Kommentar in
       ui-helpers.js für die Begründung, warum das NICHT für .modal-overlay
       insgesamt gilt. padding-top/-bottom bleiben unverändert bestehen (kein
       "align-items:center" direkt auf .modal-overlay ohne diese beiden) -
       dadurch zentriert sich die kurze Begrüßungs-Box exakt im sichtbaren
       Bereich ZWISCHEN Header und Fußleisten, nicht im vollen, teils vom
       Header verdeckten Viewport. */
    .modal-overlay.modal-overlay-centered { align-items: center; }
    /* Der eigentliche Mobile-padding-top für .modal-overlay-with-logo sitzt
       jetzt bei dessen Basisregel weiter unten (dort auch als eigene
       @media-Regel, NACH statt VOR der unconditional Basisregel - eine
       frühere Fassung stand hier, wurde aber von der gleich spezifischen,
       später im Stylesheet stehenden Basisregel unbemerkt überschrieben und
       griff dadurch nie: Mobile bekam faktisch densel­ben 9vh-Wert wie
       Desktop). */
  }
  /* Großes Wortmarken-Logo über der Begrüßungs-Dialog-Box (Nutzeranfrage
     27.08.2026, "oberhalb des Welcome-Screens noch recht groß das
     MarketBorn Logo auf dem Hintergrund"), NUR aktiv, wenn showInfoDialog()
     mit { bigLogo: true } aufgerufen wurde (siehe #infoWelcomeLogo-Kommentar
     in index.html sowie showInfoDialog()/resolveInfoDialog() in
     ui-helpers.js) - #infoWelcomeLogo selbst bleibt ohne diese Klasse
     ausgeblendet, damit kein anderer der vielen anderen showInfoDialog()-
     Aufrufe im Spiel es ungewollt mitanzeigt.
     flex-direction:column stapelt Logo und Box übereinander, statt wie im
     Standardfall (row) nebeneinander - align-items bleibt unverändert
     center, zentriert dadurch beide auch horizontal weiterhin gleich.
     Direkt auf dem Foto-Seitenhintergrund statt in einer eigenen Karte
     (Nutzerwunsch "auf dem Hintergrund") - Text/Strich dafür in Weiß statt
     der theme-abhängigen --text/--muted-Farben (die auf dem Foto je nach
     Theme zu schwach oder als dunkler Text auf hellen Bildbereichen kaum
     lesbar wären), ein dunkler drop-shadow sorgt für Kontrast auch über
     hellen Bildbereichen wie dem Himmel.

     Nachtrag (27.08.2026, Nutzeranfrage) - "eine ganze Ecke weiter nach
     oben, sodass das Logo auf dem Leuchtturm drauf ist": justify-content
     (statt des von .modal-overlay geerbten "center") rückt den ganzen
     Logo+Box-Stapel stattdessen an den oberen Rand, ein eigenes
     padding-top setzt den tatsächlichen Abstand von dort - deutlich
     kleiner auf Mobile (schmalerer/höherer Viewport, siehe Media Query
     unten), damit das Logo dort nicht ins Header-Blickfeld rutscht. */
  .modal-overlay.modal-overlay-with-logo { flex-direction: column; justify-content: flex-start; padding-top: 9vh; }
  #infoWelcomeLogo { display: none; }
  .modal-overlay-with-logo #infoWelcomeLogo {
    display: block;
    width: min(480px, 80vw);
    /* Halbiert von 28px auf 14px (Nutzeranfrage 28.08.2026, "Abstand zwischen
       Logo und Box um die Hälfte reduzieren", gilt für alle Screens mit
       großem Logo über der Box) - .choose-zone-logo/#authLogo unten bekommen
       denselben halbierten Wert, damit die "Logo-Position und obere Kante der
       Box müssen exakt gleich bleiben"-Vorgabe (27.08.2026-Nachtrag) weiter
       zutrifft. */
    margin-bottom: 14px;
    filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.55));
  }
  .modal-overlay-with-logo #infoWelcomeLogo svg { width: 100%; height: auto; display: block; }
  @media (max-width: 820px) {
    .modal-overlay-with-logo #infoWelcomeLogo { width: min(320px, 82vw); margin-bottom: 8px; }
    /* Muss NACH der unconditional Basisregel oben stehen (siehe deren
       Kommentar) - bei gleicher Spezifität (zwei Klassen) gewinnt sonst rein
       die Quellreihenfolge, unabhängig von der Media Query, und Mobile
       bekäme faktisch densel­ben 9vh-Wert wie Desktop.
       +50px (Nutzeranfrage 05.09.2026, "Logo und Boxen ganz zu Beginn des
       Spiels auf Mobile 50px nach unten schieben") - betrifft über diese
       eine Klasse sowohl den Begrüßungs- als auch den Startzonen-Info-Dialog
       (beide `bigLogo:true`, siehe showChooseZoneScreen() in bootstrap.js).
       #view-choose-zone/#view-auth unten ziehen ihren eigenen padding-top
       weiterhin direkt von diesem Wert ab (siehe deren Kommentare) - bekommen
       die +50px dadurch automatisch mit, ohne dass die "exakt gleiche Höhe"-
       Beziehung zwischen allen dreien auseinanderläuft. */
    .modal-overlay.modal-overlay-with-logo { padding-top: calc(5vh + 50px); }
  }
  /* Logo + Kartenabstand auf der Zonenwahl-Ansicht (Nutzeranfrage 27.08.2026,
     "auch bei Auswahl der Startzone das Logo oben drüber anzeigen und die Box
     auf der Höhe platzieren, auf der auch die Welcome-Message war", sowie
     Nachtrag 27.08.2026 "Logo-Position und obere Kante der Box müssen nach
     Klick auf 'Los geht's' exakt gleich bleiben") - anders als #infoWelcomeLogo
     (in einem position:fixed-Overlay, das direkt am Viewport-Rand beginnt)
     steht #view-choose-zone in normalem Fluss innerhalb von <main>, das
     unabhängig vom (hier noch unsichtbaren, siehe <header style="display:none">
     in index.html) Header IMMER 20px eigenen margin-top mitbringt (Desktop:
     "margin: 20px auto 18px" oben; Mobile: dieselben 20px über
     --mobile-header-height, das syncHeaderControlsReserve() bei fehlendem
     Header explizit auf 0 setzt, siehe dortigen Kommentar in ui-helpers.js).
     Ohne Korrektur läge die Zonenwahl-Ansicht dadurch auf beiden
     Breakpoints exakt 20px tiefer als der Begrüßungs-Dialog (dessen fixiertes
     Overlay diesen Versatz nicht kennt) - padding-top zieht ihn hier deshalb
     jeweils vom padding-top der .modal-overlay-with-logo-Basisregel (9vh
     bzw., Mobile, 5vh) ab. */
  #view-choose-zone { padding-top: calc(9vh - 20px); }
  /* Nutzeranfrage 05.09.2026, "Auf Desktop den Text auf die volle Breite
     bringen und den Button jeweils darunter, aber weiterhin rechts" - nimmt
     die vorige Fassung dieses Abschnitts zurück (dort wortwörtlich das
     Gegenteil: Text schmäler machen, damit der Button DANEBEN statt
     DARUNTER passt). Nur auf echter Desktop-Breite (Mobile bricht den
     Button ohnehin schon durch die schmalere Kartenbreite zuverlässig um,
     unabhängig von .item-infos Breite - siehe Abschnitt 124/131). */
  @media (min-width: 821px) {
    /* flex-basis:100% zwingt .item-info, die GESAMTE erste Zeile für sich
       einzunehmen - der Button findet dort keinen Platz mehr und bricht in
       eine eigene, zweite Zeile um. */
    #chooseZoneList .item-info { flex-basis: 100%; min-width: 100%; }
    /* Auf dieser zweiten Zeile steht nur noch der Button allein -
       .item-rows justify-content:space-between (Basisregel) bräuchte dafür
       ein zweites Element, um ihn "dazwischen" nach rechts zu schieben; mit
       nur einem Kind bleibt er sonst links. margin-left:auto verbraucht
       stattdessen den gesamten freien Platz auf der eigenen Zeile links vom
       Button und schiebt ihn dadurch zuverlässig an den rechten Rand. */
    #chooseZoneList .item-row button { margin-left: auto; }
  }
  .choose-zone-logo {
    width: min(480px, 80vw);
    /* Halbiert von 28px auf 14px (Nutzeranfrage 28.08.2026), gleicher Wert wie
       #infoWelcomeLogo oben - siehe dortiger Kommentar. Gilt über diese
       Klasse auch für #authLogo (Login/Registrieren/Passwort vergessen,
       siehe index.html), das dieselbe Klasse wiederverwendet. */
    margin: 0 auto 14px;
    filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.55));
  }
  .choose-zone-logo svg { width: 100%; height: auto; display: block; }
  /* Genauso vernebelt wie beim vorangehenden Begrüßungs-Dialog (Nutzeranfrage
     27.08.2026) - .modal-overlay legt dort zusätzlich zum ohnehin auf
     body/body::before liegenden Foto-Deckkraft-Schleier ein rgba(0,0,0,0.45)
     darüber (siehe .modal-overlay-Grundregel weiter oben). Ohne dieses
     Pseudoelement hier würde der Hintergrund beim Schließen des Dialogs
     sichtbar heller. Gleiche z-index:-1-Technik wie body::before oben (bei
     gleichem z-index entscheidet die - hier spätere, also gewinnende -
     Dokumentreihenfolge), pointer-events:none, da rein dekorativ. */
  #view-choose-zone::before {
    content: "";
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    pointer-events: none;
    z-index: -1;
  }
  @media (max-width: 820px) {
    /* +50px, siehe Kommentar bei .modal-overlay.modal-overlay-with-logo oben -
       zieht dessen (jetzt ebenfalls um 50px höheren) Wert weiterhin um main's
       eigene 20px versetzt ab, bleibt also exakt auf gleicher Höhe. */
    #view-choose-zone { padding-top: calc(5vh + 30px); }
    .choose-zone-logo { width: min(320px, 82vw); margin: 0 auto 8px; }
  }
  /* Login-/Registrieren-Ansicht auf dieselbe Höhe wie der Begrüßungs-Dialog
     gehievt, inklusive derselben Hintergrund-Vernebelung (Nutzeranfrage
     27.08.2026, "Login-Screen auf die selbe Höhe wie die Welcome-Box
     verschieben und den Hintergrund bitte auch vernebeln") - gleiches Prinzip
     wie bei #view-choose-zone oben: padding-top zieht die main-eigenen 20px
     vom padding-top der .modal-overlay-with-logo-Basisregel (9vh bzw.,
     Mobile, 5vh) ab, das Pseudoelement legt denselben rgba(0,0,0,0.45)-
     Schleier über den Foto-Hintergrund.
     Nachtrag (28.08.2026, Nutzeranfrage) - jetzt genau wie #view-choose-zone
     AUCH mit großem Logo außerhalb der Karte (#authLogo, dieselbe
     .choose-zone-logo-Klasse, siehe dortiger Kommentar in index.html sowie
     showAuthPanel() in navigation.js fürs Ein-/Ausblenden je nach Panel) -
     #authCard/#forgotCard tragen ihr vormals eigenes kleineres Logo deshalb
     nicht mehr selbst. #checkEmailCard behält seines unverändert (nicht Teil
     dieser Anfrage), #resetCard hatte ohnehin nie eines. Das feste
     margin:60px auto dieser vier Karten (in index.html) blieb bei margin:0
     auto (unverändert seit 27.08.2026) - dieselbe padding-top-Formel wie
     #view-choose-zone reicht als Abstand von oben, #authLogo selbst bringt
     über .choose-zone-logo bereits seinen eigenen Abstand zur Karte mit (14px,
     seit 28.08.2026 halbiert - siehe dortiger Kommentar). */
  #view-auth { padding-top: calc(9vh - 20px); }
  #view-auth::before {
    content: "";
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    pointer-events: none;
    z-index: -1;
  }
  @media (max-width: 820px) {
    /* +50px, siehe Kommentar bei .modal-overlay.modal-overlay-with-logo oben. */
    #view-auth { padding-top: calc(5vh + 30px); }
  }
  /* Gleicher Holzsteg wie section.card/.kpi (Nutzeranfrage 22.08.2026),
     dieselbe background-clip-Technik, siehe dortigen Kommentar. */
  .modal-box {
    border: 4px solid transparent;
    border-radius: 14px;
    background: linear-gradient(var(--card), var(--card)) padding-box, linear-gradient(155deg, #8a6a49, #4c3a28) border-box;
    padding: 20px;
    max-width: 380px;
    width: 100%;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
  }
  /* Wider variant for content with more than a one-line question, e.g. the
     Zeitalter-unlock announcement's bullet list (see showInfoDialog()). */
  .modal-box.wide { max-width: 440px; }
  /* Postfach-Popup auf Desktop ~doppelt so breit wie .wide (Nutzeranfrage
     26.08.2026) - eigene, #mailboxOverlay-gescopte Regel statt .wide selbst
     zu ändern, da .wide auch von anderen, unbeteiligten Popups (z.B.
     Zeitalter-Freischaltung, siehe Kommentar oben) geteilt wird. Nur ab
     821px: auf Mobile bestimmt ohnehin die Viewport-Breite (.modal-box'
     width:100% + .modal-overlay's 20px-Padding) die tatsächliche Breite,
     ein höherer max-width-Wert dort wäre wirkungslos. */
  @media (min-width: 821px) {
    #mailboxOverlay .modal-box { max-width: 880px; }
  }
  /* Diamanten-Shop-Paket-Buttons (10_MONETARISIERUNG.md Abschnitt 6d,
     Nutzeranfrage 14.08.2026/Nachtrag) - die generische feste
     200px/42px-Buttongröße reichte nicht mehr, seit Bonus-/
     Willkommenspaket-Badges dazugekommen sind (renderDiamondShopContent()).
     Füllt stattdessen die volle Breite des Shop-Popups aus und wächst in
     der Höhe mit dem Inhalt statt ihn per Ellipsis abzuschneiden. Das Badge
     (falls vorhanden) steht als eigene, zentrierte Zeile OBERHALB von Menge
     und Preis (.shop-pkg-badge-row) statt inline daneben - dafür ist der
     Button selbst jetzt eine Spalte (flex-direction:column), die zweite
     Zeile (.shop-pkg-main-row) bricht auf schmalen Bildschirmen weiterhin
     sauber um, der Preis-Span bleibt dabei über margin-left:auto immer
     rechtsbündig. */
  .shop-pkg-btn {
    width: 100%;
    box-sizing: border-box;
    height: auto;
    min-height: 42px;
    padding: 10px 14px;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    row-gap: 4px;
    text-align: left;
  }
  /* Nutzeranfrage 14.08.2026 - Kontrast von durchgestrichenem Grundwert/Pfeil
     (.shop-pkg-strike) und Diamantenmenge (.shop-pkg-qty) in beiden
     Button-Zuständen: solange die beiden Zustimmungs-Checkboxen (Widerrufs-
     verzicht/Alters-Selbstauskunft) noch nicht angehakt sind, ist der Button
     disabled (fest hellgrau, #cfd6d3, siehe button:disabled weiter oben) -
     danach aktiv (grün, --btn-bg/--btn-bg-hover). Beide Button-Flächen sind
     bewusst theme-unabhängig fest (derselbe Hex-Wert in Hell und Dunkel,
     siehe die Kommentare bei --btn-bg/button:disabled) - die Textfarben hier
     sind es deshalb ebenfalls, statt var(--muted)/var(--text) zu nutzen, die
     im Dark Mode in die falsche Richtung kippen würden:
     - aktiv/grün: var(--muted) war zu dunkel, kaum lesbar -> helles Weiß.
     - disabled/grau: die von button:disabled geerbte Farbe (#8b9591) reicht
       für die große, prominente Diamantenmenge nicht -> dunkles Grün-Grau. */
  .shop-pkg-btn:not(:disabled) .shop-pkg-strike { color: rgba(255, 255, 255, 0.78); }
  .shop-pkg-btn:disabled .shop-pkg-strike,
  .shop-pkg-btn:disabled .shop-pkg-qty,
  .shop-pkg-btn:disabled .shop-pkg-price { color: #3f4846; }
  .shop-pkg-badge-row { width: 100%; text-align: center; }
  .shop-pkg-main-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    row-gap: 4px;
    column-gap: 10px;
    width: 100%;
  }
  .shop-pkg-main-row .shop-pkg-label { flex: 1 1 auto; min-width: 0; }
  .shop-pkg-main-row .shop-pkg-price { margin-left: auto; white-space: nowrap; }
  /* Nutzeranfrage 21.08.2026 - "Anzahl Diamanten größer, dazu mit dem Preis
     auf gleicher Höhe zentriert". Größere Schrift allein hätte die Zeile
     bereits automatisch korrekt zentriert (.shop-pkg-main-row hat schon
     align-items:center), macht die Höhen-Differenz zum bisher gleich
     großen Preis aber erst überhaupt sichtbar/nötig. */
  .shop-pkg-qty { font-size: calc(22px + 1pt); font-weight: 700; }
  .modal-message {
    font-size: calc(14px + 1pt);
    line-height: 1.5;
    white-space: pre-line;
    margin-bottom: 18px;
  }
  .modal-message ul { margin: 8px 0 0; padding-left: 20px; }
  .modal-message li { margin-bottom: 4px; }
  /* Rundes Emoji-Badge über der Überschrift eines Info-Dialogs (Nutzeranfrage
     13.09.2026, Joint-Venture-"Startet bald"-Popup) - eigene Klasse statt
     Inline-Styles direkt im i18n-String, da Letzterer je Sprache dupliziert
     würde. */
  .modal-message .icon-hero-badge {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(22px + 1pt);
    margin: 0 auto 14px;
  }
  .modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
  }
  .modal-actions button { width: auto; padding: 8px 18px; }
  /* Vorschau eines Premium-Kosmetik-Elements direkt im Kauf-Bestätigungs-
     Popup (Nutzeranfrage 03.08.2026, siehe buyCosmeticUnlock() /
     cosmeticPreviewHtml()) - leer/versteckt für Bestätigungen ohne Vorschau
     (die meisten anderen showConfirmDialog()-Aufrufe im Spiel). */
  .confirm-preview {
    margin-bottom: 18px;
    padding: 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 10px;
  }

  /* ---------- Map ---------- */
  .map-wrap {
    position: relative;
    width: 100%;
    aspect-ratio: 16/8;
    background: #a9c97e; /* fallback in case the scenery SVG hasn't painted yet */
    border-radius: 18px;
    overflow: hidden;
    border: 1px solid var(--border);
  }
  /* Fix (30.07.2026, Nutzeranfrage): bei schmaler Breite brach
     layoutMapBuildings()'s Nicht-Überlappungs-Garantie sichtbar (12_UI_UX.md
     Abschnitt 4b) - der 16:8-Panorama-Zuschnitt lässt bei z. B. 343px Breite
     nur ~171px Höhe, wovon Ränder (marginY) einen erheblichen Teil auffressen.
     Für die übliche Gebäudeanzahl (7 Kern-Gebäude + Firmen der Zone) reicht
     das Rejection-Sampling in einem derart flachen Rechteck oft nicht aus.
     Ein etwas höheres Seitenverhältnis auf schmalen Bildschirmen gibt genug
     Fläche, ohne die Kartenoptik zu verzerren - die Szenerie-SVGs nutzen
     bereits `preserveAspectRatio="xMidYMid slice"` (Zuschnitt statt Streckung,
     siehe MAP_SCENERY weiter unten), zeigen bei einem höheren Format also
     einfach einen anderen Ausschnitt derselben Zeichnung.
     Konkret nachgemessen bei 375px Breite (12 sichtbare Gebäude): bei 4/3
     bleiben nur 3 Zeilen à 61px, was 4 Spalten erzwingt und die Boxen auf
     76px drückt - drei Label ("Firmenzentrale", "Apfelplantage",
     "Forstplantage") brauchen aber ~81px und wurden abgeschnitten. Bei 1/1
     passen 5 Zeilen, es genügen 3 Spalten, und die Boxen dürfen ~102px breit
     werden - alle Label bleiben vollständig lesbar. */
  @media (max-width: 820px) {
    .map-wrap { aspect-ratio: 1/1; }
  }
  .map-scenery {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
  .map-scenery svg {
    display: block;
    width: 100%;
    height: 100%;
  }
  .map-scenery.scenery-image {
    background-size: cover;
    background-position: center;
    opacity: 0.7;
  }
  /* Die Zonen-Fotos (zone-*.png) sind immer taghell fotografiert und hatten
     bisher kein Dark-Mode-Gegenstück - blieben dadurch unverändert hell,
     während die restliche Oberfläche nachgedunkelt wurde (Nutzerreport
     21.08.2026, "nichts soll blenden"). Nur die Foto-Ebene selbst gedimmt
     (filter statt einer zusätzlichen Overlay-Fläche, da .map-scenery hier
     ausschließlich das Bild trägt - die Gebäude-Icons/Labels liegen als
     eigenständige Geschwisterelemente in .map-wrap, nicht als Kinder hier
     drin, filter wirkt also nicht versehentlich auf sie mit). */
  :root[data-theme="dark"] .map-scenery.scenery-image { filter: brightness(0.55); }
  .map-scenery.scenery-temperate { background-image: url("/assets/zones/zone-temperate.png"); }
  .map-scenery.scenery-mediterranean { background-image: url("/assets/zones/zone-mediterranean.png"); }
  .map-scenery.scenery-tropical { background-image: url("/assets/zones/zone-tropical.png"); }
  .map-scenery.scenery-arid { background-image: url("/assets/zones/zone-arid.png"); }
  .map-scenery.scenery-arctic { background-image: url("/assets/zones/zone-arctic.png"); }
  .map-scenery.scenery-mountains { background-image: url("/assets/zones/zone-mountains.png"); }
  .map-scenery.scenery-outback { background-image: url("/assets/zones/zone-outback.png"); }
  .building {
    position: absolute;
    transform: translate(-50%, -50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    cursor: pointer;
    width: 110px; /* fallback before layoutMapBuildings() sizes this inline to fit its grid cell */
    text-align: center;
    user-select: none;
  }
  .building .tile {
    position: relative;
    width: 62px; /* fallback - see above, layoutMapBuildings() sets this inline per viewport */
    height: 62px;
    border-radius: 18px;
    /* --tile-bg hält denselben Wert als benannte Variable fest (Nutzerreport
       25.08.2026, "Rahmenfarbe wählen ändert auch die Hintergrundfarbe des
       Symbols") - firmFrameBorderCss()/applyFirmFrameBorder() (cosmetics.js)
       brauchen für den Verlaufs-Rahmen "pride" (einzige FIRM_FRAMES-Option
       mit Farbverlauf statt einzelnem Ton, siehe deren Kommentar) einen
       zweiten, undurchsichtigen Hintergrund NUR im padding-box-Bereich, damit
       der Verlauf im border-box-Bereich sauber abgerundet bleibt
       (background-clip respektiert border-radius, border-image nicht). Der
       nutzte bisher hart codiert var(--card) statt der tatsächlichen
       Kachelfarbe hier - überschrieb dadurch das Beige/Hellgrau der Kachel
       mit der (helleren) Kartenfarbe, sobald "pride" gewählt war. Über diese
       Variable verwenden beide Stellen jetzt garantiert denselben Wert. */
    --tile-bg: #e6e2d6; /* Nachtrag 24.08.2026, Nutzeranfrage - "etwas dunkler, Richtung Beige/Hellgrau" statt reinem Weiß, danach zweimal nachjustiert (#efece3 → #ddd8c9 → #e6e2d6, letzteres der Mittelwert der beiden vorigen Töne auf Nutzerwunsch "etwas heller, zwischen jetzigem und vorigem") */
    background: var(--tile-bg);
    box-shadow: 0 4px 10px rgba(0,0,0,.10);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(28px + 1pt);
    border: 2px solid var(--accent);
    transition: transform .15s ease, box-shadow .15s ease, width .15s ease, height .15s ease;
  }
  .building:hover .tile { transform: translateY(-4px); box-shadow: 0 8px 18px rgba(0,0,0,.14); }
  /* Nutzerfolge auf die gedimmten Zonen-Fotos oben (Nutzeranfrage 21.08.2026,
     "ja, mach die auch dunkler"): die Kachel selbst war bislang in beiden
     Themes weiß - jetzt, wo der Fotohintergrund im Dark Mode abgedunkelt ist,
     wirkt eine weiterhin strahlend weiße Kachel erst recht wie ein
     Fremdkörper. Dark-Mode-Gegenstück auf --card, passend zu jeder anderen
     Karte/Fläche im dunklen Design. */
  :root[data-theme="dark"] .building .tile { --tile-bg: var(--card); background: var(--tile-bg); }

  /* Kosmetik-Auswahl (10_MONETARISIERUNG.md Abschnitt 2a, Punkte 2 und 6).
     Bewusst kleine Farbfelder statt einer Dropdown-Liste - die Auswahl ist
     rein visuell, da hilft eine Vorschau mehr als ein Name. */
  /* Fix (01.08.2026, Nutzeranfrage, ursprünglich für den inzwischen wieder
     entfernten Gebäude-Skin, Abschnitt 2c) - overflow:hidden saß ursprünglich
     direkt auf .tile, um gezeichnete Kachel-Inhalte an den runden Ecken zu
     beschneiden - das schnitt aber auch den Stufen-Badge ab, der mit
     negativen bottom/right-Werten bewusst über den Kachelrand hinausragt
     (siehe .level-badge unten). Die Beschneidung sitzt seitdem auf einer
     eigenen inneren Ebene (.tile-clip) - Badges bleiben direkte,
     unbeschnittene Kinder von .tile. Trägt weiterhin den Ambiente-Effekt
     (Punkt 4, .ambience-art), damit dessen Animation an denselben runden
     Ecken beschnitten wird. */
  .tile-clip {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: 18px;
    pointer-events: none;
  }
  .ambience-art {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
  .tile-icon { position: relative; z-index: 1; line-height: 1; }
  /* Firmenschild (Punkt 1) - hebt sich leicht vom normalen Typnamen ab, damit
     erkennbar ist, dass hier ein selbst vergebener Name steht. */
  .building .label.plaque { font-style: italic; border-color: var(--accent); }

  /* Börsenkürzel (Punkt 16) - kompakt und monospaced, damit es neben dem
     Firmennamen als Kürzel erkennbar ist und nicht als Namensbestandteil. */
  .ticker-tag {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.82em;
    background: var(--accent-light);
    color: var(--accent-dark);
    padding: 1px 5px;
    border-radius: 4px;
  }

  /* Firmen-Illustration in der Detailansicht (Nutzeranfrage 20.08.2026,
     zunächst nur für die Ölraffinerie) - oben rechts über den Kartentext
     gelegt statt in den Dokumentfluss eingereiht, damit die Karte dadurch
     nicht höher wird als ohne Bild. #firmDetail (nicht die umgebende
     section.card) ist der positionierte Bezugsrahmen, weil sich renderFirmDetail()
     ausschließlich innerhalb dieses div austauscht. */
  #firmDetail { position: relative; }
  /* Immer in der großen Größe, unabhängig vom Besitzstatus (Nutzeranfrage
     20.08.2026, Nachtrag 7 - "immer so groß wie aktuell bei der
     Ölraffinerie, auch wenn die Firma noch nicht gegründet wurde"; vorher
     gab es hier eine kleinere Variante nur für noch nicht gegründete
     Firmen). Breit genug, um ungefähr auf Höhe des "Auto-Verkauf"/
     "Experte für 7 Tage"-Buttonpaars in der Aktionsreihe einer gegründeten
     Firma zu enden ("in etwa", wie vom Nutzer selbst mit einem roten Rahmen
     markiert - keine pixelgenaue Ausrichtung an den Buttons, da deren
     Position je nach Text-/Preislänge und Bildschirmbreite schwankt).
     Wrapper statt Größe/Position direkt am <img> (Nutzeranfrage 20.08.2026,
     Nachtrag 2 "wie durch ein Fenster darauf schauen") - der Fensterrahmen
     (Steg + Glasspiegelung, siehe ::after unten) sitzt als Padding/
     Pseudo-Element auf diesem Div, .firm-detail-illustration-img füllt es
     lediglich zu 100% aus.
     Nachtrag (28.08.2026, Nutzeranfrage) - die zwischenzeitliche, per JS
     (syncFirmIllustrationWidth(), 21.08.2026) auf die Breite bis zum
     Auto-Verkauf-Button verengte Fassung ist wieder entfernt: Sie griff nur
     bei Firmentypen MIT Auto-Verkauf-Button, das Forschungslabor (nie einen
     solchen Button, auch nicht als der bei anderen Firmentypen inzwischen
     reservierte unsichtbare Platzhalter, siehe firms.js) blieb dadurch bei
     dieser breiteren CSS-Basisgröße stehen - "das große Bild bei den Firmen
     ist kleiner als beim Labor". Auf Wunsch vereinheitlicht: jetzt gilt für
     ALLE Firmentypen dieselbe, unverengte Basisbreite. */
  .firm-detail-illustration {
    position: absolute;
    /* 10px statt 0 (Nutzeranfrage 20.08.2026, "bündig mit dem Icon oben"):
       .item-row (der erste Kindblock in #firmDetail, enthält das Icon) trägt
       selbst padding:10px 0 - das Icon sitzt dadurch 10px unter der oberen
       Kante von #firmDetail, nicht direkt daran. */
    top: 10px;
    right: 0;
    width: 67%;
    max-width: 1000px;
    height: 490px;
    border-radius: 14px;
    pointer-events: none;
    box-sizing: border-box;
    /* Rahmensteg (6px) fürs Fenster-Gefühl - siehe firm-detail-illustration-img
       und die ::after-Regel weiter unten. */
    padding: 6px;
    background: linear-gradient(155deg, #8a6a49, #4c3a28);
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.28), inset 0 0 0 1px rgba(255, 255, 255, 0.18);
  }
  .firm-detail-illustration-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 8px;
    /* Sanftes Einblenden statt hartem Pop-in (Nutzerreport 21.08.2026) - das
       Start-opacity:0/onload-Umschalten auf opacity:1 sitzt inline im
       generierten <img> selbst (firms.js), hier nur der weiche Übergang
       dazwischen. */
    transition: opacity 0.25s ease;
  }
  /* Firmenfotos sind wie die Zonen-Fotos immer taghell und hatten kein
     Dark-Mode-Gegenstück (Nutzerreport 21.08.2026, "nichts soll blenden") -
     nur das Foto selbst gedimmt, die Glasspiegelung (::after auf dem Wrapper
     direkt darunter) bleibt davon unberührt, da sie kein Kind dieses <img>
     ist. */
  :root[data-theme="dark"] .firm-detail-illustration-img { filter: brightness(0.55); }
  /* Glasspiegelung statt Fenstersprosse (Nutzeranfrage 20.08.2026, Nachtrag 4
     - "Kreuz weg, stattdessen eine Art Spiegelung, damit es wie eine
     Glasscheibe aussieht"): zwei versetzte diagonale Lichtstreifen, wie
     Lichtreflexe auf echtem Glas - ein kräftigerer oben links, ein
     schwächerer, breiterer unten rechts. Deckt sich mit dem inneren
     (gepolsterten) Bildbereich, daher inset:6px statt 0. */
  .firm-detail-illustration::after {
    content: "";
    position: absolute;
    inset: 6px;
    border-radius: 8px;
    background:
      linear-gradient(125deg, transparent 0%, transparent 24%, rgba(255, 255, 255, 0.55) 34%, rgba(255, 255, 255, 0.15) 43%, transparent 54%),
      linear-gradient(125deg, transparent 62%, rgba(255, 255, 255, 0.2) 76%, transparent 90%);
  }
  /* Reserviert Platz rechts neben Name/Beschreibung, damit die Illustration
     nie über den Text läuft - v.a. auf schmalen Screens, wo Name/Beschreibung
     sonst die volle Kartenbreite beanspruchen und unter das Bild wandern
     würden. Nur gesetzt, wenn eine Illustration tatsächlich vorhanden ist
     (siehe illustrationHtml in firms.js), sonst bräuchte der Text diesen
     Freiraum nicht. */
  .firm-title-block { padding-right: 69%; }
  /* Nutzeranfrage 27.08.2026 ("Text darf niemals unterhalb des Bilds rechts
     sein") - die Rahmenfarbe-Kachelreihe (frameSection, firms.js) einer
     bereits gegründeten Firma bekam bislang KEINE der beiden im JS-Kommentar
     bei .firm-content-with-illustration beschriebenen Freiraum-Reservierungen
     - trotz des Kommentars existierte für diese Klasse überhaupt keine
     CSS-Regel (weder Desktop noch die dort erwähnte Mobile-Variante), nur
     .firm-title-block oben hatte tatsächlich eines. Live nachgemessen (Bild
     abgeglichen mit den tatsächlichen Bounding-Boxen der Kachel-Buttons) lag
     das Fenster-Bild dadurch bei jeder Breite zwischen der 820px-Mobile-
     Grenze und rund 1400px sichtbar ÜBER mehreren Rahmenfarbe-Kacheln, weil
     deren umgebende Zeile (anders als .firm-title-block) die volle
     Kartenbreite beanspruchte statt sich auf die schmale linke Spalte neben
     dem Bild zu beschränken.
     frameSection ist im Markup immer das erste Kind von
     .firm-content-with-illustration, WENN diese Klasse überhaupt existiert
     (beide - frameSection und das direkt danach folgende ownedSection - sind
     an dieselbe owned-Bedingung geknüpft, stehen also entweder beide oder
     keines von beiden dort) - :first-child trifft dadurch zuverlässig NUR
     frameSection, nie versehentlich den ersten Absatz von ownedSection
     (Bewertungszeile/Wartungszustand/Aktionen-Überschrift/action-grid) -
     die dürfen bewusst die volle Kartenbreite behalten, siehe deren eigenen
     Kommentar bei .action-grid weiter unten (dort startet der Inhalt dank
     der jetzt garantiert ausreichenden Höhe von frameSection ohnehin schon
     unterhalb des Bildes, eine eigene Einschränkung wäre dort nur die
     sorgfältig austarierte 6-Spalten-Ausrichtung der Aktions-Buttons kaputt
     machen). Dieselben 69%/122px wie .firm-title-block (Desktop-Basisregel
     bzw. Mobile-Media-Query unten) - beide Blöcke sollen sich optisch an
     derselben rechten Kante ausrichten. */
  .firm-content-with-illustration > .item-row:first-child { padding-right: 69%; }
  /* Nachtrag (29.08.2026, Nutzerreport per Screenshot) - die obige Annahme
     ("dort startet der Inhalt dank der jetzt garantiert ausreichenden Höhe
     von frameSection ohnehin schon unterhalb des Bildes") stimmte nicht
     zuverlässig: frameSection besteht bei manchen Firmentypen/Zuständen nur
     aus Rahmenfarbe + Firmenschild (schmal, wenig Höhe) - dann reichte die
     kumulierte Höhe bis zum ersten Absatz von ownedSection (Produktionsrate/
     Mitarbeiter/Experte/Wartungszustand) nicht aus, dieser Absatz lag
     sichtbar UNTER dem Bild und wurde davon verdeckt (position:absolute
     rendert oberhalb von normalem Fluss-Inhalt, unabhängig von der
     DOM-Reihenfolge). Bekommt deshalb jetzt dieselbe Freiraum-Reservierung
     wie frameSection/.firm-title-block - eigene Klasse `.firm-production-info`
     statt eines weiteren `:first-child`/`:nth-child`-Selektors, da dieser
     Absatz nicht zuverlässig an einer festen Geschwister-Position steht
     (hängt u. a. davon ab, ob frameSection gerendert wird). Absichtlich NUR
     dieser eine Absatz, nicht ganz ownedSection - Aktionen-Überschrift und
     .action-grid dürfen wie zuvor beschrieben die volle Breite behalten. */
  .firm-content-with-illustration .firm-production-info { padding-right: 69%; }
  /* Nutzeranfrage 11.09.2026 ("Höhe aller Firmen-Detailsichten soll immer
     gleich sein") - dieser Absatz variiert sonst in der Höhe je Firma
     (Rohstoffmangel-/Mitarbeiter-/Experten-/Auto-Verkauf-Hinweise und der
     Wartungs-Effizienzverlust erscheinen nur teils und mit unterschiedlich
     langem Text, siehe deren jeweilige Bedingung in renderFirmDetail()),
     wodurch die direkt danach folgende Aktionen-Überschrift/.action-grid bei
     jeder Firma an einer leicht anderen Höhe startete - der Nutzerwunsch
     ausdrücklich auch: "die Buttons alle auf jedem Screen auf der selben
     Höhe sind".
     Nachtrag (11.09.2026, Nutzerreport) - ursprünglich EIN min-height auf
     die ganze .firm-production-info, kalibriert auf den worst case ALLER
     vier Zeilen gleichzeitig (Rohstoffmangel + Mitarbeiter + Experte +
     Wartungs-Effizienzverlust). Der ungenutzte Freiraum jeder kürzeren
     Kombination pool­te dadurch komplett am Ende des Absatzes - sichtbar als
     überdimensionierte Lücke vor der nächsten Zeile (Nutzerreport: "Abstand
     nach 'Wartung durchführen' viel zu groß"). Jetzt stattdessen VIER
     einzelne min-heights, je auf den worst case NUR dieser einen Zeile
     kalibriert (siehe .firm-rate-row/.firm-shortage-row/.firm-notes-row/
     [data-maintenance-state] weiter unten) - der Freiraum verteilt sich
     dadurch auf die jeweils tatsächlich kürzere Zeile, statt sich am Ende
     des ganzen Absatzes aufzustauen. Referenzbreiten unverändert 1350px
     (Desktop, dieselbe "ab hier verlässlich"-Grenze wie beim 6-Spalten-Grid
     der Aktions-Buttons) und 375px (Mobile). */
  .firm-rate-row { min-height: 29px; }
  .firm-shortage-row { min-height: 21px; }
  .firm-notes-row { min-height: 40px; }
  [data-maintenance-state] { min-height: 70px; }
  @media (max-width: 820px) {
    .firm-rate-row { min-height: 50px; }
    .firm-shortage-row { min-height: 41px; }
    .firm-notes-row { min-height: 60px; }
  }
  /* Gleicher Gedanke wie bei .firm-production-info direkt oberhalb, für den
     Beschreibungstext unter dem Firmennamen - Gutname/Zone/Mechanik machen
     jede Beschreibung unterschiedlich lang, wodurch Rahmenfarbe (und alles
     danach) sonst bei jeder Firma an einer anderen Höhe startete. min-height
     auf die tatsächlich längste vorkommende Beschreibung kalibriert (bei
     1350px/375px, siehe Media Query unten) - kürzere Texte lassen darunter
     einfach etwas Freiraum bis Rahmenfarbe/Firmenschild beginnen. */
  .firm-desc { min-height: 40px; }
  @media (max-width: 820px) {
    .firm-desc { min-height: 80px; }
  }
  /* Verhindert, dass Inhalt UNTER der Illustration liegt (Nutzerreport
     20.08.2026, "Bild liegt eine Ebene über dem Gründen-Button, falls die
     Firma noch nicht gegründet ist") - .firm-detail-illustration ist
     position:absolute und beansprucht dadurch selbst keinen Platz im
     Dokumentfluss. Bei einer bereits gegründeten Firma sorgte bislang genug
     nachfolgender Karteninhalt (Kosmetik-Auswahl, Aktionsreihe) zufällig für
     ausreichend Höhe; bei einer noch nicht gegründeten Firma (nur Icon/Titel
     + Gründen-Button, ohne Kosmetik-Sektion) reichte das nicht - das Bild lag
     über dem Button. min-height auf genau dieser (ersten) Zeile in
     #firmDetail garantiert unabhängig vom nachfolgenden Karteninhalt, dass
     alles Weitere erst unterhalb des Bildes beginnt. align-items:flex-start
     überschreibt das geerbte center von .item-row, sonst würden Icon/Titel
     in der jetzt hohen Zeile mittig statt oben (bündig mit dem Bild) sitzen. */
  .firm-illustration-row {
    align-items: flex-start;
    min-height: 500px;
  }
  /* Reserviert denselben rechten Freiraum wie .firm-title-block, aber nur auf
     Mobile (siehe Media Query unten) - frameSection/ownedSection (Rahmenfarbe/
     Bauform/Ambiente/Firmenschild-Auswahl bei bereits gegründeten Firmen)
     enthalten anders als der Titelblock volltextbreite Beschriftungen/Hinweise,
     die auf schmalen Screens sonst unter dem Bild verschwanden (Nutzerreport
     21.08.2026). Auf Desktop bewusst ohne Wirkung (kein Regeleintrag dort) -
     dort sorgt die schmale Kosmetik-Auswahl selbst dafür, dass nichts mit dem
     Bild kollidiert (siehe .firm-illustration-row oben, Nachtrag 9). */
  @media (max-width: 820px) {
    /* Firmen-Icon (.firm-icon-plate) bleibt auf Mobile ausgeblendet
       (Nutzeranfrage 24.08.2026, "in der mobilen Version soll es weiterhin
       nicht angezeigt werden") - anders als auf Desktop, wo es nach einer
       vorherigen Komplett-Entfernung wieder eingebaut wurde. display:none
       statt gar keinem Markup zu rendern, damit derselbe HTML-String für
       beide Breakpoints reicht; ein ausgeblendetes Element beansprucht
       innerhalb von .item-info (flex) ohnehin keinen Platz, Name/
       Beschreibung bekommen auf Mobile also weiterhin die volle Breite. */
    .firm-icon-plate { display: none; }
    /* Kleiner als zuvor (130x90 -> 96x68, Nutzerreport 21.08.2026, "Bilder
       kleiner machen, Text muss immer vollständig sichtbar sein") - lässt auf
       schmalen Screens mehr Breite für Name/Beschreibung/Kosmetik-Text.
       Nachtrag (24.08.2026, Nutzeranfrage) - auf 112x80 vergrößert, nachdem
       das Icon oben (.firm-icon-plate) auf Mobile ausgeblendet wurde: Der
       dadurch auf Mobile frei gewordene Platz (Icon + Abstand, zusammen
       66px) ist größer als der Größenzuwachs hier, das Text-
       Vollständigkeits-Problem von damals sollte dadurch nicht
       zurückkommen. Ausdrücklich NUR Mobile -
       die Desktop-Größe (siehe .firm-detail-illustration Basisregel oben)
       bleibt unverändert. */
    .firm-detail-illustration { width: 34%; max-width: 112px; height: 80px; }
    /* Rahmensteg auf Mobile deutlich dünner als die 6px-Desktop-Basisregel
       (Nutzeranfrage 21.08.2026, "Rahmen um die Bilder in den Abschnitten der
       Firmen wesentlich dünner"), auf erneute Nutzeranfrage von 2px auf 4px
       nachgezogen ("2px breiter"). inset auf ::after (Glasspiegelung) im
       selben Wert, sonst säße die Spiegelung nicht mehr bündig mit dem
       Rahmen. */
    .firm-detail-illustration { padding: 4px; }
    .firm-detail-illustration::after { inset: 4px; }
    .firm-title-block { padding-right: 122px; }
    /* Mobile-Gegenstück zur Desktop-Regel oben (Nutzeranfrage 27.08.2026) -
       dieselben 122px wie .firm-title-block direkt darüber, statt der dort
       für Desktop genutzten 69%. Das winzige 112px-Mobile-Bild wird zwar
       meist schon durch den Titelblock/die Kartenhöhe geräumt, bevor
       frameSection beginnt, ganz sicher ist das aber nicht (z. B. bei einem
       Firmentyp mit kurzem Namen ohne Beschreibungsumbruch) - dieselbe
       Reservierung wie bei .firm-title-block schließt auch diesen
       Rand-fall zuverlässig aus, statt sich auf zufällig ausreichenden
       Karteninhalt zu verlassen. */
    .firm-content-with-illustration > .item-row:first-child { padding-right: 122px; }
    /* Nutzeranfrage 28.08.2026, Nachtrag, erweitert 29.08.2026 (Nutzerreport
       "Textbereiche sollen die volle Breite nutzen, wenn rechts daneben kein
       anderes Element steht") - Rahmenfarbe-/Firmenschild-Beschriftungen,
       -Hinweistexte, -Kacheln und Firmenschild-Eingabefeld/Speichern-Button
       sollen bei Firmentypen MIT großem Bild (FIRM_ILLUSTRATIONS, firms.js)
       dieselbe volle Kartenbreite nutzen wie bei Firmentypen ohne Bild - dort
       greift die 122px-Reservierung direkt oberhalb gar nicht erst, da
       .firm-content-with-illustration für sie überhaupt nicht existiert. Bei
       Firmentypen MIT Bild galt die Reservierung (siehe deren Kommentar oben,
       "reserviert faktisch für den gesamten Rest der Karte Leerraum") bisher
       unverändert auch für diese Elemente, obwohl sie längst unterhalb des
       nur 90px hohen Mobile-Bilds sitzen. .item-sub deckt die "Rahmenfarbe"/
       "Firmenschild"-Beschriftungen samt ihrer Hinweiszeilen ab, .cosmetic-row
       sowohl die Rahmenfarbe-Kachelreihe als auch die Firmenschild-
       Eingabezeile (beide teilen sich dieselbe Klasse, siehe frameSection in
       firms.js), das dritte Selektorglied trifft die eigene .item-row-Zeile
       des Speichern-Buttons. Negatives margin-right hebt die geerbte
       Polsterung gezielt nur für diese Elemente wieder auf. Titel/Beschreibung
       der Firma direkt darüber (.firm-title-block, eigene Regel) behalten
       bewusst die Reservierung, da dort rechts daneben tatsächlich noch das
       Bild sitzt. */
    .firm-content-with-illustration > .item-row:first-child .item-sub,
    .firm-content-with-illustration > .item-row:first-child .cosmetic-row,
    .firm-content-with-illustration > .item-row:first-child .item-row {
      margin-right: -122px;
    }
    /* Gleiche Überlegung wie bei frameSection direkt darüber, auf
       .firm-production-info (29.08.2026, siehe Desktop-Regel oben)
       angewendet: Das Mobile-Bild ist nur 80-90px hoch, frameSection davor
       (Rahmenfarbe ist bei einer gegründeten Firma immer vorhanden) reicht
       dafür zuverlässig aus - die 69%-Reservierung wird auf Mobile daher
       nicht gebraucht und würde den Produktions-Absatz nur unnötig
       einengen. Direktes Zurücksetzen statt eines Margin-Tricks, da hier
       (anders als bei den geerbten 122px oben) kein fester px-Wert
       gegenzurechnen ist. */
    .firm-content-with-illustration .firm-production-info { padding-right: 0; }
    .firm-illustration-row { min-height: 90px; }
    /* Entfernt (Nutzerreport 21.08.2026, "Bereich unterhalb des Bildes ist
       tot, soll wieder komplette Breite nutzen") - das Bild ist auf Mobile
       nur noch 80px hoch (+10px oberer Abstand, siehe .firm-illustration-row
       min-height oben), frameSection/ownedSection (Rahmenfarbe/Bauform/
       Ambiente/Firmenschild, Mitarbeiter/Wartung/Experten/Auto-Verkauf-
       Buttons) beginnen aber erst NACH der Titel-Zeile (seit Nachtrag
       24.08.2026 ohne Icon davor, nur noch Name/Beschreibung), die für sich
       allein schon fast genau diese 90px hoch ist - der 122px-Rand rechts
       reservierte hier faktisch für den gesamten, oft sehr langen Rest der
       Karte Leerraum neben einem Bild, das an dieser Stelle längst zu Ende
       ist. */
  }
  .cosmetic-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
  .cosmetic-swatch {
    width: 26px;
    height: 26px;
    min-width: 26px;
    padding: 0;
    border-radius: 8px;
    cursor: pointer;
    box-shadow: none;
    /* Nutzeranfrage 26.08.2026, "orangene Rahmen dürfen nicht um die
       Auswahlfelder der Rahmenfarbe sein": Der globale button-Rahmen (siehe
       dortiger Kommentar) läuft hier gegen den eigentlichen Zweck der Kachel
       - eine Farb-/Musterauswahl braucht ihre tatsächliche Farbe als
       Vorschau ohne fremden Ring drumherum, und .selected markiert die
       aktuelle Wahl bereits über einen eigenen, gezielten Rahmen (siehe
       unten) statt des generischen. Betrifft alle Auswahlfelder dieser
       Familie (Firmen-/Avatar-Rahmenfarbe, Namensfarbe, Banner, Fahrzeug-
       Lackierung), nicht nur Rahmenfarbe im engeren Sinn -
       dieselbe Farb-Vorschau-Logik gilt für sie alle gleichermaßen. */
    outline: none;
    /* Der globale button-Reset setzt overflow:hidden (für den Text-Ellipsis
       normaler Buttons) - das schnitt hier das 💎-Freischalt-Abzeichen ab,
       das bewusst über den Rand hinausragt (siehe .locked::after unten).
       Nutzeranfrage: "Diamanten müssen overlay sein, sind abgeschnitten". */
    overflow: visible;
  }
  .cosmetic-swatch.selected { outline: 2px solid var(--text); outline-offset: 2px; }
  /* Noch nicht mit Diamanten freigeschaltete Optionen bleiben sichtbar UND
     klickbar (startet den Kauf-Flow, siehe wireCosmeticSwatches) - erkennbares
     Angebot statt versteckter Funktion, gleiches Muster wie die 3-/7-Tage-
     Expertenstufen (05_UNTERNEHMEN.md Abschnitt 4a). Ein kleines 💎-Abzeichen
     unten rechts macht "kaufen statt auswählen" auf den ersten Blick klar. */
  .cosmetic-swatch.locked { opacity: 0.65; position: relative; }
  /* Diamanten-Symbol als kleines SVG-Icon statt des 💎-Emoji (Nutzeranfrage
     14.08.2026, siehe diamondIconHtml() weiter unten im Skript für dieselbe
     Formsprache) - ein CSS-content-Pseudoelement kann keinen JS-Funktions-
     aufruf einbetten, deshalb hier als eigenständige, url-encodete SVG-
     Daten-URI mit denselben zwei Flächenfarben (kein Gradient nötig, bei
     14px kaum sichtbar). */
  .cosmetic-swatch.locked::after {
    content: ""; position: absolute; bottom: -4px; right: -4px;
    width: 14px; height: 14px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpolygon points='6.8,3 17.2,3 22,9 2,9' fill='%235ec8f0'/%3E%3Cpolygon points='2,9 22,9 12,21' fill='%231a7fc4'/%3E%3Cpath d='M6.8,3 L2,9 M17.2,3 L22,9 M2,9 L12,21 M22,9 L12,21 M7,9 L12,21 M17,9 L12,21' stroke='rgba(255,255,255,0.55)' stroke-width='0.8' fill='none'/%3E%3C/svg%3E");
    background-size: contain; background-repeat: no-repeat;
  }
  .cosmetic-swatch:hover:not(:disabled) { transform: translateY(-2px); }
  /* Firmen-Rahmenfarbe doppelt so groß wie die allgemeine .cosmetic-swatch-
     Basisgröße (Nutzeranfrage 21.08.2026, "Auswahlfelder für die
     Rahmenfarbe doppelt so groß") - eigene Klasse statt die Basisgröße
     selbst zu ändern, damit Fahrzeug-Lackierung (transport.js, nutzt
     dieselbe .cosmetic-swatch-Basis ohne eigene Größenklasse) unangetastet
     bleibt. */
  .firm-frame-swatch {
    width: 52px;
    height: 52px;
    min-width: 52px;
  }
  /* 4 Rahmenfarben pro Zeile auf Mobile (Nutzeranfrage 21.08.2026) - freies
     flex-wrap ließ je nach Kartenbreite mal 3, mal 4 Swatches pro Zeile
     umbrechen. :has() statt einer eigenen Zeilen-Klasse, da .cosmetic-row
     sonst überall sonst (Firmenschild-Zeile etc.) generisch bleibt - hier
     matcht sie gezielt nur die Zeile, die tatsächlich Rahmenfarbe-Swatches
     enthält.
     Nachtrag (28.08.2026, Nutzeranfrage) - "Icons auf die volle Breite
     bringen, bleib aber bei zwei Reihen mit jeweils 4 Elementen, vergrößere
     aber den Abstand untereinander": justify-items:center (jede Kachel
     behält ihre feste 52px-Größe, Rest der Spalte bleibt leer) auf
     Nutzerwunsch zurückgenommen - die Kacheln selbst werden jetzt über die
     .firm-frame-swatch-Regel unten je Breakpoint auf 100% Spaltenbreite
     gestreckt (Standard-justify-items:stretch reicht dafür, sobald die
     Kachel selbst keine feste width mehr hat), gap von den geerbten 6px
     (.cosmetic-row-Basisregel) auf 14px erhöht. */
  @media (max-width: 820px) {
    .cosmetic-row:has(.firm-frame-swatch) {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 14px;
    }
    .cosmetic-row:has(.firm-frame-swatch) .firm-frame-swatch {
      width: 100%;
      height: auto;
      aspect-ratio: 1;
      min-width: 0;
    }
  }
  /* Fahrzeug-Lackierung als Chip hinter dem Emoji - dieselbe Technik wie beim
     Firmen-Rahmen, nur als Hintergrund, damit Streifenmuster möglich sind. */
  .livery-chip {
    width: 38px;
    height: 38px;
    border-radius: 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: calc(22px + 1pt);
    border: 1px solid var(--border);
    flex-shrink: 0;
  }
  .building .label {
    font-size: calc(11.5px + 1pt);
    font-weight: 700;
    /* Nachtrag 24.08.2026, Nutzeranfrage - Hintergrund an die Kachel (.tile)
       angeglichen statt eines eigenen reinen Weiß, plus ein sichtbarer
       silberner Rahmen statt des vorher kaum wahrnehmbaren var(--border).
       Rahmen auf Nutzeranfrage 25.08.2026 ("Umrandung machen wie bei den
       Bildern") nochmal an .tile oben angeglichen (2px var(--accent) statt
       1px Silber) - .label.plaque unten überschreibt weiterhin nur die
       Farbe, nicht die Breite, übernimmt die 2px also automatisch mit. */
    background: #e6e2d6;
    border: 2px solid var(--accent);
    /* Explicitly dark, not var(--text) (user request 29.07.2026): the map
       labels and tiles kept their white background in both themes (the drawn
       scenery behind them was always light), so the light dark-mode --text
       would be white-on-white and effectively invisible. Superseded for the
       dark theme by the override right below - now that the zone photos and
       the label background itself are dimmed in dark mode too (Nutzeranfrage
       21.08.2026, "mach die auch dunkler"), --text becomes readable again. */
    color: #22292a;
    /* Vertikales Padding von 3px auf 1.2px reduziert (Nutzeranfrage
       06.08.2026, "Abstand oben/unten um ca. 60% reduzieren") - horizontales
       Padding (9px) bewusst unverändert, da nur "oben und unten" gefordert
       war. Weiter auf 0.6px reduziert (Nutzeranfrage 09.08.2026, "kann der
       Abstand nach oben und unten etwas reduziert werden"): Diese Box ist auf
       Mobile das eine noch verbleibende Stellglied für die Zwei-Zeilen-Breite
       aus dem "so breit machen, dass auch der längste Text in zwei Zeilen
       angezeigt werden kann"-Fix (layoutMapBuildings()) - kleineres Padding
       verkleinert boxHeight, was auf schmalen Bildschirmen mehr Zeilen pro
       verfügbarer Kartenhöhe zulässt und dadurch (über weniger benötigte
       Spalten) mehr Breitenbudget pro Box freigibt. Die Boxhöhe unten
       (height: calc(...)) rechnet dieselbe Zahl mit ein und musste
       entsprechend mitgezogen werden. */
    padding: 0.6px 9px;
    border-radius: 999px;
    /* width:100% ergänzt (Nutzerreport 06.08.2026, "Textboxen sind nicht alle
       gleich breit, obwohl das so besprochen war"): Die Breite wurde bislang
       einzig über das inline gesetzte width auf .building (layoutMapBuildings(),
       boxWidth) erwartet - .label selbst hatte aber nur max-width:100% (eine
       OBERGRENZE, kein fester Wert). .building ist ein Flex-Container mit
       align-items:center statt des Flexbox-Standards "stretch" (siehe .building
       oben, bewusst für die Kachel/Icon-Zentrierung so gewählt) - dadurch
       wurde .label NIE auf die Breite seines Elternelements gestreckt, sondern
       sizte sich auf seinen eigenen Inhalt (max-width griff nur als Kappung,
       nicht als Zwang). Ein kurzer Name wie "Lager" blieb dadurch sichtbar
       schmaler als "Apfelplantage" - live nachgemessen: 51,7px vs. 83,2px bei
       identischem boxWidth. width:100% erzwingt jetzt tatsächlich die gleiche
       Breite für jedes Label, unabhängig vom Textinhalt. */
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
    /* Einheitliche Boxgröße für JEDES Gebäude, unabhängig vom Namen
       (Nutzeranfrage 06.08.2026) - die Breite wird seither in
       layoutMapBuildings() nicht mehr aus dem längsten Label-Inhalt
       abgeleitet, sondern rein aus der (für alle Gebäude eines Layout-
       Durchlaufs ohnehin gleichen) Kachelgröße berechnet
       (labelBoxWidth = tileSize * MAP_LABEL_WIDTH_FACTOR, siehe dort) -
       jedes Label ist dadurch von sich aus schon gleich breit, ganz ohne
       CSS-Zutun. Die Höhe war hier bereits vorher ein reiner CSS-Fixwert
       (nicht content-abhängig), bleibt also ebenfalls automatisch einheitlich.

       Einzeilig statt mehrzeilig (Nutzeranfrage 07.08.2026, "such das
       längste Wort, das auf der Karte angezeigt werden kann, und setze die
       Breite des Textfelds auf die Breite dieses Wortes, verringere die
       Höhe entsprechend auf eine Zeile") - löst die vorherige 3-Zeilen-Lösung
       ab: MAP_LABEL_WIDTH_FACTOR wurde dafür in layoutMapBuildings() neu aus
       dem tatsächlich breitesten aktuell vorkommenden Label hergeleitet
       (nicht mehr geraten), wodurch für JEDES bekannte Label eine einzige
       Zeile reicht - kein Umbruch mehr nötig. 1.35em = genau eine Zeile bei
       der line-height von 1.35 (siehe .label-text) - `em` löst gegen die
       eigene, von JS pro Kachel gesetzte font-size auf (siehe
       layoutMapBuildings(), label.style.fontSize), die Boxhöhe skaliert
       automatisch mit der Kachelgröße mit. 3.2px = Rahmen(2px) + vertikales
       Padding(1.2px = 0.6px oben + 0.6px unten, siehe padding oben - seit
       09.08.2026 weiter reduziert, ursprünglich 4.4px bei 1.2px Padding).

       display:flex + align-items:center zentriert die Beschriftung
       vollständig (horizontal UND vertikal) in der Box, auch wenn die Box
       (auf Mobile, siehe die height-Regel im Media-Query unten) höher ist als
       der Text tatsächlich braucht - das eigentliche Kürzen/Umbrechen
       (white-space/hyphens/ellipsis) steht bewusst NICHT hier, sondern auf
       dem inneren .label-text-Span (siehe unten): Ein Flex-Container gibt
       jedem Kind (auch einem rohen Textknoten als anonymes Flex-Item)
       automatisch eine Mindestbreite in Höhe seines Inhalts, sofern nichts
       anderes gesetzt ist ("min-width:auto") - ein echtes Element mit eigenem
       min-width:0 als Flex-KIND umgeht das, dieselbe Technik wie schon beim
       Ortsnamen-Text im Header (Abschnitt 13). */
    height: calc(1.35em + 3.2px);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Nutzeranfrage 27.08.2026 - der Hover-Effekt der Kachel (.tile, "transform:
       translateY(-4px); box-shadow: ...", siehe .building:hover .tile weiter
       oben) sollte auch auf dieser Textbox darunter greifen, nicht nur auf dem
       Bild. Übergang dafür hierher übernommen, sonst würde die Box beim
       Hover/Un-Hover hart springen statt sanft anzuheben. */
    transition: transform .15s ease, box-shadow .15s ease;
  }
  .building:hover .label { transform: translateY(-4px); box-shadow: 0 8px 18px rgba(0,0,0,.14); }
  /* Nutzerfolge auf die gedimmten Zonen-Fotos (Nutzeranfrage 21.08.2026, "ja,
     mach die auch dunkler") - siehe Kommentar an der obigen color-Regel. */
  :root[data-theme="dark"] .building .label { background: var(--card); color: var(--text); }
  /* Umbruch/Silbentrennung statt Einzeiligkeit (Nutzeranfrage 08.08.2026,
     "Suche das längste Wort, trenne es sauber anhand der Silben und
     vergrößere die Höhe der Textfelder ... vollständig", ursprünglich nur
     mobil - seit Nutzeranfrage 24.08.2026 "Textfelder ... müssen auch am
     Desktop zweizeilig sein können, wie bei der mobilen Version, damit auch
     lange Texte, z. B. auf Spanisch, Platz finden" auch am Desktop): Eine
     rein einzeilige Box wie ursprünglich am Desktop (Abschnitt 35,
     12_UI_UX.md) passt nur, solange labelBoxWidth in layoutMapBuildings()
     tatsächlich für JEDES vorkommende Label reicht - eine lange Übersetzung
     (z. B. Spanisch) sprengt das im Zweifel, Ellipsis-Kürzung war dafür keine
     akzeptable Dauerlösung. hyphens:auto trennt lange Wörter an echten
     Silbengrenzen (nutzt document.documentElement.lang, siehe setLanguage()).
     Die dafür nötige Boxhöhe wird nicht mehr geraten (siehe die frühere
     4.05em-3-Zeilen-Pauschale, 12_UI_UX.md Abschnitt 22), sondern in
     layoutMapBuildings() direkt aus der tatsächlich gerenderten Höhe des am
     Ende am meisten umbrechenden Labels gemessen und als label.style.height
     allen Boxen einheitlich zugewiesen (überschreibt die height-Regel oben,
     Inline-Style gewinnt immer) - "vollständig", da dadurch nie mehr
     geschnitten werden muss. Kürzere Label bleiben durch das display:flex an
     .building .label weiterhin horizontal UND vertikal in der jetzt höheren
     Box zentriert.

     KEIN overflow-wrap:break-word (Nutzeranfrage 09.08.2026, "Zeilenumbrüche
     nur, wenn es auch sauber getrennt ist, also nach Silben"): Das
     ursprünglich als Sicherheitsnetz gedachte overflow-wrap:break-word bricht
     ein Wort ohne bekannte Trennstelle an einer BELIEBIGEN Zeichengrenze um -
     genau die Art "hässlicher", nicht an Silbengrenzen orientierter Umbruch,
     die hier ausdrücklich vermieden werden soll. Ohne diese Eigenschaft
     bricht der Browser ausschließlich an von hyphens:auto erkannten echten
     Silbengrenzen um; ein einzelnes Wortsegment ohne solche Trennstelle, das
     breiter als die Box ist, überläuft nicht mehr sichtbar, sondern wird über
     text-overflow:ellipsis sauber mit "…" abgeschnitten (kein Zeilenumbruch
     mitten im Wort, kein seitliches Herauslaufen) - der seltenere, aber
     ehrlichere Kompromiss, passend zur "ehrlich degradieren statt eine
     falsche Garantie vorzutäuschen"-Linie dieses Algorithmus. Dieser Fall
     betrifft praktisch nur Umgebungen ohne installiertes Sprach-Wörterbuch
     für hyphens:auto (live an dieser Stelle nachgewiesen: ohne Wörterbuch
     bricht ein einzelnes langes Wort NIE um, unabhängig von der Boxbreite) -
     auf den allermeisten Endgeräten (Sprach-Wörterbücher sind Standard, z. B.
     Android Chrome, aktuelle Desktop-Browser) greift die echte Silbentrennung
     stattdessen wie vorgesehen. layoutMapBuildings()s Binärsuche
     (targetLabelWidth) prüft deshalb sowohl die resultierende Zeilenzahl
     (max. 2) ALS AUCH horizontalen Überlauf - eine reine Höhenprüfung wurde
     testweise verworfen, da sie bei einem nicht trennbaren Wort (offsetHeight
     bleibt dort bei jeder Breite fälschlich bei einer Zeile stehen) die Suche
     bis auf die kleinstmögliche Breite kollabieren ließ, selbst für kurze,
     unproblematische Wörter. Für ein untrennbares Wort konvergiert die Suche
     stattdessen zur oberen Suchgrenze (labelBoxWidth) - derselbe Deckel wie
     ohne diese Suche, siehe der ausführliche Kommentar an der Suchstelle
     selbst. */
  .building .label .label-text {
    min-width: 0;
    max-width: 100%;
    white-space: normal;
    overflow: hidden;
    text-overflow: ellipsis;
    /* hyphens:auto (bis 28.08.2026 hier gesetzt) ließ den Browser zusätzlich
       zu den handkuratierten Soft-Hyphens (MAP_LABEL_SOFT_HYPHENS,
       constants.js) sein eigenes Wörterbuch mitreden UND kollidierte dabei
       mit -webkit-line-clamp weiter unten: bei zusammengesetzten Wörtern mit
       nur einer bewussten Trennstelle (z. B. "Forschungs­labor") berechnete
       die Ellipsis-Logik die verfügbare Breite falsch und schnitt mitten im
       Wort ab ("Forschun…" statt "Forschungs-" / "labor"), obwohl der Text
       sauber in zwei Zeilen gepasst hätte (Nutzerreport 28.08.2026). Ohne
       hyphens (Standardwert "manual") zählen ausschließlich die echten
       Soft-Hyphen-Zeichen aus der Tabelle als Trennstellen. */
    /* Von 1.2 auf 1.35 angehoben (Nutzerreport 06.08.2026, "das g in Lager
       wird unten abgeschnitten"): Bei 1.2 reservierte die Zeilenbox bei
       manchen System-Schriftarten (z. B. Segoe UI, dessen Descent-Metrik
       vergleichsweise groß ausfällt) weniger vertikalen Raum, als der
       Buchstaben-Unterlänge (g/y/p/q/j) tatsächlich braucht - gilt
       unverändert weiter, auch mit Umbruch. Auf Mobile weiterhin auf 1.25
       reduziert, siehe Media-Query unten. */
    line-height: 1.35;
    text-align: center;
  }
  @media (max-width: 820px) {
    .building .label .label-text {
      /* Von 1.35 auf 1.25 reduziert (Nutzeranfrage 09.08.2026, "Zeilenabstand
         geringer machen, dadurch Abstand nach oben/unten größer") - der so
         freiwerdende Raum wandert direkt ins vertikale Padding von .label
         darunter (0.6px -> 2px, siehe dort), statt einfach ungenutzt zu
         verschwinden. Bewusst nur ein moderater Schritt, nicht zurück auf
         1.2: Bei 1.2 wurde die Unterlänge von g/y/p/q in manchen
         System-Schriftarten (z. B. Segoe UI) tatsächlich abgeschnitten
         (Nutzerreport 06.08.2026, siehe die Begründung bei line-height in der
         Basisregel oben) - live mit "Lager" nachgemessen, dass 1.25 diese
         Unterlänge weiterhin vollständig innerhalb der Zeilenbox hält. Nur
         mobil - am Desktop bleibt es bei den großzügigeren 1.35 der
         Basisregel, dort gibt es diesen Platzdruck nicht. */
      line-height: 1.25;
      /* Echtes Zwei-Zeilen-Clamping, NUR mobil (Nutzeranfrage 24.08.2026,
         "auf Mobile immer dieselbe Icon-/Textbox-Größe, unabhängig von der
         Sprache"): Bislang begrenzte nichts hier tatsächlich die Höhe auf
         zwei Zeilen - overflow:hidden schneidet nur, wenn das Element selbst
         schon eine fixe Höhe hat, .label-text wächst als Flex-Kind
         (align-items:center, keine :stretch) aber frei mit seinem Inhalt mit.
         Bisher unschädlich, weil layoutMapBuildings() auf Mobile die
         Boxbreite so lange nach der jeweils sichtbaren Sprache gesucht hat,
         bis JEDES Label ohnehin in zwei Zeilen passte (variable Spaltenzahl).
         Die Boxbreite ist seit derselben Anfrage aber eine feste,
         sprachunabhängige Geometrie (siehe MAP_MOBILE_FIXED_COLS in
         layoutMapBuildings()) - eine ungewöhnlich lange Übersetzung kann
         dort selbst mit Silbentrennung drei oder mehr Zeilen brauchen.
         -webkit-line-clamp (trotz Präfix Standard in allen aktuellen
         Browsern, nicht nur WebKit) deckelt hart auf zwei Zeilen und hängt
         automatisch "…" an - derselbe "ehrlich degradieren"-Kompromiss wie
         beim einzelnen unteilbaren Wort weiter oben, jetzt auch für den
         Mehrzeilen-Fall. Bewusst NICHT in der Basisregel (also nicht auch am
         Desktop): display:-webkit-box (von -webkit-line-clamp vorausgesetzt)
         ist ein eigener, vom normalen Blockfluss abweichender Layout-Modus,
         der die dortige, weiterhin inhaltsbasierte Binärsuche in
         layoutMapBuildings() live nachweisbar verfälscht hat (mehrere zuvor
         sauber zweizeilig passende Label wurden dadurch am Desktop
         fälschlich mit "…" abgeschnitten) - für den Desktop-Suchpfad ist die
         Boxbreite ohnehin schon so bestimmt, dass sie garantiert reicht,
         das Clamping wird dort nicht gebraucht. */
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }
    /* Horizontales Padding von 9px auf 4px reduziert (Nutzeranfrage
       09.08.2026, "wieder 4 Elemente nebeneinander"): Reines Chrome-Gewicht,
       das - anders als der eigentliche Textinhalt - keinen Lesbarkeitswert
       hat, aber auf jede Box gleichermaßen aufgeschlagen wird. Bei einem auf
       zwei Zeilen umbrechenden Label (siehe die Soft-Hyphen-Tabelle oben)
       macht das den Unterschied zwischen 2 und 3 (bzw. 3 und 4) gerade noch
       passenden Spalten aus, siehe die Herleitung in layoutMapBuildings()
       (targetLabelWidth/gutter/edgeGap). Nur mobil - am Desktop bleibt das
       großzügigere 9px-Padding aus der Basisregel oben unverändert, dort
       gibt es diesen Platzdruck nicht.

       Vertikal von 0.6px auf 2px angehoben (Nutzeranfrage 09.08.2026, siehe
       line-height oben) - oben UND unten bewusst identisch (CSS-Shorthand
       "2px 4px" setzt automatisch beide vertikalen Seiten gleich), der
       kleinere line-height gibt dafür den nötigen Spielraum frei, ohne dass
       die Zwei-Zeilen-Boxhöhe (twoLineMaxPx, layoutMapBuildings()) dadurch
       merklich wächst - beide Werte lesen dieselbe CSS-Regel live per
       getComputedStyle, keine zweite, potenziell abweichende Kopie in JS. */
    .building .label {
      padding: 2px 4px;
    }
    /* Kachel und Label näher zusammenrücken (Nutzeranfrage) - die Basisregel
       (7px, siehe .building oben) war für den Desktop-Abstand gedacht und
       wirkte auf den ohnehin schon engen Mobile-Kacheln unnötig luftig. */
    .building {
      gap: 3px;
    }
  }
  /* Shared base for round icon badges (lock/warning symbols on buildings, and future similar
     symbols). Emoji glyphs in many fonts aren't exactly centered in their own line box -
     padding:0 + line-height:1 compensate for that so the symbol appears optically centered.
     Always use this class for new icon-in-circle elements instead of rewriting the rules. */
  .icon-badge {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    line-height: 1;
    font-size: calc(11px + 1pt);
    color: white;
    border-radius: 50%;
    /* Nutzeranfrage 02.09.2026 ("Icon der Firma muss eine Ebene unterhalb der
       Warn-/Stufen-Badges sein") - ohne eigenen z-index landeten diese
       position:absolute-Badges (lock/warn/level/build/expert/shortage) in der
       Stacking-Reihenfolge auf "Stufe 0", .tile-icon (siehe dortige Regel)
       setzt dagegen explizit z-index:1 - ein positiver z-index gewinnt gegen
       "Stufe 0" IMMER, unabhängig von der DOM-Reihenfolge, das Firmen-Icon lag
       den Badges dadurch optisch im Weg. z-index:2 hier reicht, um über
       .tile-icon zu liegen, ohne .tile-icon selbst anzufassen (das braucht
       seinen z-index:1 weiterhin, um über dem Ambiente-Hintergrund zu
       bleiben, siehe dortiger Kommentar). */
    z-index: 2;
  }
  /* Inline-Variante von .icon-badge für Fließtext statt einer an einer Kachel
     fest positionierten Ecke (Nutzerreport 29.08.2026, Rohstoffmangel-Hinweis
     in der Firmen-Detailsicht) - display:inline-flex statt des geerbten
     flex, damit das Badge im Textfluss neben anderem Inhalt sitzt statt eine
     eigene Blockzeile zu erzwingen. Nutzt bewusst ein echtes "!"-Zeichen statt
     des ❗-Emojis: Emoji-Glyphen bringen ihre eigene, per CSS nicht
     überschreibbare Farbe mit (auf manchen Systemen ein dunkles Ausrufezeichen
     auf rundem Untergrund) - ein Textzeichen erbt dagegen ganz normal die
     weiße color: white von .icon-badge oben. */
  .icon-badge.inline-badge {
    display: inline-flex;
    /* Nutzerreport 29.08.2026, "Symbol auf derselben Höhe wie der Text" -
       Wert per Messung (Range.getBoundingClientRect() um den Textlauf vs.
       das Badge) ermittelt, keine Schätzung: ein inline-flex-Element hat
       ohne eigene baseline-fähige Kinder keine "normale" Text-Baseline, rein
       rechnerisch hergeleitete Werte lagen daher spürbar daneben. */
    vertical-align: 2px;
    background: var(--danger);
    font-weight: 700;
  }
  .building .lock-badge {
    position: absolute;
    top: -6px;
    right: -6px;
    background: var(--muted);
    display: none;
    /* 🔒 sits measured slightly right/below the symbol center in this font - correction by measurement (see note above) */
    transform: translate(-1px, -1.3px);
  }
  .building .lock-badge.show { display: flex; }
  /* Vorschau auf das nächste Zeitalter (Nutzeranfrage 03.08.2026): sichtbar,
     aber erkennbar noch nicht benutzbar.

     Abgeblendet wird bewusst der INHALT der Kachel (Zeichnung, Icon, Name),
     nicht die Kachel selbst: opacity auf .tile würde das Schloss-Badge als
     Kind zwangsläufig mit verblassen lassen - ein Kind kann die Deckkraft
     seines Vorfahren nicht wieder anheben. Genau das Schloss soll aber klar
     lesbar bleiben, es ist ja der Hinweis auf den Zustand.

     cursor:default statt not-allowed: hier ist nichts "verboten", es gibt an
     dieser Kachel schlicht noch nichts zu tun. */
  .building.locked .tile-icon,
  .building.locked .tile-clip,
  .building.locked .label { opacity: 0.4; }
  .building.locked .tile { border-color: var(--border); box-shadow: 0 2px 6px rgba(0,0,0,.06); }
  .building.locked { cursor: default; }
  /* Rohstoff-Spezialisierungs-Cap einer Zone (Nutzerreport 29.08.2026) -
     bewusst deutlich schwächer abgeblendet als .locked oben: Anders als eine
     Zeitalter-Vorschau ist dieser Zustand rein käuflich/auflösbar (zusätzlicher
     Slot gegen Diamanten oder eine andere Firma abreißen, siehe
     05_UNTERNEHMEN.md Abschnitt 6h), die Kachel bleibt außerdem anklickbar
     (kein cursor:default hier, .building erbt weiterhin cursor:pointer) - das
     soll auch optisch "noch erreichbar", nicht "gesperrt" wirken. */
  .building.raw-locked .tile-icon,
  .building.raw-locked .tile-clip,
  .building.raw-locked .label { opacity: 0.8; }
  .building .warn-badge {
    position: absolute;
    top: -6px;
    left: -6px;
    background: var(--danger);
    display: none;
    /* ⚠️ sits measured slightly right/below the symbol center in this font - correction by measurement (see note above) */
    transform: translate(-1px, -2px);
  }
  .building .warn-badge.show { display: flex; }
  .building .level-badge {
    position: absolute;
    bottom: -6px;
    right: -6px;
    /* Anders als lock-/warn-badge (Emoji, CSS-Farbe wirkungslos) zeigt dieser
       Badge eine echte Ziffer - var(--accent) gab der weißen Schrift (siehe
       .icon-badge) nur 3,00:1 im Dark Mode. */
    background: var(--btn-bg);
    display: none;
    font-weight: 700;
    /* Plain digits (unlike the lock/warning emoji above) sit centered by the
       shared .icon-badge flex rules already - no font-specific correction transform needed. */
  }
  .building .level-badge.show { display: flex; }
  /* Bauzeit-System (Nutzeranfrage) - zeigt, dass für diesen Firmentyp gerade
     eine Bau-/Ausbaustufe läuft. Oben links, derselbe freie Platz, den das
     nie tatsächlich verwendete .warn-badge (siehe oben) schon immer belegte -
     bewusst eine eigene, klar benannte Klasse statt der toten Regel
     wiederzuverwenden. Grün wie level-badge (bereits kontrastgeprüft), da
     "wird ausgebaut" ein neutraler/positiver Zustand ist, keine Warnung. */
  .building .build-badge {
    position: absolute;
    top: -6px;
    left: -6px;
    background: var(--btn-bg);
    display: none;
  }
  .building .build-badge.show { display: flex; }
  /* Aktiver Experten-Vertrag (Nutzeranfrage 21.08.2026, "zusätzlich auch auf
     der Karte anzeigen") - unten links, die letzte freie der vier Ecken
     (oben rechts: lock-badge, unten rechts: level-badge, oben links:
     build-badge). Akzentgrün wie level-/build-badge - ein laufender,
     bezahlter Vertrag ist ein positiver Zustand, keine Warnung. */
  .building .expert-badge {
    position: absolute;
    bottom: -6px;
    left: -6px;
    background: var(--btn-bg);
    display: none;
  }
  .building .expert-badge.show { display: flex; }
  /* Laufende Forschung auf der Forschungslabor-Kachel (Nutzeranfrage
     31.08.2026, "ebenfalls ein Symbol auf der Karte am Forschungslabor,
     falls gerade eine Forschung läuft") - oben links, dieselbe Position wie
     .build-badge: die Kachel des Labors trägt nie gleichzeitig ein
     Baustellen-Badge (das Labor liegt außerhalb des zone-firm-Loops, siehe
     map.js), daher keine Überlappung möglich. Grün wie build-/expert-badge -
     ein laufendes Forschungsprojekt ist ein positiver, kein warnender Zustand. */
  .building .research-badge {
    position: absolute;
    top: -6px;
    left: -6px;
    background: var(--btn-bg);
    display: none;
  }
  .building .research-badge.show { display: flex; }
  /* Aktiver Transportauftrag aus/in diese Zone (Nutzeranfrage 02.09.2026,
     "Icon an der Transportamt-Kachel, falls aktuell ein Transport aus oder
     in diese Zone läuft") - oben links, dieselbe Position wie .build-badge/
     .research-badge: #btn-transport ist wie das Forschungslabor ein festes,
     zonenunabhängig positioniertes Gebäude außerhalb des zone-firm-Loops und
     trägt nie gleichzeitig ein Baustellen-Badge, keine Überlappung möglich.
     Grün wie build-/expert-/research-badge - ein laufender Transport ist ein
     neutraler/positiver Zustand, keine Warnung. */
  .building .transport-badge {
    position: absolute;
    top: -6px;
    left: -6px;
    background: var(--btn-bg);
    display: none;
  }
  .building .transport-badge.show { display: flex; }
  /* Rohstoffmangel (Nutzerreport 29.08.2026) - fünfte Badge-Position, da alle
     vier Ecken bereits belegt sind (oben rechts/links = Schloss/Baustelle,
     unten rechts/links = Stufe/Experte). Unten mittig, spiegelbildlich zur
     Saisonalen Dekoration (oben mittig, siehe .seasonal-badge unten) -
     dieselbe "außerhalb der vier Ecken"-Position, nur am anderen Rand, damit
     sich beide nie überlappen. Rot wie das nie tatsächlich verwendete
     .warn-badge oben (var(--danger)) - eine echte Warnung, kein positiver
     Zustand wie Baustelle/Experte. */
  .building .shortage-badge {
    position: absolute;
    bottom: -8px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--danger);
    display: none;
    /* Echtes "!"-Zeichen statt des ❗-Emojis (Nutzerreport 29.08.2026, "das
       Ausrufezeichen im Kreis... immer noch nicht weiß") - dieselbe
       Eigenfarben-Problematik wie beim .inline-badge-Pendant in der
       Firmen-Detailsicht, hier zusätzlich mit font-weight, da ein reines
       Textzeichen ohne die kräftige Emoji-Zeichnung sonst zu dünn wirkt. */
    font-weight: 700;
  }
  .building .shortage-badge.show { display: flex; }
  /* Lager "fast voll" (12_UI_UX.md, Nutzeranfrage 03.09.2026) - dieselbe
     Position/Form wie .shortage-badge oben (unten mittig, echtes "!"-Zeichen),
     hier aber am festen #btn-storage-Gebäude statt an einer Firmen-Kachel -
     #btn-storage trägt nie ein .shortage-badge (das ist ein reines
     Firmen-Konzept), keine Überlappung möglich. Eigene Warnfarbe (--warning,
     orange) statt --danger - eine 90%-Frühwarnung ist noch keine tatsächlich
     pausierte Produktion. */
  .building .storage-warn-badge {
    position: absolute;
    bottom: -8px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--warning);
    display: none;
    font-weight: 700;
  }
  .building .storage-warn-badge.show { display: flex; }
  /* Rot statt Orange, sobald mindestens ein Lager tatsächlich bei 100% steht
     statt nur "fast voll" (Nutzeranfrage 03.09.2026, "dieselbe Farbe wie die
     wichtigste Füllstandsanzeige an den Lagern") - dieselben zwei Farben wie
     .progress-bar.full/.near-full auf der Lagerübersicht selbst (siehe dort). */
  .building .storage-warn-badge.full { background: var(--danger); }
  /* Saisonale Dekoration (10_MONETARISIERUNG.md Abschnitt 2a/2e, Punkt 5) -
     globaler Admin-Schalter, kein per-Spieler-Kosmetik-Feld, deshalb kein
     Swatch-Picker wie bei Rahmenfarbe/Ambiente. Sitzt wie ein Hut oben
     auf der Kachel, außerhalb von .tile-clip, damit sie nicht mit beschnitten
     wird. */
  .building .seasonal-badge {
    position: absolute;
    top: -12px;
    left: 50%;
    transform: translateX(-50%);
    pointer-events: none;
    display: none;
    z-index: 3;
  }
  .building .seasonal-badge.show { display: block; }
  .warn-badge-inline {
    font-size: calc(12px + 1pt);
    margin-left: 4px;
  }
  .map-hint {
    text-align: center;
    color: var(--muted);
    font-size: calc(13px + 1pt);
    margin-top: 10px;
  }

  /* ---------- Marketplace ---------- */
  /* "Alles sofort verkaufen" stand bis 20.08.2026 zentriert auf Mobile (siehe
     Git-Historie dieser Regel) - auf Nutzeranfrage 05.09.2026 stattdessen
     rechtsbündig wie auf Desktop: #sellAllBtn steckt seither in einem
     eigenen Flex-Wrapper (index.html, display:flex;justify-content:flex-end)
     statt direkt in section.card zu liegen, die vorige, hier per Mobile-
     Media-Query erzwungene Zentrierung (display:block + margin:auto) wäre
     dem in den Weg gelaufen und ist deshalb ersatzlos entfernt - die Breite
     des Buttons hängt davon nicht ab, die kommt unabhängig von der
     allgemeinen button-Mobile-Regel weiter oben (116px/100%). */
  /* gap: 8px statt 16px (Nutzeranfrage 26.08.2026, "Abstand zwischen Name
     und Graph etc. um die Hälfte reduzieren") - betrifft denselben Abstand
     zwischen .market-left/.market-mid/.market-right. .order-toggle-row
     weiter unten übernimmt zur Button-Zentrierung unter dem Graphen
     absichtlich denselben Wert - bei einer künftigen erneuten Anpassung
     dieses Gaps IMMER beide Stellen zusammen ändern, sonst rutscht der
     "Ordereingabe öffnen/schließen"-Button wieder aus der Spalte. */
  .market-card {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 0;
    border-bottom: 1px solid var(--border);
    flex-wrap: wrap;
  }
  .market-card:last-child { border-bottom: none; }
  /* Nutzeranfrage 26.08.2026, "Trennlinie zwischen Unternehmen fetter machen"
     - nur die Börsen-Marktübersicht ([data-stock-card]), der Warenmarkt
     teilt sich zwar dieselbe .market-card-Basis, war aber nicht gemeint. */
  .market-card[data-stock-card] { border-bottom-width: 2px; }
  /* align-self:flex-start (26.08.2026, direkt im Anschluss an den
     .market-mid-Fix oben): Bei einer Börsen-Karte ist .market-right
     (Mengenfeld + Kaufen/Verkaufen) oft die höchste der drei Spalten - ohne
     eigenes align-self hätte .market-card's align-items:center weiterhin
     .market-left UND .market-right mittig an dessen Höhe ausgerichtet,
     während nur .market-mid oben sitzt, was Name/Firmendetails wieder ein
     paar Pixel unterhalb des Graphen hätte beginnen lassen. Alle drei
     Spalten jetzt einheitlich an der Zeilenoberkante. */
  .market-left { display: flex; align-items: center; gap: 12px; min-width: 150px; align-self: flex-start; }
  /* Nutzeranfrage 26.08.2026, "sicherstellen, dass die alle bündig
     untereinander sind, egal wie lang der Name etc. ist": .market-left war
     bislang nur min-width:150px, wuchs an Firmen-/Warennamen sonst frei mit
     (kein max-width) - eine Karte mit langem Namen schob dadurch .market-mid
     samt dem jetzt fest 250px breiten Graphen (siehe oben) ein Stück nach
     rechts, während eine Karte mit kurzem Namen bei ihrer natürlichen
     Inhaltsbreite blieb: Graphen unterschiedlicher Zeilen standen dadurch
     nicht mehr untereinander bündig. Nur für Karten MIT .market-mid (also
     Waren-/Aktienmarkt, nicht der Kreditmarkt/Festgeld in bank.js, die
     .market-left ganz ohne .market-mid nutzen und weiterhin frei wachsen
     sollen) eine feste Breite. Längere Namen brechen einfach in eine zweite
     Zeile um (kein eigenes overflow/ellipsis nötig - .item-name/.price-tag/
     .item-sub haben ohnehin kein white-space:nowrap), statt die Spalte zu
     verbreitern.
     Nachtrag (26.08.2026, Nutzeranfrage "linken Bereich verbreitern, damit
     Graph und Preise weiter nach rechts rücken"): ursprünglich 233px (an der
     bisherigen natürlichen Breite der Börsenkarten orientiert), auf
     380px angehoben - rein optischer Wert, kein inhaltlicher Grund
     dahinter.
     Zweiter Nachtrag (26.08.2026, Nutzeranfrage "bei zwei Dritteln
     Fensterbreite sieht es teilweise nicht gut aus"): fixer 380px-Wert durch
     var(--market-left-col-width) ersetzt (clamp, siehe :root) - schrumpft
     jetzt oberhalb von 820px kontinuierlich mit statt starr bei 380px zu
     bleiben, siehe ausführlichen Kommentar bei der Variable selbst. Dieselbe
     Variable auch bei .order-toggle-spacer weiter unten (statt bislang
     eigenem, unabhängigem 380px-Wert dort) - verhindert, dass beide Stellen
     bei einer künftigen Breitenanpassung wieder auseinanderlaufen. */
  .market-card:has(.market-mid) .market-left { width: var(--market-left-col-width); min-width: 0; }
  .market-card:has(.market-mid) .market-left > div { min-width: 0; }
  /* Nutzeranfrage 26.08.2026, "Graph auf die volle Breite des Textes
     unterhalb erweitern" - der Graph war fix auf die ursprüngliche
     140px-SVG-Größe gedeckelt, obwohl .market-mid (sein Elternelement,
     dasselbe wie beim Kurs-/Bestandstext darunter) durch flex:1 längst
     breiter ist. width:100% auf den Wrapper allein reicht nicht - das SVG
     selbst trägt noch die eigenen width="140"-HTML-Attribute aus
     sparklineSvg() (ui-helpers.js), die CSS-width ohne eigene Regel auf dem
     <svg> nicht überschreibt. Höhe bewusst fix (34px, wie im viewBox), ein
     Sparkline verbreitert sich beim Strecken nur horizontal statt das
     Seitenverhältnis beizubehalten - genau das seitherige Verhalten bei der
     festen 140px-Breite, nur jetzt auf jeder Breite.
     Nachtrag (26.08.2026, Nutzeranfrage): "Graph sollte weiterhin direkt
     über dem Verkaufs-/Kaufkurs sein, nicht irgendwo daneben angezeigt
     werden, das gehört ja komplett zusammen" - der alte min-width:140px auf
     .market-mid reichte bei Börsen-Karten (.market-left mit Ticker/Name UND
     .market-right mit Kaufen/Verkaufen-Buttons + Mengenfeld nehmen dort
     zusammen deutlich mehr Platz weg als beim Warenmarkt) gerade bei
     "mittleren" Desktop-Breiten (grob 820-1000px) aus, um trotzdem noch in
     dieselbe Zeile wie .market-left/.market-right zu passen - .market-mid
     wurde dadurch bis auf 140px zusammengequetscht statt umzubrechen, der
     Graph blieb zwar technisch oberhalb des Kurstexts, aber beide wirkten
     als schmaler, an den Kaufen/Verkaufen-Buttons klebender Streifen statt
     als eigener, breiter Block. min-width jetzt 220px (ein Graph braucht
     diese Mindestbreite, um überhaupt als Kurve erkennbar zu sein) - passt
     .market-mid dann nicht mehr neben .market-left+.market-right in dieselbe
     Zeile, bricht die vorhandene flex-wrap:wrap-Regel auf .market-card (statt
     eines CSS-Sonderfalls) es korrekt in eine eigene, volle Kartenbreite
     nutzende Zeile um - dieselbe Mechanik, die auf Mobile ohnehin schon
     greift, jetzt nur bei einer entsprechend breiteren Schwelle.
     Zweiter Nachtrag (26.08.2026, Nutzerreport "Graph wird bei mir immer
     noch nebenan angezeigt", live nachvollzogen): Der Breiten-Fix allein
     reichte nicht - .market-card zentriert seine drei Spalten vertikal
     (align-items:center), bei einer Börsen-Karte ist .market-left aber DREI
     Zeilen hoch (Name, "Am Markt verfügbar", Gewinnbeteiligung) und
     .market-mid nur ZWEI (Graph, Kurs). Zentriert lag .market-mid dadurch
     mittig in der höheren .market-left-Box - der (jetzt große statt kleine)
     Graph begann optisch auf Höhe des Firmennamens, der Kurstext eine
     Zeile darunter auf Höhe von "Am Markt verfügbar"/Gewinnbeteiligung,
     wodurch Graph und Kurs trotz korrekter DOM-Reihenfolge und identischer
     Breite NICHT wie ein zusammengehöriger Block wirkten, sondern der Graph
     wie neben den Firmennamen gesetzt. align-self:flex-start auf .market-mid
     (dieselbe Technik wie schon bei [data-demolish] weiter oben, "Ausbauen
     und Abreißen sollen auf gleicher Höhe stehen") hängt Graph+Kurs stattdessen
     an die Oberkante der Zeile - der Graph beginnt jetzt immer exakt auf
     Höhe des Firmennamens, der Kurs folgt lückenlos direkt darunter. */
  /* Dritter Nachtrag (26.08.2026, Nutzeranfrage): "Graph wird über die
     gesamte Bildschirmbreite bis zu den Buttons gezogen, das soll nicht so
     sein - fester Bereich vom V von 'Verkauf' bis zum C des Kaufpreis-MC."
     .market-mid selbst bleibt flex:1 (füllt weiterhin die Zeile bis
     .market-right, das ändert an der Grundausrichtung/-position nichts) -
     aber Graph und Kurstext sollen NICHT mehr die volle .market-mid-Breite
     ausfüllen. Erster Versuch: display:grid mit einer einzigen max-content-
     Spalte, bemessen am breitesten Kind (.price-tag, echter Text mit eigener
     Inhaltsbreite) - .sparkline-wrap übernahm darüber exakt dieselbe Breite.
     Vierter Nachtrag (26.08.2026, direkte Folgeanfrage): "fixe Breite
     unabhängig der konkreten Preise, egal ob 300 MC oder 300.000.000 MC
     immer dieselbe Breite" - genau das hatte der max-content-Ansatz NICHT
     geleistet, er passte die Graph-Breite ja gerade an die tatsächliche
     Ziffernanzahl des Kurstexts an (kurzer Kurs = schmaler Graph, langer
     Kurs = breiterer Graph). display:grid/max-content ist damit hinfällig -
     .sparkline-wrap bekommt stattdessen eine feste Pixelbreite (250px,
     ausdrücklich ein Platzhalter, an der zuletzt sichtbar für richtig
     befundenen Breite orientiert), unabhängig vom Kurstext darunter. Der
     Kurstext selbst bleibt unverändert seine eigene, natürliche Breite - nur
     der Graph braucht den festen Wert, damit alle Börsen-Karten gleich
     aussehen statt je nach Kurshöhe unterschiedlich breite Graphen zu
     zeigen. */
  .market-mid { flex: 1; min-width: 220px; align-self: flex-start; }
  .sparkline-wrap { width: var(--market-price-col-width); }
  .sparkline-wrap svg { display: block; width: 100%; height: 34px; }
  /* Nutzeranfrage 26.08.2026, "Verkaufspreis linksbündig unter dem Graph,
     Kaufpreis rechtsbündig, das Dreieck dazwischen zentriert": Die Kurszeile
     unter dem Graph (.price-tag.price-split, drei <span>-Kinder statt reinem
     Fließtext mit "·"-Trenner) ist jetzt ein 3-Spalten-Grid (1fr auto 1fr) -
     die beiden äußeren 1fr-Spalten wachsen bei unterschiedlich langen
     Verkaufs-/Kaufpreis-Texten automatisch gegenläufig mit, wodurch die
     mittlere auto-Spalte (das Dreieck) immer exakt in der Mitte der ganzen
     Zeile bleibt, unabhängig davon, wie lang die beiden Preise sind -
     flex mit justify-content:space-between hätte das nicht garantiert (dort
     verschiebt sich die "Mitte" mit ungleich breiten Nachbarn). Farbe kommt
     weiterhin einheitlich vom vererbten .price-up/.price-down auf dem
     äußeren .price-tag - die Kursrichtung bezieht sich auf den Kurs als
     Ganzes, nicht getrennt auf Verkaufs-/Kaufpreis. Dieselbe feste 250px-
     Breite wie .sparkline-wrap direkt darüber (sonst wäre .price-tag als
     einfacher Block ungebremst auf die volle .market-mid-Breite gewachsen,
     "rechtsbündig" hätte dann am rechten Rand von .market-mid geendet statt
     am rechten Rand des Graphen). */
  .price-tag.price-split { display: grid; grid-template-columns: 1fr auto 1fr; align-items: baseline; column-gap: 6px; width: var(--market-price-col-width); }
  /* Nutzeranfrage 26.08.2026, "Button 'Ordereingabe öffnen'/'schließen'
     zentral unter den Graphen auf gleicher Höhe der Aktions-Order-Angaben
     schieben": Der Button folgte bislang einfach unter dem "Aktionärs-
     Orders"-Text (eigene Zeile, marginierte Block-Reihenfolge). Statt die
     Zielposition per fest verdrahteten Pixeln nachzubilden (die bei der
     nächsten Breitenänderung von .market-left/.sparkline-wrap sofort wieder
     auseinanderliefen), übernehmen .order-toggle-spacer/.order-toggle-btn-slot
     dieselben Breiten wie die eigentlichen Spalten darüber (380px wie
     .market-left, 250px wie .sparkline-wrap/.price-tag.price-split) - der
     Button-Slot landet dadurch automatisch exakt unter dem Graphen und
     bleibt es auch, falls diese Breiten künftig nochmal angepasst werden.
     .order-toggle-spacer trägt trotz des Namens den "Aktionärs-Orders"-Text
     selbst (keine leere Box) - er übernimmt nur dieselbe Spaltenbreite wie
     .market-left darüber, damit der Button-Slot direkt danach exakt an der
     Stelle beginnt, an der .market-mid/der Graph in der Zeile darüber
     beginnt.
     Auf Mobile (wo .market-left/.market-mid ohnehin nicht mehr nebeneinander
     stehen, siehe .market-card{flex-wrap:wrap}) ergibt eine "Spalte unter dem
     Graphen" keinen Sinn mehr - dort fällt die Zeile zurück auf simples
     Untereinander wie zuvor, der Spacer verliert seine feste Breite. */
  /* gap: 8px (statt vormals 16px) - muss exakt .market-card's gap
     entsprechen, siehe Kommentar dort (Nutzeranfrage 26.08.2026, "Abstand
     zwischen Name und Graph etc. um die Hälfte reduzieren"), sonst landet
     der Button nicht mehr exakt unter dem Graphen. */
  .order-toggle-row { display: flex; align-items: center; gap: 8px; }
  /* var(--market-left-col-width) statt eigenem, unabhängigem 380px-Wert
     (Nutzeranfrage 26.08.2026, "bei zwei Dritteln Fensterbreite sieht es
     teilweise nicht gut aus") - dieselbe Variable wie bei .market-left
     darüber, damit beide zusammen schrumpfen statt auseinanderzulaufen. */
  .order-toggle-spacer { width: var(--market-left-col-width); flex-shrink: 0; }
  .order-toggle-btn-slot { width: var(--market-price-col-width); flex-shrink: 0; display: flex; justify-content: center; }
  /* Nutzeranfrage 26.08.2026, "sicherstellen, dass auf mobil nichts über
     den sichtbaren Bereich hinausgeht": .market-left (380px fest) und
     .sparkline-wrap/.price-tag.price-split (var(--market-price-col-width) =
     400px fest) sind beide für Desktop-Spaltenausrichtung gedacht - auf
     schmalen Bildschirmen (wo .market-card ohnehin per flex-wrap:wrap
     umbricht und jede Spalte ihre eigene volle Zeile bekommt) würde ein
     380px/400px-Fixwert einen Großteil mobiler Displays (z.B. 375px
     Standardbreite) und deren Innenraum abzüglich Padding sicher
     überschreiten. width:100% (statt der festen Pixelwerte) lässt jede
     Spalte auf Mobile wieder die volle verfügbare Kartenbreite einnehmen,
     genau wie es vor den Desktop-Fixbreiten-Änderungen bereits der Fall
     war.
     Schwelle 1040px statt der sonst im Stylesheet üblichen 820px (Nutzer-
     anfrage 26.08.2026, "bei zwei Dritteln Fensterbreite sieht es teilweise
     nicht gut aus" - live nachvollzogen): .market-left/.sparkline-wrap/
     .price-tag.price-split sind inzwischen fluide (clamp(), siehe :root),
     schrumpfen oberhalb von 820px bereits mit - trotzdem bricht
     .market-right (Kaufen/Verkaufen-Buttons, eigene Breite je nach Karte
     406px Börse / 496px Warenmarkt, siehe dessen Kommentar weiter unten)
     zwischen ca. 820px und ca. 1040px trotzdem noch aus der gemeinsamen
     Zeile aus, weil .market-left selbst am unteren clamp()-Rand (200px) und
     .market-mid an seiner Mindestbreite (220px) angekommen sind, ohne dass
     für .market-right noch Platz bleibt - dort landete es dann bislang
     verwaist mit einer sichtbaren Lücke rechtsbündig statt sauber
     umzubrechen. 1040px = Padding (84px) + .market-left-Untergrenze (200px)
     + zwei 8px-Gaps + .market-mid-Mindestbreite (220px) + breiterer der
     beiden .market-right-Fälle (496px, Warenmarkt) + Puffer - deckt beide
     Kartentypen ab. Bewusst NUR für diesen Block (nicht der globale 820px-
     Wert, der an anderer Stelle im Stylesheet für unabhängige Header-/
     Button-Layouts gilt). */
  @media (max-width: 1040px) {
    .order-toggle-row { flex-direction: column; align-items: stretch; gap: 6px; }
    .order-toggle-spacer { width: auto; }
    .order-toggle-btn-slot { width: auto; }
    .market-card:has(.market-mid) .market-left { width: 100%; }
    .sparkline-wrap { width: 100%; }
    .price-tag.price-split { width: 100%; }
  }
  .price-tag.price-split .price-tag-dir { text-align: center; }
  .price-tag.price-split .price-tag-buy { text-align: right; }
  /* "Mein Unternehmen"-Karte in der Börse (renderIpoCard(), stocks.js): kein
     Kauf/Verkauf-Split (eigene Aktien sind nicht handelbar), nur eine
     einzelne Kurs+Richtungsdreieck-Zeile - die erbte bisher nicht die feste
     .price-tag.price-split-Breite, wuchs dadurch auf die volle (breitere)
     .market-mid-Breite und stand linksbündig statt wie der Graph darüber
     eingerückt (Nutzeranfrage 26.08.2026, "Aktienkurs und Richtungs-Dreieck
     unterhalb des Graphs zentrieren"). Dieselbe feste Breite wie
     .sparkline-wrap direkt darüber plus zentrierter Text. */
  .price-tag.own-stock-price { width: var(--market-price-col-width); text-align: center; }
  @media (max-width: 1040px) {
    .price-tag.own-stock-price { width: 100%; }
  }
  /* stretch (statt center) + space-between (user request 28.07.2026): die
     Stepper-Zeile hat ein anderes Gesamtbreite als die Kaufen/Verkaufen-Zeile
     darunter -- mit reinem center-Alignment lag jede Zeile für sich mittig,
     wodurch die Ränder der beiden Zeilen nicht übereinander lagen. stretch
     lässt beide Zeilen die Breite der breiteren Zeile annehmen, space-between
     schiebt die jeweils äußersten Buttons exakt an deren linken/rechten Rand.
     NUR noch der Desktop-Wert (>1040px, siehe Nachtrag 26.08.2026 bei der
     Media-Query direkt darunter) - auf schmaleren Screens überschreibt die
     Regel direkt darunter das bewusst wieder mit center (Nutzeranfrage 20.08.2026,
     "alle Buttons im Markt horizontal zentrieren"), das frühere
     Rand-Ausrichtungs-Argument war eine Desktop-Beobachtung und bleibt dort
     unverändert gültig. */
  .market-right { display: flex; flex-direction: column; gap: 6px; align-items: stretch; margin-left: auto; }
  .market-right .sell-controls { justify-content: space-between; }
  /* Zurückgenommen (Nutzerreport 01.09.2026, mit Vergleichs-Screenshot): Die
     bewusste Verbreiterung auf 100% (Nutzeranfrage 21.08.2026, sollte das
     Eingabefeld so breit wie Kaufen/Verkaufen darunter zusammen machen) hatte
     einen Nebeneffekt, der bei der ursprünglichen Anfrage übersehen wurde -
     ein width:100%-Element kann sich mit den umgebenden 6 Stepper-Buttons
     keine Zeile mehr teilen (flex-wrap:wrap), wodurch die Börsen-Marktkarten
     entgegen dem Rest des Spiels (Marktplatz, Kreditmarkt, siehe deren
     Kommentare bei .qty-input.market-qty oben) in drei statt einer Zeile
     umbrachen. Ohne eigene Regel gilt für Börsen-Karten jetzt wieder dieselbe
     Basisbreite (200px, .qty-input.market-qty) wie überall sonst. */
  /* Eingabefeld + Kaufen/Verkaufen auf Mobile über die volle Kartenbreite
     statt gemeinsam in der rechten Hälfte zusammengedrängt (Nutzeranfrage
     21.08.2026) - .market-right hatte hier bislang kein flex-basis, wodurch
     es beim Umbruch (Karte ist flex-wrap:wrap) nur seine eigene Inhaltsbreite
     einnahm und wegen margin-left:auto rechtsbündig blieb, statt die ganze
     Zeile unter Sparkline/Kurs zu füllen.
     Nachtrag (26.08.2026, Nutzeranfrage "bei zwei Dritteln Fensterbreite
     sieht es teilweise nicht gut aus"): Ursprünglich nur für Börsen-Karten
     ([data-stock-card]), weil zum Zeitpunkt der ursprünglichen Anfrage nur
     dort getestet wurde - live nachvollzogen tritt dieselbe verwaiste Lücke
     unverändert auch im Warenmarkt auf (dieselbe .market-right/margin-left:
     auto-Mechanik, keine Börsen-Besonderheit). Selektor daher verallgemeinert
     auf :has(.market-mid) (Waren- UND Aktienmarkt, weiterhin nicht der
     Kreditmarkt ohne .market-mid), Schwelle auf 1040px angehoben - siehe
     ausführliche Begründung beim .order-toggle-row-Media-Query weiter oben,
     dieselbe Schwelle, damit .market-right zeitgleich mit .market-left/
     .market-mid in den Stapel-Modus wechselt statt einzeln dazwischen
     auszubrechen. */
  @media (max-width: 1040px) {
    .market-card:has(.market-mid) .market-right { flex-basis: 100%; margin-left: 0; }
  }
  /* Zentriert beide .sell-controls-Zeilen (Stepper + Kaufen/Verkaufen) auf
     Mobile (Nutzeranfrage 20.08.2026) - überschreibt das Desktop-
     space-between von oben. "Zusammen in einer Zeile" für die Stepper-
     Buttons bleibt dabei unangetastet garantiert: das ist Sache der
     order/flex-basis-Regel auf .market-qty weiter unten (unverändert,
     bricht das Eingabefeld bei Bedarf sauber auf eine eigene Zeile aus,
     statt einzelne Buttons mittendrin abzutrennen) - justify-content
     beeinflusst nur die Ausrichtung INNERHALB einer bereits feststehenden
     Zeile, nie die Umbruch-Entscheidung selbst.
     Schwelle 1040px (Nachtrag 26.08.2026, wie bei den beiden .market-right-
     Media-Queries weiter oben) - muss mit der Schwelle synchron bleiben, bei
     der .market-right selbst in den Stapel-Modus wechselt (flex-basis:100%),
     sonst würde hier zwischen 820-1040px zwar schon die volle Zeilenbreite
     gelten, die Buttons darin aber noch am Desktop-space-between statt
     zentriert hängen. */
  @media (max-width: 1040px) {
    .market-right .sell-controls { justify-content: center; }
  }
  /* Der 7-Buttons-Stepper (max./-5/-1/Menge/+1/+5/max.) passt auf schmalen
     Bildschirmen nicht mehr in eine Zeile und bricht um - ohne Steuerung
     würde flex-wrap dabei irgendein Element (auch mitten in den Buttons)
     an die Umbruchstelle setzen, statt zuverlässig zwischen den 6 Buttons
     und dem Eingabefeld zu trennen. Bewusst weiterhin bei 480px statt der
     üblichen 820px-Schwelle: bei 820px passt die Zeile bereits wieder in
     eine Reihe, das Umbruch-Problem betrifft nur echte Mobile-Breiten. */
  @media (max-width: 480px) {
    /* Alle 6 Stepper-Buttons (max./-5/-1/+1/+5/max.) in eine gemeinsame Zeile,
       das Mengen-Eingabefeld darunter (Nutzeranfrage 09.08.2026) - der
       ungesteuerte flex-wrap brach die Zeile bisher rein nach verfügbarem
       Platz um, wodurch je nach Bildschirmbreite auch Buttons ins Feld-Zeile
       hinein- oder aus ihr herausrutschen konnten. order:99 verschiebt das
       Eingabefeld visuell ans Ende (unabhängig von seiner DOM-Position
       zwischen -1 und +1 - data-step/data-qty-Selektoren bleiben davon
       unberührt, nur die Anzeige-Reihenfolge ändert sich), flex-basis:100%
       erzwingt einen Zeilenumbruch direkt davor, da ein 100%-breites Element
       sich mit nichts mehr eine Zeile teilen kann - dasselbe bereits
       etablierte Muster wie bei den Header-Zeilenumbrüchen (.header-break,
       Abschnitt 4v in 12_UI_UX.md), nur ohne einen eigenen Platzhalter, da
       hier bereits ein echtes Element (das Eingabefeld selbst) den Umbruch
       auslösen kann. */
    .market-right .sell-controls .market-qty { order: 99; flex-basis: 100%; }
    /* Kreditmarkt-Zeile (Nutzeranfrage 09.08.2026) - teilt sich die generischen
       .market-right/.sell-controls/.market-qty-Klassen mit dem Marktplatz-
       Mengen-Stepper oben, wurde von dessen order:99/flex-basis:100%-Regel
       aber unbeabsichtigt mitgetroffen: Statt neben "Kredit aufnehmen" zu
       stehen, sprang das Eingabefeld auf eine eigene volle Zeile darunter.
       .loan-controls (nur auf dieser einen Zeile gesetzt, siehe
       renderBankMarketList()) hat als zusätzliche Klasse höhere Spezifität
       und gewinnt daher unabhängig von der Regel-Reihenfolge - order:0 hält
       die ursprüngliche DOM-Reihenfolge (Eingabefeld vor Button), width:auto
       ersetzt die feste 116px-Breite aus der Basisregel (.qty-input.market-
       qty), flex:1 1 auto lässt das Feld exakt den Platz füllen, der neben
       dem 200px breiten Button in der Zeile übrig bleibt (Nutzervorgabe:
       "auf die verfügbare Breite in dieser Zeile kürzen"). Bewusst flex-basis
       0% statt auto: Die Zeilenumbruch-Entscheidung von flex-wrap:wrap
       vergleicht die UNGESCHRUMPFTE hypothetische Breite jedes Elements
       (seine flex-basis, nicht das Ergebnis von flex-shrink) gegen die
       verfügbare Breite - bei flex-basis:auto wäre das die intrinsische
       Breite eines Texteingabefelds (deutlich mehr als die ~114px, die neben
       dem Button noch bleiben), wodurch das Feld trotz flex-shrink:1
       zuverlässig in eine eigene Zeile umbrach. Mit 0% ist die hypothetische
       Breite 0, der Umbruch bleibt aus, und flex-grow:1 füllt den übrigen
       Platz danach ganz regulär auf. min-width:0 erlaubt das Schrumpfen
       unter den Platzhaltertext hinaus, falls nötig. */
    .market-right .sell-controls.loan-controls .market-qty {
      order: 0;
      flex: 1 1 0%;
      min-width: 0;
    }
  }
  /* Schwarzer Rahmen/Schrift (user request 28.07.2026), transparenter
     Hintergrund. Ursprünglich einfach nur Rahmen-/Textfarbe-Overrides auf
     button.secondary, das damals selbst transparent mit grünem Rahmen war
     ("-- .secondary bleibt für den transparenten Hintergrund/Hover
     erhalten"). button.secondary wurde seither zweimal umgestylt (zuletzt
     Nutzeranfrage 02.09.2026, jetzt ein gefüllter Grünton, siehe dortiger
     Kommentar) und riss die Stepper-Buttons dabei ungewollt mit - ein reiner
     Farb-Override ohne eigenen Hintergrund hat keine Möglichkeit, sich davon
     unabhängig zu machen. Nutzeranfrage 03.09.2026 ("-5/-1/+1/+5 und max.
     müssen wieder so sein wie vor der Button-Farben-Anpassung, Hintergrund
     war weiß/transparent"): background/border jetzt vollständig selbst
     gesetzt statt nur der Farbe, dadurch unabhängig von button.secondary und
     zukünftigen Änderungen daran. */
  /* button.step-btn (not just .step-btn) to match button.secondary's own
     specificity (element+class) -- a plain class selector would lose that
     tie despite coming later in the file, since button.secondary is also
     element+class. */
  .step-btn { width: 37px; text-align: center; padding-left: 0; padding-right: 0; }
  /* Nutzerreport 20.08.2026, "-5/-1/+1/+5 etwas breiter machen, damit max.
     linksbündig/rechtsbündig mit Eingabefeldern und Buttons ist": Auf schmalen
     Bildschirmen (siehe .market-right .sell-controls .market-qty weiter oben,
     erzwingt dort eine eigene volle Zeile fürs Mengenfeld) füllen
     Eingabefeld UND die Kaufen/Verkaufen-Buttons jeweils die komplette
     Zeilenbreite, die 6 Stepper-Buttons als Gruppe bleiben aber bei ihrer
     festen 37/56px-Breite schmaler und werden dadurch mit sichtbarer Lücke
     links/rechts zentriert statt die Ränder zu erreichen. flex-grow:1 auf
     genau den 4 mittleren Buttons (nicht .max-btn, die sollen ihre kompakte
     Breite behalten) verteilt exakt diese Lücke gleichmäßig auf sie und
     zieht die äußeren max.-Buttons dadurch bis an den Rand - bei JEDER
     Bildschirmbreite, nicht nur bei einer einzelnen fest verdrahteten. Wo
     ohnehin schon kein Platz übrig ist (z.B. 481-820px, wo Eingabefeld noch
     in derselben Zeile mitläuft), hat flex-grow nichts zu verteilen und
     bleibt wirkungslos - flex-basis:37px hält dabei die bisherige
     Mindestbreite, width:37px oben bleibt als Fallback für ältere Browser
     ohne flex-basis-Unterstützung stehen. */
  @media (max-width: 820px) {
    .step-btn { flex: 1 1 37px; }
  }
  button.step-btn { background: transparent; color: #000; border: 1px solid #000; }
  /* Eigener Hover statt der von button.secondary:hover geerbten
     Hintergrundfarbe (die inzwischen ein volles Grün ist, siehe oben) - sehr
     helle Grün-Tönung, dieselbe wie vor der button.secondary-Umstellung. */
  button.step-btn:hover:not(:disabled) { background: var(--accent-light); }
  /* Das schwarze Rahmen/Schrift-Paar oben ist unbedingt gesetzt und war
     dadurch im dunklen Design faktisch unsichtbar (Schwarz auf --card
     #1c2226). Nutzeranfrage: heller Rahmen + helle Schrift im Dark Mode.
     var(--text) statt eines festen Weiß, damit die Stepper dieselbe
     Schriftfarbe tragen wie der restliche Text der Marktplatz-Zeile.
     :root-Präfix ist hier nötig, nicht kosmetisch: button.step-btn oben ist
     (0,1,1) spezifisch, ein reines [data-theme="dark"] .step-btn wäre nur
     (0,2,0)+... - mit :root davor gewinnt die Regel eindeutig, unabhängig
     von der Quellreihenfolge (dieselbe Falle wie bei .warning-btn:hover,
     siehe 12_UI_UX.md Abschnitt 4p). */
  :root[data-theme="dark"] button.step-btn { color: var(--text); border-color: var(--text); }
  /* Sperr-Begründung bei laufendem Bau in derselben Zone (Nutzeranfrage
     17.08.2026) - der Grund hängt am deaktivierten Button als title-Tooltip,
     der auf Mobile mangels Hover unerreichbar ist. Dort steht er deshalb
     zusätzlich als normaler Text darunter; auf Desktop bleibt es beim
     Tooltip, damit die Zeile dort nicht unnötig höher wird. Siehe
     buildBlockedActionHtml() in ui-helpers.js. */
  /* align-items:center statt flex-end (Nutzeranfrage 20.08.2026, "Info +
     Restdauer soll zentriert unterhalb des jeweiligen Buttons sein") - beide
     Kinder (Button, Countdown-Notiz) sitzen dadurch auf derselben Mittelachse
     der Spalte, die Notiz landet automatisch mittig unter dem Button, egal
     wie schmal/breit ihr jeweiliger eigener Inhalt ist. Auf Desktop ohne
     sichtbare Wirkung, da dort nur der Button (einziges sichtbares Kind)
     in dieser Spalte steht - flex-end vs. center macht bei einem einzelnen,
     spaltenfüllenden Kind keinen sichtbaren Unterschied. */
  .build-blocked-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }
  .build-blocked-note { display: none; }
  @media (max-width: 820px) {
    .build-blocked-note { display: block; text-align: center; }
  }
  .max-btn { width: 56px; text-align: center; padding-left: 0; padding-right: 0; }
  .trade-btn { padding-left: 6px; padding-right: 6px; font-size: calc(12px + 1pt); text-align: center; }
  .storage-link-btn { padding-left: 6px; padding-right: 6px; text-align: center; }
  .found-btn { padding-left: 4px; padding-right: 4px; font-size: var(--text-size-button); text-align: center; }
  /* Zweizeiliges Experten-Label ("Experte einstellen" / Laufzeit+Kosten,
     Nutzeranfrage 29.07.2026) - reduziertes vertikales Padding + line-height,
     damit beide Zeilen innerhalb der festen 42px-Buttonhöhe Platz finden,
     ohne die globale button-Höhe/-breite für alle anderen Buttons anzutasten. */
  .expert-btn-2line { padding-top: 2px; padding-bottom: 2px; line-height: 1.2; }
  /* Kosten-Trenner für Gründen/Ausbauen/Slot kaufen/Lagerausbau
     (Nutzeranfrage 20.08.2026, "auf Mobile möglichst einzeilig, Wort und
     Kosten mit Bindestrich trennen"; weiterer Nachtrag "auf Desktop sollen
     die Kosten weiterhin in der zweiten Zeile stehen") - firmActionButtonLabel()
     (firms.js) und die entsprechenden Stellen in market.js/i18n.js setzen
     dafür ein leeres <span class="cost-sep"> zwischen Aktionswort und
     Kosten, statt zweier separat gepflegter Label-Varianten im JS:
     Auf Mobile fügt ::before einen sichtbaren Bindestrich ein (ergibt ein
     einzeiliges "Wort - Kosten"), auf Desktop wird der Bindestrich
     unsichtbar (leerer content) UND das Element selbst zu einem Block, was
     denselben Zeilenumbruch-Effekt wie das zuvor genutzte <br> erzeugt -
     button:has(.cost-sep) bekommt dafür dieselbe Padding/line-height-
     Anpassung wie .expert-btn-2line oben, damit beide Zeilen innerhalb der
     festen 42px-Buttonhöhe Platz finden. */
  /* Geschützte Leerzeichen (\00a0) statt normaler Leerzeichen (Nutzerreport
     21.08.2026, "kein Abstand um den Bindestrich auf Mobile") - Buttons sind
     spielweit display:inline-flex (siehe die generische "button"-Regel oben),
     und Flexbox behandelt jeden zusammenhängenden Text- bzw. Element-Lauf als
     eigenes anonymes Flex-Item, wobei normale (kollabierbare) Leerzeichen an
     dessen Rändern verschluckt werden - betraf dadurch jeden Button mit
     diesem Trenner ("Gründen - 624 MC" wurde optisch zu "Gründen-624 MC").
     \00a0 gilt nicht als kollabierbarer Leerraum und übersteht das. */
  .cost-sep::before { content: "\00a0-\00a0"; }
  @media (min-width: 821px) {
    /* Weder "display: block" noch ein Zeilenumbruch-Zeichen (\A) im
       generierten Inhalt bricht hier auf eine zweite Zeile um (Nutzerreport
       21.08.2026, "Aktionswort und Kosten laufen auf Desktop zusammen",
       beide Ansätze live getestet und verworfen): Buttons sind spielweit
       display:inline-flex (row, siehe generische "button"-Regel) - Flex-Items
       werden als GRUPPE entlang der Hauptachse in EINER Flex-Zeile
       positioniert, unabhängig vom "display"-Eigenwert oder Textinhalt
       einzelner Items; ein Zeilenumbruch-Zeichen kann höchstens den
       INTERNEN Inhalt eines einzelnen Flex-Items umbrechen, nie mehrere
       Geschwister-Items auf verschiedene Zeilen verteilen.
       Stattdessen der etablierte Flexbox-Trick für erzwungene Zeilenumbrüche:
       flex-wrap:wrap auf dem Button + flex-basis:100% auf dem leeren
       Trenner-Span - das Span beansprucht die volle verbleibende Zeilenbreite
       für sich, wodurch für das nachfolgende Kosten-Item kein Platz mehr auf
       derselben Flex-Zeile bleibt und es in die nächste umbricht. */
    .cost-sep { flex-basis: 100%; }
    .cost-sep::before { content: ""; }
    button:has(.cost-sep) { flex-wrap: wrap; padding-top: 2px; padding-bottom: 2px; line-height: 1.2; }
  }
  /* Login/Register-Bildschirm (user request 28.07.2026): 1.6x der normalen
     Button-Breite (320px statt 200px) -- eigener Platzhalter statt Umbau der
     spielweiten Uniform-Breite, da nur diese eine Ansicht betroffen ist. */
  /* max-width wie bei .auth-field: ohne sie blieben die Buttons auf schmalen
     Geräten starr bei 320px, während die Felder auf die Kartenbreite
     einschrumpfen - beide würden dann wieder unterschiedlich breit sein. */
  .auth-btn { width: 320px; max-width: 100%; }
  /* Dashboard-Kennzahlen (user request 30.07.2026): Die vier wichtigsten
     Zahlen zuerst und groß, Detailkarten darunter. Vorher standen sie als
     gleich große Textzeilen zwischen Firmenlisten - auf einem Überblicks-
     bildschirm sollen sie aber auf einen Blick lesbar sein, ohne dass man
     Zeilen vergleichen muss. tabular-nums, damit die Ziffern beim
     2-Sekunden-Poll nicht sichtbar zappeln, wenn sich der Wert ändert. */
  .kpi-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
    margin-bottom: 18px;
  }
  @media (max-width: 820px) {
    .kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 12px; }
  }
  /* Gleicher Holzsteg wie section.card (Nutzeranfrage 21.08.2026) - dieselbe
     weiße, abgerundete Box-Optik wie dort, sollte also auch denselben Rahmen
     tragen statt der alten 1px-Grau-Linie. */
  .kpi {
    /* 3px statt 4px (Nutzeranfrage 22.08.2026, "alle Rahmen einen px
       schmaler") - gilt für die ganze Holzsteg-Familie, siehe Kommentar bei
       header's border-bottom. */
    border: 3px solid transparent;
    border-radius: 14px;
    background: linear-gradient(var(--card), var(--card)) padding-box, linear-gradient(155deg, #8a6a49, #4c3a28) border-box;
    padding: 13px 15px;
    min-width: 0; /* sonst sprengt eine lange Zahl die Grid-Spalte */
    text-align: center; /* Nutzeranfrage 21.08.2026 - Überschrift und Wert zentriert statt linksbündig */
  }
  /* Bewusst ohne nowrap/Ellipsis: bei 320px brauchen "Unternehmenswert" und
     "Forschungspunkte" ~121px, die Kachel bietet 107px. Ein gekürztes
     "Unternehmens…" hilft niemandem - die Beschriftung darf stattdessen
     zweizeilig werden, die Kacheln einer Grid-Zeile sind ohnehin gleich hoch. */
  .kpi-label {
    font-size: calc(12px + 1pt);
    font-weight: 700;
    color: var(--muted);
    display: flex;
    align-items: flex-start;
    justify-content: center; /* Nutzeranfrage 21.08.2026 - zentriert das Icon+Text-Paar, .kpi selbst zentriert nur Blockinhalte, kein flex-Kind */
    gap: 5px;
    line-height: 1.3;
    /* min-height fuer 2 Zeilen (Nutzeranfrage 23.08.2026) - vorher stand der
       Wert direkt unter einem 1-zeiligen Label hoeher als unter einem
       2-zeiligen Label in einer Nachbar-Kachel, obwohl die Kacheln durch das
       Grid-Stretching gleich hoch waren (der Freiraum landete nur unten statt
       die Werte auszurichten). Reserviert jetzt immer den Platz fuer 2 Zeilen,
       damit .kpi-value in allen vier Kacheln auf derselben Hoehe startet. */
    min-height: 2.6em;
  }
  .kpi-value {
    /* -5pt (Nutzeranfrage 21.08.2026) gegenüber der zuvor allgemein um 1pt
       erhöhten Basisgröße - betrifft alle vier Dashboard-Kacheln
       (Unternehmenswert/Coins/Gewinn-pro-Tag/Forschungspunkte), die sich
       hier denselben kpi()-Template-Aufruf teilen (siehe renderDashboardKpis()
       in dashboard.js). */
    font-size: calc(21px - 4pt);
    font-weight: 700;
    margin-top: 5px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  @media (max-width: 380px) { .kpi-value { font-size: calc(18px - 4pt); } }
  .kpi-unit { font-size: calc(13px + 1pt); font-weight: 600; color: var(--muted); margin-left: 3px; }
  /* Eingabefelder, Label und Hinweistexte teilen sich exakt dieselbe Breite
     und Mittelachse wie die Buttons darunter (user request 30.07.2026) -
     vorher liefen sie über die volle Kartenbreite und standen dadurch
     sichtbar weiter außen als die Buttons. max-width hält das auf sehr
     schmalen Geräten trotzdem innerhalb der Karte. */
  .auth-field { width: 320px; max-width: 100%; margin-left: auto; margin-right: auto; }
  /* Gleiche Abstände auf allen vier Seiten (Nutzeranfrage 01.08.2026).
     Referenz ist der bestehende Abstand Rahmen -> Eingabefeld von 50px: Die
     Felder sind 320px breit und zentriert, bei 420px Kartenbreite ergaben die
     auto-Ränder links/rechts 50px, das padding oben/unten aber nur 17px.
     Auf Wunsch halbiert (01.08.2026): 24px Innenabstand (+1px Rahmen = 25px)
     ringsum. Kartenbreite und padding gehören dabei zusammen und stehen
     deshalb in derselben Regel statt im inline-style der vier Panels: Nur
     wenn der Inhaltsbereich exakt so breit ist wie die 320px-Felder
     (320 + 2*24 + 2*1 = 370px), stammen alle vier Abstände aus dem padding.
     Wäre die Karte breiter, kämen links/rechts die auto-Ränder der Felder
     obendrauf und der Abstand wäre dort wieder größer als oben/unten. */
  #authCard, #checkEmailCard, #forgotCard, #resetCard { padding: 24px; max-width: 370px; }
  @media (max-width: 480px) {
    /* Auf dem Telefon nochmal kleiner, sonst bliebe für die Felder zu wenig
       Breite - weiterhin auf allen vier Seiten gleich. */
    #authCard, #checkEmailCard, #forgotCard, #resetCard { padding: 14px; }
  }
  /* `section.card h2` (Spezifität 0,1,2) setzt margin:0 und schlägt damit das
     margin:auto der Klasse (0,1,0) - die Überschrift stünde sonst als einziges
     Element linksbündig statt auf der gemeinsamen Mittelachse. */
  section.card h2.auth-field { margin-left: auto; margin-right: auto; }
