/* ══════════════════════════════════════════════════════════════════════════════
   BSS Kicker – Basis-Styles (Material Design 3 Expressive)

   Konsumiert AUSSCHLIESSLICH Design-Tokens aus static/css/m3-tokens.css
   (--md-sys-* / --md-ext-*). Hell/Dunkel schaltet html[data-theme] um.
   Referenz-Implementierung aller Komponenten: docs/m3-muster.html.
   Fonts + .msym-Icon-Basis liegen in m3-tokens.css (eine Quelle inkl. Kiosk).
══════════════════════════════════════════════════════════════════════════════ */

/* ── Reset & Basis-Typografie ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ⚠️ `[hidden]` MUSS staerker sein als jedes `display` (2026-09-13).

   Der Browser bringt `[hidden] { display: none }` selbst mit - aber nur
   mit der Staerke einer Typregel. Jede Klasse schlaegt sie: `.btn` ist
   `inline-flex`, und ein `<button hidden class="btn">` stand sichtbar da.
   Gefunden am Schliessen-Knopf der TV-Vorschau, auf dem Bildschirmfoto.

   `!important` ist hier richtig und nicht faul: `hidden` ist kein
   Gestaltungswunsch, sondern eine Aussage ueber den Inhalt - es gibt
   keinen Fall, in dem etwas Verborgenes doch sichtbar sein soll. */
[hidden] { display: none !important; }
html { font-size: 16px; }
body {
  background: var(--md-sys-color-background);
  color: var(--md-sys-color-on-surface);
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-large-size);
  /* Verhältnis statt fester px (24px/16px = 1.5): Ein vererbter px-Zeilenabstand
     gilt sonst auch für große Schrift – eine 80px-Überschrift bekäme eine
     24px-Zeilenbox und würde die Nachbarelemente überlappen. */
  line-height: 1.5;
  letter-spacing: var(--md-sys-typescale-body-large-tracking);
  min-height: calc(100dvh / var(--ui-zoom, 1));
  overflow-x: hidden;
  -webkit-tap-highlight-color: transparent;
  /* Globale Oberflächen-Skalierung. Auf <body>, damit auch Modal, Banner,
     Toast und Screensaver (Geschwister von #app) mitskalieren. */
  zoom: var(--ui-zoom, 1);
}
a { color: inherit; text-decoration: none; }
button, input, select, textarea {
  font-family: inherit;
  font-size: inherit;
  touch-action: manipulation;
}
img { display: block; max-width: 100%; }

/* ── Scrollbars (schlank & themenfarben) ──────────────────────────────────── */
* { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--md-sys-shape-corner-small);
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ── State-Layer & Ripple-Basis (alle interaktiven M3-Hosts) ──────────────────
   Hover 8 % / Fokus 10 % als ::after-Tönung in currentColor; die Pressed-Welle
   (.ripple-ink) erzeugt static/js/ripple.js ab dem Berührpunkt.               */
.btn, .tab, .nav-link, .pin-key, .player-card, .goal-btn,
.chip, .seg-btn, .modal-close, [data-ripple] {
  position: relative;
  overflow: hidden;
  -webkit-user-select: none;
  user-select: none;
}
.btn::after, .tab::after, .nav-link::after, .pin-key::after, .player-card::after,
.goal-btn::after, .chip::after, .seg-btn::after,
.modal-close::after, [data-ripple]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}
.btn:hover::after, .tab:hover::after, .nav-link:hover::after, .pin-key:hover::after,
.player-card:hover::after, .goal-btn:hover::after, .chip:hover::after,
.seg-btn:hover::after,
.modal-close:hover::after, [data-ripple]:hover::after {
  opacity: var(--md-sys-state-hover-state-layer-opacity);
}
.btn:focus-visible, .tab:focus-visible, .nav-link:focus-visible, .pin-key:focus-visible,
.chip:focus-visible, .seg-btn:focus-visible { outline: none; }
.btn:focus-visible::after, .tab:focus-visible::after, .nav-link:focus-visible::after,
.pin-key:focus-visible::after, .chip:focus-visible::after, .seg-btn:focus-visible::after {
  opacity: var(--md-sys-state-focus-state-layer-opacity);
}
.btn:disabled::after { display: none; }
.ripple-ink {
  position: absolute;
  border-radius: 50%;
  background: currentColor;
  pointer-events: none;
  opacity: var(--md-sys-state-pressed-state-layer-opacity);
  transform: scale(0);
  transition: transform var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-standard-decelerate),
              opacity var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}

/* ── Typescale- & Farb-Utilities (Phase-5-Zielklassen) ────────────────────── */
.display-small  { font-size: var(--md-sys-typescale-display-small-size);  line-height: var(--md-sys-typescale-display-small-line-height);  font-weight: var(--md-sys-typescale-display-small-weight); }
.headline-medium{ font-size: var(--md-sys-typescale-headline-medium-size);line-height: var(--md-sys-typescale-headline-medium-line-height);font-weight: var(--md-sys-typescale-headline-medium-weight); }
.headline-small { font-size: var(--md-sys-typescale-headline-small-size); line-height: var(--md-sys-typescale-headline-small-line-height); font-weight: var(--md-sys-typescale-headline-small-weight); }
.title-large    { font-size: var(--md-sys-typescale-title-large-size);    line-height: var(--md-sys-typescale-title-large-line-height);    font-weight: var(--md-sys-typescale-title-large-weight); }
.title-medium   { font-size: var(--md-sys-typescale-title-medium-size);   line-height: var(--md-sys-typescale-title-medium-line-height);   font-weight: var(--md-sys-typescale-title-medium-weight);   letter-spacing: var(--md-sys-typescale-title-medium-tracking); }
.title-small    { font-size: var(--md-sys-typescale-title-small-size);    line-height: var(--md-sys-typescale-title-small-line-height);    font-weight: var(--md-sys-typescale-title-small-weight);    letter-spacing: var(--md-sys-typescale-title-small-tracking); }
.body-medium    { font-size: var(--md-sys-typescale-body-medium-size);    line-height: var(--md-sys-typescale-body-medium-line-height);    letter-spacing: var(--md-sys-typescale-body-medium-tracking); }
.body-small     { font-size: var(--md-sys-typescale-body-small-size);     line-height: var(--md-sys-typescale-body-small-line-height);     letter-spacing: var(--md-sys-typescale-body-small-tracking); }
.label-large    { font-size: var(--md-sys-typescale-label-large-size);    line-height: var(--md-sys-typescale-label-large-line-height);    font-weight: var(--md-sys-typescale-label-large-weight);    letter-spacing: var(--md-sys-typescale-label-large-tracking); }
.label-medium   { font-size: var(--md-sys-typescale-label-medium-size);   line-height: var(--md-sys-typescale-label-medium-line-height);   font-weight: var(--md-sys-typescale-label-medium-weight);   letter-spacing: var(--md-sys-typescale-label-medium-tracking); }
.text-primary { color: var(--md-sys-color-primary); }
.text-error   { color: var(--md-sys-color-error); }
.text-success { color: var(--md-ext-color-success); }
.text-warning { color: var(--md-ext-color-warning); }
.text-team1   { color: var(--md-ext-color-team1); }
.text-team2   { color: var(--md-ext-color-team2); }

/* ── Navbar (Top App Bar + Navigation-Pills) ──────────────────────────────── */
/* Die obere Leiste. ⚠️ Sie REAGIERT auf das Scrollen (M3: „on scroll
   surface-container"): solange die Seite oben steht, sitzt sie auf derselben
   Flaeche wie der Inhalt und wirkt wie ein Teil davon; sobald gescrollt wird,
   hebt sie sich ab. Der Rahmen unten kommt erst dann - vorher war er immer da
   und zog eine Linie ueber eine Seite, die keine brauchte.

   Die Klasse `is-gescrollt` setzt `App.ui` am Fensterlauscher, denselben, der
   `.stuck` an der klebenden Kopfzeile setzt. */
#navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--nav-h);
  background: var(--md-sys-color-surface);
  border-bottom: 1px solid transparent;
  transition: background-color var(--md-sys-motion-duration-short4)
                               var(--md-sys-motion-easing-standard),
              border-color var(--md-sys-motion-duration-short4)
                           var(--md-sys-motion-easing-standard);
  display: flex;
  align-items: center;
  /* Einzeln und nicht als Kurzform (2026-09-30): oben der Abstand zur Notch,
     seitlich zu den abgerundeten Ecken im Querformat. `--nav-h` enthaelt den
     oberen Abstand schon, mit `border-box` bleibt der Innenraum gleich hoch. */
  padding-top: env(safe-area-inset-top, 0px);
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  gap: 8px;
  z-index: 100;
}
#navbar.is-gescrollt {
  background: var(--md-sys-color-surface-container);
  border-bottom-color: var(--md-sys-color-outline-variant);
}
.nav-links {
  display: flex;
  gap: 4px;
  /* `0 1 auto` statt `1` seit dem 2026-09-30: die Pillen stehen in einer
     grauen Schiene, und die soll so breit sein wie ihr Inhalt - mit `flex: 1`
     liefe sie bis zur Uhr. Nach rechts draengt weiter die Uhr
     (`.nav-clock { margin-left: auto }`). */
  flex: 0 1 auto;
  /* NOWRAP, und das ist eine Fehlerbehebung, nicht Geschmack: die Pillen
     brauchten mit `flex-wrap: wrap` unter etwa 1085 px eine zweite Reihe von
     56 px – in einem Balken mit `height: var(--nav-h)` = 64 px. Sie lief also
     nach unten in den Inhalt, und zwar auf jedem Tablet im Hochformat und in
     jedem halben Fenster.
     Wischbar statt abgeschnitten ist dasselbe Mittel, das `.tabs` schon
     benutzt – und es braucht KEINEN Umbruchpunkt, gilt also für jede Breite
     und hält die Zahl der Marken bei zwei.
     `min-width: 0` ist Pflicht: als Flex-Element wäre die Mindestbreite sonst
     die min-content-Breite der nowrap-Kinder, und dann schrumpft nichts
     (dieselbe Begründung wie bei `.table-wrap`). */
  flex-wrap: nowrap;
  overflow-x: auto;
  min-width: 0;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Die Schiene des Entwurfs (2026-09-30). */
  background: var(--md-sys-color-surface-container-high);
  border-radius: var(--md-sys-shape-corner-full);
  padding: 4px;
}
.nav-links::-webkit-scrollbar { display: none; }
.nav-links .nav-link { flex: 0 0 auto; }
.nav-link {
  padding: 8px 18px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-large-size);
  font-weight: var(--md-sys-typescale-label-large-weight);
  letter-spacing: var(--md-sys-typescale-label-large-tracking);
  color: var(--md-sys-color-on-surface-variant);
  transition: background var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
              color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
  white-space: nowrap;
  min-height: var(--min-touch);
  display: flex;
  align-items: center;
  gap: 6px;
}
.nav-link.active {
  /* Weiss in der grauen Schiene statt violetter Pille (2026-09-30). */
  background: var(--md-ext-color-schiene-aktiv);
  color: var(--md-sys-color-on-surface);
  box-shadow: var(--md-sys-elevation-level1);
}
/* Der Seitentitel am Telefon (2026-09-30, Betreiber: „Gerne mit Seitentitel.
   so wie in dem Entwurf."). Er ersetzt dort die Uhr; ueber 600 px stehen die
   Pillen und die Uhr wie bisher, deshalb hier verborgen. Gefuellt von
   `App.ui.updateNav`. */
.nav-titel {
  display: none;
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--md-sys-typescale-headline-medium-size);
  line-height: var(--md-sys-typescale-headline-medium-line-height);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--md-sys-color-on-surface);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ⚠️ Hier stand bis zum 2026-09-07 `.nav-status` - der Punkt fuer den
   Datenbankstatus in der Ecke der Kopfzeile. Der Betreiber wollte ihn nur noch
   in Einstellungen -> Verbindungen sehen; dort traegt ihn `.status-dot`, das
   es schon gab (Reiter Sensoren macht es vor). */

/* ── Read-Only-Modus (Remote-Clients ohne Admin-PIN) ──────────────────────── */
.nav-auth {
  background: transparent;
  border: 1px solid var(--md-sys-color-outline);
  color: var(--md-sys-color-on-surface-variant);
  border-radius: var(--md-sys-shape-corner-full);
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; flex-shrink: 0;
  transition: background var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}
.nav-auth:hover { background: var(--md-sys-color-surface-container-highest); }
/* Der Avatar im Konto-Knopf. Feste Größe, KEIN --avatar-scale: der Knopf ist
   44 px, und der Betreiber hat die Avatare auf 2,6 skaliert – das Bild würde
   sonst aus seinem Kreis herausragen. 30 px, damit es nicht kleiner wirkt als
   das 20-px-Symbol, das es ersetzt. */
.nav-avatar {
  width: 30px; height: 30px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
.nav-auth.active {
  border-color: var(--md-sys-color-primary);
  color: var(--md-sys-color-primary);
}

/* Die Glocke: Form, Rahmen und 44 px kommen von .nav-auth, damit die zwei
   Knöpfe in der Leiste garantiert gleich aussehen. Hier nur der Zähler.

   Bewusst KEIN .nav-auth.active an der Glocke: das umgefärbte Signal gehört dem
   Konto-Knopf („angemeldet"). Die Glocke signalisiert über eine ZAHL – rot
   allein wäre für rot-grün-blinde Augen keine Aussage. */
.nav-bell { position: relative; }

/* Aufsitzender Zähler – ein neues Muster, absichtlich sparsam. `.badge` weiter
   unten ist eine Textpille IM FLIESSTEXT und hat kein Gegenstück für „sitzt auf
   einem Knopf"; sie hier zu verbiegen hätte zwei Bedeutungen in eine Klasse
   gelegt.
   Gezeichnet aus data-count, damit die Glocke EIN Element bleibt: ein zweites
   müsste an jeder Stelle mitverborgen werden, an der die Leiste verschwindet
   (App._nurAnmeldung, Ersteinrichtung).
   `:not([data-count=""])` ist Pflicht – sonst hängt ein leerer roter Punkt an
   der Glocke, sobald das Attribut gesetzt aber leer ist. */
.nav-bell[data-count]:not([data-count=""])::after {
  content: attr(data-count);
  position: absolute;
  top: 2px; right: 2px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-error);
  color: var(--md-sys-color-on-error);
  font-size: 10px; font-weight: 700; line-height: 16px;
  text-align: center;
  pointer-events: none;
}

/* Ohne angemeldeten Benutzer gibt es niemanden, der jemanden herausfordern
   könnte – dann verschwinden Glocke UND Menüpunkt. Die Klasse dafür setzt
   App.auth.apply() längst (classList.toggle('signed-in', …)); das kostet also
   keine Zeile JavaScript und schafft keine zweite Wahrheit. */
body:not(.signed-in) .nav-bell { display: none; }

/* Am Kickertisch braucht niemand die Kanalliste (er STEHT vor dem Monitor) und
   niemand verwaltet dort Turniere - das macht man am Schreibtisch. Wunsch des
   Betreibers am 2026-09-03.

   Ein Selektor fuer BEIDE Leisten: oben und unten stehen dieselben
   `data-route`-Werte, `.nav-link` traegt jede Kachel. Das Markup bleibt dabei
   unangetastet - vier Zusicherungen in test_responsive.py zaehlen die sechs
   Kacheln, vergleichen die Mengen oben/unten und halten die Reihenfolge
   woertlich fest. Verbergen ist eine Frage der Anzeige, nicht des Bestands.

   Die Routen bleiben erreichbar: ein gespeicherter Link, der ins Leere
   fuehrt, waere schlimmer als ein Menuepunkt zu viel. */
body.am-tisch .nav-link[data-route="/tv"],
body.am-tisch .nav-link[data-route="/tournaments"] { display: none; }

/* Und aus dem Netz ist "Turnier" eine RECHTEFRAGE: nur Administratoren und
   Spielleiter (`tournament.manage`) sehen ihn. Wunsch des Betreibers am
   2026-09-03.

   Eine zweite Regel und keine Erweiterung der ersten: das sind zwei
   verschiedene Gruende, denselben Menuepunkt zu verbergen (Ort und Recht),
   und zusammengefasst waere keiner der beiden mehr lesbar.

   Die Klasse setzt `App.auth.apply()`, dort steht auch, warum eine Anlage
   ohne Benutzer den Punkt behaelt. Die Route bleibt wie oben erreichbar. */
body:not(.kann-turniere) .nav-link[data-route="/tournaments"] { display: none; }

/* Das Untermenue am Konto-Knopf - das erste Aufklappmenue im Projekt.

   Im Grundabschnitt und ohne @media: es haengt am Knopf, und der ist bei jeder
   Breite gleich gross. Damit bleibt die Zahl der Umbruchpunkte bei zwei.
   `position: fixed`, weil der Knopf in einer fixierten Leiste sitzt - absolut
   positioniert wanderte das Menue beim Scrollen mit dem Inhalt. */
.konto-menu {
  position: fixed;
  z-index: 200;
  min-width: 200px;
  padding: 6px;
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium);
  box-shadow: var(--md-sys-elevation-level2);
}
.konto-menu .km-punkt {
  display: flex; align-items: center; gap: 10px;
  width: 100%; min-height: var(--min-touch);
  padding: 8px 12px;
  background: transparent; border: none; cursor: pointer;
  color: var(--md-sys-color-on-surface);
  font: inherit; text-align: left;
  border-radius: var(--md-sys-shape-corner-small);
}
.konto-menu .km-punkt:hover {
  background: var(--md-sys-color-surface-container-highest);
}
/* Ausgegrauter Punkt (2026-10-09, Figuren-Kontextmenue): eine Figur ohne
   Treffer hat keine Ballverfolgung zu zeigen - derselbe Token wie bei
   `.btn:disabled` weiter oben, nicht neu erfunden. */
.konto-menu .km-punkt:disabled {
  cursor: not-allowed;
  color: rgba(var(--md-sys-color-on-surface-rgb) / 0.38);
}
.konto-menu .km-punkt:disabled:hover { background: transparent; }
/* Das Menue am Spielernamen (2026-09-24, js/spielermenue.js) - dasselbe
   Bauteil wie das Kontomenue. Die Uebersteuerung steht HINTER dessen Regeln.

   ⚠️ `z-index: 500` statt 200: drei Namen stehen IN der schwebenden Karte,
   und `#modal-overlay` liegt auf 400. Mit 200 ginge das Menue dort unsichtbar
   hinter der Karte auf. 500 wie `.em-tafel` und `.ew-liste`, unter den
   Hinweisen (600). */
.konto-menu.spieler-menue {
  z-index: 500;
}

/* ══ Das Aktionsmenue am Telefon (2026-09-04) ═══════════════════════════════
   Wunsch des Betreibers: "Sollten zu viel Aktionstasten fuer das smartphone
   vorhanden sein, erstelle ein 'Burgermenue' um die Funktionen zu sammlen -
   das gilt nur fuer die Ansicht auf dem Smartphone."

   Zwei Leisten sammelt es ein: die fuenf Aktionen des laufenden Spiels
   (`.game-controls`) und die drei der Benachrichtigungen (`.nb-kopf`). Bei
   360 px standen die fuenf in DREI Reihen ueber dem Spielstand.

   ⚠️ Der Knopf steht bei JEDER Breite im Markup und ist hier im Grundabschnitt
   verborgen - sichtbar macht ihn allein der 600er-Block. Das ist die Hausregel
   "die Breite entscheidet das Stylesheet, nicht das JavaScript"
   (static/js/display.js:21-23), und es haelt die Zahl der Umbruchpunkte bei
   zwei (tests/test_responsive.py::test_only_two_width_breakpoints). Dieselbe
   Ueberlegung wie beim Kontomenue darueber.

   ⚠️ Der Zustand liegt in `aria-expanded` am Knopf und in KEINER Klasse. Das
   Attribut muss fuer die Vorlesehilfen ohnehin gefuehrt werden, und damit
   reicht der Geschwisterselektor `+` -
   kein Rahmen um die Leiste, kein `:has()`, und die Leiste behaelt ihr
   Klassenattribut Zeichen fuer Zeichen. Letzteres ist nicht kosmetisch: drei
   Zusicherungen greifen die woertliche Markup-Zeichenkette
   `<div class="game-controls">` bzw. `<div class="nb-kopf">` und brechen mit
   ValueError ab, wenn dort etwas dazukommt. */
/* ⚠️ `.btn.ak-knopf` und nicht `.ak-knopf`, und das ist ein GEMESSENER
   Fehler: `.btn` setzt selbst `display: inline-flex` und steht weiter unten in
   dieser Datei. Bei gleicher Spezifitaet (0,1,0) gewinnt die spaetere Regel -
   der Knopf war am Bildschirm bei 1280, 900 und 700 px SICHTBAR, obwohl er
   dort nichts einklappt.

   Zwei Klassen statt Umsortieren: die Rangfolge soll im Selektor stehen und
   nicht in der Zeilennummer. Dieselbe Entscheidung traegt `.bn-link > .bn-text`
   weiter unten - eine Klasse laesst sich von einem angehaengten Element nicht
   verschieben, eine Position wie `:last-child` schon. */
.btn.ak-knopf { display: none; }

/* Die drei Striche, gezeichnet statt geschrieben.

   ⚠️ `menu` und `menu_open` stehen NICHT im Zeichensatz
   (static/fonts/material-symbols.subset.txt). Sie nachzuliefern hiesse die
   Vollschrift aus der Git-Historie holen und neu einkochen, und
   scripts/subset_icons.py warnt: "eine neuere Fassung von Google Fonts
   koennte veraenderte Umrisse mitbringen, und dann aendert ein neues Icon
   nebenbei das Aussehen aller anderen." Fuer ein Symbol aus drei Rechtecken
   ist das kein Preis, den man zahlt.

   ⚠️ `::before` und NICHT `::after`: `::after` ist an `.btn` die
   M3-Zustandsschicht (siehe ganz oben, `.btn::after`) - sie zu ueberschreiben
   hiesse, Hover und Fokus an genau diesem Knopf abzuschalten.

   Ein Verlauf und keine drei Kindelemente: `::after` ist belegt, ein zweites
   Pseudoelement gibt es nicht, und drei Kinder im Knopf waeren drei Knoten fuer
   eine Form. Die Masse sind die des Material-Symbols `menu` im 24er-Raster:
   Balken 2 px, Luft 5 px, insgesamt 20x16.

   `currentColor` und kein eigenes Token: der Knopf ist ein `.btn-outline`, und
   seine Farbe IST schon die Rolle. Ein zweites Token waere eine zweite Stelle,
   an der ein Design danebengreifen kann. */
.ak-knopf::before {
  content: "";
  width: 20px;
  height: 16px;
  background: linear-gradient(to bottom,
    currentColor 0 2px, transparent 2px 7px,
    currentColor 7px 9px, transparent 9px 14px,
    currentColor 14px 16px);
}

/* Offen wird derselbe Zustand gefaerbt, der die Tafel oeffnet - kein Umbau der
   Striche zu einem X. Zwei Gruende: ein Verlauf laesst sich nicht drehen, und
   ein X verspricht "das schliesst die ganze Ueberlagerung", waehrend die Tafel
   nur ein Anhaengsel dieses Knopfes ist.

   ⚠️ Und deshalb braucht `prefers-reduced-motion` hier KEINEN Eintrag: es wird
   `display: none` gegen `flex` getauscht, und `display` ist ohne
   `transition-behavior: allow-discrete` nicht animierbar. Es gibt keine
   Bewegung zu unterdruecken. Das steht hier, damit der Naechste sie nicht
   nachtraegt. */
.ak-knopf[aria-expanded="true"] {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}

.readonly-banner {
  position: fixed;
  left: 20px;
  bottom: 24px;
  z-index: 300;
  max-width: min(560px, calc(100vw - 40px));
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 14px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  box-shadow: var(--md-sys-elevation-level2);
  font-size: var(--md-sys-typescale-body-medium-size);
}
.readonly-banner.hidden { display: none; }
.readonly-banner > span:nth-child(2) { flex: 1; min-width: 200px; }
.readonly-banner .banner-close {
  background: transparent; border: none; cursor: pointer;
  font-size: 1.3rem; line-height: 1;
  color: inherit; opacity: .7; padding: 0 4px;
}

/* Schreibende Bedienelemente im Nur-Lese-Modus abschwächen.
   Kein pointer-events:none – ein Klick soll den PIN-Dialog öffnen. */
body.readonly [data-requires-edit] {
  opacity: .38;
  filter: grayscale(1);
  cursor: not-allowed;
}
body.readonly [data-requires-edit="hide"] { display: none !important; }

/* Aktionen, die nur am Kickertisch Sinn haben (ein Spiel starten, Tore zählen).
   Vorgabe ist ausgegraut und nicht versteckt - im laufenden Spiel IST der Knopf
   der Inhalt, und sein Verschwinden liesse die Ansicht unerklaerlich aussehen.
   Kein pointer-events:none, damit der Klick den Hinweis zeigen kann. */
body.nicht-am-tisch [data-requires-table] {
  opacity: .38;
  filter: grayscale(1);
  cursor: not-allowed;
}
/* ⚠️ Eine benannte Ausnahme davon, seit dem 2026-09-03: `data-admin-auch`.

   Zwei Aktionen darf ein Administrator auch aus dem Netz - ein Spiel beenden
   und ein Spiel abbrechen. Der Grund des Betreibers: "ein Spiel, das am Tisch
   vergessen wurde, blockiert die Anlage, und niemand ist mehr dort." Die
   Rechteschicht dazu steht in `access_control.SPIEL_STEUERUNG_ADMIN` und in
   `routers/games.py::_am_tisch_oder_admin`; das hier ist nur die Sicht.

   Ein eigenes Merkmal und nicht `data-requires-table="admin"`: der Wert dort
   heisst schon "verbergen statt ausgrauen", und zwei Bedeutungen in einem Wert
   waeren beim naechsten Fall nicht mehr auseinanderzuhalten.

   Die Klasse `kann-admin` setzt `App.auth.apply()` ueber
   `App.auth.kannAdmin()` - dieselbe Frage wie beim Zahnrad und den drei
   Kaesten. Und sie ist NICHT die Sperre: die stehen im Server. */
body.nicht-am-tisch.kann-admin [data-requires-table][data-admin-auch] {
  opacity: 1;
  filter: none;
  cursor: pointer;
}
/* Das Gegenstueck zu `data-requires-edit="hide"`, und aus demselben Grund
   ausdruecklich anzufordern statt vorgegeben: es gilt seit dem 2026-09-01 fuer
   genau EIN Element, den grossen Startknopf der Startseite. Der ist 100 %
   breit, 30 px gepolstert und steht ueber der ganzen Uebersicht - ausgegraut
   frisst er aus dem Netz einen Bildschirmabschnitt fuer eine Aktion, die dort
   nie geht. Wunsch des Betreibers: „remote soll der Button gar nicht erst zu
   sehen sein, den Platz kann man dann fuer die Ansicht nutzen."

   `!important`, weil `.btn` sein eigenes `display` setzt.

   ⚠️ Seit dem 2026-09-04 ist es NICHT mehr genau ein Element. Dazu kamen in
   der laufenden Spielansicht: Pause, „Seiten tauschen" und die vier
   Tor-Knoepfe. Der Betreiber: „Diese Aktionen sollen fuer Remotespieler nicht
   zu sehen sein" und „Den Platz koennen wir uns sparen, da Remote niemand
   diese Aktionen druecken darf." Sachlich richtig - alle sechs stehen in
   `access_control.SPIEL_STEUERUNG` und sind `nur_lokal`; kein Recht und kein
   PIN heben das auf. Vier Tor-Knoepfe von 130x100 px sind ein Drittel der
   Spielerkiste. `tests/test_readonly_middleware.py` fuehrt die Liste. */
body.nicht-am-tisch [data-requires-table="hide"] { display: none !important; }

/* Und die Umkehrung der Ausnahme oben: „Beenden" und „Abbrechen" verschwinden
   aus dem Netz fuer den, der sie dort NICHT darf (2026-09-04).

   Der Betreiber wollte es genau so gestaffelt: „Diese Aktionen sollen fuer
   Remotespieler nicht zu sehen sein und bei Admins nur die sehen, die er
   Remote auch ausfuehren kann."

   ⚠️ Diese zwei tragen deshalb `data-requires-table` OHNE den Wert „hide":
   „hide" gilt fuer alle, und ein Administrator soll sie behalten. Ausgegraut
   bleiben sie nirgends - entweder er darf (dann macht die Regel darueber sie
   wieder scharf) oder er sieht sie nicht.

   ⚠️ `:not(.kann-admin)` und nicht eine zweite Klasse: `kann-admin` ist die
   EINE Frage (`App.auth.kannAdmin()`), und sie hat schon vier Antwortstellen.
   Eine fuenfte Klasse waere eine fuenfte Gelegenheit, sie auseinanderlaufen zu
   lassen. */
body.nicht-am-tisch:not(.kann-admin) [data-requires-table][data-admin-auch] {
  display: none !important;
}

/* ── Und die Gegenrichtung: was es NUR aus dem Netz gibt (2026-09-18) ───────

   Der Betreiber: "Wenn ich am Tisch bin (localhost) moechte ich an keiner
   Stelle Videos oeffnen oder abspielen koennen. Es sollen alle Links und
   Icons fuer Videos, Likes, Kommentare etc. auf dem localhost nicht angezeigt
   werden. Ist man Remote angemeldet, soll alles gehen."

   ⚠️ EIN eigenes Merkmal und nicht ein dritter Wert an `data-requires-table`.
   Das Attribut darueber kennt „grau" und „weg" und meint IMMER „nur am
   Tisch"; zwei Richtungen in einem Attribut waeren beim naechsten Fall nicht
   mehr auseinanderzuhalten.

   ⚠️ HINTER dem Block darueber, nicht davor (Falle ④): tests/
   test_readonly_middleware.py schneidet dort mit `index()` auf dem Selektor
   der ersten Regel dieses Blocks. Eine neue Regel davor zoege den Schnitt mit
   und braeche Zusicherungen, die mit dieser Runde nichts zu tun haben. Der
   Selektor steht hier deshalb NICHT woertlich - ein Kommentar, der eine
   Schnittmarke nennt, ist selbst eine.

   ⚠️ `body.am-tisch` POSITIV. Die verneinte Form traefe jeden Besucher,
   solange /api/auth/context noch nicht geantwortet hat - die Knoepfe blitzten
   dann bei jedem Seitenaufruf kurz auf. Dieselbe Regel wie beim Zahnrad und
   bei `.rk-anfeuern`; tests/test_responsive.py prueft sie fuer das ganze
   Stylesheet. */
body.am-tisch [data-nur-aus-dem-netz] {
  display: none !important;
}

/* ⚠️ Das Chatfenster ueber die KENNUNG und nicht ueber das Attribut darueber:
   `<div id="chat-dock"></div>` in static/index.html ist woertlich eingefroren
   (tests/test_community.py haelt fest, dass der Wirt leer ist - das ganze
   Fenster haengt daran). Ein Attribut im Markup waere dort eine Aenderung an
   einer Zeile, die aus gutem Grund unveraendert bleiben soll. */
body.am-tisch #chat-dock { display: none; }

/* ── Main content ─────────────────────────────────────────────────────────── */
/* ⚠️ Das senkrechte Polster ist am 2026-09-21 von 24 auf 16 px gefallen
   (Wunsch „weniger Scrollen"). Waagerecht bleiben die 20 px: sie sind der
   Seitenrand, an dem die Karten ausgerichtet sind, und die klebende
   Kopfzeile gleicht genau diesen Wert wieder aus.

   ⚠️ Am Telefon bleibt das untere Polster gross: der 600er Block setzt
   `padding-bottom: calc(var(--bn-h) + 16px + safe-area)`, damit das letzte
   Element nicht unter der Leiste verschwindet. Diese Zeile hier rührt daran
   nicht - sie steht vorher und wird dort überschrieben. */
#content {
  min-height: calc(100dvh / var(--ui-zoom, 1) - var(--nav-h));
  padding: 16px 20px;
  max-width: 1400px;
  margin-left: auto;
  margin-right: auto;
  margin-top: var(--nav-h);
}
/* ⚠️ Die Nachrichtenseite nimmt die ganze Breite (2026-09-04).

   Der Betreiber: „Der linke Teil mit den Gesprächen ist nicht linksbündig am
   Menü dran. Sondern irgendwie mitten drin. Bitte Linksbündig schalten."

   Nicht `nachrichten.js` war schuld, sondern die Schale: `#content` trägt
   `max-width` und `margin: auto`, damit der Inhalt im Fenster zentriert
   bleibt. Für die Startseite ist das richtig. Für einen Zweispalter, dessen
   linke Spalte eine Liste ist, nicht: bei 1920 px bleiben
   (1920 − 1440) / 2 = 240 px Luft, und die Gesprächsliste beginnt mitten im
   Raum.

   ⚠️ Der Befund kam am 2026-09-04 auf, als das Seitenmenü links am `body`
   noch zusätzlich Platz reservierte. Das Menü ist am 2026-09-07
   zurückgebaut - die Ursache ist damit nicht weg, sondern nur kleiner: die
   Zentrierung von `#content` allein genügt für dasselbe Bild.

   ⚠️ `:has()` und keine Rumpfklasse: eine Klasse müsste `App.router.navigate`
   setzen und beim Verlassen abräumen, und das ist eager Code in einer Datei,
   die auf ein Byte gerechnet ist. Der Selektor trifft genau, was er meint -
   `#content` mit dieser einen Ansicht darin. Chromium und die aktuellen
   Firefox/Safari kennen ihn; wo nicht, bleibt es beim heutigen Bild, und das
   ist kein Ausfall, sondern der Zustand von gestern. */
#content:has(> .nr-raster) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
}

/* ── Buttons (M3: filled · tonal · elevated · outlined · text) ───────────────
   Aliase für Bestandscode: .btn-primary = filled, .btn-outline = outlined.
   Expressive: gedrückt morpht die Pille zu einer eckigeren Form.             */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 24px;
  min-height: var(--min-touch);
  border: none;
  border-radius: var(--md-sys-shape-corner-full);
  cursor: pointer;
  font-size: var(--md-sys-typescale-label-large-size);
  font-weight: var(--md-sys-typescale-label-large-weight);
  letter-spacing: var(--md-sys-typescale-label-large-tracking);
  background: transparent;
  color: var(--md-sys-color-primary);
  transition: box-shadow var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
              border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing),
              transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing);
  touch-action: manipulation;
  /* ⚠️ Ein Knopf schrumpft NICHT unter seine Beschriftung (2026-09-10).

     Die Sichtpruefung hat es an zwei Stellen gefunden: die Blaetterknoepfe
     „Zurueck"/„Weiter" der Spielerdetails bei 390 px (Inhalt 96 px in
     einem 93-px-Kasten) und „Senden" in der Anfeuerleiste (118 in 114).
     Beide liegen in einer Flexzeile mit `justify-content: space-between`;
     passt die Zeile nicht, schrumpfen die Kinder - und weil jeder Knopf
     fuer die Wellenanimation `overflow: hidden` traegt (die Regel am
     Dateianfang), verschwindet der letzte Buchstabe STILL.

     `flex-shrink: 0` macht daraus einen SICHTBAREN Fehler statt eines
     unsichtbaren: passt es nicht, laeuft die Zeile ueber - und genau das
     meldet die Sichtpruefung. Ein abgeschnittenes Wort meldet niemand. */
  flex-shrink: 0;
}
.btn:active:not(:disabled) { border-radius: var(--md-sys-shape-corner-medium); transform: scale(0.97); }

/* ── Die Expressive-Groessenleiter ────────────────────────────────────────────
   Fuenf Stufen (ANALYSE.md §8). Die Vorgabe von `.btn` ist **M** und bleibt es
   – siehe die Begruendung an den Tokens: M3 nennt fuer den gewoehnlichen Knopf
   Stufe S, und das waere am Kicker-Touch eine 29 Prozent kleinere Trefffläche.

   ⚠️ `.btn-sm` und `.btn-xs` sind fuer Elemente, die KEINE primaeren Ziele
   sind. Wer sie an einen Knopf schreibt, den jemand am Tisch trifft, macht die
   Bedienung schlechter – `--min-touch` ist die Vorgabe und keine Untergrenze,
   die man unterschreiten „darf".

   `.btn-lg` gab es schon (60 px) und bleibt, wo es ist: es sitzt zwischen M
   und L und traegt den Startknopf. Ein Sprung auf 96 px waere dort ein Balken
   und kein Knopf. */
.btn-xs { min-height: var(--md-sys-comp-size-xs); padding: 4px 12px;
          font-size: var(--md-sys-typescale-label-medium-size); }
.btn-sm { min-height: var(--md-sys-comp-size-s);  padding: 8px 16px; }
.btn-xxl { min-height: var(--md-sys-comp-size-xl); padding: 32px 56px;
           font-size: var(--md-sys-typescale-headline-small-size); }
.btn:disabled {
  cursor: not-allowed;
  box-shadow: none;
  background: rgba(var(--md-sys-color-on-surface-rgb) / 0.12);
  color: rgba(var(--md-sys-color-on-surface-rgb) / 0.38);
}
.btn-primary, .btn-filled { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.btn-tonal    { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }
.btn-elevated { background: var(--md-sys-color-surface-container-low); color: var(--md-sys-color-primary); box-shadow: var(--md-sys-elevation-level1); }
.btn-elevated:hover:not(:disabled) { box-shadow: var(--md-sys-elevation-level2); }
.btn-outline, .btn-outlined { border: 1px solid var(--md-sys-color-outline-variant); color: var(--md-sys-color-on-surface); }
.btn-outline:disabled { background: transparent; border-color: rgba(var(--md-sys-color-on-surface-rgb) / 0.12); }
.btn-text { padding: 10px 12px; }
.btn-danger  { background: var(--md-sys-color-error); color: var(--md-sys-color-on-error); }
.btn-xl {
  font-size: var(--md-sys-typescale-title-medium-size);
  padding: 20px 40px;
  min-height: 72px;
  border-radius: var(--md-sys-shape-corner-extra-large);
}
.btn-xl:active:not(:disabled) { border-radius: var(--md-sys-shape-corner-large); }
.btn-icon {
  width: var(--min-touch); height: var(--min-touch);
  padding: 0; font-size: 1.4rem;
  border-radius: var(--md-sys-shape-corner-full);
}
/* Die drei M3-Varianten des Icon-Knopfes. Bisher gab es nur die durchsichtige
   Fassung; wer Betonung brauchte, schrieb `.btn-primary` daneben und bekam die
   Polsterung eines Textknopfes dazu.

   ⚠️ Die Groesse bleibt `--min-touch` (Stufe M) und wird NICHT auf die 40 px
   des Kits gesetzt - siehe die Begruendung an der Groessenleiter. Ein
   Icon-Knopf ist am Tisch oft das einzige Ziel in einer Tabellenzelle. */
.btn-icon-filled {
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}
.btn-icon-tonal {
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
}
.btn-icon-outlined {
  border: 1px solid var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface);
}

/* ── Weicher Designwechsel ────────────────────────────────────────────────────
   Nur waehrend des Wechsels gesetzt (`App.theme._setMode` raeumt die Klasse
   nach 520 ms wieder ab).

   ⚠️ `background-color`, `color` und `border-color` - NICHT `all`. Ein `all`
   wuerde auch Groessen und Positionen animieren, und die Kopfzeile springt
   beim Wechsel dann sichtbar.

   ⚠️ Ohne `.theme-transition` an der Wurzel ist die Regel nicht aktiv - der
   Wechsel kostet also nur waehrend seiner halben Sekunde etwas. Dauerhaft
   gesetzt waere sie bei jedem Neuzeichnen einer Ansicht ein Posten, und die
   Ansichten werden hier per innerHTML neu gebaut. */
html.theme-transition,
html.theme-transition *,
html.theme-transition *::before,
html.theme-transition *::after {
  transition: background-color var(--md-sys-motion-duration-long2)
                               var(--md-sys-motion-easing-emphasized),
              color var(--md-sys-motion-duration-long2)
                    var(--md-sys-motion-easing-emphasized),
              border-color var(--md-sys-motion-duration-long2)
                           var(--md-sys-motion-easing-emphasized);
}


/* ── Trennlinie ───────────────────────────────────────────────────────────────
   Ein Baustein statt eines `border-top` an jeder Stelle. Wir bauen Trennlinien
   heute je Komponente selbst nach (Ranglistenzeilen, Turnierlisten, die
   Kanalliste) - mit dreimal derselben Farbe und zweimal einer anderen Breite.

   ⚠️ `.md-divider` heisst es und nicht `.divider`: die Klasse `.divider` gab
   es hier schon, sie war unbenutzt und ist am 2026-09-03 als toter Code
   gefallen. Ein Name, den es einmal gab, kommt nicht mit anderer Bedeutung
   zurueck - sonst greift ein alter Stylesheet-Rest aus einem Cache. */
.md-divider {
  height: 1px;
  border: 0;
  margin: 0;
  background: var(--md-sys-color-outline-variant);
}
.md-divider-inset { margin-inline-start: var(--md-sys-spacing-4); }


/* ── Expressive-Formen (SVG-Masken) ───────────────────────────────────────────
   15 Formen aus dem Kit, seit dem 2026-09-03 in `static/img/shapes/`. Vorher
   gab es KEIN SVG unter `static/`.

   Nutzung:  <div class="m3-form m3-form-clover4"><span class="msym">…</span></div>

   ⚠️ Die Maske liegt auf `::before` und nicht am Element selbst: sonst wuerde
   der Inhalt (Icon, Zahl, Bild) mitbeschnitten. Ein Bild als Inhalt wird
   dagegen absichtlich selbst maskiert - dafuer die Regel darunter.

   ⚠️ `services/assets.py` stempelt `/static/**` mit `?v=<hash>`; die Masken
   bekommen ihre Fassung also von selbst und brauchen keinen Eintrag. */
.m3-form {
  position: relative;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  color: var(--md-sys-color-on-primary-container);
  --m3-form-fill: var(--md-sys-color-primary-container);
}
.m3-form > * { position: relative; z-index: 1; }
.m3-form::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--m3-form-fill);
  mask-image: var(--m3-form-mask);
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-image: var(--m3-form-mask);
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
}
.m3-form > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  mask-image: var(--m3-form-mask);
  mask-size: contain;
  mask-repeat: no-repeat;
  -webkit-mask-image: var(--m3-form-mask);
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
}
.m3-form-secondary {
  --m3-form-fill: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
.m3-form-tertiary {
  --m3-form-fill: var(--md-sys-color-tertiary-container);
  color: var(--md-sys-color-on-tertiary-container);
}
.m3-form-circle    { --m3-form-mask: url('/static/img/shapes/circle.svg'); }
.m3-form-square    { --m3-form-mask: url('/static/img/shapes/square.svg'); }
.m3-form-clover4   { --m3-form-mask: url('/static/img/shapes/clover-4.svg'); }
.m3-form-clover8   { --m3-form-mask: url('/static/img/shapes/clover-8.svg'); }
.m3-form-cookie4   { --m3-form-mask: url('/static/img/shapes/cookie-4.svg'); }
.m3-form-cookie6   { --m3-form-mask: url('/static/img/shapes/cookie-6.svg'); }
.m3-form-cookie7   { --m3-form-mask: url('/static/img/shapes/cookie-7.svg'); }
.m3-form-cookie9   { --m3-form-mask: url('/static/img/shapes/cookie-9.svg'); }
.m3-form-cookie12  { --m3-form-mask: url('/static/img/shapes/cookie-12.svg'); }
.m3-form-sunny     { --m3-form-mask: url('/static/img/shapes/sunny.svg'); }
.m3-form-burst     { --m3-form-mask: url('/static/img/shapes/burst.svg'); }
.m3-form-pill      { --m3-form-mask: url('/static/img/shapes/pill.svg'); }
.m3-form-pentagon  { --m3-form-mask: url('/static/img/shapes/pentagon.svg'); }
.m3-form-gem       { --m3-form-mask: url('/static/img/shapes/gem.svg'); }
.m3-form-flower    { --m3-form-mask: url('/static/img/shapes/flower.svg'); }
/* Die Pille ist doppelt so breit wie hoch - sonst waere sie ein Kreis. */
.m3-form-pill { aspect-ratio: 2 / 1; }


/* ── Cards (hell: elevated · dunkel: tonale Container statt Schatten) ─────── */
/* Die Karte. Radius `corner-medium` (12), wie M3 es fuer Karten nennt -
   `docs/M3-ABGLEICH.md` §8a, `docs/ANALYSE.md` §8. Hier stand bis zum
   2026-09-03 `corner-large` (16); das war die einzige Formabweichung, die von
   den dreizehn Bauteilzeilen des Abgleichs uebrig war.

   ⚠️ DAS POLSTER IST SEIT DEM 2026-09-21 ZWEIGETEILT: 16 px oben und
   unten, 20 px links und rechts. Vorher standen hier 20 px rundum, mit
   dieser Begründung:

     „Unsere Karten tragen Ranglisten und Tabellen, keine Fliesstexte. Bei 16
      stehen die Zahlenspalten am Rand."

   Die Begründung stimmt – aber nur waagerecht. Der Rand, an dem die
   Zahlenspalte klebt, ist der SEITLICHE; er bleibt bei 20. Senkrecht trägt
   das Polster nichts als sich selbst, und M3 nennt 16 px ausdrücklich als
   Kartenmass.

   Der Betreiber am 2026-09-21: „Bitte prüfe alle Abstände ob unnötigerweise
   zu viel Platz verschwendet wird." Gerechnet 8 px je Karte, auf der
   Startseite mit vier Karten 32 px, in den Einstellungen bis 48.

   ⚠️ Die übrigen Masse dieser Zeile bleiben, was sie waren: Knöpfe Stufe M
   (56 statt 40), Reiter 56 statt 48, `.list-item` 56. Das sind
   Treffflächen, und an denen wird nicht gespart - der Unterschied zu einem
   Polster ist der ganze Punkt dieser Runde. */
.card {
  background: var(--md-sys-color-surface-container-lowest);
  /* Neugestaltung (2026-09-30): eine Haarlinie statt Schatten und 24 px statt
     12 px Radius, wie im freigegebenen Entwurf. Das Polster bleibt - die
     Begruendung davor gilt weiter. */
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-ext-shape-card);
  padding: 16px 20px;
  box-shadow: none;
}
html[data-theme="dark"] .card {
  background: var(--md-sys-color-surface-container);
  box-shadow: none;
}
.card-filled   { background: var(--md-sys-color-surface-container-highest); box-shadow: none; }
.card-outlined { background: var(--md-sys-color-surface); border: 1px solid var(--md-sys-color-outline-variant); box-shadow: none; }
.card-title {
  /* Satzschreibung, halbfett, in der Farbe des Textes (2026-09-30) statt der
     kleinen gesperrten Versalienzeile in Nebentextfarbe. Die Versalien waren
     eine bewusste Abweichung von M3 (M3-RULES Regel 8, fuer den Blick aus
     mehreren Metern); der Entwurf der Neugestaltung hat sie fuer die App
     aufgehoben - der TV behaelt seine eigene Regel in kiosk.html. */
  font-size: var(--md-sys-typescale-title-medium-size);
  line-height: var(--md-sys-typescale-title-medium-line-height);
  font-weight: 600;
  letter-spacing: 0;
  color: var(--md-sys-color-on-surface);
  text-transform: none;
  /* 12 statt 16 seit dem 2026-09-21: 16 px loesten den Titel optisch von
     seiner eigenen Karte. */
  margin-bottom: 12px;
}
/* Kartentitel links, ein zweites Stueck rechts - heute die Startseite, wo bei
   genau EINEM aktiven Turnier der Turniername neben „Rangliste - N Punkte im
   Umlauf" steht statt darunter. Der Betreiber am 2026-09-16: „dann rutscht die
   ganze Liste an Spielern hoch" - gespart wird die Kopfzeile des Turnierblocks.

   `baseline` und nicht `center`: die beiden Teile haben verschiedene Groesse,
   und nur die Schriftlinie bringt sie optisch auf eine Hoehe. `flex-wrap`
   bleibt aus demselben Grund wie beim Turnierkopf: bei 360 px und einem langen
   Turniernamen rutscht er sonst aus der Karte. */
.card-title.kt-paar {
  /* 8 statt der 16 px, die jede andere Karten-Ueberschrift traegt: hier steht
     die Beschriftung der Tabelle unmittelbar darunter und gehoert dazu.
     Gemessen: der Abstand faellt damit von 55 auf 16 px. */
  margin-bottom: 8px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}
/* Die Spanne rechts in der Ueberschrift des Punkteverlaufs (2026-09-17):
   "Min 1012 · Max 1104 Elo".

   ⚠️ HINTER `.card-title.kt-paar` und nicht davor (Falle ④): eine
   Zusicherung schneidet den Rumpf jener Regel mit `index(".card-title.kt-paar
   {")` heraus. Eine Regel davor, die dieselbe Zeichenkette traegt, zoege den
   Schnitt mit und braeche einen Test, der mit dieser Aenderung nichts zu tun
   hat.

   Leer, solange die Kurve laedt - `:empty` nimmt sie dann aus dem Fluss,
   damit die Ueberschrift nicht schon Platz fuer etwas reserviert, das noch
   nicht da ist. */
.kt-spanne {
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface-variant);
}
.kt-spanne:empty { display: none; }

/* ── Forms ────────────────────────────────────────────────────────────────── */
/* Abstand unter einer Karten-Überschrift.

   Ohne ihn überlagert das erste Feld die Überschrift: der globale Reset setzt
   `margin: 0` auf alles, `h3` hat also keinen Abstand, und ein schwebendes
   `.form-label` ragt im gehobenen Zustand bewusst um die halbe Zeilenhöhe über
   die Feldoberkante hinaus (die „Notch"-Optik, siehe unten) – mit deckendem
   Hintergrund. Es malt die untere Hälfte der Überschrift zu.

   12 px, weil `.card-title` mit 16 px beweist, dass es reicht. Ein
   `margin-top` auf `.form-group` waere die Alternative und verschöbe jedes
   Formular im Projekt.
   (Als Begründung stand hier bis zum 2026-09-03 `.login-card h2` – die Regel
   ist mit der eigenständigen Anmeldeseite entfallen, `login.html` bringt ihr
   Aussehen selbst mit.) */
.card > h3 { margin-bottom: 12px; }

/* 12 statt 18 seit dem 2026-09-21. Auf der Einstellungsseite stehen bis zu
   zwölf Felder untereinander - das sind 72 px je Reiter, die niemand sieht
   und durch die jeder scrollt. Die Feldhöhe selbst bleibt. */
.form-group { margin-bottom: 12px; }

/* Abstand zwischen einem erklärenden Absatz und dem Feld darunter – aus
   demselben Grund wie `.card > h3` oben: das schwebende `.form-label` ragt um
   die halbe Zeilenhöhe über die Feldoberkante und hat einen DECKENDEN
   Hintergrund. Es malte den Absatz zu (aufgefallen in den SSO-Einstellungen
   bei „Anwendungs-ID (Client)“). Der globale Reset nimmt jedem <p> sein
   margin-bottom, und `margin-top` am Absatz zeigt in die andere Richtung.

   Bewusst als Nachbarschafts-Regel und nicht als `margin-top` auf
   `.form-group`: das würde jedes Formular im Projekt verschieben. Sichtbar
   wird der Fehler nur bei GEFÜLLTEM Feld – dann schwebt das Label dauerhaft. */
.card p + .form-group.field { margin-top: 12px; }
.form-label {
  display: block;
  font-size: var(--md-sys-typescale-body-small-size);
  letter-spacing: var(--md-sys-typescale-body-small-tracking);
  color: var(--md-sys-color-on-surface-variant);
  margin-bottom: 6px;
}
.form-input {
  width: 100%;
  padding: 12px 14px;
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-extra-small);
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-body-large-size);
  transition: border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
  min-height: var(--min-touch);
}
html[data-theme="dark"] .form-input { background: var(--md-sys-color-surface-container-low); }
.form-input:focus {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: -2px;
  border-color: var(--md-sys-color-primary);
}
.form-select { appearance: none; }

/* ── M3 Floating-Label-Felder (outlined) ──────────────────────────────────────
   Aktiv, wenn der .form-group-Wrapper zusätzlich .field trägt und das Label
   NACH dem Input steht:  <div class="form-group field"><input…><label…></div>
   Das Label ruht in der Feldmitte und „schwebt" bei Fokus/Inhalt animiert auf
   den Rahmen (Notch-Optik über --field-surface = Hintergrund des Containers).
   Selects haben immer einen Wert → Label dauerhaft gefloatet.                 */
:root { --field-surface: var(--md-sys-color-background); }
.card { --field-surface: var(--md-sys-color-surface-container-lowest); }
html[data-theme="dark"] .card { --field-surface: var(--md-sys-color-surface-container); }
#modal-box { --field-surface: var(--md-sys-color-surface-container-high); }

.form-group.field { position: relative; }
.field > .form-input { background: transparent; }
html[data-theme="dark"] .field > .form-input { background: transparent; }
.field > .form-input + .form-label {
  position: absolute;
  left: 12px;
  top: calc(var(--min-touch) / 2);
  transform: translateY(-50%);
  margin: 0;
  padding: 0 4px;
  pointer-events: none;
  font-size: var(--md-sys-typescale-body-large-size);
  color: var(--md-sys-color-on-surface-variant);
  background: transparent;
  white-space: nowrap;
  transition: top var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              font-size var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
}
.field > .form-input:focus + .form-label,
.field > .form-input:not(:placeholder-shown) + .form-label,
.field > select.form-input + .form-label {
  top: 0;
  font-size: var(--md-sys-typescale-body-small-size);
  background: var(--field-surface);
}
.field > .form-input:focus + .form-label { color: var(--md-sys-color-primary); }

/* ⚠️ `flex-start` und nicht `center`, seit dem 2026-09-13. Die meisten
   Schalterzeilen tragen unter der Beschriftung einen Hinweis, und der ist am
   Telefon vier bis sechs Zeilen lang. Mittig ausgerichtet stand der Schalter
   dann in der MITTE des Absatzes und gehoerte optisch zu nichts - dieselbe
   Falle wie in der Benachrichtigungstabelle, und derselbe Satz gilt: der
   Schalter gehoert auf die Hoehe der Ueberschrift.

   ⚠️ Das Polster am Etikett holt die Mitte fuer EINZEILIGE Zeilen zurueck:
   die Schiene ist 32 px hoch, eine Textzeile rund 20 - die 6 px setzen die
   eine Zeile genau in die Mitte des Schalters. Ohne sie saessen alle
   einzeiligen Schalter im Haus ploetzlich zwei Pixel zu hoch. */
.toggle-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
}
.toggle-label { font-size: var(--md-sys-typescale-body-medium-size);
                min-width: 0; padding-top: 6px; }
/* ⚠️ Der Hinweis steht IMMER auf seiner eigenen Zeile. Die meisten Erbauer im
   Haus setzen dafuer ein `<div>`, zwei setzten eine `<span>` - und die lief
   hinter der Beschriftung weiter („Abstand automatisch Wie bisher: der Abstand
   waechst mit der Bildschirmgroesse …"), bis sie am Schalter ankam. Der
   Betreiber am 2026-09-13: „Ich moechte das alles sauber und klar dargestellt
   werden. Gerne mit Umbruechen."

   Die Regel steht HIER und nicht in den Erbauern: sie gilt fuer jeden, auch
   fuer den naechsten, der wieder eine `<span>` nimmt. */
.toggle-label > .text-dim,
.toggle-label > .text-sm { display: block; margin-top: 2px; font-weight: normal; }
/* ── Die Vorschau des TV-Kanals (2026-09-13) ────────────────────────
   Ein Rahmen mit der ECHTEN Kioskseite - kein Nachbau (die Begruendung
   steht bei `_kioskVorschau` in components/settings.js).

   ⚠️ `aspect-ratio: 16 / 9`: ein Flurmonitor ist breit, und eine Vorschau
   in einem anderen Verhaeltnis zeigte die Lage der Tafel falsch - genau
   das, was hier eingestellt wird.

   ⚠️ `pointer-events: none`: es ist ein Bild und keine Bedienung. Ohne
   das koennte man in der Vorschau klicken, und der Rahmen scrollte oder
   nahme die Tastatur - bei einer Seite, die gar nicht bedient werden
   soll. */
.ki-vorschau { margin-top: 12px; }
.ki-vorschau iframe {
  display: block; width: 100%; aspect-ratio: 16 / 9; border: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  box-shadow: var(--md-sys-elevation-level1);
  background: var(--md-sys-color-surface-container-low);
  pointer-events: none;
}

/* ── Switch (M3-Morphologie: 52×32-Track, wachsender Handle mit Spring) ────── */
.toggle {
  /* ⚠️ `display` ist hier PFLICHT und hat bis zum 2026-09-10 gefehlt.
     Der Betreiber: „Die Schalter fuer die Benachrichtigungseinstellungen sind
     nicht sauber. Der Schalter fuer die Glocke ist nicht sichtbar."

     Das Bauteil ist ein <label>, also von Haus aus `display: inline` - und bei
     einem Inline-Element wirken `width` und `height` NICHT. Beide Kinder sind
     absolut positioniert (`input`, `.toggle-slider`), das Label hat also auch
     keinen Inhalt im Fluss: die Box wird 0 x 0, und sichtbar bleibt nur der
     2-px-Rand der Schiene.

     Jahrelang fiel das nicht auf, weil jeder Schalter im Haus in einer
     `.toggle-row` oder in `.sensor-indicator` lag - beide `display: flex`, und
     ein Flex-Kind wird blockifiziert. Die Benachrichtigungstabelle vom
     2026-09-10 setzt ihn zum ersten Mal in eine TABELLENZELLE, und dort bleibt
     er inline. Gemessen: 0 x 0 statt 52 x 32, elf von dreizehn Schaltern
     unsichtbar (die zwei in `.bt-stapel` gingen, weil Grid ebenfalls
     blockifiziert).

     Die Zeile steht deshalb am BAUTEIL und nicht an der Tabelle: fuer jede
     vorhandene Verwendung aendert sie nichts, und die naechste Stelle
     ausserhalb eines Flex-Elternteils ist von selbst richtig. `inline-block`
     und nicht `block`, damit der Schalter weiter neben Text stehen kann. */
  display: inline-block;
  position: relative;
  width: 52px; height: 32px;
  flex-shrink: 0;
}
.toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
.toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--md-sys-color-surface-container-highest);
  /* Ohne Kontur (2026-09-30): den Aus-Zustand traegt der dunkle Griff. */
  border: 2px solid transparent;
  border-radius: var(--md-sys-shape-corner-full);
  transition: background var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
              border-color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
  cursor: pointer;
}
.toggle-slider::before {
  content: '';
  position: absolute;
  width: 16px; height: 16px;
  left: 6px; top: 50%;
  margin-top: -8px;
  background: var(--md-sys-color-outline);
  border-radius: 50%;
  transition: transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing),
              width var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              height var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              margin-top var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
}
.toggle input:checked + .toggle-slider {
  background: var(--md-sys-color-primary);
  border-color: var(--md-sys-color-primary);
}
.toggle input:checked + .toggle-slider::before {
  width: 24px; height: 24px;
  margin-top: -12px;
  transform: translateX(18px);
  background: var(--md-sys-color-on-primary);
}
.toggle input:active + .toggle-slider::before { width: 28px; height: 28px; margin-top: -14px; }

/* ── Page headers ─────────────────────────────────────────────────────────── */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
  flex-wrap: wrap;
  gap: 12px;
}
/* ⚠️ EIN KOPF OHNE UEBERSCHRIFT RUECKT AN SEINE LISTE (2026-09-21).

   Der Betreiber: „Die Abstände von den Listen und den Aktionen sollen nicht
   so viel Freiraum haben. Ich möchte keine Überschneidungen der Aktionen mit
   den Listen. Aber die Abstände sind mir so viel zu groß."

   24 px sind der Abstand zwischen einer ÜBERSCHRIFT und dem, was sie
   überschreibt - dort richtig. Seit Spielerübersicht und Turniere nur noch
   ein „+" im Kopf tragen (2026-09-21) überschreibt dieser Kopf nichts: er
   ist eine Aktion, die zur Liste darunter gehört. 8 px binden sie zusammen,
   ohne dass sich etwas berührt - die geforderte Überschneidungsfreiheit
   kommt aus der eigenen Höhe des Kopfes (56 px Knopf plus Polster), nicht
   aus dem Abstand.

   ⚠️ `:has()` statt einer Klasse im Markup: die Kopfzeile wird an
   fünfzehn Stellen geschrieben, und eine Klasse, die man an vierzehn davon
   NICHT vergessen darf, ist eine Falle. Der Selektor fragt, was wirklich
   zählt: steht da eine Überschrift? Präzedenzfall ist
   `#content:has(> .nr-raster)` (2026-09-04); wo `:has()` fehlt, bleibt es
   beim Bild von gestern und nichts ist kaputt.

   ⚠️ VOR `.sticky-header`, nicht dahinter. Beide sind (0,2,0) stark, und
   die klebende Kopfzeile des Spielaufbaus setzt `margin` als Kurzform - sie
   muss gewinnen, sonst verliert sie ihren Randausgleich. Bei Gleichstand
   gewinnt die spätere Regel, also steht diese hier oben. */
.page-header:not(:has(h1, h2, h3, .page-title)) { margin-bottom: 8px; }

/* Kopfzeile mit Primäraktion, bleibt beim Scrollen unter der Navbar stehen.
   top in var(--nav-h) (nicht px) → bleibt bei jedem --ui-zoom korrekt. */
.page-header.sticky-header {
  position: sticky;
  top: var(--nav-h);
  z-index: 50;                     /* unter #navbar (100) und #modal-overlay (400) */
  background: var(--md-sys-color-background);
  /* ⚠️ Die -16 oben folgen `#content` (2026-09-21 von 24 gesenkt). Wer
     eins von beiden ändert und das andere vergisst, bekommt oben einen
     hellen Streifen oder eine Kopfzeile, die unter die Navigationsleiste
     rutscht - beides faellt erst beim Scrollen auf. */
  margin: -16px -20px 24px;        /* #content-Padding ausgleichen → randlos */
  padding: 12px 20px;
  border-bottom: 1px solid transparent;
  transition: box-shadow var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
              border-color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}
.page-header.sticky-header.stuck {
  box-shadow: var(--md-sys-elevation-level2);
  border-bottom-color: var(--md-sys-color-outline-variant);
}
.btn-lg {
  min-height: 60px;
  padding: 14px 28px;
  font-size: var(--md-sys-typescale-title-medium-size);
}

.page-title {
  font-size: var(--md-sys-typescale-headline-small-size);
  line-height: var(--md-sys-typescale-headline-small-line-height);
  font-weight: 700;
}
/* Die Knopfgruppe einer Seitenkopfzeile.

   `margin-left: auto` ist der Ausgleich fuer die Ueberschriften, die am
   2026-08-21 aus den vier Menuezielen gefallen sind: `.page-header` ist
   `justify-content: space-between`, und mit nur EINEM Kind rutschen die Knoepfe
   nach links. Der Aussenabstand haelt sie rechts und ist wirkungslos, solange
   links noch ein Titel steht - deshalb keine Modifikatorklasse und keine
   Sonderregel je Ansicht.

   `flex-wrap` und `align-items` standen als Inline-Stil am Chip-Kasten der
   Statistiken und stehen jetzt hier: drei Kiosk-Chips brauchen bei mittlerer
   Breite einen Umbruch, und im 600-px-Abschnitt weiter unten stand das schon,
   nur zu spaet. */
.page-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.page-header > .page-actions { margin-left: auto; }

/* Eine Ausnahme von dem Aussenabstand darueber, am 2026-09-03 bestellt.
   Als MODIFIKATOR und nicht als Aenderung der geteilten Regel: an ihr haengen
   drei Ansichten (Spielerliste, Turnierliste, Spielaufbau), und die wollen seit
   diesem Tag verschiedene Dinge. Wer die Regel selbst anfasst, verschiebt
   "+ Turnier" mit, das niemand angefasst haben wollte.

   ⚠️ HIER STAND BIS ZUM 2026-09-21 `.page-header.aktionen-links`.

   Der Betreiber am 2026-09-03: "In der Spielerübersicht soll die Aktion
   Hinzufügen nicht rechts, sondern linksbündig sein." Und derselbe am
   2026-09-21: "Die Aktion Hinzufügen in der Spielerübersicht soll nicht
   links, sondern Rechts sein." Damit ist der Modifikator wieder das, was er
   vorher war - ueberfluessig: die geteilte Regel darueber haelt den Knopf
   rechts, und sie wurde nie angefasst. Die Regel ist deshalb entfernt und
   nicht auf `auto` gesetzt; eine Ausnahme, die nichts ausnimmt, ist eine
   Falle fuer den Naechsten. */

/* Spielaufbau: "Spiel starten zentriert und breiter."
   Beidseitig `auto` zentriert das einzige Kind eines Flex-Kastens. Die
   Ueberschrift ist dafuer aus der Kopfzeile gefallen (siehe game.js) - mittig
   geht nicht, solange links etwas anderes um dieselbe Zeile konkurriert. */
.page-header.aktion-mitte > .page-actions {
  margin-left: auto;
  margin-right: auto;
  /* ⚠️ `width: 100%` seit dem 2026-09-04, und ohne es waere der Knopf darin
     wirkungslos breit: `.big-start-btn` traegt `width: 100%`, und das loest
     sich gegen DIESE Gruppe auf. Eine inhaltsbreite Gruppe ergibt einen
     inhaltsbreiten Knopf - beide Regeln stimmen, und nichts passiert. */
  width: 100%;
}
/* Und "breiter" heisst seit dem 2026-09-04: SO BREIT WIE AUF DER STARTSEITE.
   Der Betreiber: "Der Button Spiel starten auf der Seite eines neuen Spiels,
   soll genauso gross sein, wie der Neues Spiel starten auf der Startseite."

   Deshalb traegt der Knopf jetzt `.big-start-btn` und nicht `.btn-lg` - eine
   geteilte Klasse statt zweier Groessen, die man aneinander vorbeipflegt. Die
   frueheren 320 px Mindestbreite sind damit erledigt: aus einer Mindestbreite
   ist die ganze Zeile geworden.

   ⚠️ Der Aussenabstand der Klasse gilt hier nicht - in einer klebenden
   Kopfzeile schoebe er den Knopf von seinem eigenen Rand weg. */
.page-header.aktion-mitte .big-start-btn { margin-bottom: 0; }

/* ── Tables ───────────────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th {
  text-align: left;
  padding: 10px 16px;
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: var(--md-sys-typescale-label-medium-weight);
  letter-spacing: var(--md-sys-typescale-label-medium-tracking);
  color: var(--md-sys-color-on-surface-variant);
  text-transform: uppercase;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
/* `vertical-align: top` und nicht `middle`.

   Grund ist die Spielerliste: die Spalte „Anmeldung" trägt drei bis vier Zeilen,
   die Nachbarspalten je eine. Mittig ausgerichtet rutschten Name, Art und
   Gruppen in die MITTE der hohen Zeile und standen dadurch neben nichts - die
   Spalten liefen optisch ineinander. Oben ausgerichtet beginnt jede Spalte auf
   derselben Grundlinie. Zellen, die nur Knöpfe tragen, holen sich die Mitte
   über `.zelle-mitte` zurück. */
/* ⚠️ DAS SENKRECHTE POLSTER IST AM 2026-09-21 VON 14 AUF 8 px GEFALLEN.

   Der Betreiber: „Bitte prüfe alle Abstände ob unnötigerweise zu viel Platz
   verschwendet wird. In Listen möchte ich weniger Scrollen müssen."

   Das ist der größte einzelne Posten im ganzen Haus: 12 px je Zeile, und
   zwar in JEDER Tabelle.

   GEMESSEN am 2026-09-21 an der Spielerliste, 1080 px Fensterhöhe, mit
   `offsetHeight` (Falle ⑤): Zeilenhöhe 80 px statt 92, **11 sichtbare
   Zeilen statt 8**. Das Polster steht danach auf `8px 16px`.

   ⚠️ WAAGERECHT BLEIBEN DIE 16 px. Sie trennen die Spalten, und eine
   Zahlenspalte, die an der Nachbarspalte klebt, ist nicht dichter, sondern
   unlesbar. Gespart wird nur dort, wo der Platz wirklich leer war.

   ⚠️ Die Trefffläche fällt NICHT mit. Eine Zelle ist kein Bedienelement;
   was in ihr angeklickt wird, bringt sein eigenes Mass mit (`.aktion` 32 px,
   `.btn` 56 px) und macht die Zeile nötigenfalls selbst wieder höher. Genau
   deshalb bleibt `.list-item` bei seinen 56 px - das IST ein Bedienelement. */
td {
  padding: 8px 16px;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  vertical-align: top;
  font-size: var(--md-sys-typescale-body-medium-size);
}
/* ⚠️ `th` gehoert hier dazu, seit dem 2026-09-13. Die Regel darueber gibt
   BEIDEN Zellenarten eine Unterkante (`th` in der `th`-Regel, `td` in der
   `td`-Regel), die Ausnahme fuer die letzte Zeile nannte aber nur `td`.

   Sichtbar wurde es in der Vorschau: dort ist die erste Spalte ein
   `<th scope="row">` (der Zeilenkopf mit dem Spielernamen), die zwei
   Wertspalten sind `td`. In der letzten Zeile verloren die `td` ihre Linie,
   der `th` behielt sie - und die unterste Linie lief nur ueber die erste
   Spalte und brach dort ab, wo die Werte beginnen. Genau der gemeldete
   "abgeschnittene" Strich. */
tr:last-child td, tr:last-child th { border-bottom: none; }
tr:hover td { background: rgba(var(--md-sys-color-on-surface-rgb) / 0.04); }
/* ⚠️ HINTER der geteilten Regel und als Ausnahme fuer EINE Tabelle
   (2026-09-21): „Das Highlighten der Spielerzeile ist nicht notwendig."

   Die Regel darueber gilt fuer jede Tabelle im Haus - Benutzer, Turniere,
   Aufzeichnungen, Verlauf. Sie ersatzlos zu loeschen waere Fernschaden an
   Ansichten, die niemand angefasst haben wollte, und keine Zusicherung haengt
   daran, die es gemerkt haette. Die Spielerliste hat seit heute ihre eigene
   Rueckmeldung: die Pille um den Namen. Zwei Hervorhebungen fuer dieselbe
   Bewegung sind eine zu viel. */
.sp-tabelle tr:hover td { background: none; }

/* Zellen mit Knöpfen bleiben mittig – siehe die Begründung an `td` oben. */
td.zelle-mitte { vertical-align: middle; }
/* Der Name in der Spielerliste, RECHTS vom Bild und nicht darunter. Ein fester
   Einzug ginge nicht: die Bildgrösse hängt an `--avatar-scale` (in dieser
   Anlage 2,6), aus 24 px werden also 62. Deshalb ein Flex-Kasten.
   Die Auszeichnung stand vorher als Inline-Stil am `<a>`.

   ⚠️ `.sp-name-echt` ist am 2026-09-04 ENTFALLEN, und zwar überall gleich: es
   trug den echten Namen klein unter dem Spitznamen. Der Betreiber wollte
   entweder den Spitznamen ODER den Namen sehen, nie beides („Hinter seinem
   Nickname steht Abdalrhman - also sein Name. Das bitte Ändern."). Damit ist
   `App.playerLabel` die einzige Antwort auf die Frage, und `.sp-name-haupt`
   bleibt als einzige Zeile - es kostet nichts und hält den Namen als Block
   neben dem Bild. */
/* ⚠️ OHNE `color: var(--blue)` seit dem 2026-09-21. Der Betreiber: „In der
   Spieler-Übersicht sollen die Spielernamen nicht farblich abgesetzt sein.
   Wenn ich mit der Maus über den Namen gehe, soll wie auf der Startseite eine
   Pille um den Namen erscheinen."

   Die Pille ist `.player-link` (weiter unten) und kein neues Bauteil - es ist
   dieselbe, die die Startseite seit jeher benutzt. Sie liegt um den NAMEN und
   nicht um die Zeile: bei `--avatar-scale: 2.6` ist der Avatar 62 px hoch, und
   eine Pille darum waere ein Balken quer durch die Zelle. */
.sp-name { display: flex; align-items: center; font-weight: 600; }
.sp-name-haupt { display: block; }
/* Datum und Uhrzeit der letzten Anmeldung brechen nicht um: „29.8.2026," auf
   der einen und „07:44:14 Kennwort" auf der nächsten Zeile ist unlesbar. Die
   Spalte darf dafür breiter werden, die Tabelle scrollt notfalls (.table-wrap). */
.anm-zeit { white-space: nowrap; }

/* ── Tabs (primary, animierter Indikator) ─────────────────────────────────── */
.tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  /* 12 statt 24 seit dem 2026-09-21 („weniger Scrollen"): die Leiste trägt
     bereits eine Trennlinie, der Abstand darunter muss sie nicht noch einmal
     abgrenzen. Die Höhe der Reiter selbst (56 px, `--min-touch`) bleibt
     unberührt - das ist eine Trefffläche, kein Freiraum. */
  margin-bottom: 12px;
  /* ⚠️ Wischbar auf JEDER Breite und nicht erst unter 600 px
     (2026-09-10). Seit die neun globalen Reiter ein Symbol tragen, ist die
     Leiste der Einstellungsseite 1422 px breit - gemessen bei 1280 px
     Fensterbreite, wo 1270 px zur Verfuegung stehen. Ohne Symbole waren es
     1185 px, es passte also gerade noch.

     Ohne diese Zeilen schrumpfen die Knoepfe (Flex-Vorgabe), die
     Beschriftungen brechen um, und die Leiste wird zweizeilig und krumm; die
     Konto-Leiste mit ihren elf Reitern (2543 px) traf das schon vorher. Das
     Mittel ist dasselbe, das die schmale Ansicht seit jeher benutzt - es
     stand nur in der falschen Umbruchmarke. Eine dritte kommt damit NICHT
     dazu. */
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

/* ── Die zwei Pfeile an einer zu breiten Reiterleiste (2026-09-13) ───────────

   Der Betreiber: „Es ist für mich nur nicht zu erkennen, dass es nach Rechts
   noch mehr Einstellungen gibt. Ich möchte einen kleinen Pfeil überlagernd am
   rechten Rand sehen … Das Ganze soll für die Linke Seite genau gleich gelten."

   ⚠️ Der Wickel und nicht die Leiste trägt die Pfeile. Ein Kind mit
   `position: absolute` IN einem Rollbereich rollt mit und wäre nach zwei
   Reitern aus dem Bild; der Wickel steht still, die Leiste rollt darin.

   ⚠️ Die Leiste behält ihren Aussenabstand, der Wickel bekommt keinen — sonst
   stünden zwischen Leiste und Panel plötzlich 48 px. Deshalb hier KEIN
   `margin`.

   ⚠️ Und keine zweite `.tabs`-Regel in einer Umbruchmarke: die verbietet
   `tests/test_responsive.py` ausdrücklich („eine Fassung genuegt"). Alles hier
   hängt am Wickel und an den Pfeilen. */
.tabs-wrap { position: relative; }

/* ⚠️ `hidden` schaltet den Pfeil ab, und das Attribut braucht hier ein
   `!important`: `.btn`-artige Elemente bekommen im Haus ein `display`, das die
   Vorgabe des Browsers schlägt. Dieselbe Falle wie beim Speichern-Knopf der
   Einstellungen am 2026-09-08 — hier von vornherein vermieden, weil der Pfeil
   die meiste Zeit AUS ist. */
.tabs-pfeil[hidden] { display: none !important; }

.tabs-pfeil {
  position: absolute;
  top: 0;
  bottom: 1px;                      /* die Trennlinie der Leiste bleibt frei */
  width: 40px;
  display: flex;
  align-items: center;
  border: none;
  cursor: pointer;
  padding: 0;
  color: var(--md-sys-color-on-surface-variant);
  /* Ein Verlauf statt einer Fläche: man soll sehen, dass darunter Inhalt
     weiterläuft, und nicht, dass dort ein Knopf klebt. */
  /* ⚠️ `transparent` und kein rgba-Token: fuer `surface` gibt es keines
     (die -rgb-Fassungen decken error, primary, tertiary, scrim und die
     on-Farben ab). Moderne Browser rechnen Verlaeufe vormultipliziert,
     der alte Grauschleier zu `transparent` entsteht also nicht mehr. */
  background: linear-gradient(to right,
              var(--md-sys-color-surface) 45%, transparent 100%);
}
.tabs-pfeil.links  { left: 0;  justify-content: flex-start; }
.tabs-pfeil.rechts { right: 0; justify-content: flex-end;
  background: linear-gradient(to left,
              var(--md-sys-color-surface) 45%, transparent 100%);
}
.tabs-pfeil:hover { color: var(--md-sys-color-primary); }
.tabs-pfeil .msym { font-size: 1.5rem; }
/* Die Tastatur sieht, wo sie steht - der Rest des Hauses macht es genauso. */
.tabs-pfeil:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: -2px;
  border-radius: var(--md-sys-shape-corner-small);
}
.tab {
  flex: 0 0 auto;
  white-space: nowrap;
  padding: 12px 20px;
  min-height: var(--min-touch);
  border-radius: var(--md-sys-shape-corner-small) var(--md-sys-shape-corner-small) 0 0;
  font-size: var(--md-sys-typescale-title-small-size);
  font-weight: var(--md-sys-typescale-title-small-weight);
  letter-spacing: var(--md-sys-typescale-title-small-tracking);
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  transition: color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
  border: none;
  background: transparent;
}
.tab::before {  /* Indikator: wächst aus der Mitte (emphasized) */
  content: '';
  position: absolute;
  left: 12px; right: 12px; bottom: 0;
  height: 3px;
  /* ⚠️ 3px, und dafuer gibt es kein Token: die M3-Formleiter beginnt bei
     `corner-extra-small` (4px). Ein Reiter-Indikator von 3 px Hoehe mit 4 px
     Radius waere oben runder als hoch. Benannte Ausnahme. */
  border-radius: 3px 3px 0 0;
  background: var(--md-sys-color-primary);
  transform: scaleX(0);
  transition: transform var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-emphasized);
}
.tab.active { color: var(--md-sys-color-primary); }
.tab.active::before { transform: scaleX(1); }

/* ── Chips ────────────────────────────────────────────────────────────────── */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 16px;
  border: 1px solid var(--md-sys-color-outline-variant);
  /* Pille statt 8 px (2026-09-30, Entwurf). */
  border-radius: var(--md-sys-shape-corner-full);
  background: transparent;
  font-size: var(--md-sys-typescale-label-large-size);
  font-weight: var(--md-sys-typescale-label-large-weight);
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}
.chip .msym { font-size: 18px; margin-left: -6px; }
.chip.selected {
  /* Schwarz bzw. weiss - Auswahl ist der Akzent (M3-RULES Regel 2). */
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  border-color: transparent;
}

/* ── Segmented Buttons ────────────────────────────────────────────────────── */
.seg-group {
  display: inline-flex;
  /* Die Schiene des Entwurfs (2026-09-30): grauer Grund, das gewaehlte
     Segment hebt sich weiss davon ab - statt Umriss, Trennlinien und Haekchen. */
  border: none;
  background: var(--md-sys-color-surface-container-high);
  padding: 4px;
  gap: 4px;
  border-radius: var(--md-sys-shape-corner-full);
  overflow: hidden;
}
.seg-btn {
  border: none;
  background: transparent;
  font-size: var(--md-sys-typescale-label-large-size);
  font-weight: var(--md-sys-typescale-label-large-weight);
  color: var(--md-sys-color-on-surface-variant);
  padding: 10px 20px;
  min-height: var(--min-touch);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  border-radius: var(--md-sys-shape-corner-full);
}
/* ⚠️ Hier standen bis zum 2026-09-30 die Trennlinie zwischen den Segmenten
   und das M3-Haekchen im gewaehlten (als `::before`). In der Schiene traegt
   die weisse Flaeche die Auswahl; ein Haekchen daneben waere dieselbe
   Auskunft zweimal - und es verbreiterte das Segment beim Waehlen, der Text
   daneben sprang. */
.seg-btn.selected {
  background: var(--md-ext-color-schiene-aktiv);
  color: var(--md-sys-color-on-surface);
  font-weight: 600;
  box-shadow: var(--md-sys-elevation-level1);
}
.seg-btn .msym { font-size: 18px; }

/* ── Slider (styled range input, z. B. Sound-Lautstärke) ──────────────────── */
/* ⚠️ 32 px HOCH und nicht 16, seit dem 2026-09-11 (Mobilpruefung).
   Sichtbar bleibt die 16 px hohe Schiene - die Hoehe gilt dem GRIFFBEREICH.
   Gemessen im Reiter „Sounds" bei 390 px: 26 Regler zu 141 x 16 px. Einen
   16 px hohen Streifen mit dem Daumen zu treffen und dann zu ziehen ist am
   Telefon Gluecksache, und am Kickertisch (Touch) genauso.
   32 ist `--md-sys-comp-size-xs`, die Stufe der Hausleiter fuer ein
   nichtprimaeres Ziel. Die Schiene wird mit `margin-block: 8px` wieder
   mittig gesetzt, damit sich optisch nichts aendert. */
.slider { -webkit-appearance: none; appearance: none; height: 32px; background: transparent; cursor: pointer; }
.slider::-webkit-slider-runnable-track {
  height: 16px;
  margin-block: 8px;          /* die 32 px Griffbereich optisch ausgleichen */
  border-radius: var(--md-sys-shape-corner-small);
  background: linear-gradient(to right,
    var(--md-sys-color-primary) var(--fill, 50%),
    var(--md-sys-color-secondary-container) var(--fill, 50%));
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 4px; height: 32px;
  margin-top: -8px;
  border-radius: 2px;
  background: var(--md-sys-color-primary);
  box-shadow: 0 0 0 6px var(--md-sys-color-background);
}

/* ── Linear Progress ──────────────────────────────────────────────────────────

   Hier standen bis zum 2026-09-03 `.progress-linear`, `.progress-linear .bar`,
   `.progress-linear.indet .bar` und die Kehre `pl-indet` - ein
   Fortschrittsbalken, den niemand benutzt hat.

   ⚠️ Er stand auf der Löschliste von Paket 1.2 („der größte Block") und blieb
   trotzdem stehen. Die Ausnahme jener Aufräumrunde war „M3-Vokabular, das
   Paket 3 gleich benutzt" – und Paket 3 ist gebaut und hat es nicht benutzt.
   Damit gilt die Regel und nicht die Ausnahme.

   Das Muster ist nicht verloren: `docs/m3-muster.html` trägt eine eigene
   Fassung (die Seite ist die Referenz, nicht die Auslieferung), und
   `docs/M3-ABGLEICH.md` §8a nennt das Kit-Gegenstück `.md-progress-linear`
   mit seinen Maßen. Wer einen Balken braucht, findet dort beides – und baut
   ihn dann für einen Verbraucher, den es gibt.

   Der Spinner (`.spinner`) bleibt: den benutzt die Anwendung wirklich. */

/* ── Player grid ──────────────────────────────────────────────────────────── */
.player-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 12px;
}
.player-card {
  background: var(--md-sys-color-surface-container-low);
  border: 2px solid transparent;
  border-radius: var(--md-sys-shape-corner-large);
  padding: 16px 10px;
  text-align: center;
  cursor: pointer;
  color: var(--md-sys-color-on-surface);
  transition: background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              border-color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing);
  min-height: 88px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
html[data-theme="dark"] .player-card { background: var(--md-sys-color-surface-container-high); }
.player-card:hover { border-color: var(--md-sys-color-outline-variant); }
.player-card.selected {
  border-color: var(--md-ext-color-team1);
  box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset;
  background: var(--md-ext-color-team1-container);
  color: var(--md-ext-color-on-team1-container);
  transform: scale(1.03);
}
.player-card.selected-team2 {
  border-color: var(--md-ext-color-team2);
  box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset;
  background: var(--md-ext-color-team2-container);
  color: var(--md-ext-color-on-team2-container);
  transform: scale(1.03);
}
.player-card.disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
.player-avatar {
  width: calc(44px * var(--avatar-scale-lg, 1));
  height: calc(44px * var(--avatar-scale-lg, 1));
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Personen-Icon muss mit dem Kreis wachsen, sonst wirkt der Platzhalter
     neben echten Avataren zu klein. */
  font-size: calc(1.4rem * var(--avatar-scale-lg, 1));
}
.player-name { font-size: var(--md-sys-typescale-label-large-size); font-weight: var(--md-sys-typescale-label-large-weight); word-break: break-word; }
.player-nick { font-size: var(--md-sys-typescale-body-small-size); color: var(--md-sys-color-on-surface-variant); }
.player-card.selected .player-nick, .player-card.selected-team2 .player-nick { color: inherit; opacity: 0.75; }

/* ── Active game view ─────────────────────────────────────────────────────── */
/* Vier Zeilen: Kopf, Spielzeit, Spielstand, Fuss. Die Spielzeit hat ihre eigene
   Zeile bekommen (2026-08-31) - vorher sass sie rechts in der Kopfzeile und
   stand damit ueber der Mitte einer Flex-Zeile, deren Breite von den Knoepfen
   abhing, nicht ueber der Mitte des Fensters. Die Kopfzeile wird dadurch
   niedriger, die Gesamthoehe bleibt also etwa gleich - wichtig, weil #game-view
   eine feste Hoehe hat und der Spielstand nur den Rest bekommt. */
#game-view {
  display: grid;
  /* ⚠️ VIER Zeilen, und das `1fr` liegt am ENDE - seit dem 2026-09-07.
     Vorher waren es drei mit dem `1fr` beim Spielstand.

     Die Ansicht hat aber VIER Kinder in dieser Spalte (Kopf, Spielstand,
     Statusleiste, Torprotokoll). Das vierte fiel in eine implizite Zeile und
     lief unten aus dem Raster - der 600er-Block nannte das ausdruecklich und
     half sich mit `height: auto`. Gemessen bei 1280 x 900: die Seite rollte
     senkrecht.

     Und das `1fr` gehoert nicht zum Spielstand: der soll seit dem 2026-09-07
     INHALTSHOCH sein (siehe `.game-score-area`, "der Kasten ist zu hoch").
     Bekaeme er trotzdem den Rest, stuende zwischen ihm und der Statusleiste
     eine Luecke. Das Torprotokoll ist die Liste, die wirklich waechst - es
     bekommt den Platz und rollt in sich. */
  grid-template-rows: auto auto auto 1fr;
  /* ⚠️ KEINE feste Hoehe mehr, seit dem 2026-09-10. Der Betreiber: „In
     dem Laufenden Spiel wird das Torprotokoll in der Hoehe begrenzt. Das soll
     aber so nicht sein. Das soll immer weiter laufen."

     Hier stand `height: calc(100dvh / var(--ui-zoom,1) - var(--nav-h) - 48px)`,
     und das Torprotokoll rollte in sich (`overflow-y: auto` unten). Am Telefon
     war die Zeile schon seit jeher aufgehoben - jetzt gilt fuer alle Breiten,
     was dort galt: die SEITE rollt, die Liste laeuft weiter.

     Das `1fr` in der letzten Zeile bleibt trotzdem stehen. Es verteilt jetzt
     nichts mehr (bei `height: auto` gibt es keinen Rest), aber es ist die
     VIERTE Zeile - und ohne sie fiele ein viertes Kind in eine implizite
     Zeile. Genau diese Falle hat am 2026-09-07 zugeschlagen. */
  gap: 12px;
}
/* ⚠️ ZWEI SPALTEN, seit dem 2026-09-07: der Anfeuer-Verlauf steht rechts
   neben dem Spiel statt darunter. Der Betreiber: "die Historie des Anfeuerns
   und der Zurufe nicht unter den Spielern sehen, sondern rechts am Rand."

   ⚠️ `:has()` und keine Klasse am Markup, aus zwei Gruenden: die Spalte gibt
   es nur, wenn die Reaktionen ueberhaupt eingeschaltet sind
   (`App.state.reaktionenAn`), und eine leere zweite Spalte kostete 12 px
   Abstand am rechten Rand. Eine Klasse muesste game.js an derselben Bedingung
   setzen - der Selektor liest sie stattdessen ab. Der Praezedenzfall ist
   `#content:has(> .nr-raster)` weiter oben.

   ⚠️ UND JEDES KIND WIRD AUSDRUECKLICH PLATZIERT. Ohne das verteilt die
   automatische Platzierung sechs Kinder reihum auf zwei Spalten - der
   Spielstand landete neben der Kopfzeile. Genau diese Falle hat schon einmal
   zugeschlagen, als das Torprotokoll als viertes Kind in eine implizite Zeile
   fiel (siehe den 600er-Block weiter unten).

   Die Breite: 220 px sind zwei Zurufe nebeneinander lesbar, 24 vw halten den
   Anteil bei breiten Fenstern, 340 px sind die Grenze - darueber nimmt die
   Spalte dem Spielstand mehr, als der Verlauf wert ist. */
/* ⚠️ AM TISCH STEHT ALLES UNTEREINANDER, seit dem 2026-09-08. Der
   Betreiber: „Die Zurufe und Anfeuerungen sollen am Tisch (Localhost) nicht
   rechts am Rand stehen. Diese sollen Unterhalb der Spieler zwischen
   Torprotokoll und Spielern stehen. Remote soll alles so bleiben wie es ist!"

   Der Umbau vom 2026-09-07 war richtig fuer den Netz-Zuschauer und falsch
   fuer den Tisch: dort ist niemand angemeldet, es gibt keine Sendeleiste, und
   die Spalte nahm dem Spielstand Breite, sobald jemand aus dem Netz
   angefeuert hatte.

   Das braucht KEINE neue Reihenfolge im Markup - die ist schon richtig:
   Kopf, Spielstand, Spalte, Statusleiste, Torprotokoll. Am Tisch genuegt es,
   in EINER Spur zu bleiben.

   ⚠️ FUENF Zeilen und nicht vier. Die Grundregel an `#game-view` hat vier
   (`auto auto auto 1fr`), weil dort nur vier Kinder in einer Spur stehen -
   die Spalte lag ja in der zweiten. In einer Spur sind es fuenf, und das
   fuenfte fiele in eine implizite Zeile und liefe unten aus dem Raster.
   Genau diese Falle ist am 2026-09-07 schon einmal zugeschlagen (siehe die
   Begruendung an `grid-template-rows` oben).

   ⚠️ Und JEDES Kind wird ausdruecklich platziert, auch hier: ist die Spalte
   `display: none` (am Tisch, solange niemand angefeuert hat), faellt sie aus
   dem Raster - mit automatischer Platzierung rutschte das Torprotokoll dann
   in Zeile 4 und bekaeme nicht mehr das `1fr`. Die leere Zeile 3 kostet die
   12 px des `gap`, und das ist derselbe Fall wie bei der verborgenen
   Statusleiste, die es hier schon immer gibt. */
#game-view:has(> .rk-spalte) {
  grid-template-rows: auto auto auto auto 1fr;
}
#game-view:has(> .rk-spalte) > .game-header     { grid-row: 1; }
#game-view:has(> .rk-spalte) > .game-score-area { grid-row: 2; }
#game-view:has(> .rk-spalte) > .rk-spalte       { grid-row: 3; }
#game-view:has(> .rk-spalte) > .game-statusbar  { grid-row: 4; }
#game-view:has(> .rk-spalte) > .card            { grid-row: 5; }

/* ⚠️ ZWEI SPUREN NUR AUS DEM NETZ (`body.nicht-am-tisch`), und positiv
   geschrieben aus demselben Grund wie ueberall im Projekt: `:not(.am-tisch)`
   traefe jeden Besucher, solange `/api/auth/context` noch nicht geantwortet
   hat. `app.js` setzt beide Klassen genau deshalb. */
body.nicht-am-tisch #game-view:has(> .rk-spalte) {
  /* ⚠️ `auto` und nicht `clamp(...)` direkt, damit eine VERBORGENE Spalte
     keine Spur bleibt: die Breite steht an der Spalte selbst (unten), und ist
     sie `display: none`, faellt die Spur auf 0. Mit der Breite in der Spur
     stuenden 338 px Leere da - gemessen, siehe die Regel weiter unten. */
  grid-template-columns: 1fr auto;
  grid-template-rows: auto auto auto 1fr;
}
/* ⚠️ Die Kopfzeile endet, wo die SPIELER enden - seit dem 2026-09-10.

   Der Betreiber: „Die Aktion Livestream ist Mittig ueber dem Rechten Spieler
   in der Liveansicht. Der soll aber Rechtsbuendig ueber dem rechten Spieler
   sein."

   Bis dahin stand hier `1 / -1`, die Kopfzeile lief also ueber BEIDE Spalten,
   waehrend der Spielstand nur die erste bekommt. Gemessen bei 1280 px: die
   Spieler enden bei 846, die Kopfzeile erst bei 1245 - und damit stand nicht
   nur der Livestream-Knopf falsch, sondern die ganze Aktionsleiste
   („Beenden", „Abbrechen") bei 956 bis 1245, also ueber der Anfeuer-Spalte
   statt ueber dem Spiel. Aufgefallen ist nur der Knopf, weil er der einzige
   ist, den man aus dem Netz auch benutzen darf.

   Mit `grid-column: 1` endet die Kopfzeile buendig mit dem rechten Spieler.
   Zeile 1 der zweiten Spalte bleibt leer - die Anfeuer-Spalte beginnt
   ohnehin erst in Zeile 2. */
body.nicht-am-tisch #game-view:has(> .rk-spalte) > .game-header    { grid-column: 1; grid-row: 1; }
body.nicht-am-tisch #game-view:has(> .rk-spalte) > .game-score-area { grid-column: 1; grid-row: 2; }
body.nicht-am-tisch #game-view:has(> .rk-spalte) > .game-statusbar { grid-column: 1; grid-row: 3; }
body.nicht-am-tisch #game-view:has(> .rk-spalte) > .card           { grid-column: 1; grid-row: 4; }
/* ⚠️ Hier stand bis zum 2026-09-10 `overflow-y: auto` - das Torprotokoll
   rollte in sich, damit es die Seite nicht verlaengert. Genau das ist jetzt
   gewollt: die Liste laeuft weiter, und statt ihrer bleiben Kopf und
   Spielstand stehen (unten). `min-height: 0` bleibt: die automatische
   Mindesthoehe eines Rasterkindes ist sein Inhalt, und das ist hier richtig. */
#game-view > .card { min-height: 0; }

/* ⚠️ AM TISCH BLEIBT DAS TORPROTOKOLL AUS (2026-09-17). Der Betreiber: „Am
   Tisch selbst, nimmt die einfach zu viel Platz weg." Remote bleibt es - dort
   ist es der einzige Weg, den Verlauf zu lesen, ohne danebenzustehen.

   ⚠️ AUSGEBLENDET, nicht entfernt: `_renderLiveGoals` und elf Zusicherungen in
   tests/test_torhistorie.py haengen daran.

   ⚠️ `body.am-tisch` POSITIV - die verneinte Form traefe jeden, solange der
   Kontext noch nicht geantwortet hat, und das Protokoll spraenge remote
   nachtraeglich ins Bild. Dieselbe Begruendung wie bei `.rk-anfeuern`.

   ⚠️ Und diese Regel steht HINTER der Grundregel darueber (Falle ④): mehrere
   Zusicherungen schneiden an `#game-view > .card {`. */
body.am-tisch #game-view > .card { display: none; }

/* ── Kopf und Spielstand bleiben oben stehen ──────────────────────────
   Entschieden mit dem Betreiber am 2026-09-10: ohne die feste Hoehe rollt die
   Seite, und ohne diese Regeln waere der Spielstand nach zehn Toren aus dem
   Bild. Wer alte Tore liest, will weiter sehen, wie es gerade steht.

   ⚠️ Ein Rasterkind KANN kleben - das war die offene Frage, und sie ist
   gemessen: bei 60 Toren und vier Scrollstaenden (200, 600, 1200, 2400 px)
   steht der Kopf auf 0 und der Stand unmittelbar darunter. (Die Sorge war,
   dass ein Rasterkind nur innerhalb SEINER Rasterzelle kleben darf und damit
   gar nicht.)

   ⚠️ Die Klebehoehe des Spielstands wird GEMESSEN, nicht geraten: die
   Kopfzeile bricht bei schmalen Fenstern um (`flex-wrap`), ihre Hoehe ist also
   keine Konstante. `App.game._klebehoehe()` schreibt sie als `--gv-kopf-h`;
   der Vorgabewert hier gilt, solange sie noch nicht gemessen ist. Dasselbe
   Muster wie `--ui-zoom` (js/display.js) und `--cd-h` (js/chatdock.js).

   ⚠️ `margin-top` gegen den Rasterabstand: zwischen den zwei Zeilen
   liegen 12 px `gap`, und die gehoeren keinem der beiden Kaesten. Ohne die
   negative Aussenkante blieb dort ein durchsichtiger Streifen, durch den die
   Tore hindurchrollen. Der Innenabstand stellt den Abstand im Kasten wieder
   her, den die negative Kante genommen hat. */
#game-view > .game-header {
  position: sticky;
  top: var(--nav-h);
  z-index: 40;              /* unter #navbar (100) und dem Modal (400) */
  background: var(--md-sys-color-background);
}
#game-view > .game-score-area {
  position: sticky;
  top: calc(var(--nav-h) + var(--gv-kopf-h, 56px));
  z-index: 39;              /* eine Stufe unter dem Kopf */
  background: var(--md-sys-color-background);
  margin-top: -12px;
  padding-top: 12px;
}
/* ⚠️ Die Anfeuer-Spalte reicht von Zeile 2 bis unten (`grid-row: 2 / -1`).
   Ohne diese Zeile wuerde sie mit der Seite mitwachsen: bei zwanzig Toren
   waere sie ueber 2000 px hoch, und ihre Sendeleiste stuende ganz unten am
   Seitenende. `start` laesst sie ihre Inhaltshoehe behalten. */
/* ⚠️ `stretch` statt `start`, seit dem 2026-09-17 - und das ist die Reparatur
   einer Regel, die eine andere aufhob.

   Am 2026-09-08 bekam die Zuruf-Leiste `margin-top: auto`, damit sie „nicht
   oben links oder bei Eingaben nach unten wandert". Am 2026-09-10 kam hier
   `align-self: start` dazu, damit die Spalte „nicht bei zwanzig Toren ueber
   2000 px waechst". Nur: `start` macht das Rasterkind INHALTSHOCH, und in
   einer inhaltshohen Flex-Spalte hat `margin-top: auto` nichts zu verteilen.
   Die Leiste klebte seither wieder oben.

   GEMESSEN am 2026-09-17, 1280 px, remote: Spaltenhoehe 56 px, Unterkante der
   Spielerkiste 634 px, Unterkante der Leiste 212 px - 422 px zu hoch.

   `stretch` gibt der Spalte eine DEFINITE Hoehe (die der Zeile), und damit
   wirkt `margin-top: auto` wieder. Der Zweck von 2026-09-10 bleibt erfuellt:
   der Verlauf rollt, statt zu wachsen - nur ueber eine definite Hoehe statt
   ueber Inhaltshoehe. */
body.nicht-am-tisch #game-view:has(> .rk-spalte) > .rk-spalte { align-self: stretch; }
/* ⚠️ Und sie bleibt beim Scrollen stehen (2026-10-01). Der Betreiber: „Wenn
   ich die [Torhistorie] hoch und runter scrolle, scrollt die Eingabe der
   Zurufe mit. Die soll aber fixiert bleiben." `sticky` an der ganzen Spalte:
   Verlauf und Eingabe bleiben oben im Bild, solange die Spielansicht laeuft.
   Erst ab 841 px - darunter steht die Spalte in EINER Spur ueber der ganzen
   Breite und laege beim Scrollen auf dem Torprotokoll. */
@media (min-width: 841px) {
  body.nicht-am-tisch #game-view:has(> .rk-spalte) > .rk-spalte {
    position: sticky; top: calc(var(--nav-h) + 12px);
  }
}
/* Die Spalte reicht vom Spielstand bis zum unteren Rand - sie soll die ganze
   Hoehe haben, damit der Verlauf Platz hat, nach oben zu wachsen. */
/* ⚠️ `grid-row: 2` und nicht mehr `2 / -1` (2026-09-17): die Spalte endet mit
   der SPIELERKISTE statt mit der Seite. Der Betreiber wollte die Leiste „am
   unteren Ende der Spieler-Kiste ausgerichtet" und „von dort auch nicht weiter
   runterwandern".

   ⚠️ Damit erledigt sich sein zweiter Satz von selbst: stehen die Einblicke
   UNTER den Kisten (`app_lage = unten`), sind sie ein Kind derselben Flaeche
   (`.game-score-area` mit `grid-column: 1 / -1`). Zeile 2 waechst mit ihnen,
   und die Leiste richtet sich an ihnen aus - ohne Fallunterscheidung und ohne
   eine Zeile JavaScript. */
body.nicht-am-tisch #game-view:has(> .rk-spalte) > .rk-spalte      { grid-column: 2; grid-row: 2; }
.rk-spalte {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;          /* sonst ist die Mindesthoehe der Inhalt, und der Verlauf gibt nicht nach */
}
/* 220 px sind zwei Zurufe nebeneinander lesbar, 24 vw halten den Anteil bei
   breiten Fenstern, 340 px sind die Grenze - darueber nimmt die Spalte dem
   Spielstand mehr, als der Verlauf wert ist.

   ⚠️ Nur aus dem Netz: am Tisch steht die Spalte in EINER Spur ueber die
   ganze Breite, und 340 px waeren dort eine Saeule am linken Rand. */
body.nicht-am-tisch .rk-spalte { width: clamp(220px, 24vw, 340px); }
/* ⚠️ EINE LEERE SPALTE NIMMT KEINEN PLATZ, und das ist ein GEMESSENER
   Befund vom 2026-09-07, nicht eine Vorsichtsmassnahme.

   Am Kickertisch ist niemand angemeldet: dort gibt es keine Sendeleiste, und
   der Verlauf ist leer, solange niemand angefeuert hat. Die Spalte enthielt
   damit ein Element mit `display: none` - sie selbst war 338 px breit und
   vollstaendig leer. Nachgemessen bei 1280 px, in einem laufenden Spiel.

   Vorher fiel das nicht auf, weil der Verlauf eine ZEILE ueber die ganze
   Breite war: `.rk-feed:empty { display: none }` liess dort schlicht nichts
   uebrig.

   Die zwei Bedingungen sind die Spiegelung dessen, was game.js rendert: keine
   Sendeleiste (nicht angemeldet) UND ein leerer Verlauf heisst "nichts zu
   zeigen". Beide sind LEBEND - kommt der erste Zuruf, trifft `:empty` nicht
   mehr, und die Spalte erscheint ohne eine Zeile JavaScript. */
.rk-spalte:not(:has(> .rk-leiste)):has(> .rk-feed:empty) { display: none; }
.game-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  /* ⚠️ Bezugsrahmen fuer die Klapptafel des Aktionsmenues am Telefon
     (`.ak-knopf[aria-expanded="true"] + .game-controls` im 600er-Block
     schwebt darunter). Hier im
     Grundabschnitt und nicht in der Breitenabfrage: eine Positionierung, die
     nur manchmal gilt, ist schwerer zu lesen als eine, die immer gilt - und
     wirkungslos ist sie ohne ein positioniertes Kind ohnehin.
     Nachgesehen: in dieser Kopfzeile ist nichts absolut positioniert, die
     Angabe aendert also heute nichts. */
  position: relative;
}
/* Turnier, Spielzeit, „Pausiert" und die Ausfallmeldung - der linke Block der
   Kopfzeile.

   ⚠️ Bis zum 2026-09-03 war das eine eigene Rasterzeile MITTIG ueber beiden
   Mannschaften, und die Begruendung dafuer war gut: rechts in einer Flex-Zeile
   sass die Uhr ueber der Mitte der Knoepfe und nicht ueber der Mitte des
   Fensters. Das ist weiter wahr - der Betreiber wollte trotzdem den Platz, und
   an der Stelle der Spielart steht die Uhr links und nicht in einer Mitte, die
   von Knopfbreiten abhaengt.

   Zwei Ebenen: aussen untereinander (Turnier ueber der Uhr, wie vorher die
   Spielart), innen NEBENeinander (Uhr, „Pausiert", Ausfallmeldung). Damit ist
   die Kopfzeile genau so hoch wie vorher - zwei Textzeilen - und die dritte
   Rasterzeile entfaellt. */
.game-uhr {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}
.game-uhr-zeile {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}
.game-timer {
  /* Kleiner als die 2,4 rem von vorher: die Uhr sitzt jetzt in der Kopfzeile
     und nicht in einer eigenen Zeile - wuerde sie ihre alte Groesse behalten,
     waere die Kopfzeile hoeher und der gesparte Platz wieder weg. Der
     Spielstand darunter ist mit `clamp(5rem, 15vw, 9rem)` ohnehin die Zahl,
     die den Raum beherrscht. */
  font-size: 2rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface);
  min-width: 90px;
  /* Linksbuendig, seit die Uhr in der Kopfzeile steht: zentriert innerhalb
     ihrer 90 px wanderte die Zahl beim Wechsel von 9:59 auf 10:00 nach links.
     Am linken Rand steht sie fest. */
  text-align: left;
}
.game-timer.urgent { color: var(--md-sys-color-error); animation: pulse 0.8s infinite; }
.game-controls { display: flex; gap: 8px; }
.game-score-area {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  /* ⚠️ DIE MASSE DER TORTASTEN STEHEN HIER und nicht an `.goal-btn`
     (2026-09-13). Der Betreiber: „Das Eigentor darf gerne zentriert und
     größer sein. Gerne in der halben höhe wie das + und - | Der Like
     Button soll auch die Größe von + und - haben.“

     „Halb so hoch“ und „genauso gross“ sind damit GERECHNET und nicht
     abgeschrieben: Tortaste, Eigentor und Like lesen dieselbe Zahl, und
     die zwei Breitenblöcke am Dateiende ändern nur sie. Drei
     abgeschriebene Zahlenpaare wären beim nächsten Block wieder
     auseinandergelaufen.

     ⚠️ An der Fläche und nicht an `.team-panel`, damit auch der
     Like-Knopf sie erbt: der steht seit dem 2026-09-13 als erstes Kind
     der Kiste und nicht mehr unter der Punktzeile. */
  --tor-w: 130px;
  --tor-h: 100px;
  /* ⚠️ `align-content: start`, seit dem 2026-09-07. Der Betreiber: "Der
     Kasten um den jeweiligen Spieler sind jetzt nur zu hoch, bitte reduziere
     die Höhe, dass der Abstand von den beiden Action Buttons + und - den
     gleichen Abstand zu untern Rand haben, wie der Avatar zum oberen Rand."

     Die Kisten haben KEINE Hoehenangabe - sie kam von aussen: `#game-view`
     hat eine feste Hoehe, dieser Kasten steht in der `1fr`-Zeile und wurde
     darauf gestreckt, und `.team-panel` hat kein `justify-content`. Der ganze
     Restplatz lag damit UNTER den Tor-Knoepfen.

     `align-content: start` macht die (implizite) Zeile dieses Rasters
     inhaltshoch, statt sie zu strecken. Die zwei Kisten bleiben dabei GLEICH
     hoch - sie strecken weiter auf diese Zeile, die Zeile richtet sich aber
     nach der hoeheren der beiden. `align-items: start` waere das Falsche
     gewesen: dann waere jede Kiste einzeln so hoch wie ihr Inhalt, und zwei
     verschieden lange Namen ergaeben zwei verschieden hohe Kisten. */
  align-content: start;
}
/* ⚠️ `min-width: 0` an den Kisten, seit dem 2026-09-04. Eine `1fr`-Spalte hat
   die Mindestbreite ihres INHALTS, und seit Minus, Zaehler und Plus in einer
   Zeile stehen, ist dieser Inhalt bei zweistelligem Stand breiter als die
   halbe Flaeche. Gemessen bei 390 px: die linke Kiste nahm sich 206 px, die
   rechte bekam 177 - und die Seite lief quer. Mit `min-width: 0` gibt die
   Spalte nach, und was darin nachgibt, ist der Zaehler (`flex: 1`). */
.game-score-area > .team-panel { min-width: 0; }

/* ══ Einblicke: Bilanz, Siegchance, Form, Rekorde, Tempo (2026-09-16) ══════

   Der Betreiber: „Ich moechte am Tisch oder Remote im Spiel ein paar Insights
   / Statistiken sehen. Die koennen Links und oder Rechts am Rand zusaetzlich
   erscheinen."

   ⚠️ SIE SIND EIN KIND DER SPIELSTANDFLAECHE UND NICHT VON #game-view, und
   das ist die tragende Entscheidung. `tests/test_community.py` nagelt das
   Raster von #game-view fest: dort darf „grid-template-columns" gar nicht
   vorkommen, und das Torprotokoll muss in der letzten Zeile stehen - die
   Zusicherung schuetzt sein `1fr`, das schon zweimal in eine implizite Zeile
   gefallen ist. Hier drin gibt es dagegen schon ein Raster, und beide Lagen
   sind darin eine Zeile.

   ⚠️ Die Lage-Klasse traegt der BEHAELTER, nicht die Flaeche: drei
   Zusicherungen suchen die Zeichenkette `class="game-score-area"` woertlich.
   Die Flaeche liest sie per `:has()` ab - derselbe Griff wie bei
   `#game-view:has(> .rk-spalte)` weiter oben. */
.ek-kacheln {
  display: flex;
  gap: 8px;
  min-width: 0;
}
.ek-kachel {
  flex: 1 1 auto;
  min-width: 0;
  /* ⚠️ `overflow: hidden` ist kein Schmuck: ohne das schiebt der laengste
     Kacheltext („aus 2:6 zurueck") die Mindestbreite des Behaelters nach
     aussen durch, und die zwei Spielerkisten geben nach. Das Optionale gibt
     nach, nicht das Spiel. */
  overflow: hidden;
  padding: 8px 10px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
}
.ek-titel {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: var(--md-sys-typescale-label-small-size);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}
.ek-titel .msym { font-size: 15px; }
.ek-wert {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ek-zusatz {
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Eine Kachel ohne Wert bleibt STEHEN und zeigt einen Strich. Verschwaende
   sie, rueckten die anderen nach - und die Spalte sprenge bei jedem Tor. */
.ek-kachel.is-leer .ek-wert { color: var(--md-sys-color-on-surface-variant); }
/* Wem der Wert gerade gilt. Dieselben zwei Farben wie die Spielerkisten - so
   sieht man ohne zu lesen, auf welcher Seite die Zahl steht. */
/* Kontur-Ring zusaetzlich zum seitlichen Streifen (Mehrfachschatten, durch
   Komma getrennt) - derselbe Grund wie bei `.sd-punkt` oben. */
.ek-kachel[data-seite="team1"] { box-shadow: inset 3px 0 0 var(--md-ext-color-team1), inset 0 0 0 1px var(--md-ext-color-figur-kontur); }
.ek-kachel[data-seite="team2"] { box-shadow: inset 3px 0 0 var(--md-ext-color-team2), inset 0 0 0 1px var(--md-ext-color-figur-kontur); }
/* Derselbe kurze Sprung wie am Spielstand, wenn sich eine Zahl aendert. */
.ek-wert.bump, .ek-zusatz.bump { animation: ek-bump .4s ease; }
@keyframes ek-bump { 0% { transform: scale(1); } 40% { transform: scale(1.08); } 100% { transform: scale(1); } }

/* ── Lage „unten": eine Zeile quer unter beiden Kisten ───────────────────── */
.ek-kacheln.ek-unten { grid-column: 1 / -1; flex-wrap: wrap; }
.ek-kacheln.ek-unten > .ek-kachel { flex: 1 1 140px; }

/* ── Lage „links": eine eigene Spur links neben den Kisten ────────────────
   ⚠️ Die Tortasten geben mit nach - sonst stuenden dort 130er Tasten und die
   Namen fielen heraus.

   ⚠️ DIE ZAHL IN DIESEM KOMMENTAR WAR FALSCH, und sie war falsch, weil sie
   gerechnet und nicht gemessen war: hier stand „352 px je Kiste bei 841 px“.
   Nachgemessen am 2026-09-14 (Treiberbefehl `breiten`, ein laufendes Spiel,
   alle Werte in CSS-Pixeln):

     Fenster   Spur   Kiste am Tisch   Kiste remote (Anfeuer-Spalte daneben)
      841 px   112          290          174   (Spalte 220)
      900 px   112          317          201   (Spalte 220)
     1000 px   120          358          232   (Spalte 240)
     1129 px   135          409          267   (Spalte 271)
     1280 px   154          468          309   (Spalte 307)

   Die Rechnung hatte den Abstand des Rasters und das Polster der Kiste
   unterschlagen und remote die Anfeuer-Spalte ganz vergessen. Der Name wird
   in keinem dieser Faelle gekuerzt (`scrollWidth` gegen `offsetWidth`), und
   waagerecht laeuft nichts ueber. */
.game-score-area:has(> .ek-kacheln.ek-links) {
  grid-template-columns: var(--ek-w, clamp(112px, 12vw, 200px)) 1fr 1fr;
  --tor-w: clamp(76px, 9vw, 130px);
  --tor-h: clamp(64px, 7vw, 100px);
}
.ek-kacheln.ek-links {
  grid-column: 1;
  grid-row: 1;
  flex-direction: column;
  /* ⚠️ `align-self: start` und `flex: 0 0 auto` an den Kacheln: ohne beides
     streckt das Raster die Spalte auf die Hoehe der Spielerkisten, und die
     fuenf Kacheln verteilen die ueberschuessige Hoehe unter sich - gemessen
     90 px hohe Kaesten mit 30 px Inhalt. Sie sollen sich an ihren Inhalt
     schmiegen und oben stehen bleiben. */
  align-self: start;
}
.ek-kacheln.ek-links > .ek-kachel { flex: 0 0 auto; }
/* Team-Panels: Container-Farbrollen (Team 1 = primary, Team 2 = tertiary) */
.team-panel {
  border: none;
  border-radius: var(--md-sys-shape-corner-extra-large);
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
/* ⚠️ AM TISCH ENGER (2026-09-17). Der Betreiber: „Auf der Seite des aktiven
   Spiels am Localhost, soll der Kasten der Spieler nicht ganz so hoch sein.
   […] wir gewinnen in Summe nach unten Platz."

   ⚠️ Nur am Tisch - und das rettet eine Messung: die 20 px oben stammen vom
   2026-09-07 („der Abstand der Tortasten zum unteren Rand gleich dem des
   Avatars zum oberen"), und `tests/test_spielaktionen.py` haelt sie fest.
   Remote gilt sie unveraendert; am Tisch, wo der Platz knapp ist, wird sie
   bewusst enger gezogen. Die Symmetrie bleibt dabei erhalten - es schrumpfen
   BEIDE Raender gleich.

   ⚠️ Der Like-Knopf bleibt, wie er ist: er liest `--tor-w`/`--tor-h` wie die
   Tortasten, und „genauso gross wie + und −" war eine Ansage vom 2026-09-13.
   Was hier schrumpft, ist der Rand, nicht das Bedienelement.

   ⚠️ `body.am-tisch` POSITIV - siehe die Regel am Torprotokoll. Und HINTER
   der Grundregel (Falle ④). */
body.am-tisch .team-panel { padding: 12px; gap: 8px; }
.team-panel.team1 { background: var(--md-ext-color-team1-container); color: var(--md-ext-color-on-team1-container); }
.team-panel.team2 { background: var(--md-ext-color-team2-container); color: var(--md-ext-color-on-team2-container); }
/* ⚠️ Hier standen bis zum 2026-09-05 `.team-label` und
   `.team1 .team-label, .team2 .team-label`. Die Ueberschriften "Spieler 1" und
   "Spieler 2" sind aus der Spielansicht entfallen (Wunsch des Betreibers, die
   Begruendung steht am Markup in components/game.js), und damit hatten die
   zwei Regeln kein Element mehr.

   Sie stehenzulassen waere kein kleiner Preis: `.team-label` ist in
   docs/SPEZIFIKATION.md und in tests/test_spieler_statt_team.py ausdruecklich
   als TECHNISCHER Name genannt, der die Umbenennung "Team -> Spieler"
   ueberlebt hat. Ein technischer Name ohne Traeger schickt den naechsten Leser
   auf die Suche nach einem Element, das es nicht gibt. */
/* Kein opacity mehr: der Container trägt jetzt die großen Avatare (.team-players-big) */
.team-players { font-size: var(--md-sys-typescale-body-medium-size); text-align: center; width: 100%; }

/* Große Spieleranzeige in der laufenden Partie – Avatar über dem Namen.
   Referenz ist die Kiosk-Live-Ansicht (20vh), hier eine Nummer größer.
   Der vw-Anteil verhindert, dass die beiden Avatare eines 2v2-Teams breiter
   werden als ihr halbes Panel und in die nächste Zeile umbrechen.
   dvh/vw werden durch --ui-zoom geteilt, weil `zoom` auf <body> sie sonst
   zusätzlich mitskaliert. */
.team-players-big {
  --gt-av-size: clamp(96px, min(calc(24dvh / var(--ui-zoom, 1)),
                                calc(20vw / var(--ui-zoom, 1))), 300px);
  display: flex;
  gap: 18px;
  justify-content: center;
  align-items: flex-start;
  flex-wrap: wrap;
  width: 100%;
}
.gt-p {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.gt-av {
  width: var(--gt-av-size);
  height: var(--gt-av-size);
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 3px solid rgba(var(--md-sys-color-on-surface-rgb) / 0.18);
  background: rgba(var(--md-sys-color-on-surface-rgb) / 0.08);
}
.gt-av-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(var(--md-sys-color-on-surface-rgb) / 0.45);
}
.gt-av-empty .msym { font-size: calc(var(--gt-av-size) * 0.55); }
.gt-name {
  font-size: clamp(1rem, calc(2.4dvh / var(--ui-zoom, 1)), 1.9rem);
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  overflow-wrap: anywhere;
}

/* ── Live-Prognose: „#3 → #2 ↑  1012  (+16)" ───────────────────────────────
   Wird nur gefüllt, wenn das Spiel zu einem Turnier gehört; :empty entfernt
   sonst auch den Flex-Abstand. */
.gt-proj {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: .4em;
  /* Bewusst gross: das ist im laufenden Spiel die Information, auf die alle
     schauen. Vorher clamp(.8rem, 1.8dvh, 1.3rem) - aus zwei Metern nicht mehr
     lesbar. Die Teilung durch --ui-zoom bleibt, sonst wirkt die
     Oberflaechenskalierung doppelt. */
  font-size: clamp(1.4rem, calc(3.4dvh / var(--ui-zoom, 1)), 2.6rem);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  transition: transform var(--md-sys-motion-duration-short4)
                        var(--md-sys-motion-easing-standard);
}
.gt-proj:empty { display: none; }
.gt-proj.bump { transform: scale(1.1); }
.gt-proj.is-unrated { opacity: .6; }

/* Die Prognosezeile in der Vorschau und im Endbanner-Umfeld.

   Ohne diese Regel liefen die Teile zusammen: `#1` und `1237` standen als
   `#11237` da. `projLine` verbindet seine Teile ohne Trenner, und der Abstand
   kam allein vom Pfeil-Glyph - fehlte also genau dann, wenn sich die
   Platzierung NICHT aendert.

   Dieselben Werte wie bei `.gt-proj` in der Live-Ansicht (flex, baseline,
   gap: .4em), damit beide Orte gleich aussehen - dort war es von Anfang an
   richtig, nur `.proj-line` hatte ueberhaupt keine Regel. */
.proj-line {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .4em;
}
.proj-rank { opacity: .55; }
.proj-rank.is-now { opacity: 1; }
.proj-sep { opacity: .45; font-weight: 400; }
.proj-val { font-weight: 800; }
.proj-tag {
  font-size: .78em;
  font-weight: 600;
  opacity: .6;
  text-transform: uppercase;
  letter-spacing: .04em;
}
/* Farbe ist nie das einzige Signal – Pfeil und Vorzeichen tragen dasselbe */
.proj-dir.is-up,   .proj-delta.is-up   { color: var(--md-ext-color-success); }
.proj-dir.is-down, .proj-delta.is-down { color: var(--md-sys-color-error); }
.proj-dir.is-flat, .proj-delta.is-flat { opacity: .6; }

/* ── Trend in den Ranglisten ──────────────────────────────────────────────────
   Ausgang des letzten Spiels (S/N/U) und Platzentwicklung seither. Gefuellt von
   App.rating.standingTrend; erscheint in allen Ranglisten. Buchstabe UND Farbe,
   damit die Information nicht allein an der Farbe haengt. */
/* Spaltentitel der Kurzrangliste auf der Startseite. Stehen im GLEICHEN Raster
   wie die Zeilen, damit sie zwangsläufig darüber sitzen. Bewusst zurückhaltend:
   die Karte soll nicht wie eine Tabelle wirken. */
/* Die Kopfzeile der Startseiten-Rangliste haengt an ihrer Ueberschrift, die
   Spielerzeilen teilen sich den Rest.

   ⚠️ Vorher verteilte `align-content: space-evenly` die geschenkte Kartenhoehe
   auf ALLE Zeilen - und die Kopfzeile ist eine davon. Gemessen am 2026-09-16
   bei 1280 px: 55 px zwischen Ueberschrift und Spaltentiteln, davon 31 aus
   dieser Verteilung. Der Betreiber: „Ich moechte, dass die beiden Zeilen viel
   dichter zusammen sind."

   `grid-template-rows: auto` macht die ERSTE Zeile inhaltshoch, `grid-auto-rows:
   1fr` gibt jeder weiteren denselben Anteil an der Restluft. Die Karte verteilt
   ihre Hoehe also weiterhin - der Wunsch vom 2026-09-02 gilt unveraendert -,
   nur die Beschriftung zaehlt nicht mehr als Zeile mit.

   ⚠️ Die Spalten bleiben INLINE: ihre Zahl haengt davon ab, ob das
   Bewertungssystem eine Anteilsspur hat, und `tests/test_responsive.py` laesst
   genau dieses eine Inline-Raster in dashboard.js zu. Hier stehen nur die
   Zeilen. */
/* ⚠️ EIN TAG HIN UND ZURÜCK - und der Rückweg ist die eigentliche Lektion.

   Am Vormittag des 2026-09-21 stand hier `grid-auto-rows: 1fr`. Der
   Betreiber:

     „Mir fällt auf, dass auf der Startseite das Ranking der Spieler zu viel
     Abstand zwischen den Spielern hat. Wieso ist das so? Wenn Avatare laut
     Einrichtung größer angezeigt werden, ist das vielleicht okay. Aber es
     soll nicht einfach so der Platz verschwendet werden."

   Die Antwort auf das „wieso" war richtig und gilt weiter: es gibt keinen
   Abstand und kein Polster. `1fr` gibt jeder Zeile denselben Anteil an der
   Restluft, und die Restluft stammt von der Karte nebenan („Letzte Spiele"
   mit fünf zweizeiligen Einträgen, siehe `.card-fill`). Bei wenigen Spielern
   ist dieser Anteil groß.

   Die Zeile bekam daraufhin ein festes Maß am Avatar:
   `minmax(min-content, calc(20px * var(--avatar-scale, 1) + 12px))`.

   ⚠️ UND DAS BRACH DIE STARTSEITE IM BETRIEB. Dieses Raster ist ein Grid
   mit `flex: 1` in einer gestreckten Karte. Es hat GENAU EINE `auto`-Spur -
   die Kopfzeile (`grid-template-rows: auto`) -, und der Vorgabewert
   `align-content: normal` bedeutet STRETCH. Stretch verteilt die geschenkte
   Höhe ausschließlich auf `auto`-Spuren. Also auf die Kopfzeile, und auf sie
   allein. Solange `1fr` dastand, war nie etwas übrig; mit einem festen Maß
   blieb der ganze Überschuss stehen, und die Spaltentitel schwebten mittig in
   einer mehrere hundert Pixel hohen Spur.

   GEMESSEN am 2026-09-21 mit `getComputedStyle(...).gridTemplateRows`, nicht
   mit `getBoundingClientRect` (Falle ⑤). Die Nachbarkarte wurde dabei
   erzwungen hoch gemacht:

     Nachbarkarte     Kopfzeile     Spielerzeile
     286 px            16,3 px         64 px
     600 px           330    px        64 px
     800 px           530    px        64 px

   Eins zu eins der Überschuss.

   ⚠️ WARUM ES HIER NIEMAND SAH: im Entwicklungsbestand ist die
   Ranglistenkarte die HÖHERE der beiden - drei Spieler, drei letzte Spiele,
   kein Überschuss. Sichtlauf und 2705 Zusicherungen blieben grün. Die
   Messung am Vormittag hatte die Nachbarkarte zwar erzwungen hoch gemacht,
   aber nur die ZWEITE Spur abgelesen - die Spielerzeile, 64 px. Die stimmte.
   Die erste hat niemand angesehen.

   ⚠️ VERWORFEN, obwohl es wirkt: das feste Maß behalten und
   `align-content: start` danebenstellen. Nachgemessen am 2026-09-21, die
   Kopfzeile bleibt damit bei 16,3 px, auch bei 800 px Nachbarkarte. Der
   Betreiber hat es abgelehnt, nachdem er das Fehlerbild gesehen hatte: der
   Rest säße dann als rund 250 px hohes Loch unter dem letzten Spieler.
   Zwischen „Zeilen etwas zu weit auseinander" und „ein sichtbares Loch in der
   Karte" hat er die weiteren Zeilen gewählt.

   ⚠️ WER DAS FESTE MASS WIEDER EINBAUEN WILL, MUSS `align-content: start`
   MITBRINGEN. `1fr` ist nicht nur eine Höhenangabe, es ist zugleich das
   einzige, was die Restluft überhaupt aufbraucht. Eine Zusicherung in
   `tests/test_dashboard_standings.py` hält dieses Paar seither zusammen.

   ⚠️ `grid-template-rows: auto` bleibt: die Kopfzeile hängt an ihrer
   Überschrift und ist keine Spielerzeile (2026-09-16, 31 px). */
/* ⚠️ SEIT DEM 2026-09-30 KOMPAKT (Neugestaltung): der freigegebene Entwurf
   zeigt feste Zeilen von 56/59 px und die Karte endet unter dem letzten
   Platz. Die Entscheidung vom 2026-09-21 („lieber weitere Zeilen als ein
   Loch") ist damit hinfaellig - der Betreiber: „Die alten Entscheidungen sind
   hinfaellig." Das Paar aus festem Mass und `align-content: start` bleibt
   zusammen, wie es der Waechter verlangt. */
.mini-rang {
  grid-template-rows: auto;
  grid-auto-rows: auto;
  align-content: start;
}

.mini-head {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}

.st-trend {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  vertical-align: middle;
  /* Nicht zusammenquetschen lassen: der Trend steht in Flex-Zeilen neben einem
     Namen, der per Ellipse gekuerzt wird. */
  flex-shrink: 0;
}
.st-res {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.55em;
  height: 1.55em;
  padding: 0 .25em;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: .78em;
  font-weight: 800;
  line-height: 1;
}
.st-res.is-win  { background: var(--md-ext-color-success-container, #C6F0D0);
                  color: var(--md-ext-color-on-success-container, #04361A); }
.st-res.is-loss { background: var(--md-sys-color-error-container);
                  color: var(--md-sys-color-on-error-container); }
.st-res.is-draw { background: var(--md-sys-color-surface-variant);
                  color: var(--md-sys-color-on-surface-variant); }

/* ── Die Form-Kachel der Einblicke (2026-09-15) ──────────────────────────────
   Der Betreiber sah dort "N N N N N / gegen S N N N N" und fragte, was das
   sein soll. Dieselbe Aussage steht in der Rangliste seit jeher als farbige
   Pille mit Buchstaben - in der Kachel war sie nackter Text.

   ⚠️ Diese Regeln stehen HIER und nicht oben bei den uebrigen .ek-*-Regeln:
   `.ek-form-pillen .st-res {` enthaelt die Zeichenkette `.st-res {`. Weiter
   oben eingefuegt traefe `css.index(".st-res {")` diese Regel statt der
   Ranglistenregel - Falle ④ aus CLAUDE.md, und sie braeche eine Zusicherung,
   die mit dieser Runde nichts zu tun hat.

   ⚠️ Und .st-res selbst wird NICHT angefasst. Die Regel gilt in vier
   Ranglisten und im Kiosk; sie kleiner zu machen, um eine Kachel zu retten,
   waere Fernschaden. Uebersteuert wird nur im Kachelkontext. */
.ek-wert.ek-hat-pillen {
  display: flex;
  flex-direction: column;
  gap: 3px;
  /* Die Grundregel setzt nowrap samt Ellipse - fuer eine Textzeile richtig,
     fuer zwei Pillenreihen faengt sie die Reihe ab, bevor sie umbrechen kann. */
  white-space: normal;
}
.ek-form-zeile {
  display: flex;
  align-items: center;
  /* ⚠️ Umbruch der ZEILE, nicht der Kette (2026-09-22). Passt die Kette nicht
     neben den Namen, wandert sie als Block darunter. Gemessen vorher: bei 200 px
     Kastenbreite stand je eine Kugel in einer eigenen Zeile, fuenf Reihen hoch,
     und der Name rutschte unter die Kugelspalte. */
  flex-wrap: wrap;
  /* 4px und nicht 8: bei 841 px hat die Kachel 92 px Innenraum, und die
     Marke (3) plus fuenf Pillen (5 x 15) plus vier Zwischenraeume (4 x 1)
     brauchen 86. Gemessen am 2026-09-15 - mit 5px Abstand und 16px Pillen
     brach die Reihe um, und die Kachel wuchs von 71 auf 105 px. */
  gap: 4px;
  min-width: 0;
}
/* Wem die Reihe gilt - dieselben zwei Farben wie die Spielerkisten und wie
   `.ek-kachel[data-seite]`. Damit faellt das Wort "gegen" weg: die Zuordnung
   macht die Farbe, und in 112 px passt ohnehin kein Name. */
.ek-form-marke {
  flex: 0 0 3px;
  align-self: stretch;
  min-height: 1.1em;
  border-radius: 2px;
  background: var(--md-sys-color-outline-variant);
}
/* Figurenfarbe + Kontur-Ring, siehe `.sd-punkt` oben. */
.ek-form-zeile[data-seite="team1"] .ek-form-marke,
.ek-form-zeile[data-seite="team2"] .ek-form-marke { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.ek-form-zeile[data-seite="team1"] .ek-form-marke { background: var(--md-ext-color-team1); }
.ek-form-zeile[data-seite="team2"] .ek-form-marke { background: var(--md-ext-color-team2); }
/* Der Name steht nur, wo Platz ist: in der Lage `unten` und am Flurmonitor.
   In der 112-px-Spur traegt die Farbmarke die Zuordnung allein. Die
   Entscheidung faellt im CSS und nicht im JavaScript - so ist das Markup in
   beiden Lagen dasselbe, und der Zwillingstest vergleicht eine Sache. */
/* ⚠️ FESTE Spur statt inhaltsbreit, seit dem 2026-09-17. Der Betreiber: „In
   Abhaengigkeit der Namenslaenge des Spielers, faengt die Kette an einer
   anderen Stelle an und hoert an einer anderen auf."

   `flex: 0 0 7em` statt `0 1 auto`: dieselbe Breite wie das bisherige
   `max-width`, nur jetzt auch als Mindestmass. Fuer lange Namen aendert sich
   nichts - sie wurden schon bei 7em abgeschnitten; kurze schieben die Kette
   nicht mehr nach links. */
.ek-form-wer {
  flex: 0 0 7em;
  max-width: 7em;
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 500;
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ek-kacheln.ek-links .ek-form-wer { display: none; }
.ek-form-pillen {
  display: flex;
  gap: 1px;
  /* ⚠️ Das Sicherheitsnetz: bei 841 px hat die Spur 92 px Innenraum, fuenf
     Pillen brauchen rund 90. Und `form_anzahl` laesst bis zu zehn Spiele zu -
     dann MUSS umgebrochen werden, sonst schneidet `overflow: hidden` an der
     Kachel die letzten Pillen lautlos ab. Die Sichtpruefung findet das nicht:
     sie meldet abgeschnittenen Text nur fuer Elemente mit eigenem Textknoten. */
  flex-wrap: wrap;
  /* ⚠️ `max-content` und nicht `auto`: die Zeile trifft ihre Umbruchentscheidung
     damit an der UNGEBROCHENEN Breite der Kette. Ohne das brechen die Kugeln
     einzeln um, statt gemeinsam unter den Namen zu wandern - genau der Fehler
     vom 2026-09-22. In der zweiten Zeile darf sie dann wieder schrumpfen
     (`min-width: 0`), und erst dort greift das Netz darueber. */
  flex-basis: max-content;
  flex-shrink: 1;
  min-width: 0;
}
/* Kleiner als in der Rangliste - dort steht die Pille einzeln in einer Zeile,
   hier stehen fuenf bis zehn davon in einer 112-px-Spur. */
.ek-form-pillen .st-res {
  font-size: 10px;
  min-width: 1.5em;
  height: 1.5em;
  padding: 0 .2em;
  font-weight: 700;
}
/* ⚠️ DER PLATZHALTER fuer ein noch nicht gespieltes Spiel (2026-09-17). Der
   Betreiber wollte „als Platzhalter einfach nur ein leerer Kreis".

   ⚠️ HINTER der Grundregel darueber (Falle ④), und ausdruecklich NICHT an
   `.st-res` selbst: das faerbt vier Ranglisten und den TV-Kanal mit.

   ⚠️ `border-radius: 50%` und kein px-Wert - `tests/test_kiosk_mobil.py`
   laesst fuer diese Pillen nur die Prozentform zu.

   Der Kreis ist ein Umriss ohne Fuellung: er soll die Stelle halten, nicht um
   Aufmerksamkeit bitten. */
.ek-form-pillen .st-res.is-leer {
  background: transparent;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: 50%;
  opacity: .55;
}

/* ── Die Kette der letzten Spiele in der Rangliste (2026-09-15) ─────────────
   Dieselben Masse wie in der Einblick-Kachel darueber, und das ist Absicht:
   der Betreiber wollte in der Rangliste „genau das, was im laufenden Spiel
   steht". Die Zahlen sind dort gemessen und verteidigt - der erste Anlauf mit
   11-px-Pillen brach bei 841 px um.

   ⚠️ Bewusst NICHT mit `.ek-form-pillen` zusammengefasst. Zwei Gruende: die
   Rangliste haenge sonst an einer Klasse, die nach der Kachel benannt ist,
   und `css.count(".ek-form-pillen .st-res {") == 1` verlangt den Selektor
   allein stehend. Dafuer diese Notiz in beiden Regeln. */
.st-kette {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  /* Nicht zusammenquetschen lassen - dieselbe Begruendung wie bei .st-trend:
     die Kette steht in einer Flexzeile neben einem Namen mit Ellipse. */
  flex-shrink: 0;
}
.st-kette .st-res {
  font-size: 10px;
  min-width: 1.5em;
  height: 1.5em;
  padding: 0 .2em;
  font-weight: 700;
}
.st-move { font-size: .85em; font-weight: 700; }
.st-move.is-up   { color: var(--md-ext-color-success); }
.st-move.is-down { color: var(--md-sys-color-error); }
.st-move.is-flat { color: var(--md-sys-color-on-surface-variant); opacity: .75; }
.st-move.is-new  { color: var(--md-sys-color-on-surface-variant);
                   font-size: .72em; font-weight: 700;
                   text-transform: uppercase; letter-spacing: .06em; }

/* Flache Displays: dort passen Avatar + Prognosezeilen sonst nicht in die
   feste Höhe von #game-view. Bei Full HD bleiben die Avatare unangetastet. */
@media (max-height: 900px) {
  #game-view.has-proj .team-players-big {
    --gt-av-size: clamp(80px, min(calc(18dvh / var(--ui-zoom, 1)),
                                  calc(17vw / var(--ui-zoom, 1))), 240px);
  }
}
.team-score {
  /* ⚠️ `flex: 1` und `text-align: center`, seit dem 2026-09-04 - und beides
     hat denselben Grund: der Zaehler steht jetzt ZWISCHEN den zwei
     Tor-Knoepfen (siehe `.team-punkte`), und aus dem Netz verschwinden die.

     Ohne `flex: 1` waeren dann drei Kinder mit `space-between` ploetzlich
     eines, und ein einzelnes Kind rutscht bei `space-between` an den linken
     Rand - der Spielstand haette fuer jeden Zuschauer schief gestanden. So
     nimmt der Zaehler immer die Mitte, mit Knoepfen und ohne.

     ⚠️ `flex: 1 0 auto` und nicht `flex: 1`, seit dem 2026-09-04 abends -
     und die Begruendung, die hier vorher stand, war FALSCH. Sie lautete: "Was
     bei Platznot nachgibt, ist der Zaehler und nicht der Knopf." Genau das war
     der Fehler. `flex: 1` heisst `1 1 0%`: Grundgroesse 0, also bekommt der
     Zaehler nur UEBRIGEN Platz - und schrumpfen kann er bis auf nichts,
     waehrend die Schrift bei `15vw` gross bleibt. Die Ziffer quoll dann aus
     ihrem Kasten heraus.

     Gemessen an einem Freundschaftsspiel, ueber die ganze Breitenskala: bei
     830 px war der Kasten 62 px breit und die Ziffer 78 - sie lief bis 315,
     der Plus-Knopf begann bei 308. Bei 880 px Kasten 29 gegen Ziffer 82. Von
     etwa 601 bis 950 px UEBERLAPPTE die Ziffer den Knopf; bei 1280 px, wo ich
     es zuerst gemessen hatte, passte es zufaellig.

     Jetzt gibt der KNOPF nach (`flex-shrink: 1`, Boden `min-width: 64px` an
     `.team-punkte .goal-btn`) und der Zaehler behaelt seine Ziffernbreite
     (`shrink: 0`, Grundgroesse `auto`). Er waechst weiter in freien Platz
     hinein (`grow: 1`) und bleibt damit mittig. */
  /* ⚠️ `flex: 1 0 auto` ist am 2026-09-11 an `.team-mitte` gewandert: der
     Zaehler ist seither nicht mehr das direkte Kind der Punktzeile, sondern
     steht mit dem Eigentor-Knopf in einer Spalte. Wer den Rest der Zeile
     bekommen soll, muss ihr Kind sein. */
  min-width: 0;
  text-align: center;
  font-size: clamp(5rem, 15vw, 9rem);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  transition: transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing);
}
.team-score.pop { transform: scale(1.15); }
.team1 .team-score, .team2 .team-score { color: inherit; }

/* ⚠️ DER ZAEHLER FOLGT SEINER KISTE UND NICHT DEM FENSTER, solange die Spur
   steht - und auch das ist gemessen und nicht vorgesorgt.

   `.team-score` ist `clamp(5rem, 15vw, 9rem)` gross, also am FENSTER
   bemessen. Die Kiste haengt aber an der Spur: bei 841 px ist das Fenster
   unveraendert 841 px breit, die Kiste jedoch 290 statt 352 px (remote gar
   174). Mit einem EINSTELLIGEN Stand faellt das nicht auf - der Befund kam
   erst, als die Messung „10“ erzwungen hat:

     Fenster   Fall            Innenraum   Ziffer „10“   Ueberlauf der Zeile
      841 px   ohne Kacheln       312/196       155              0
      841 px   links, Tisch           250       155             35
      841 px   links, remote          134       155             60   (*)
      900 px   links, remote          161       166             46
     1000 px   links, remote          192       177             29
     1280 px   links                428/269     177              0

   (*) Dort lief die Ziffer 15 px aus der Kiste heraus. Das ist derselbe
   Fehler wie am 2026-09-04 an den Tor-Knoepfen und am 2026-09-08 am
   Anfeuer-Knopf, nur eine Ebene hoeher: nicht ein Kind sprengt die Zeile,
   sondern die Zeile ihren Kasten.

   `29cqw` ist der gemessene Anteil und keine runde Zahl: der Ziffer bleibt
   die Kiste minus Polster (40) minus die zwei Tortasten auf ihrem Boden
   (2x64 + 16 Abstand) - bei 290 px also 106, und „10“ ist 1,23 mal so breit
   wie seine Schriftgroesse. `min(15vw, ...)` behaelt die alte Obergrenze:
   die Ziffer wird nie GROESSER als heute, nur kleiner, wenn die Kiste es
   verlangt.

   ⚠️ `min-width: 841px` ist KEINE neue Umbruchmarke, sondern die Rueckseite
   der vorhandenen: unterhalb davon nimmt der 840er Block die Spur ohnehin
   zurueck, die Kiste ist dann wieder so breit wie die halbe Flaeche, und der
   Zaehler soll dort weiter am Fenster haengen. Als `max-width`-Rueck­nahme
   geschrieben muesste diese Regel den 600er Block mit ueberstimmen, der dem
   Zaehler am Telefon 2.6rem gibt.

   ⚠️ `container-type: inline-size` ist an der Kiste gefahrlos: sie enthaelt
   nichts absolut Gesetztes (nachgesehen - der Like-Knopf ist ein Flex-Kind),
   und ihre Breite kommt ohnehin vom Raster und nie vom Inhalt. */
@media (min-width: 841px) {
  .game-score-area:has(> .ek-kacheln.ek-links) > .team-panel {
    container-type: inline-size;
  }
  .game-score-area:has(> .ek-kacheln.ek-links) .team-score {
    font-size: clamp(2.6rem, min(15vw, 29cqw), 9rem);
  }
  /* ⚠️ AUS DEM NETZ IST MEHR PLATZ, und die 29 waeren dort zu streng: der
     Anteil ist fuer die zwei Tortasten gerechnet, und die stehen nur am
     Tisch. Gemessen bei 1280 px remote: Kiste 309, davon Polster 40 und
     Anfeuer-Knopf 71 - der Ziffer bleiben 190, sie bekam aber nur 96. Mit 45
     sind es 171, und bei 1920 px greift wieder die Obergrenze von 9rem.

     ⚠️ `body.nicht-am-tisch` POSITIV und nicht `:not(.am-tisch)`: die zweite
     Fassung traefe jeden Besucher, solange `/api/auth/context` noch nicht
     geantwortet hat - am Tisch waere die Ziffer dann kurz zu gross und die
     Punktzeile liefe ueber. Dieselbe Begruendung steht am Anfeuer-Knopf. */
  body.nicht-am-tisch .game-score-area:has(> .ek-kacheln.ek-links) .team-score {
    font-size: clamp(2.6rem, min(15vw, 45cqw), 9rem);
  }
}
/* ── Tor-Knoepfe UM den Zaehler (2026-09-04) ────────────────────────────────
   Wunsch des Betreibers: "links minus rechts plus jeweils linksbuendig und
   rechtsbuendig neben dem Torzaehler des jeweiligen Spielers."

   Vorher stand der Zaehler darueber und die zwei Knoepfe als eigene Reihe
   darunter (`.goal-buttons`, `gap: 32px`). Jetzt umfassen sie ihn: der Daumen
   findet beide an der Kante der Kiste, ohne die Hand zu verschieben, und die
   Kiste wird eine Reihe kuerzer.

   `space-between` drueckt Minus an die linke und Plus an die rechte Kante -
   "linksbuendig und rechtsbuendig" woertlich. `align-self: stretch` ist
   dafuer noetig: `.team-panel` ist eine Flex-Spalte mit `align-items: center`,
   und ohne das waere diese Zeile nur so breit wie ihr Inhalt - dann gaebe es
   nichts, wovon sich etwas absetzen koennte.

   ⚠️ Die Knoepfe schrumpfen NICHT (`flex-shrink: 0`): sie sind 130 px breit
   und die Treffflaeche am Tisch, und ein Zaehler mit drei Stellen ("10") darf
   sie nicht zusammendruecken. Was bei Platznot nachgibt, ist der Zaehler -
   er hat `flex: 1` und `min-width: 0`. */
.team-punkte {
  display: flex;
  /* ⚠️ `end` und nicht `center`, seit dem 2026-09-07 - die zweite Haelfte
     der Antwort auf "der Kasten ist zu hoch" (die erste steht an
     `.game-score-area`).

     Die Zeilenhoehe folgt dem ZAEHLER und nicht den Knoepfen: die Ziffer ist
     `clamp(5rem, 15vw, 9rem)` (bei 1280 px also 144 px), der Knopf 100 px.
     Mittig ausgerichtet sassen die Knoepfe damit 22 px ueber der Unterkante
     der Zeile - und dieser Rest kam zum Polster der Kiste hinzu. Der Abstand
     nach unten war 42 px, der des Avatars nach oben 20.

     Mit `end` sitzen die Knoepfe auf der Unterkante, und ihr Abstand zum
     Kistenrand ist genau das `padding: 20px` von `.team-panel` - also
     derselbe wie oben. Gemessen bei 1280 x 900. */
  align-items: end;
  justify-content: space-between;
  align-self: stretch;
  gap: 8px;
}
/* ⚠️ `flex-shrink: 1` mit Boden, seit dem 2026-09-04 abends. Vorher stand
   hier `flex-shrink: 0` mit der Begruendung, 130 px seien die Treffflaeche am
   Tisch und ein zweistelliger Zaehler duerfe sie nicht zusammendruecken. Am
   Tisch stimmt das - dort ist das Fenster 1920 px breit und nichts schrumpft.
   Nur galt die Regel auch bei 610 px, und dort passen 2x143 px Knopf plus
   Zaehler nicht in 232 px Innenraum: die Ziffer lief unter den Plus-Knopf
   (siehe die Messung an `.team-score`).

   64 px ist der Boden, und es ist keine neue Zahl - der Telefonblock gibt den
   Knoepfen ohnehin genau diese Groesse. Geschrumpft wird also nur zwischen
   130 und 64 px, und nur wenn es sonst nicht passt. */
.team-punkte .goal-btn { flex-shrink: 1; min-width: 64px; }

/* ── Anfeuern in der Spielerkiste (2026-09-08) ──────────────────────────────
   Der Knopf steht dort, wo aus dem Netz das Minus bzw. das Plus fehlt: bei
   `team1` als erstes, bei `team2` als letztes Kind von `.team-punkte`.

   ⚠️ AM TISCH IST ER VERBORGEN, und diese Regel macht explizit, was bisher
   nur der Zufall leistete. Bis zum 2026-09-08 hingen die Anfeuer-Knoepfe
   ALLEIN an `App.state.user` - es gab keine einzige Regel, die sie an den Ort
   band. Am Tisch fehlten sie nur FAKTISCH, weil dort niemand angemeldet ist;
   wer sich dort anmeldet, haette Anfeuern UND Minus/Plus im selben Slot. Der
   Betreiber wollte sie ausdruecklich nur aus dem Netz.

   ⚠️ `body.am-tisch` POSITIV und nicht `:not(.nicht-am-tisch)`: die zweite
   Fassung traefe jeden Besucher, solange `/api/auth/context` noch nicht
   geantwortet hat, und der Knopf flackerte weg und wieder her.

   ⚠️ `flex-shrink: 0` mit `min-width`: der Zaehler ist `flex: 1 0 auto` und
   waechst in den Rest - er bleibt also mittig, obwohl aus dem Netz nur noch
   zwei Kinder in der Zeile stehen. Genau die Vorsorge, die an `.team-score`
   ausgeschrieben ist: „ein einzelnes Kind rutscht bei `space-between` an den
   linken Rand." */
/* ⚠️ DER KNOPF MUSS IN DEN KASTEN PASSEN, und das ist ein GEMESSENER Befund
   vom 2026-09-08 - er war der Grund fuer drei Entscheidungen hier.

   Gemessen (CSS-Pixel, Zoom 1,25, angemeldet aus dem Netz) steht neben der
   Ziffer je Seite so viel Platz zur Verfuegung:

     Fenster   Kasten   Ziffer   je Seite frei
     1920 px   458      208      117
     1440 px   330       80      117
     1280 px   282       80       93
     1000 px   204       80       54
      900 px   174       75       41
      841 px   150       70       32
      840 px   266       70       90   (die Spalte faellt nach unten)
      390 px   167       31       60

   Der Knopf mit Symbol und Wort war 117 px breit - ab 1280 px passte er nicht
   mehr, und weil `flex-shrink: 0` daranstand, LIEF DIE ZEILE UEBER: die Ziffer
   sass bei 841 px um 106 px neben der Mitte. Genau derselbe Fehler wie am
   2026-09-04 bei den Tor-Knoepfen, und dort steht die Kette schon
   ausgeschrieben (siehe `.team-score`).

   Drei Dinge folgen daraus:

   1. `flex-shrink: 1` mit `min-width: 0`. Der Knopf gibt nach, statt die
      Zeile zu sprengen - und weil das Gegengewicht dasselbe tut, bleibt die
      Ziffer dabei MITTIG. Gemessen: Versatz 0 bei jeder Breite der Tabelle,
      und kein Ueberlauf.
   2. KEIN SYMBOL, und das ist gemessen und nicht Geschmack. Das Megafon
      kostete 22 px; mit ihm war der Knopf 105 px breit und das Wort stand
      erst ab 1440 px ganz da. 1280 px ist die Breite, an der dieses Projekt
      seit immer misst - dort muss das Wort stehen, denn es ist der
      ausdrueckliche Wunsch ("In dem Buttons soll nur 'Anfeuern' stehen").
   3. DAS WORT ist 12 px gross (`0.75rem`) und nicht die 14 px von `.btn-sm`.
      Damit braucht es 53 statt 62 px, und es steht bis etwa 1130 px
      vollstaendig (gemessen bei 1130: 71 px Knopf, Wort ganz) - zwei
      Breitenstufen mehr, ohne dass es klein wirkt: der
      Knopf daneben ist ohnehin ein Nebenweg und keine Hauptaktion.
   4. Und wenn es trotzdem nicht passt, kuerzt DAS WORT mit
      Auslassungspunkten (`.rk-anfeuern-text`), nicht der Knopf.

   ⚠️ **EIN BAND BLEIBT, und es gehoert benannt: 841 bis 1129 px.**
   Gemessen: bei 1000 px 54 px je Seite, bei 900 px 41, bei 841 px 32 - fuer
   das Wort zu wenig, mit oder ohne Symbol. Dort nimmt die Anfeuer-Spalte
   220-270 px, und die zwei Kisten teilen sich den Rest. Das liesse sich nur mit einer DRITTEN Umbruchmarke loesen, und die
   erlaubt dieses Projekt ausdruecklich nicht
   (`tests/test_responsive.py::test_only_two_width_breakpoints`). Bei 840 px
   ist es wieder gut, weil die Spalte dort nach unten faellt.

   ⚠️ Das Polster WAECHST MIT (`clamp(2px, 1vw, 10px)`) und ist nicht fest.
   Bei 1280 px sind es die 10 px, die das Wort ganz zeigen; im schmalen Band
   schrumpft es und gibt dem Wort die letzten Pixel. */
.rk-anfeuern {
  flex: 0 1 auto;
  min-width: 0;
  padding-inline: clamp(2px, 1vw, 10px);
  overflow: hidden;
}
.rk-anfeuern-text {
  font-size: 0.75rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
body.am-tisch .rk-anfeuern { display: none; }

/* ── „Livestream" im laufenden Spiel: NUR aus dem Netz (2026-09-10) ─────────
   Der Betreiber: „Wenn Remote ein Spiel angeschaut wird, soll über den
   Spieler die Aktion 'Livestream' zu Verfügung stehen. […] Die Aktion steht
   nur Remote und nicht am Tisch (Localhost) zu Verfügung."

   ⚠️ Dieselbe Bauart wie beim Anfeuer-Knopf darüber, und aus demselben Grund:
   `data-requires-table` kennt nur „grau" und „weg" und ist damit die falsche
   Richtung. Gebraucht wird das Gegenteil - sichtbar nur AUSSERHALB des
   Tisches.

   ⚠️ `body.am-tisch` POSITIV und nicht `:not(.nicht-am-tisch)`: die zweite
   Fassung träfe jeden Besucher, solange /api/auth/context noch nicht
   geantwortet hat. Der Knopf wäre einen Moment weg und erschiene dann - die
   Begründung steht ausgeschrieben bei den Menüpunkten weiter oben. */
body.am-tisch .gv-livestream { display: none; }
/* ⚠️ Und er haengt sich an die Aktionsleiste, statt in der Mitte zu
   schweben (2026-09-10). `.game-header` ist ein Flex mit
   `justify-content: space-between`, und am Bildschirm sind darin drei
   sichtbare Kinder: Uhr, dieser Knopf, Steuerleiste. `space-between` setzt
   das mittlere genau in die Mitte - der Knopf hatte nie eine eigene
   Lageregel, seine Lage war ein Nebeneffekt.

   `margin-left: auto` ist das Hausmuster dafuer (`.page-header >
   .page-actions`, `.nr-g-aktionen`): eine automatische Aussenkante schluckt
   den freien Platz VOR `space-between` und schiebt damit alles ab hier nach
   rechts.

   ⚠️ Die Kante sitzt am ERSTEN Kind der rechten Gruppe, und das ist
   am Bildschirm die STEUERLEISTE - nicht dieser Knopf, der seit dem
   2026-09-10 hinter ihr steht. Zwei automatische Kanten waeren ein Fehler
   und sind es beim Bauen auch gewesen: sie TEILEN sich den freien Platz.
   Gemessen mit beiden: die Leiste stand bei 285 bis 574 und der Knopf bei
   696 bis 846 - 122 px Loch dazwischen, und die Leiste klebte fast an der
   Uhr.

   Der Burgerknopf taugt nicht als Traeger: er ist am Bildschirm
   `display: none` und hat gar keine Box, an der eine Aussenkante wirken
   koennte.

   ⚠️ Am Telefon ist es umgekehrt, deshalb steht die Kante dort an
   diesem Knopf (600er-Block): fuer Remote ohne Adminrecht verbirgt der
   Block die ganze Leiste, und dann ist er das einzige rechte Kind.

   Zusammen mit `grid-column: 1` an der Kopfzeile (oben) endet die Gruppe
   buendig mit dem rechten Spieler - gemessen 846 zu 846. */
.game-header > .game-controls { margin-left: auto; }
/* ⚠️ Das Gegengewicht auf der anderen Seite des Zaehlers - GEMESSEN, nicht
   vorsorglich. Ohne es sass die Ziffer bei 1280 px um 78 px (die halbe
   Knopfbreite) zu weit rechts: der Zaehler ist `flex: 1 0 auto`, sein Kasten
   begann hinter dem Knopf und endete an der Kante, und der mittige Text folgte
   dem Kasten. Die Begruendung im Markup nennt auch, warum es ein `span` ist. */
.rk-anfeuern-platz { visibility: hidden; }

/* ── „Eigentor“ in der Punktzeile (2026-09-11) ───────────────────
   Der Betreiber wollte ihn zwischen die zwei Tortasten. Er steht deshalb IN
   dieser Zeile - und zwar in der MITTELSPALTE, unter der Zahl.

   ⚠️ Das ist die zweite Fassung. Die erste setzte ihn zwischen Zaehler und
   Plus, mit einem unsichtbaren Gegengewicht auf der anderen Seite - dieselbe
   Bauart wie beim Anfeuer-Knopf darueber. Gemessen am 2026-09-11:

     * 840 px: Knopf 18 px breit, Beschriftung abgeschnitten, und die
       Tortasten von 130 auf ihre Untergrenze 64 px gedrueckt;
     * 1280 px: Knopf 59 px, ebenfalls abgeschnitten.

   Der Grund ist Arithmetik: ein Gegengewicht kostet die Breite ZWEIMAL, und
   eine Spielerkiste hat bei 840 px 333 px innen. In der Mittelspalte kostet
   der Knopf gar keine Breite - er steht unter der Zahl, die die Mitte ohnehin
   belegt. Der Zaehler bleibt damit ohne jeden Ausgleich mittig, und die
   Messung des Anfeuer-Gegengewichts vom 2026-09-08 bleibt gueltig.

   ⚠️ Die Mittelspalte traegt das `flex: 1 0 auto`, das bis dahin am Zaehler
   stand: sie ist jetzt das Kind zwischen den zwei Tortasten. */
.team-mitte {
  flex: 1 0 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
/* ⚠️ GRÖSSER UND MITTIG, seit dem 2026-09-13. Der Betreiber: „Die
   Aktionsbuttons Eigentor und Like im laufenden Spiel sind zu klein. Das
   Eigentor darf gerne zentriert und größer sein. Gerne in der halben höhe
   wie das + und -.“

   Vorher hatte er gar kein eigenes Mass: `.btn-sm` gab ihm 40 px Höhe und die
   Breite seiner Beschriftung (gemessen 80 x 40). Jetzt füllt er die
   Mittelspalte bis zur Breite einer Tortaste und ist genau halb so hoch.

   ⚠️ `width: 100%` MIT `max-width`, und nicht `width: var(--tor-w)`: die
   Mittelspalte ist bei 390 px schmaler als 130 px, und eine feste Breite
   liefe dort aus der Kiste. Eine Prozentbreite zählt ausserdem beim
   Errechnen der Spaltenbreite nicht mit (sie gilt dabei als `auto`) - der
   Knopf kostet die Punktzeile damit weiterhin KEINE Breite, und die
   Begründung an `.team-mitte` bleibt gültig.

   ⚠️ `max(...)` mit Stufe S als Boden und nicht schlicht die halbe Höhe:
   am Telefon ist die Tortaste 64 px, die Hälfte davon wären 32 - Stufe XS,
   die das Haus für Chips und Marken vorsieht und nicht für eine Aktion
   (SPEZIFIKATION 10.1: kein Treffmass verkleinern). Am Tisch, wo der Knopf
   überhaupt nur steht, greift der Boden nie: 100 / 2 = 50. */
.gv-eigentor {
  flex: 0 0 auto;
  width: 100%;
  max-width: var(--tor-w, 130px);
  /* ⚠️ UND EIN BODEN AUS DER EIGENEN BESCHRIFTUNG - gemessen, nicht
     vorsorglich. Mit `max-width` allein war der Knopf bei 390 px 64 px
     breit (die Tortaste ist dort so schmal), das Wort braucht 58 plus
     Polster: gemessen Kasten 46, Inhalt 58, und `.gv-eigentor-text`
     kürzte es STILL mit Auslassungspunkten. Genau der Fehler, den
     `.btn` mit `flex-shrink: 0` vermeidet („ein abgeschnittenes Wort
     meldet niemand“) - nur kam er hier nicht vom Schrumpfen, sondern
     von der Obergrenze. `min-width` schlägt `max-width`, also gilt der
     Boden. */
  min-width: min-content;
  height: max(var(--md-sys-comp-size-s), calc(var(--tor-h, 100px) / 2));
  min-height: 0;
  padding-inline: 10px;
}
.gv-eigentor-text { font-size: .95rem; overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; min-width: 0; }

/* Tor-Buttons: Extended-FAB-artig, Shape-Morph + Scale-Spring beim Drücken */
.goal-btn {
  width: var(--tor-w, 130px); height: var(--tor-h, 100px);
  border-radius: var(--md-sys-shape-corner-large);
  font-size: 3rem;
  font-weight: 500;
  border: none;
  cursor: pointer;
  transition: border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing),
              transform var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing),
              box-shadow var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
  touch-action: manipulation;
}
.goal-btn:active { border-radius: var(--md-sys-shape-corner-full); transform: scale(0.92); }
.goal-btn-minus {
  background: transparent;
  border: 1px solid var(--md-sys-color-outline);
  color: var(--md-sys-color-on-surface-variant);
}
.team1 .goal-btn-minus, .team2 .goal-btn-minus { border-color: rgba(var(--md-sys-color-on-surface-rgb) / 0.3); color: inherit; }
.goal-btn-plus-t1 { background: var(--md-ext-color-team1); color: var(--md-ext-color-on-team1); box-shadow: var(--md-sys-elevation-level1); }
.goal-btn-plus-t2 { background: var(--md-ext-color-team2); color: var(--md-ext-color-on-team2); box-shadow: var(--md-sys-elevation-level1); }
/* ⚠️ EINE LEERE STATUSLEISTE VERSCHWINDET, seit dem 2026-09-07 - und das ist
   die FOLGE davon, dass das Torziel entfallen ist (der Betreiber: "Ich moechte
   in allen Spielansichten das Ziel nicht mehr sehen").

   Die Leiste traegt drei Dinge: den Sensorhinweis (nur mit Sensorik), die
   gemessene Geschwindigkeit (`#speed-badge`, im Regelfall `.hidden`) und bis
   dahin das Torziel. Ohne Sensorik und ohne Torziel blieb ein leerer,
   umrandeter Balken quer unter den Spielerkisten stehen - gefunden auf dem
   Bild nach der Aenderung, nicht gemeldet.

   `:not(:has(> :not(.hidden)))` heisst "kein sichtbares Kind". Ein
   `:empty` traefe nicht: `#speed-badge` steht IMMER im Markup, nur mit
   `.hidden`. Und der Selektor ist LEBEND - misst die Anlage einen Schuss,
   verliert das Abzeichen seine Klasse und die Leiste erscheint. */
.game-statusbar:not(:has(> :not(.hidden))) { display: none; }
.game-statusbar {
  background: var(--md-sys-color-surface-container-low);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-large);
  padding: 12px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  font-size: var(--md-sys-typescale-body-medium-size);
}
.sensor-indicator { display: flex; align-items: center; gap: 6px; }
.sensor-dot {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--md-sys-color-error);
}
.sensor-dot.on { background: var(--md-ext-color-success); animation: pulse 1.5s infinite; }
.speed-badge {
  font-size: var(--md-sys-typescale-title-medium-size);
  color: var(--md-ext-color-warning);
  font-weight: 700;
  animation: fadeIn var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-emphasized-decelerate);
}

/* ── Fullscreen banners (Spring-Entrance) ─────────────────────────────────── */
.fullscreen-banner {
  position: fixed;
  inset: 0;
  background: rgba(var(--md-sys-color-scrim-rgb) / 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
  backdrop-filter: blur(8px);
}
.fullscreen-banner.hidden { display: none; }
.banner-content {
  text-align: center;
  /* Das Banner hatte weder max-height noch overflow: bei 2v2 mit sechs
     Bewertungszeilen bleiben auf 1920x1080 nur rund 95 px Luft, und Bild plus
     Rangspalte lassen die Zeilen wachsen. Die Kiosk-Seite begrenzt genauso.

     ⚠️ Durch den Zoom geteilt seit dem 2026-09-29: `dvh` zoomt nicht mit,
     und ohne die Teilung war die Grenze am Tisch (Zoom 1,25) 1296 statt
     1037 Bildpunkte hoch - es rollte nie, der Inhalt lief ueber den Rand.
     Enger (Innenrand 40 auf 24, Abstand 20 auf 12) fuer die Reihe mit
     Rueckspiel und neuem Spiel. GEMESSEN bei 1920x1080, Zoom 1,25, mit
     Bild, Serie, zwei Bewertungszeilen und sichtbarer Ruecknahme: vorher
     Bild oben 108 px abgeschnitten, danach 22 bis 1058 px im Rahmen. Im
     vollsten Fall rollt es: der Heimweg endet bei 1095, die Ruecknahme bei
     1180 - beide sind durch Wischen erreichbar statt abgeschnitten. Der
     Betreiber hat diese Fassung gegen ein kleineres Siegerbild (150 px)
     gewaehlt, das Bild bleibt gross. */
  max-height: calc(96dvh / var(--ui-zoom, 1));
  overflow-y: auto;
  padding: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  /* ⚠️ BENANNTE AUSNAHME. Auf dem dunklen Scrim braucht es in BEIDEN Modi
     helle Schrift - `on-surface` waere im Hellen dunkel und unlesbar. Ein
     Token „hell, unabhaengig vom Modus" gibt es fuer Neutraltoene nicht (die
     `*-fixed`-Rollen deckt nur die Farbrollen ab). Der Wert ist das dunkle
     `on-surface`; er wurde am 2026-09-03 vom alten #E6E0E9 nachgezogen. */
  color: #E6E0EF;
  animation: banner-in var(--md-sys-motion-spring-default-spatial-duration) var(--md-sys-motion-spring-default-spatial-easing);
}
.banner-icon { font-size: 5rem; line-height: 1; }
.banner-content h1 {
  font-size: clamp(2.5rem, 8vw, 5rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
.banner-content h2 {
  font-size: clamp(1.2rem, 4vw, 2rem);
  opacity: 0.75;
  line-height: 1.2;
}
.banner-score {
  /* ⚠️ Deutlich groesser seit dem 2026-09-03 (war clamp(2rem, 6vw, 4rem)) -
     Wunsch des Betreibers. Es ist die Zahl, nach der am Tisch gefragt wird,
     und sie stand kleiner da als die Ueberschrift darueber.

     Die Klasse traegt AUCH den Stand im Kopf eines beendeten Spiels
     (`.fg-stand` in game.js). Dort steht sie zwischen den zwei Avatarseiten
     der Buehne, und die Buehne ist ein Flex mit `flex-wrap` - bei 600 px
     nimmt der Stand ohnehin eine eigene Zeile (`flex-basis: 100%`). */
  font-size: clamp(2.6rem, 8vw, 5.5rem);
  font-weight: 700;
  line-height: 1.1;
}

/* Avatar(e) des Siegerteams (nur wenn hinterlegt) */
.banner-avatars { display: flex; gap: 16px; justify-content: center; }
.banner-avatars:empty { display: none; }   /* sonst bliebe eine Lücke im Flex-Gap */
.banner-avatars img {
  /* ⚠️ DOPPELT so gross seit dem 2026-09-04 (war clamp(72px, 12vh, 120px)).
     Wunsch des Betreibers: „Der Pokal soll verschwinden. Ich moechte den Platz
     nutzen und das Bild des Spielers doppelt so gross anzuzeigen."

     Der Platz kommt vom Pokal (5 rem plus der 20-px-Abstand der Flex-Spalte).
     Ueber bleibt es trotzdem nicht: `.banner-content` hat `max-height: 96dvh`
     und `overflow-y: auto` - bei einem hohen Bild und vielen Zeilen rollt der
     Inhalt, statt aus dem Bild zu laufen. */
  width: clamp(144px, 24vh, 240px);
  height: clamp(144px, 24vh, 240px);
  border-radius: 50%;
  object-fit: cover;
  border: 4px solid rgba(255,255,255,0.85);
  box-shadow: var(--md-sys-elevation-level3);
}
/* Die zwei Teamfarben auf dem Vollbild-Banner.

   ⚠️ Hier standen bis zum 2026-09-03 die dunklen Farbtoene als ZAHLEN, mit
   der richtigen Begruendung („Scrim ist dunkel", also braucht es in beiden
   Modi den dunklen Ton) und der falschen Umsetzung: `#D0BCFF` war das ALTE
   dunkle `primary` und ist beim Seed-Wechsel nicht mitgewandert.

   Die `*-fixed-dim`-Rollen sind per M3-Definition genau das - der dunkle Ton,
   in beiden Modi identisch. Damit ist die Ausnahme keine mehr, und der
   naechste Seed-Wechsel zieht sie mit.

   ⚠️ Seit dem 2026-10-06 OHNE Teamfarbe (Betreiber: die Figuren sind schwarz
   und weiss, "so, dass man sie deutlich sehen kann" - kein Hyperkontrast,
   aber auch kein Text, der auf seinem eigenen Grund verschwindet). Schwarz
   auf dunklem Scrim bzw. Weiss auf hellem Grund waeren genau das Gegenteil.
   Die Seite steht ohnehin per Name und Position daneben; die Farbe am Text
   war nur Zierde. */
.banner-score .t1, .banner-score .t2 { color: inherit; }
.banner-score .sep { opacity: 0.6; margin: 0 12px; }

/* ── Modal → M3-Dialog (28 px, Spring-Entrance) ───────────────────────────── */
#modal-overlay {
  position: fixed;
  inset: 0;
  /* Scrim 32 %, wie M3 es nennt (ANALYSE.md §8) - vorher 0,5.
     ⚠️ Wir haben zusaetzlich `backdrop-filter: blur(4px)`, das das Kit nicht
     hat. Die Trennung kommt hier also aus zwei Quellen, und mit beiden ist
     0,32 deutlich genug; 0,5 verdeckte den Hintergrund zweimal. */
  background: rgba(var(--md-sys-color-scrim-rgb) / 0.32);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 400;
  backdrop-filter: blur(4px);
}
#modal-overlay.hidden { display: none; }
#modal-box {
  background: var(--md-sys-color-surface-container-high);
  border: none;
  border-radius: var(--md-sys-shape-corner-extra-large);
  padding: 32px;
  max-width: 520px;
  /* M3 nennt 280-560 px. Die Obergrenze stand hier, die untere fehlte.

     ⚠️ `min(280px, 100%)` und nicht `280px`: die Breite ist `90vw / zoom`,
     und bei Kicker-Zoom 1,1 auf einem 320-px-Display sind das 262 px. Ein
     hartes `min-width: 280px` waere dort breiter als der Bildschirm - und
     genau dieser Fehler steckte schon einmal im Toast darunter
     („400 + right:20 sprengt ein 390-px-Display"). Dieselbe Loesung wie bei
     der Indikator-Pille der Navigation. */
  min-width: min(280px, 100%);
  width: calc(90vw / var(--ui-zoom, 1));
  max-height: calc(80dvh / var(--ui-zoom, 1));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
  box-shadow: var(--md-sys-elevation-level3);
  animation: dlg-in var(--md-sys-motion-spring-default-spatial-duration) var(--md-sys-motion-spring-default-spatial-easing);
}
/* Nur der Inhalt scrollt – der ×-Schließen-Button bleibt oben fixiert. */
#modal-content { overflow-y: auto; min-height: 0; }
.modal-close {
  position: absolute;
  top: 12px; right: 12px;
  width: 36px; height: 36px;
  background: transparent;
  border: none;
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-surface-variant);
  font-size: 1.2rem;
  cursor: pointer;
}
.modal-title {
  font-size: var(--md-sys-typescale-headline-small-size);
  line-height: var(--md-sys-typescale-headline-small-line-height);
  font-weight: 500;
  margin-bottom: 20px;
}

/* ── Bild-Ebene (Avatar vergroessert, 2026-10-08) ─────────────────────────────
   Betreiber: „beim Klick auf den Avatar [soll sich] das Bild in die Mitte des
   Bildschirms vergrößern … mit einem leichten Schatten, dass man sieht, dass
   das als Layer über der aktuellen Ansicht ist - wie bei Instagram." EIGENES
   Overlay und nicht `#modal-overlay`: der ist ein Singleton (ein
   `#modal-content`), ein zweiter Aufruf darin ueberschriebe die gerade offene
   Spielerkarte statt sie zu behalten. Scrim, Blur und die Aufklapp-Animation
   sind dieselben wie beim Modal oben - nur der z-index liegt darueber, siehe
   die Reihenfolge bei `.fullscreen-banner` (500) und `#toast-container`
   (600). */
#bild-overlay {
  position: fixed;
  inset: 0;
  background: rgba(var(--md-sys-color-scrim-rgb) / 0.32);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 450;
}
#bild-overlay.hidden { display: none; }
#bild-overlay-img {
  /* 480 px: die hochgeladenen Avatare sind serverseitig auf hoechstens 512 px
     Kante (Foto) bzw. 256 px Kante (GIF) begrenzt (`players.js::AVATAR_MAX_PX`/
     `AVATAR_GIF_MAX_KANTE`) - groesser als das vergroessert nur das Unscharfe.
     Gerechnet, nicht an jedem einzelnen Original gemessen: die Kante streut
     je Spieler zwischen 192 und 512 px, ein GIF-Avatar wirkt hier also
     sichtbar weicher als ein Foto - das liegt an dieser bestehenden Grenze,
     nicht an der Lightbox. */
  max-width: calc(min(480px, 80vw) / var(--ui-zoom, 1));
  max-height: calc(80dvh / var(--ui-zoom, 1));
  object-fit: contain;
  border-radius: var(--md-sys-shape-corner-extra-large);
  box-shadow: var(--md-sys-elevation-level5);
  animation: dlg-in var(--md-sys-motion-spring-default-spatial-duration) var(--md-sys-motion-spring-default-spatial-easing);
}

/* Der Avatar in der fliegenden/vollen Statistik, anklickbar gemacht
   (`App.avatarKlickbarHtml`) - nur Zeiger und ein sichtbarer Fokusrahmen,
   keine Groessen-/Zuschnittregel (die traegt `avatarHtml`s Inline-Stil
   schon, siehe die Begruendung dort). */
.av-klick { cursor: pointer; }
.av-klick:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* ── PIN numpad (Shape-Morph beim Drücken) ────────────────────────────────── */
.pin-display {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 24px;
}
.pin-dot {
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 2px solid var(--md-sys-color-outline);
  transition: background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
              border-color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
}
.pin-dot.filled { background: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
.pin-numpad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  max-width: 240px;
  margin: 0 auto;
}
.pin-key {
  aspect-ratio: 1;
  min-height: 70px;
  background: var(--md-sys-color-surface-container-highest);
  border: none;
  border-radius: var(--md-sys-shape-corner-large);
  font-size: var(--md-sys-typescale-title-large-size);
  font-weight: 500;
  color: var(--md-sys-color-on-surface);
  cursor: pointer;
  transition: border-radius var(--md-sys-motion-spring-fast-spatial-duration) var(--md-sys-motion-spring-fast-spatial-easing),
              background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
}
.pin-key:active { border-radius: var(--md-sys-shape-corner-full); }
.pin-key.del { background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }

/* ── Toast → M3-Snackbar (inverse-surface, Slide-up-Spring) ───────────────── */
#toast-container {
  position: fixed;
  bottom: 24px;
  right: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 600;
}

/* ── Angedockte Chatfenster (2026-09-04) ─────────────────────────────────────
   „der Chat bleibt unten Rechts immer offen - wie bei Facebook."

   Der Stapel waechst nach LINKS, damit das juengste Fenster immer an
   derselben Stelle steht - am rechten Rand, wo die Hand hingeht.

   ⚠️ Das braucht `flex-direction: row` und NICHT `row-reverse`, und ich habe
   es zuerst falsch gehabt. Der Rahmen ist `position: fixed` mit `right: 20px`
   und ohne `left`: seine Breite kommt vom Inhalt, seine linke Kante wandert
   also von selbst nach links, wenn ein Fenster dazukommt. Bei `row` steht das
   ZULETZT angehaengte Kind rechts - und `_offen.push()` haengt das neue hinten
   an. `row-reverse` haette es nach links gelegt, also genau dorthin, wo es
   beim naechsten Fenster wieder wegrutscht.

   ⚠️ `z-index: 90` und nicht mehr: darueber liegen Modal (400),
   Vollbild-Banner (500) und die Toasts (600). Ein Chatfenster ueber einer
   Rueckfrage waere ein Fenster, das eine Entscheidung verdeckt. Unter ihm
   liegt nur der Inhalt und die untere Leiste (ebenfalls 90 - die steht am
   Telefon, und dort gibt es keine Fenster, siehe den 600er-Block).

   ⚠️ `bottom` haelt Abstand zu den Toasts: die sitzen bei 24 px, sind rund
   48 px hoch und liegen ueber allem. Ohne den Abstand verdeckte eine Meldung
   genau das Eingabefeld, in dem sie entstanden ist. */
/* ⚠️ `left` UND `right`, seit dem 2026-09-14 - und das ist die Grenze, die
   vorher fehlte. Befund der Sichtpruefung: drei Fenster bei 840 px Fenster
   ergaben eine Zeile von -274 bis 78 px. Ohne `left` hat die Zeile keine
   Breite, an der sie sich messen koennte; sie waechst nach links weiter, bis
   sie aus dem Bild laeuft. Mit beiden Raendern ist sie begrenzt, und die
   Fenster schrumpfen stattdessen bis auf ihre 16 em Untergrenze.

   Das ist die zweite Verteidigung, nicht die erste: WIE VIELE Fenster
   nebeneinander sinnvoll sind, entscheidet `chatdock.js` (`_passen()`). Diese
   Regel sorgt nur dafuer, dass auch ein alter Eintrag aus dem lokalen
   Speicher nichts aus dem Bild schiebt. */
#chat-dock {
  position: fixed;
  bottom: 0;
  left: 20px;
  right: 20px;
  z-index: 90;
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  justify-content: flex-end;     /* die Fenster bleiben rechts */
  gap: 12px;
  pointer-events: none;          /* der Rahmen selbst faengt keine Klicks */
}
#chat-dock:empty { display: none; }
.cd-fenster {
  pointer-events: auto;
  /* ⚠️ VERSTELLBAR seit dem 2026-09-08. Der Betreiber: „Kann man dieses
     Fenster in der Groesse (oben Links an der Ecke) veraendern?"

     Die gezogene Breite schreibt `chatdock.js::_zeichne` als `--cd-b` inline
     an dieses Element (je Fenster verschieden, ein freier Zahlenwert). Ohne
     Wert gelten die 20 em von vorher - der Rueckfall steht IM `var()` und
     nicht als zweite Regel, damit es nur eine Zahl gibt.

     ⚠️ Das `clamp()` ist keine Doppelung der Grenzen in `chatdock.js`,
     sondern die zweite Verteidigung: ein Eintrag aus `localStorage` kann von
     vorgestern sein, und das Fenster kann inzwischen schmaler sein, als
     damals gezogen wurde. `min(40em, ...)` haelt beides zusammen - die
     Obergrenze und den Bildschirmrand. */
  width: clamp(16em, var(--cd-b, 20em), min(40em, calc(100vw - 40px)));
  max-width: calc(100vw - 40px);
  /* ⚠️ Ohne `min-width: 0` schrumpft ein Flexkind nicht unter seinen Inhalt -
     der Titel eines langen Gruppennamens haelt das Fenster dann breit, und die
     Zeile laeuft doch wieder aus dem Bild. Gemessen an „Betriebssport Kicker
     Mitte - Turnierabsprachen 2026". */
  min-width: 0;
  position: relative;             /* Bezug fuer den Griff oben links */
  display: flex;
  flex-direction: column;
  background: var(--md-sys-color-surface-container-low);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-bottom: none;
  border-radius: var(--md-sys-shape-corner-large)
                 var(--md-sys-shape-corner-large) 0 0;
  box-shadow: var(--md-sys-elevation-level3);
  overflow: hidden;
}
/* ⚠️ Das Polster ist 4 px und nicht 6 px, seit dem 2026-09-14 - und das ist
   kein Geschmack, sondern die Gegenrechnung zu den 32 px darunter. Der Kopf
   bleibt damit **genau 40 px** hoch wie vorher, die Knöpfe darin wachsen
   trotzdem auf das Mindestmass. Wer hier wieder 6 px hinschreibt, macht das
   Fenster um 4 px höher oder die Knöpfe wieder zu klein. */
.cd-kopf {
  display: flex;
  align-items: center;
  gap: 2px;
  /* Links 32 statt 12 px: dort sitzt der Groessengriff, siehe `.cd-griff`. */
  padding: 4px 4px 4px 32px;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
/* Der Titel ist zugleich der Umschalter - „wie bei Windows" heisst auch, dass
   ein Klick auf die Leiste das Fenster ein- und ausklappt. */
.cd-titel {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  font-size: var(--md-sys-typescale-label-large-size);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
/* ⚠️ 32 px und nicht 28, seit dem 2026-09-14. Befund der Sichtprüfung: die
   vier Knöpfe der Titelzeile (Verkleinern, Aufklappen, Ganze Seite,
   Schliessen) massen 28 × 28 px und lagen damit unter der untersten Stufe der
   Hausleiter (`--md-sys-comp-size-xs`, 32 px). 98 weiche Befunde über alle
   Breiten und beide Themen - immer dieselben vier Knöpfe.

   32 px ist hier die richtige Zahl und nicht `--min-touch` (56 px): die
   Titelzeile eines angedockten Fensters ist kein Ziel, das am Tisch getroffen
   wird, und 56 px machten aus dem Balken einen Kasten. Die Leiste nennt
   genau dafür ihre unterste Stufe. */
.cd-knopf {
  flex-shrink: 0;
  width: var(--md-sys-comp-size-xs); height: var(--md-sys-comp-size-xs);
  display: grid; place-items: center;
  border: none; background: transparent; cursor: pointer;
  color: inherit;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: 18px;
}
.cd-knopf:hover { background: rgba(var(--md-sys-color-scrim-rgb) / .12); }
/* ── Der Griff oben links ───────────────────────────────────────────────────
   ⚠️ Er sitzt oben LINKS, weil das Fenster unten rechts haengt (`#chat-dock`
   ist `position: fixed` mit `right`/`bottom`): nach links und oben ziehen
   macht es groesser, und `nwse-resize` ist genau dieser Zeiger.

   ⚠️ `position: absolute` und nicht als Flex-Kind im Kopf: als Kind haette er
   die Titelzeile um 16 px verkuerzt, und der Titel ist dort das Knappste
   (`text-overflow: ellipsis` bei 20 em). So liegt er UEBER der Ecke, in der
   ohnehin nur das Polster des Kopfes steht.

   Die Flaeche ist 20 px und der Haken 9 px: am Touchscreen will man etwas
   treffen, im Bild soll nur eine Ecke zu sehen sein.

   ⚠️ Der Haken steht in einem `::before` und EINGERUECKT (4 px), nicht als
   Rahmen am Knopf selbst. Der erste Anlauf setzte `border-top`/`border-left`
   direkt auf den Knopf, mit dem Eckradius des Fensters - auf dem Bild war er
   dann von der Fensterkante nicht zu unterscheiden. Eingeruecktes Winkelmass
   liest sich als Griff. */
/* ⚠️ 32 px und nicht 20, seit dem 2026-09-16 - Befund der Sichtpruefung, und
   der letzte unter dem Hausmass (`--md-sys-comp-size-xs`). Er stand als
   einziger weicher Befund 14 mal im Bericht, immer derselbe Knopf.

   Das SICHTBARE Winkelmass bleibt, wo es war (`::before`, 4 px eingerueckt,
   9 x 9): ein Bedienziel ist die Flaeche, die Klicks annimmt, nicht die Tinte.
   Der Griff waechst also nach rechts und unten in den Fensterkopf hinein.

   Deshalb rueckt `.cd-kopf` sein linkes Polster auf 32 px: der Titel begann
   bei 12 px und lag damit schon bei 20 px Griff unter ihm - ein Klick auf
   seine ersten Zeichen zog die Groesse, statt das Fenster einzuklappen. Mit
   32 px beginnt der Titel HINTER dem Griff, und die Ueberlappung ist weg,
   die es vorher gab. */
.cd-griff {
  position: absolute;
  top: 0; left: 0;
  width: 32px; height: 32px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: nwse-resize;
  z-index: 1;
  color: var(--md-sys-color-on-secondary-container);
}
.cd-griff::before {
  content: "";
  position: absolute;
  top: 4px; left: 4px;
  width: 9px; height: 9px;
  border-top: 2px solid currentColor;
  border-left: 2px solid currentColor;
  border-top-left-radius: 2px;
  opacity: .55;
}
.cd-griff:hover::before, .cd-griff:focus-visible::before { opacity: 1; }
/* Minimiert gibt es keinen Verlauf - dann ist eine Groesse ohne Bedeutung. */
.cd-fenster.is-min .cd-griff { display: none; }
/* Minimiert bleibt genau die Leiste stehen - „man sieht nur noch einen Balken
   unten rechts zu dem Gespraech, dass man dann aber wieder oeffnen kann." */
.cd-fenster.is-min .cd-verlauf,
.cd-fenster.is-min .cd-senden { display: none; }
.cd-verlauf {
  /* Die gezogene Hoehe, siehe die Begruendung an `.cd-fenster`. */
  height: clamp(8em, var(--cd-h, 15em), 34em);
  overflow-y: auto;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.cd-n { display: flex; flex-direction: column; max-width: 88%; }
.cd-n.ich { align-self: flex-end; align-items: flex-end; }
.cd-von {
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}
.cd-text {
  padding: 6px 10px;
  border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-high);
  font-size: var(--md-sys-typescale-body-medium-size);
  /* Ein Wort ohne Leerzeichen (eine Adresse) darf das Fenster nicht
     sprengen - dieselbe Regel wie in der grossen Ansicht. */
  overflow-wrap: anywhere;
}
.cd-n.ich .cd-text {
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}
.cd-leer {
  margin: auto;
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-body-small-size);
}
/* ⚠️ Eine REIHE: der Smiley-Knopf steht links vom Feld. Vorher lag hier nur
   das Feld, und ein Polster genuegte. */
.cd-senden {
  padding: 8px 10px 10px;
  display: flex;
  align-items: center;
  gap: 4px;
}
.cd-senden .form-input { flex: 1; min-width: 0; }

/* ── Emoji-Auswahl (2026-09-08) ─────────────────────────────────────────────
   Der Knopf an den drei Eingaben (Zuruf, Nachrichtenzeile, Chatfenster).

   ⚠️ Das Zeichen im Knopf ist ein EMOJI und kein `.msym`: die Symbolschrift
   (Material Symbols) hat kein Emoji-Glyph, es kaeme ein leeres
   Kaestchen. `tests/test_icon_font.py` verwirft Nicht-ASCII in `.msym` STUMM.
   Also faellt es auf die Systemschrift zurueck - so wie 🔥 auf der Startseite
   und 🏆 im Kiosk, das ist im Projekt bekannt und in Betrieb.

   `line-height: 1` und ein festes Quadrat: ohne das macht das Emoji die Zeile
   hoeher als das Eingabefeld daneben. */
/* ⚠️ Auch in der HÖHE 32 px. Sie war es nur in der Breite - der Knopf
   erbte seine Höhe von der Eingabezeile und mass 32 × 28. Gefunden hat es
   dieselbe Prüfung wie bei `.cd-knopf`, und aus demselben Grund: eine der
   zwei Kanten reicht nicht. */
.em-knopf {
  flex: 0 0 auto;
  min-width: 0;
  width: var(--md-sys-comp-size-xs);
  min-height: var(--md-sys-comp-size-xs);
  padding: 0;
  font-size: 1.15rem;
  line-height: 1;
}
/* ⚠️ Die Tafel haengt an `document.body` und nicht in der Eingabezeile: dort
   laege sie im `overflow: hidden` des Chatfensters und waere halb
   abgeschnitten. Deshalb `position: fixed` - der Platz wird in JavaScript
   gerechnet (siehe `js/emoji.js`, samt der Division durch den Zoom).

   `z-index: 500`: ueber dem Chatfenster (90) und ueber dem Modal (400) - die
   Tafel kann aus einem Dialog heraus geoeffnet werden. Unter den Toasts
   (600), damit eine Meldung sie nicht verdeckt. */
.em-tafel {
  position: fixed;
  z-index: 500;
  display: grid;
  grid-template-columns: repeat(8, 2rem);
  gap: 2px;
  padding: 8px;
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-large);
  box-shadow: var(--md-sys-elevation-level3);
}
.em-z {
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  border: none; background: transparent; cursor: pointer;
  border-radius: var(--md-sys-shape-corner-small);
  font-size: 1.15rem;
  line-height: 1;
  padding: 0;
}
.em-z:hover, .em-z:focus-visible {
  background: rgba(var(--md-sys-color-scrim-rgb) / .12);
}
.cd-senden .form-input {
  font-size: var(--md-sys-typescale-body-medium-size);
  padding: 8px 12px;
}
.toast {
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border: none;
  border-radius: var(--md-sys-shape-corner-extra-small);
  padding: 14px 20px;
  font-size: var(--md-sys-typescale-body-medium-size);
  /* 48 px, wie M3 die einzeilige Snackbar bemisst (ANALYSE.md §8). Mit
     Polster 14 und body-medium kam sie auf rund 46 - der Unterschied ist
     klein, aber eine Meldung, die je nach Textlaenge zwei Pixel wandert,
     sieht bei zwei Toasts hintereinander unruhig aus. `flex` dazu, damit der
     Text bei der festen Hoehe mittig steht statt oben zu kleben. */
  min-height: 48px;
  display: flex;
  align-items: center;
  min-width: 240px;
  /* 400 + right:20 sprengt ein 390-px-Display; overflow-x:hidden schnitt es
     unsichtbar ab. */
  max-width: min(400px, calc(100vw - 40px));
  box-shadow: var(--md-sys-elevation-level3);
  animation: snack-in var(--md-sys-motion-spring-default-spatial-duration) var(--md-sys-motion-spring-default-spatial-easing);
  border-left: 4px solid var(--md-sys-color-inverse-primary);
}
.toast.success { border-left-color: var(--md-ext-color-success); }
.toast.error   { border-left-color: var(--md-sys-color-error); }
.toast.warning { border-left-color: var(--md-ext-color-warning); }
html[data-theme="dark"] .toast.success { border-left-color: #146C2E; }   /* inverse-surface ist hell */
html[data-theme="dark"] .toast.error   { border-left-color: #BA1A1A; }
html[data-theme="dark"] .toast.warning { border-left-color: #7D5700; }



/* ── Nachrichten (#/nachrichten, 2026-09-03) ────────────────────────────────
   Zwei Spalten am Rechner, eine am Telefon. Die Umschaltung ist KEINE
   Media-Query allein: am Telefon haengt sie zusaetzlich daran, ob ein
   Gespraech offen ist (`.zeigt-gespraech`, gesetzt von nachrichten.js).

   Eine feste Hoehe statt `min-height`: der Verlauf soll IN der Karte scrollen
   und nicht die Seite laenger machen - sonst wandert die Eingabezeile beim
   Tippen aus dem Bild, und genau das ist am Telefon der haeufigste Fall.
   Dieselbe Rechnung wie bei `#game-view`. */
.nr-raster {
  display: grid;
  grid-template-columns: minmax(0, 20em) minmax(0, 1fr);
  gap: 16px;
  height: calc(100dvh / var(--ui-zoom, 1) - var(--nav-h) - 48px);
}
.nr-liste, .nr-gespraech {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-low);
  overflow: hidden;
}
.nr-liste { overflow-y: auto; }
.nr-kopf { padding: 8px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }

.nr-eintrag {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  cursor: pointer;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  min-width: 0;
}
.nr-eintrag:hover { background: var(--md-sys-color-surface-container); }
.nr-eintrag.is-offen { background: var(--md-sys-color-secondary-container); }
.nr-e-text { min-width: 0; flex: 1; }
.nr-e-name { font-weight: 600; }
/* Die Vorschau bleibt EINE Zeile - sonst springt die Liste bei jeder
   Nachricht in eine andere Hoehe. */
.nr-e-letzt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nr-punkt {
  flex-shrink: 0;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 700;
  display: grid;
  place-items: center;
}

.nr-g-kopf {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  min-width: 0;
}
.nr-g-kopf strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nr-g-aktionen { margin-left: auto; display: flex; gap: 2px; flex-shrink: 0; }
/* ⚠️ 32 px seit dem 2026-09-07, wie `.nr-avatar` daneben. Vorher stand hier
   24 gegen einen Direktchat-Avatar von 62 px (24 x --avatar-scale 2,6):
   dieselbe Zahl im Quelltext, zweieinhalbfacher Unterschied auf dem Schirm.
   Gruppe und Person stehen an derselben Stelle und sind jetzt gleich gross. */
.nr-g-bild { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }
/* Nur am Telefon sichtbar - dort ersetzt das Gespraech die Liste. */
.nr-zurueck { display: none; }

.nr-verlauf {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.nr-n {
  align-self: flex-start;
  max-width: 80%;
  padding: 6px 10px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
  /* Ein langes Wort ohne Leerzeichen (eine Adresse) sprengte die Blase sonst
     ueber den Rand hinaus. */
  overflow-wrap: anywhere;
}
.nr-n.is-meins {
  align-self: flex-end;
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
}
.nr-n-von {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 700;
  color: var(--md-sys-color-primary);
}
.nr-n-text { display: block; }
.nr-n-zeit {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
  text-align: right;
}
/* Systemzeilen stehen mittig und ohne Blase - sie sind kein Beitrag. */
.nr-system {
  align-self: center;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
  text-align: center;
}

/* Der Smiley-Knopf steht links vom Feld, der Senden-Knopf rechts. */
.nr-senden {
  display: flex;
  gap: 8px;
  padding: 8px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
.nr-senden .form-input { flex: 1; min-width: 0; }

/* Die Auswahl beim Anlegen. Kaestchen und nicht ein Klick je Person: EIN
   Gegenueber ist ein Zweiergespraech, mehrere sind eine Gruppe. */
.nr-wahl { display: flex; flex-direction: column; gap: 4px; max-height: 40vh;
           overflow-y: auto; }
.nr-wahl-p {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: var(--md-sys-shape-corner-small);
  cursor: pointer;
  min-height: var(--min-touch);
}
.nr-wahl-p:hover { background: var(--md-sys-color-surface-container-high); }

/* ── Live-Reaktionen am laufenden Spiel (2026-09-03) ────────────────────────
   Der Anfeuer-Effekt laeuft IM Avatarkreis - deshalb der Wickel mit
   `overflow: hidden`. Ohne ihn liefe der Text ueber den Namen darunter und
   ueber den Nachbarn daneben.

   ⚠️ Die Bewegung steht WOERTLICH auch in kiosk.html. Der Zwilling ist
   etabliert (`fw-shoot` genauso), weil der Kiosk kein gemeinsames Stylesheet
   laedt; tests/test_community.py haelt beide in Schritt. */
.gt-avwrap {
  position: relative;
  display: inline-block;
  overflow: hidden;
  border-radius: 50%;
  line-height: 0;
  flex-shrink: 0;
}
@keyframes cheer-rise {
  0%   { transform: translateY(65%)  scale(.85); opacity: 0; }
  12%  { transform: translateY(35%)  scale(1);   opacity: 1; }
  75%  { transform: translateY(-30%) scale(1);   opacity: 1; }
  100% { transform: translateY(-70%) scale(1.06); opacity: 0; }
}
.cheer {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0 6%;
  text-align: center;
  line-height: 1.05;
  font-weight: 800;
  color: #fff;
  /* ⚠️ Schatten UND abdunkelnder Verlauf, nicht nur eine Farbe: darunter liegt
     ein BELIEBIGES Foto. Weisse Schrift auf einem hellen Avatar ist ohne
     beides unlesbar, und die Avatare dieser Anlage sind bunte Bilder. */
  text-shadow: 0 2px 8px rgba(0, 0, 0, .9);
  background: radial-gradient(ellipse at center, rgba(0,0,0,.62), transparent 72%);
  pointer-events: none;
  /* ⚠️ Die Kurve aus einem TOKEN und keine eigene: der Kiosk laedt von
     unserem Design nur m3-tokens.css, eine eigene Kurve wandert also nicht
     mit, wenn die Springs sich aendern - tests/test_kiosk_mobil.py verbietet
     sie dort. Hier steht sie aus demselben Grund, damit die Zwillinge gleich
     bleiben.

     Die DAUER bleibt eine Zahl: die Spring-Dauern stehen unter
     prefers-reduced-motion auf 1 ms, und ein Zuruf, der eine Millisekunde
     steht, ist keiner. Die Reduktion wird deshalb ausdruecklich behandelt. */
  animation: cheer-rise 2.6s
             var(--md-sys-motion-easing-emphasized-decelerate) forwards;
}
/* Die Groesse haengt an der Avatargroesse dieser Ansicht - die steht in
   `--gt-av-size` an `.team-players-big`, und der Wickel erbt sie. */
.gt-avwrap .cheer {
  font-size: clamp(.75rem, calc(var(--gt-av-size, 120px) * .16), 1.6rem);
}
@keyframes cheer-fade {
  0%, 100% { opacity: 0; }
  10%, 80% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  /* ⚠️ NICHT `animation: none`. Dann feuert `animationend` nie, und daran
     haengt die Warteschlange - der Text bliebe fuer immer auf dem Avatar
     stehen und der naechste Zuruf erschiene nicht. Also dieselbe Dauer, nur
     ohne Bewegung. */
  .cheer { animation-name: cheer-fade; }
}

/* Die Leiste zum Senden. Drei Dinge in einer Zeile, mittleres waechst. */
.rk-leiste {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
}
/* ⚠️ In der rechten Spalte stehen die drei Riegel UNTEREINANDER: 220-340 px
   tragen keine zwei Anfeuer-Knoepfe plus Eingabefeld in einer Zeile. Der
   Grundzustand darueber (`flex-wrap: wrap`) wuerde sie umbrechen, aber in
   beliebiger Aufteilung - hier ist die Reihenfolge Absicht: anfeuern, anfeuern,
   schreiben. */
/* ⚠️ EINE REIHE, und nicht mehr eine Spalte - seit dem 2026-09-08.
   Bis dahin standen hier DREI Riegel untereinander (zwei Anfeuer-Knoepfe plus
   Feld), und 220-340 px tragen die nicht in einer Zeile. Die Anfeuer-Knoepfe
   sind in die Spielerkisten gezogen; uebrig sind der Smiley-Knopf (32 px) und
   das Feld, und die passen nebeneinander.

   ⚠️ Nur aus dem Netz: am Tisch ist die Spalte so breit wie die Ansicht, und
   dort gilt die Reihe des Grundzustands. Am Tisch gibt es die Leiste
   ueberhaupt nur, wenn sich dort jemand anmeldet - selten, aber moeglich. */
body.nicht-am-tisch .rk-spalte > .rk-leiste {
  flex-direction: row;
  align-items: center;
  /* ⚠️ `margin-top: auto` - DAS FELD BLEIBT UNTEN, seit dem 2026-09-08. Der
     Betreiber: „Das Fenster für den Zuruf soll Remote gerne rechts bleiben,
     aber immer am rechten untern Rand. Nicht oben Links oder bei Eingaben
     nach unten Wandern (was es aktuell macht)."

     Der Befund war reproduzierbar und die Ursache stand fest: `.rk-spalte`
     ist eine Flex-Spalte OHNE `justify-content`, und `.rk-feed:empty` nimmt
     den Verlauf ganz aus dem Fluss. Solange niemand angefeuert hatte, war die
     Sendeleiste also das einzige Kind und sass bei `flex-start` - OBEN. Mit
     der ersten Zeile erschien der Verlauf mit `flex: 1 1 auto` und schob die
     Leiste nach unten: sie SPRANG.

     `margin-top: auto` in einer Flex-Spalte drueckt das Element an das Ende -
     auch wenn es das einzige ist. Damit steht das Feld von Anfang an unten,
     und der Verlauf waechst darueber nach oben. */
  margin-top: auto;
}
body.nicht-am-tisch .rk-spalte > .rk-leiste .rk-name { max-width: none; }
body.nicht-am-tisch .rk-spalte > .rk-leiste .rk-eingabe-feld { min-width: 0; }
.rk-leiste .rk-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 9em;
}
.rk-eingabe-feld {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 12em;
}
.rk-eingabe-feld .form-input { min-width: 0; }

/* Der Verlauf. `:empty` nimmt keinen Platz - solange nichts kam, ist da
   nichts, und eine leere Kachel mit Ueberschrift stuende in jedem Spiel. */
.rk-feed {
  display: flex;
  /* ⚠️ `column-reverse`, seit dem 2026-09-07. Der Betreiber will den
     Verlauf "von unten nach oben anwachsend (vergleichbar zu der Kiosk TV
     Ansicht)".

     Das kostet KEINE Zeile JavaScript: `_reaktZeile` haengt mit `prepend`
     vorn an, und in dieser Richtung ist "vorn" UNTEN. Das Neue erscheint also
     unten, das Aeltere wandert nach oben. Genau dasselbe Mittel benutzt der
     Kiosk seit dem 2026-09-02 (kiosk.html, `.rk-feed`) - und dort steht auch
     die Begruendung, warum es kein `justify-content` braucht. */
  flex-direction: column-reverse;
  gap: 2px;
  margin-top: 6px;
  overflow-y: auto;
  /* ⚠️ FESTE Hoehe statt `max-height`, seit dem 2026-09-04 - und das ist die
     Behebung eines Fehlers, den niemand gemeldet hat, weil man ihn nicht sehen
     KONNTE: bei 1280 px war dieser Verlauf **null Pixel hoch**, seit er am
     2026-09-03 gebaut wurde. Gemessen, angemeldet wie am Tisch.

     Der Grund ist eine Kette aus drei Regeln, von denen jede einzeln richtig
     ist:

       1. `#game-view` hat eine FESTE Hoehe (100dvh minus Leiste), und der
          Inhalt braucht mehr - das Raster laeuft ohnehin unten ueber.
       2. Die Zeile `1fr` (der Spielstand) beansprucht ihre Mindestgroesse,
          und die ist bei Avataren auf Stufe 2,6 gross.
       3. Fuer die `auto`-Zeilen bleibt damit negativer Platz. Sie fallen auf
          ihre automatische Mindestgroesse - und die ist bei einem Element mit
          `overflow` ausser `visible` **null**. Genau das ist dieser Kasten.

     `min-height` waere die halbe Loesung gewesen (der Kasten haette eine
     Untergrenze, die Hoehe aber weiter geschwankt). Eine feste Hoehe macht die
     Rasterzeile DEFINIT, und damit hoert das Quetschen ganz auf. 4,2 em sind
     drei Zeilen; mehr scrollt darin.

     ⚠️ Der Wert darf NICHT `max-height` bleiben und zusaetzlich ein
     `min-height` bekommen: `min-height` schlaegt `max-height`, und bei zwanzig
     Zeilen (`_REAKT_MAX`) waere daraus eine 400 px hohe Wand geworden.

     `:empty` nimmt weiter gar keinen Platz - ein Spiel ohne Reaktionen
     verliert nichts. Unter 600 px hat `#game-view` keine feste Hoehe, dort war
     der Verlauf immer sichtbar (gemessen 38 px).

     ⚠️ SEIT DEM 2026-09-07 gilt diese feste Hoehe nur noch fuer die EINE
     Spalte, also unter 841 px. In der rechten Spalte gibt die Regel darunter
     sie wieder frei - dort ist die Hoehe von aussen definit (die Spalte haengt
     an einer `1fr`-Rasterzeile), und die Falle von oben kann nicht zuschlagen.
     Ein `4.2em` dort waere eine Spalte voller Luft ueber drei Zeilen Text. */
  height: 4.2em;
}
/* ⚠️ In der rechten Spalte nimmt der Verlauf den ganzen Rest, und `4.2em`
   waere hier falsch. Die Kette aus dem Kommentar darueber greift dabei NICHT:
   die Spalte selbst haengt an einer `1fr`-Zeile des Rasters und hat damit eine
   definite Hoehe; was hier nachgibt, ist der Verlauf und nicht die Zeile.

   `min-height: 0` ist Pflicht - die automatische Mindesthoehe eines
   Flex-Kindes ist sein Inhalt, und zwanzig Zeilen (`_REAKT_MAX`) haetten die
   Spalte sonst nach unten aus dem Raster geschoben. */
/* ⚠️ Nur aus dem Netz. Am Tisch greift dann wieder die Grundregel
   `.rk-feed { height: 4.2em }` - drei Zeilen, wie vor dem 2026-09-07, und das
   ist hier richtig: der Verlauf steht in einer `auto`-Rasterzeile, und deren
   automatische Mindestgroesse ist bei einem Element mit `overflow` NULL. Die
   feste Hoehe ist genau die Behebung dieses Falls (die Kette steht
   ausgeschrieben an `.rk-feed` darueber). */
body.nicht-am-tisch .rk-spalte > .rk-feed { height: auto; flex: 1 1 auto; min-height: 0; margin-top: 0; }
.rk-feed:empty { display: none; }
.rk-zeile {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-on-surface-variant);
  min-width: 0;
}
.rk-zeile .rk-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Der Absender eines Zurufs (2026-09-13). Zwilling zu `.rk-von` in
   static/kiosk.html.

   ⚠️ `flex: 0 0 auto`: die Zeile ist ein Flex, und der Text daneben
   schrumpft (`overflow: hidden`). Ohne das gäbe der NAME nach - und ein
   gekürzter Absender ist schlechter als ein gekürzter Zuruf, weil er die
   Frage nicht mehr beantwortet, um die es hier geht. */
.rk-zeile .rk-von { flex: 0 0 auto; font-weight: 700;
                    color: var(--md-sys-color-primary); }
.rk-zeile.is-cheer { color: var(--md-sys-color-primary); font-weight: 600; }
/* ⚠️ Die automatische Tormeldung (2026-09-09) - "Deutlich und Hervorgehoben",
   woertlich. Die Farbe kommt von der SEITE und nicht vom Akzent: dann sagt
   schon der Farbeindruck, wer getroffen hat, bevor man den Satz gelesen hat.

   Zwilling zu `.rk-zeile.is-tor` in static/kiosk.html - der Kiosk laedt dieses
   Stylesheet nicht, und tests/test_kiosk_tafel.py haelt die zwei Fassungen in
   Schritt.

   ⚠️ `white-space: normal` am Text: die Grundregel darueber kuerzt mit
   Auslassungspunkten, und in der schmalen Spalte der Spielansicht bliebe von
   "TOOOOR durch Anna zum 4:6 in der 7. Spielminute" sonst das erste Drittel. */
.rk-zeile.is-tor {
  font-weight: 800;
  padding: 2px 8px;
  border-radius: var(--md-sys-shape-corner-large);
  color: var(--md-ext-color-on-team1-container);
  background: var(--md-ext-color-team1-container);
}
.rk-zeile.is-tor[data-team="team2"] {
  color: var(--md-ext-color-on-team2-container);
  background: var(--md-ext-color-team2-container);
}
.rk-zeile.is-tor .rk-text { white-space: normal; }

/* ── Live clock ───────────────────────────────────────────────────────────── */
/* ⚠️ Seit dem 2026-09-03 steht hier "Donnerstag den 04.09.2026 - 00:41:12"
   statt nur "00:41:12" - Wunsch des Betreibers. Der Text ist damit rund
   viermal so lang, und die Leiste muss das aushalten.

   Deshalb DREI Spannen und nicht eine Zeichenkette: das Stylesheet kann
   einzelne Teile weglassen, wenn der Platz nicht reicht. Weggelassen wird von
   links, also erst der Wochentag und dann das Datum - die Uhrzeit ist der
   Grund, warum die Anzeige da ist.

   Und kleiner: 1,4 rem trugen acht Zeichen, nicht vierunddreissig. Die Zahl
   der Umbruchpunkte bleibt bei zwei (600, 840) - eine dritte Marke nur fuer
   eine Uhr waere ein neuer Sonderfall im ganzen Stylesheet. */
.nav-clock {
  display: flex;
  align-items: baseline;
  gap: .4em;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--md-sys-color-on-surface);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
  margin-right: 4px;
  min-width: 0;
  /* ⚠️ Die Uhr gibt NIE nach (2026-09-30). Seit die Pillen in einer Schiene
     stehen (`.nav-links` mit `flex: 0 1 auto` statt `flex: 1`), schrumpfen
     Schiene und Uhr ANTEILIG nach ihrer Inhaltsbreite - vorher hatte die
     Schiene die Basis 0 % und gab allein nach. Gemessen in der Sichtpruefung:
     bei 840 px ragte die Uhrzeit 29 px aus ihrem Kasten unter Zahnrad und
     Glocke, in allen 112 Prueflaeufen dieser Breite. Die Schiene ist wischbar und
     verliert nichts, die Uhr schon. */
  flex-shrink: 0;
  /* ⚠️ HIER drängt die ganze rechte Hälfte der Leiste nach rechts, und das ist
     die eine Zeile, die dafür fehlte (2026-09-04). Der Betreiber: „In der
     Kopfzeile steht alles linksbündig. Der Tag, das Datum, Uhrzeit,
     Einstellungen etc. sollen rechtsbündig sein."

     `#navbar` ist ein Flex-Kasten OHNE `justify-content`, und das einzige
     wachsende Kind war `.nav-links` - die Pillen. Die sind aber bei ≥ 840 px
     (dort ist das Menü eine Rail) und bei ≤ 600 px (dort eine untere Leiste)
     `display: none`. Ohne wachsendes Kind drängt nichts, und alles klebte
     links. Genau bei 601-839 px fiel es nicht auf, weil dort die Pillen stehen.

     `margin-left: auto` an der Uhr statt `justify-content: flex-end` am
     Elternteil: die Uhr ist das erste Element der rechten Gruppe, und alles
     dahinter (Zahnrad, Glocke, Konto, Datenbankpunkt) folgt ihr von selbst.
     `justify-content` hätte auch die Pillen mitgeschoben, wo es sie gibt. */
  margin-left: auto;
}
/* Wochentag und Datum sind Beiwerk - die Uhrzeit ist die Auskunft. */
.nav-clock .uhr-tag,
.nav-clock .uhr-datum {
  font-weight: 500;
  color: var(--md-sys-color-on-surface-variant);
  letter-spacing: 0;
}
/* ⚠️ Hier stand bis zum 2026-09-10 ein Gedankenstrich zwischen Datum und
   Uhrzeit, als `::before` an der Uhrzeit. Der Betreiber: "Dort soll
   'Wochentag den DD.MM.JJJJ HH:MM:SS' stehen." - also ohne ihn.

   Der eigentliche Fund war, dass die zwei Uhren AUSEINANDERLIEFEN: der Kiosk
   (static/kiosk.html) hatte den Strich nie, die Leiste schon. Gemessen am
   2026-09-10: Leiste "Donnerstag den 10.09.2026 - 08:31:31", Kiosk
   "Donnerstag den 10.09.2026 08:31:40". Zwei Bildschirme, dieselbe Sekunde,
   zwei Schreibweisen. Jetzt beide dieselbe.

   Mit dem Strich ist auch seine Ausnahme im 600er-Block entfallen - sie
   loeschte ihn dort wieder, weil `display: none` ein Element nicht `:empty`
   macht. Eine Ausnahme ohne Regel ist Ballast. */

/* ── Dashboard ────────────────────────────────────────────────────────────── */
.dashboard-grid {
  display: grid;
  /* ⚠️ 1.6fr zu 1fr seit dem 2026-09-17, vorher 1fr 1fr. Der Betreiber wollte
     „die identische Anordnung von den Kioskseiten"; dort steht
     `#kiosk-standings` auf `flex: 1.6` und `#kiosk-sidebar` auf `flex: 1`.
     Dasselbe Verhaeltnis, rund 61,5 zu 38,5 Prozent - die Rangliste bekommt
     den Platz, den sie im TV auch hat.

     ⚠️ Der Betreiber hat am 2026-09-17 ausdruecklich NUR das Verhaeltnis
     gewaehlt und NICHT die volle TV-Anordnung. Warum, steht direkt darunter:
     der Wickel, der „Letzte Spiele" und „Aktuelle Serien" untereinander nach
     rechts gesetzt haette, ist am 2026-09-16 gemessen und verworfen worden.
     (Den Serienkasten gibt es seit dem 2026-09-17 ohnehin nicht mehr.) */
  grid-template-columns: 1.6fr 1fr;
  gap: 20px;
}
/* ⚠️ VERWORFEN am 2026-09-16: ein Flex-Wickel `.dg-spalte`, der "Letzte
   Spiele" und "Aktuelle Serien" wie in der Kiosk-Seitenspalte (`#kiosk-sidebar`)
   untereinander in die RECHTE Rasterspalte gesetzt haette.

   Gebaut, GEMESSEN und wieder ausgebaut: die rechte Spalte wurde damit um
   375 px hoeher als die linke (bei 1280 px Fenster von 913 auf 1288). Die
   Ranglistenkarte traegt `.card-fill` und verteilt geschenkte Hoehe auf ihre
   Zeilen - gemessen standen sie danach 127 px auseinander, die Spielzeilen
   daneben 100. Auseinandergerissen, nicht grosszuegig.

   Es gab nur zwei Auswege, und beide kosten mehr als der Wickel bringt:
   weniger Spielzeilen rechts (bricht "links so viele Zeilen wie rechts" vom
   2026-09-02) oder die Rangliste nicht mehr strecken (das ist die aeltere
   Klage "ein Handbreit Leerraum unter der kuerzeren Karte").

   Der Serienkasten stand deshalb ueber die volle Breite UNTER dem Raster.

   ⚠️ Und ihn gibt es seit dem 2026-09-17 gar nicht mehr - der Betreiber:
   "Das ist viel zu unuebersichtlich." Die Messung oben bleibt trotzdem
   stehen: sie gilt fuer JEDEN Wickel um zwei Karten dieser Seite, nicht nur
   fuer den einen, der sie ausgeloest hat. Wer den naechsten baut, soll nicht
   375 Pixel neu messen muessen. */
/* Karte, die ihre Hoehe auf den Inhalt verteilt.
   Das Grid streckt beide Karten der Startseite auf dieselbe Hoehe - die gibt
   "Letzte Spiele" mit fuenf zweizeiligen Eintraegen vor. Die Rangliste daneben
   hat weniger Zeilen und liess darunter Leerraum stehen (gemessen 221 px bei
   1920x1080). Mit dieser Regel wird die Karte eine Spalte, der Turnierblock
   darin nimmt den Platz ein (flex:1), und die Rangliste verteilt ihre Zeilen
   darin gleichmaessig (grid-auto-rows:1fr, siehe `.mini-rang`).

   ⚠️ Einen Vormittag lang war das anders (2026-09-21): die Zeilen bekamen
   ein festes Mass am Avatar, der Rest blieb unten stehen. Noch am selben Tag
   zurueckgenommen - es blies die Kopfzeile der Rangliste auf mehrere hundert
   Pixel auf. Die ganze Geschichte samt Messung steht an `.mini-rang`; sie
   ist der Grund, warum das `1fr` dort kein blosser Zahlenwert ist.
   Auf schmalen Geraeten steht nur eine Spalte, die Karten sind dann
   inhaltshoch - es gibt nichts zu verteilen und die Regel bleibt wirkungslos. */
.card-fill {
  display: flex;
  flex-direction: column;
}
/* ⚠️ Gedaempft statt blau, und halb so hoch (2026-09-21). Der Betreiber:
   „Die blaue Pille passt nicht zum reduzierten Design."

   Geaendert sind Farbrolle (im Markup `btn-tonal` statt `btn-primary`),
   Hoehe und Form - NICHT die Breite und NICHT die Klasse. `width: 100%`
   bleibt, weil es am 2026-09-04 ausdruecklich bestellt wurde („Der Button
   Spiel starten auf der Seite eines neuen Spiels, soll genauso gross sein,
   wie der Neues Spiel starten auf der Startseite") und zwei Zusicherungen
   daran haengen.

   GEMESSEN am 2026-09-21 mit `offsetHeight` (nicht `getBoundingClientRect`,
   Falle ⑤): **56 px** bei 1280 px UND bei 390 px Fensterbreite, gegen vorher
   116 px. Gerechnet waren 60 - die vier Pixel Unterschied sind `min-height:
   var(--min-touch)` aus `.btn`, und das ist der Boden, unter den dieser
   Knopf nicht faellt. Breite 1360 bzw. 366 px, also weiterhin voll.
   Verworfen:
   `btn-outline` - im Haus tragen umrandete Knoepfe die NEBENaktionen
   („Verbindung testen", „PIN aendern"), die Hauptaktion waere davon nicht
   mehr zu unterscheiden. Ebenso verworfen: ein erweiterter FAB, der die
   volle Breite zuruecknaehme.

   ⚠️ Seit dem 2026-09-23 wieder GEFUELLT (`btn-primary`), Hoehe und Form
   bleiben. Der Betreiber: „nicht kontraststark genug. Ich brauche die in
   einer kraeftigeren Farbe." Gestoert hatte am 21. die Pille, nicht die
   Farbe - so hat er es beim Vergleich der Varianten entschieden.

   GEMESSEN am 2026-09-23 im Browser (`getComputedStyle`), Flaeche gegen
   Seitengrund: tonal 1,22 : 1 hell / 1,99 : 1 dunkel - der Knopf war kaum
   eine Flaeche und trug dieselbe Rolle wie die aktive Navigationspille.
   Gefuellt 6,13 : 1 / 10,9 : 1, Schrift darauf 6,46 : 1 / 7,72 : 1. Hoehe
   weiterhin 56 px bei 1280 und 390 px.
   Verworfen: `secondary` gefuellt (gleich stark, aber M3 kennt keinen
   gefuellten Sekundaerknopf - eine neue Klasse fuer eine Ausnahme);
   `primary-container` (hell dieselben 1,24 : 1 wie tonal, nur bunter);
   `tertiary` (das ist Orange, die Farbe von Spieler 2). „Einstellungen
   speichern" bleibt tonal - ausdruecklich so entschieden. */
.big-start-btn {
  width: 100%;
  padding: 18px 28px;
  font-size: var(--md-sys-typescale-title-medium-size);
  border-radius: var(--md-sys-shape-corner-large);
  margin-bottom: 20px;
}
/* ── Resonanz in der Kalenderzeile (2026-09-12) ─────────────────────
   Zwei Zahlen in der Vermerkspur: wie viele mochten das Spiel, wie viele haben
   etwas dazu gesagt. Nur der Kalender schaltet sie ein.

   ⚠️ Die Kommentarzahl ist ein KNOPF in einem Verweis - erlaubt (anders als
   ein `<a>` in einem `<a>`), aber er braucht eigene Zielgroesse: 32 px, die
   Hausmarke fuer kleine Ziele. Ohne sie waere er bei 12 px Schrift knapp
   20 px hoch und am Telefon nicht zu treffen. */
.rg-echo { display: inline-flex; align-items: center; gap: 6px;
           margin-left: 8px; vertical-align: middle; }
.rg-echo-zahl {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: var(--md-sys-typescale-label-small-size, .6875rem);
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}
.rg-echo-zahl .msym { font-size: 1.1em; }
.rg-echo-knopf {
  min-height: var(--md-sys-comp-size-xs);
  padding: 0 6px;
  border: none;
  background: none;
  cursor: pointer;
  border-radius: var(--md-sys-shape-corner-small);
  color: var(--md-sys-color-primary);
}
.rg-echo-knopf:hover { background: var(--md-sys-color-surface-container-high); }
.rg-echo-knopf:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* Eine Tabelle braucht keine 1400 px. Im Turnier-Spielverlauf steht sie in
   einer Karte ueber die ganze Inhaltsbreite, und gemessen bei 1500 px Fenster
   bekam jede Spielerseite 468 px: der Name stand am linken Rand einer fast
   leeren Zelle, sein Ergebnis eine Handbreit daneben. Die Grenze gilt fuer den
   ganzen Block samt Blaetterleiste, damit „Weiter" am Rand der Tabelle sitzt
   und nicht am Rand des Fensters.

   Die Karte selbst bleibt breit; die Spaltenfrage beantwortet der Wickel
   `.sp-liste` (Block „Die Spielkachel" am Dateiende). */
.rg-block { max-width: 52em; }

/* ── Der Like-Knopf am Tisch: OBEN IN DER ECKE DER KISTE (2026-09-13) ─────
   Der Betreiber: „Der Like Button soll auch die Größe von + und - haben aber
   beim Linken Spieler oben Links im dem Kasten und beim Rechten Spieler oben
   Rechts im Kasten angezeigt werden.“

   Bis dahin stand er UNTER der Punktzeile, mittig und in Stufe M (56 px hoch,
   das `--min-touch` von `.btn`). Jetzt ist er das ERSTE Kind der Kiste und
   genauso gross wie die Tortaste darunter.

   ⚠️ IM FLUSS und nicht `position: absolute` in der Ecke. Absolut gesetzt
   läge er über dem Avatar: `.team-players-big` ist mittig und so breit wie
   sein Inhalt, und im Doppel reichen zwei Avatare samt Namen bis nah an die
   Kistenkante. Eine Reserve dagegen (Polster links und rechts) hätte genau
   die Avatare verkleinert, die am Tisch aus drei Metern lesbar sein sollen.
   Im Fluss kostet er eine Zeile - dafür bekommt die Kiste die Zeile unter
   der Punktzeile zurück, die er bisher belegt hat.

   ⚠️ `align-self: stretch`, weil `.team-panel` eine Flex-Spalte mit
   `align-items: center` ist: ohne das wäre der Wickel nur so breit wie der
   Knopf, und „links“ wie „rechts“ hätten dieselbe Wirkung - nämlich keine.
   Dieselbe Begründung steht an `.team-punkte`. */
.tor-like { display: flex; align-self: stretch; }
.team1 .tor-like { justify-content: flex-start; }
.team2 .tor-like { justify-content: flex-end; }
/* ⚠️ `min-height: 0` ist nötig und nicht Zierde: `.btn` bringt
   `--min-touch` (56) mit, und im Flachfenster-Block ist die Tortaste nur
   52 px hoch - ohne das Aufheben wäre der Like-Knopf dort höher als das
   „+“ daneben, obwohl beide dieselbe Variable lesen. */
.tor-like .btn {
  width: var(--tor-w, 130px);
  height: var(--tor-h, 100px);
  min-height: 0;
  padding: 0;
  border-radius: var(--md-sys-shape-corner-large);
}
.tor-like .msym { font-size: 2.2rem; }

/* ── Resonanz: Bewertung und Kommentare am beendeten Spiel (2026-09-10) ──────

   Der Betreiber: „Jeder angemeldeter Spieler soll an gespielten Spielen wie
   bei YouTube 'Like' 'Dislike' und Kommentare hinterlassen."

   Beide Zahlen sind sichtbar - so entschieden. Der EIGENE Klick ist zusaetzlich
   markiert, damit man sieht, wofuer man selbst gestimmt hat; das leistet die
   Farbe, nicht die Zahl. */
.echo-knoepfe { display: flex; gap: 10px; flex-wrap: wrap; }
/* ⚠️ KEINE eigene Hoehe. Der erste Entwurf stand hier auf
   einer eigenen `min-height`, die 40 px durch den Zoom TEILTE - und das war
   doppelt falsch:

   1. Die Zoomvariable ist ein CSS-`zoom` auf dem Rumpf, px-Werte skalieren also
      ohnehin mit. Nur Viewport-Einheiten muessen geteilt werden (der Satz
      steht bei der Variablen in m3-tokens.css). Die Division HOB die
      Skalierung auf: gemessen 40 Geraete-Pixel bei Zoom 1,25 statt 50 -
      der Knopf schrumpfte am Kicker-Touch relativ zu allem daneben.
   2. Und die Stufe war die falsche. `--min-touch` ist Stufe M (56) und die
      Vorgabe fuer alles, was jemand am Tisch mit dem Finger trifft;
      SPEZIFIKATION 10.1 haelt als Grundsatz fest, kein solches Mass zu
      verkleinern. S und XS sind fuer Chips und Marken.

   Also gar keine Hoehenregel: `.btn` bringt `--min-touch` und
   `corner-full` schon mit. Uebrig bleibt die schmalere Polsterung, weil zwei
   Knoepfe nebeneinander stehen. */
.echo-knopf { padding: 4px 16px; }
.echo-zahl { font-variant-numeric: tabular-nums; margin-left: 4px; }
/* Die eigene Stimme. Tonflaeche statt Rahmenfarbe: ein gefaerbter Rahmen
   verschwindet auf dem Kicker-Touch aus einem Meter Abstand. */
.echo-an {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  border-color: var(--md-sys-color-secondary-container);
}

/* ── Highlights im Bereich „TV" (2026-09-10) ────────────────────────────────
   Vier Kacheln nebeneinander, die von selbst umbrechen. KEIN Inline-Raster
   und keine dritte Umbruchmarke: `auto-fit` mit einer Mindestbreite macht
   aus vier Spalten zwei und dann eine, ohne dass eine Breite genannt wird
   (M3-RULES Regel 10 - es bleibt bei 600 und 840). */
/* ⚠️ 190 px seit dem 2026-09-17, vorher 210. Die 210 stammten vom
   2026-09-16 und waren auf VIER Knoepfe gerechnet; seit dem 2026-09-17 spielt
   die Kachel selbst ab, und der Wiedergabeknopf ist entfallen. Der Betreiber
   hat sie daraufhin ausdruecklich schmaler bestellt.

   GEMESSEN am 2026-09-17, was die drei Knoepfe wirklich brauchen: je 55 px,
   zusammen 165 px, plus 28 px Kachelpolster abzueglich der 16 px negativen
   Rands der Knopfreihe - macht 177 px. Mit zweistelligen Zahlen 199 px, mit
   dreistelligen 222 px.

   GEMESSEN, was eine kleinere Zahl wirklich bringt - denn "schmaler" ist kein
   Gewinn, eine Spalte mehr ist einer:

     Fenster   Raster    210 px          190 px         180 px
     1000 px   900 px    3 Sp., 2 Reihen 4 Sp., 1 Reihe 4 Sp., 1 Reihe
      900 px   800 px    3               3              3
      840 px   740 px    3               3              3
      700 px   600 px    2               2              2

   ⚠️ 190 und nicht 180: unter 190 kommt KEINE Spalte mehr dazu, die Messung
   zeigt es Zeile fuer Zeile. Die 13 px Luft ueber den noetigen 177 gehoeren
   damit der Zahl neben dem Symbol - bei 1000 px sind die Kacheln 215 px breit,
   eine zweistellige Likezahl (199 px) passt also noch in eine Zeile. Erst
   dreistellig braeche sie um, und dann ist die Kachel eine Zeile hoeher - kein
   Schaden, aber der Grund, warum hier nicht 180 steht.
 */
.tv-highlights {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px;
}
.tv-hl {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
}
/* Der Verweis deckt den TEXT der Kachel, nicht die Knoepfe darunter. Er sieht
   aus wie vorher die ganze Kachel - deshalb stehen Richtung, Abstand und die
   abgeschaltete Unterstreichung jetzt hier. */
.tv-hl-ziel {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-decoration: none;
  color: inherit;
}
/* Die Knoepfe tragen ihr eigenes Polster (`.aktion`). Der negative Rand holt
   sie an die Kante der Kachel zurueck, damit das erste Symbol unter dem
   ersten Buchstaben darueber steht und nicht acht Pixel weiter rechts. */
.tv-hl-aktionen {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin: 4px -8px -4px;
}
/* Der Zeitraum ist die Ueberschrift der Kachel - gesperrte Versalien wie bei
   den Kartentiteln, damit man ihn vom Namen darunter unterscheidet. */
.tv-hl-titel {
  font-size: var(--md-sys-typescale-label-small-size);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
}
.tv-hl-name { font-weight: 700; overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
/* Was gefallen ist - seit dem 2026-09-12 unter der Paarung. Zwei Zeilen und
   nicht eine: „Bodo vs. Anna · Tor von Anna zum 4:8" passt in eine 180 px
   breite Kachel nicht, und gekürzt fällt genau die Tat weg. */
.tv-hl-tat { font-size: var(--md-sys-typescale-body-small-size);
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* `.tv-hl-zahl` und `.tv-hl-reihe` sind am 2026-09-16 weggefallen: die
   Likezahl war eine reine Anzeige und ist jetzt ein Knopf (`.echo-zahl` in
   `.aktion`), und der Wiedergabeknopf liegt nicht mehr neben der Kachel,
   sondern darin. */

/* ── Verlauf: was gefiel und was gesagt wurde (2026-09-10 / 2026-09-12) ────
   Eine Zeile je Reaktion. Sie fuehrt ins Spiel; der Wiedergabeknopf daneben
   erscheint nur, wo es wirklich eine Aufzeichnung gibt.

   ⚠️ Die Zeile ist seit dem 2026-09-12 ZWEIZEILIG: oben die Paarung, darunter
   was geschah („Tor von Anna zum 4:5"). Beides in eine Spur zu legen hiesse,
   dass bei 390 px entweder die Paarung oder die Tat wegfaellt - und ohne
   Paarung ist die Tat nicht zuzuordnen. */
.vl-liste { display: flex; flex-direction: column; }
.vl-zeile {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: var(--min-touch);
  padding: 6px 4px;
  text-decoration: none;
  color: inherit;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.vl-zeile:last-child { border-bottom: none; }
/* Der Text nimmt den Rest - Zahl und Knopf bleiben rechts stehen, auch wenn er
   lang ist. `min-width: 0` laesst die Spur kuerzen. */
.vl-tor { flex: 1 1 auto; min-width: 0; display: flex;
          flex-direction: column; gap: 1px; }
.vl-paarung, .vl-tat {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vl-paarung { font-weight: 600; }
.vl-tat { font-size: var(--md-sys-typescale-body-small-size); }
/* `.vl-likes` ist am 2026-09-16 weggefallen. Die blaue Zahl war die letzte
   Anzeige, die aussah wie eine Aktion und keine war - erst im Verlauf, dann
   bei den beliebtesten Toren. Beide tragen jetzt `.aktion` mit `.echo-zahl`. */
/* Die Wiedergabe steht NEBEN dem Verweis und nicht darin: die Zeile fuehrt ins
   Spiel, und ein Knopf in einem Verweis waeren zwei Ziele an einer Stelle.
   Deshalb umfasst `.vl-reihe` beides und traegt die Trennlinie. */
.vl-reihe { display: flex; align-items: center; gap: 4px;
            border-bottom: 1px solid var(--md-sys-color-outline-variant); }
.vl-reihe:last-child { border-bottom: none; }
.vl-reihe > .vl-zeile { flex: 1 1 auto; min-width: 0; border-bottom: none; }
/* Die Aktionen der Zeile - seit dem 2026-09-16 nicht mehr ein einzelner Knopf,
   sondern Wiedergabe, Daumen und Kommentare zusammen. Sie stehen weiter NEBEN
   dem Verweis, aus demselben Grund wie vorher der eine Knopf.

   ⚠️ `flex-shrink: 0` an der Gruppe und nicht nur an ihren Knoepfen: die Zeile
   daneben darf kuerzen (`min-width: 0` am Verweis), die Knoepfe nicht - sonst
   waere die erste Zahl, die verschwindet, die neben dem laengsten Namen. */
.vl-aktionen { display: flex; align-items: center; flex: 0 0 auto; }
/* ⚠️ Nur noch die Flexangabe. Groesse, Farbe und Ecken kommen seit dem
   2026-09-16 aus `.aktion` - derselbe Knopf wie in der Torliste und in der
   Highlight-Kachel. Die tote Klasse `btn-ghost`, die im ganzen Stylesheet
   nie eine Regel hatte, ist dabei weggefallen. */
.vl-spielen { flex: 0 0 auto; }
/* Die Blaetterleiste - dieselbe Form wie bei den Spielen eines Spielers, den
   Freundschaftsspielen und dem Turnierverlauf, nur als Klasse statt dreimal
   als Inline-Stil. */
.vl-blaettern {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

/* Der Weg zurueck aus der Torsicht in die Spielsicht. Eigene Zeile ueber den
   Knoepfen, damit er nicht wie eine dritte Bewertung aussieht. */
.echo-zurueck { margin-bottom: 12px; }

/* Die Zelle je Torzeile. Die DREI Knoepfe (Daumen hoch, Daumen runter,
   Kommentare) sehen aus wie die Aktionen daneben - seit dem 2026-09-15
   buchstaeblich: `.aktion` ist dieselbe Regel. Sie sind keine Hauptaktion der
   Seite, und die Zeile traegt schon sechs Knoepfe. */
td.tor-echo { white-space: nowrap; }
/* ── „Dieses Tor war gemeint" (2026-09-12) ───────────────────────────
   Wer aus dem Verlauf, den Highlights oder der Statistik auf ein Tor klickt,
   landet bei der Resonanzkarte - die steht aber unter einer Tabelle mit
   womoeglich zwanzig Zeilen. Diese Marke sagt, welche gemeint war, und
   verschwindet nach vier Sekunden von selbst.

   ⚠️ Kein Blinken, keine Animation und kein Uebergang. Die Marke soll nach
   einem Sprung dastehen, nicht auf sich aufmerksam machen - und ein weiches
   Ausblenden braeuchte eine Regel an ALLEN Zeilen OHNE die Klasse. Die
   Tortabelle traegt keine eigene Klasse, der Selektor waere also `tr > td`
   fuer das ganze Haus gewesen: ein Uebergang an jeder Tabellenzelle der
   Anwendung, damit eine Zeile weich verblasst. */
tr.tor-gesprungen > td {
  background: var(--md-sys-color-secondary-container);
}
/* ⚠️ Hier stand bis zum 2026-09-15 `.tor-echo-knopf` mit eigener Hoehe,
   eigenem Polster und eigener Schriftgroesse - die vierte Schreibstelle fuer
   dasselbe Aussehen. Die Knoepfe der Resonanzspalte sind seither `.aktion`,
   und der Abstand kommt aus der Zelle statt aus einem Geschwisterselektor. */
td[id^="goal-actions-"] { white-space: nowrap; }
/* ⚠️ `.aktion` und NICHT `.aktion + .aktion`. Der Wiedergabeknopf steckt in
   einem Platzhalter (`#goal-play-<id>`), der `display: contents` traegt - er
   verschwindet aus dem KASTENBAUM, nicht aus dem DOM. Ein Geschwisterselektor
   sieht deshalb weiterhin die Spanne dazwischen und greift nie. */
td.tor-echo .aktion, td[id^="goal-actions-"] .aktion { margin-right: 2px; }

.echo-liste { margin-top: 16px; display: flex; flex-direction: column; gap: 12px; }
/* ⚠️ `-high` und nicht `-container`. Gemessen am 2026-09-10: im DUNKLEN steht
   die Karte selbst auf `surface-container` (siehe `html[data-theme="dark"]
   .card`), und die Zeile war damit unsichtbar - gleicher Ton auf gleichem Ton.
   Im Hellen faellt es nicht auf, weil die Karte dort `-lowest` ist. Genau der
   Fall, den M3-RULES Regel 9 meint: jede neue Komponente in BEIDEN Themes
   pruefen. Die Flaechen-Hierarchie (Regel 3) sagt dasselbe - was in einer
   Karte liegt, geht eine Stufe hoeher. */
.echo-zeile {
  padding: 10px 14px;
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-high);
}
.echo-kopf { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
/* Woran ein Kommentar hing, wenn die Karte das ganze Spiel zeigt: sonst
   waere aus „war knapp" nicht zu erraten, worauf es sich bezieht. Eine
   Marke und kein Fliesstext - sie steht in jeder zweiten Zeile. */
.echo-wozu {
  padding: 1px 8px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-high);
}
/* ⚠️ `overflow-wrap: anywhere` und nicht `break-word`: ein Kommentar ist freie
   Eingabe, und eine Kette aus vierzig Zeichen ohne Leerraum sprengt sonst die
   Karte nach rechts - auf einem 390-px-Telefon sofort sichtbar. */
.echo-text { margin-top: 4px; overflow-wrap: anywhere; white-space: pre-wrap; }
/* Die Daumen an einer Kommentarzeile (2026-09-17).

   ⚠️ HINTER `.echo-text` und nicht davor (Falle 4): eine Zusicherung
   schneidet den Rumpf jener Regel mit `index(".echo-text {")` heraus.

   Rechtsbuendig unter dem Text, wie die Aktionen im Verlauf - der Text
   ist die Aussage, die Knoepfe sind die Antwort darauf. */
.echo-aktionen { display: flex; justify-content: flex-end;
                 gap: 4px; margin-top: 4px; }


.echo-eingabe { display: flex; gap: 10px; margin-top: 16px; align-items: flex-end; }
.echo-eingabe textarea { resize: vertical; min-height: var(--min-touch); }
/* Erwaehnungen (2026-09-23): der Link im Kommentar und die Vorschlagsliste.

   ⚠️ HINTER `.echo-text` und `.echo-eingabe` (Falle 4) und unter eigenen
   Namen: Zusicherungen schneiden jene Regeln mit `index()` heraus.

   Der Link in `primary` und ohne Unterstrich, wie ein Name und nicht wie
   eine Adresse - unterstrichen erst beim Zeigen. */
.echo-erwaehnung {
  color: var(--md-sys-color-primary);
  font-weight: 600;
  text-decoration: none;
}
.echo-erwaehnung:hover, .echo-erwaehnung:focus-visible {
  text-decoration: underline;
}
/* Die Liste haengt an `body` und liegt wie die Emoji-Tafel ueber Chatfenster
   und Modal (`z-index: 500`, unter den Toasts). Lage aus js/erwaehnung.js. */
.ew-liste {
  position: fixed;
  z-index: 500;
  min-width: 200px;
  max-width: 320px;
  padding: 4px;
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium);
  box-shadow: var(--md-sys-elevation-level2);
  overflow-y: auto;
}
.ew-eintrag {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--md-sys-comp-size-xs);
  padding: 4px 8px;
  border-radius: var(--md-sys-shape-corner-small);
  cursor: pointer;
}
.ew-eintrag.is-aktiv, .ew-eintrag:hover {
  background: rgba(var(--md-sys-color-scrim-rgb) / .12);
}
.ew-name { min-width: 0; overflow: hidden; text-overflow: ellipsis;
           white-space: nowrap; }
/* Feste Groesse und nicht `--avatar-scale`: siehe `_avatar` in
   js/erwaehnung.js - skaliert wurde die Liste 630 px hoch. */
.ew-avatar {
  width: 1.5rem; height: 1.5rem;
  flex: 0 0 auto;
  border-radius: var(--md-sys-shape-corner-full);
}
.ew-avatar.is-platzhalter {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .75rem; font-weight: 600;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
/* Eigene Kommentare bearbeiten und loeschen (2026-09-24).

   ⚠️ HINTER den Regeln `.echo-text`, `.echo-eingabe` und der Erwaehnung
   (Falle 4): Zusicherungen schneiden jene mit `index()` heraus.

   Stift und Papierkorb stehen rechts neben den Daumen, durch einen feinen
   Strich abgesetzt - dieselben Knoepfe, eine andere Frage (Antwort gegen
   Verwaltung). Der Papierkorb bleibt neutral wie in der Spielerliste; rot
   ist erst der Ja-Knopf der Rueckfrage. Gemessen am 2026-09-24 im Vorschlag:
   beide 32 x 32 px, die Aktionszeile bei 390 px einzeilig (32 px hoch). */
.echo-verwalten {
  display: inline-flex;
  gap: 4px;
  margin-left: 4px;
  padding-left: 8px;
  border-left: 1px solid var(--md-sys-color-outline-variant);
}
.echo-bearbeitet { margin-left: 2px; }
.echo-editor textarea {
  width: 100%;
  resize: vertical;
  min-height: var(--min-touch);
  margin-top: 6px;
}
.echo-editor-knoepfe {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: flex-end;
  margin-top: 8px;
}
/* Das Emoji links, die zwei Entscheidungen rechts - wie in der Eingabe. */
.echo-editor-knoepfe .em-knopf { margin-right: auto; }
/* „Speichern" ist eine `App.aktion` mit `btn-primary` - und `.aktion` setzt
   eine eigene Schriftfarbe (on-surface-variant) und beim Zeigen einen
   durchsichtigen Grund. Ohne diese zwei Regeln stand dunkle Schrift auf
   Violett (Aufnahme vom 2026-09-24), und beim Zeigen verschwand der Knopf.
   Die Selektoren sind mit Absicht spezifischer als die von `.aktion`, damit
   die Reihenfolge im Stylesheet keine Rolle spielt. */
.echo-editor-knoepfe .aktion.btn-primary {
  color: var(--md-sys-color-on-primary);
  background: var(--md-sys-color-primary);
}
.echo-editor-knoepfe .aktion.btn-primary:hover:not(:disabled) {
  background: var(--md-sys-color-primary);
  box-shadow: var(--md-sys-elevation-level1);
}
/* ── Kanalliste im Bereich „TV" ──────────────────────────────────────────────

   Eine Zeile je Kanal: links Name und Beschreibung als EIN Verweis, rechts das
   Adressfeld mit dem Kopier-Knopf.

   Der Verweis umfasst Name UND Beschreibung, nicht nur den Namen: die
   Trefffläche ist damit die ganze linke Hälfte, und auf einem Touchscreen ist
   das der Unterschied zwischen „geht" und „geht meistens".

   Unter 600 px klappt die Zeile um - das Adressfeld braucht Breite, und ein
   Feld, das auf 60 px zusammengedrückt wird, zeigt vom Link drei Zeichen. Die
   Regel dafuer steht im Responsive-Abschnitt, wie alle Breitenregeln. */
.kanal-zeile {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}
.kanal-zeile:last-child { border-bottom: none; padding-bottom: 0; }
.kanal-ziel {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
  text-decoration: none;
  color: inherit;
}
.kanal-ziel:hover .kanal-name { text-decoration: underline; }
.kanal-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
}
/* Das Pfeilchen „oeffnet in neuem Tab" - kleiner als der Name und gedaempft,
   es ist ein Hinweis und keine Auskunft. */
.kanal-neu { font-size: 1em; opacity: .55; }
.kanal-aktion {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 1 auto;
}
/* ⚠️ Das Adressfeld ist seit dem 2026-09-04 UNSICHTBAR - aber es ist noch da,
   und das ist der ganze Punkt.

   Wunsch des Betreibers: „In dem Menü ,Live' sollen die URLs des jeweiligen
   Kanals nicht angezeigt werden. Die Aktion ,Link kopieren' und der Link zum
   direkten Öffnen sind komplett ausreichend." Er hat recht: die Adresse liest
   niemand, sie ist zum Weitergeben da.

   ⚠️ Und es darf NICHT `display: none` sein, auch nicht `hidden`.
   `App.ui.copy` hat drei Stufen, und die zweite ist hier Pflicht:
   `navigator.clipboard` existiert über `http://192.168.…` gar nicht (kein
   sicherer Kontext), und genau so wird diese Anwendung im Haus benutzt. Stufe
   zwei markiert den Inhalt des Feldes und ruft `document.execCommand('copy')`
   - ein verborgenes Feld hat keine Auswahl, und `tests/test_profil_und_token_links.py`
   verbietet den direkten Griff zur Zwischenablage in den Komponenten.

   Also: aus dem Bild und aus dem Fluss genommen, aber vorhanden, markierbar
   und mit seiner Vorlesehilfe. Die Beschreibung daneben bekommt dadurch die
   Breite, die vorher das Feld hatte. */
.kanal-aktion .form-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}
/* ⚠️ Hier stand bis zum 2026-09-15 `.kanal-aktion .btn { min-height: 36px; … }`.
   Sie ist ERSATZLOS weg, nicht umgeschrieben: der Kopierknopf traegt jetzt
   `.aktion` wie jede andere Aktion im Haus. Die Regel haette ihn weiter
   uebersteuert - Spezifitaet (0,2,0) gegen (0,1,0), und sie stand davor. Der
   Umbau waere unsichtbar geblieben, und keine Zusicherung haette es gemeldet:
   sie suchen die Zeichenkette `App.aktion(`, nicht das Ergebnis im Browser.
   Das `white-space: nowrap` wandert mit, siehe unten bei `.aktion`. */

/* Die Erklaerung der Link-Anhaenge (2026-09-22). Zugeklappt, weil man sie
   einmal liest - aufgeklappt beherrschte eine Tabelle die Karte, in der es um
   zwei Adressen geht.

   ⚠️ HINTER `.kanal-aktion .form-input`: eine Zusicherung schneidet dort und
   prueft den Rumpf (Falle ④). */
/* ⚠️ KEIN eigener `border-top`. Seit die Hilfe hinter der Kanalliste steht,
   ist deren letzte Zeile nicht mehr `:last-child` und behaelt ihren unteren
   Strich - ein zweiter daneben sah aus wie ein Fehler. Gemeldet am
   2026-09-22. */
.kanal-hilfe {
  margin-top: 12px;
  padding-top: 12px;
}
.kanal-hilfe > summary {
  cursor: pointer;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}
.kanal-hilfe-inhalt { margin-top: 10px; }
.kanal-hilfe-satz { margin-top: 8px; }
/* Waagerecht rollbar statt umbrechend: vier Spalten auf einem Telefon
   umzubrechen ergaebe vier Zeilen je Anhang, und die Zuordnung waere weg.
   Nur der Kasten rollt, nicht die Seite. */
.kanal-parameter {
  width: 100%;
  border-collapse: collapse;
  display: block;
  overflow-x: auto;
}
.kanal-parameter th,
.kanal-parameter td {
  text-align: left;
  vertical-align: top;
  padding: 6px 10px 6px 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.kanal-parameter th {
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: var(--md-sys-typescale-label-medium-weight);
  color: var(--md-sys-color-on-surface-variant);
}
.kanal-parameter tr:last-child td { border-bottom: none; }
.kanal-beispiele {
  margin: 10px 0 0;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.kanal-beispiele code,
.kanal-parameter code,
.kanal-hilfe-satz code {
  background: var(--md-sys-color-surface-container-high);
  border-radius: var(--md-sys-shape-corner-extra-small);
  padding: 1px 5px;
  font-size: .9em;
  word-break: break-all;
}

/* Die Kugel als Verweis und ihr Zeiger (2026-09-22).

   ⚠️ HINTER allen bestehenden `.st-res`-Regeln. Mehrere Zusicherungen
   schneiden mit `css.index(".st-res {")`, und eine neue Regel davor zoege
   ihren Schnitt mit (Falle ④).

   ⚠️ Der Zeiger sitzt IM Rahmen der Kette, nicht im Fenster: so wird nichts
   von Bildschirm- in CSS-Pixel umgerechnet, und `--ui-zoom` kann ihn nicht
   verschieben (Falle ⑤). Deshalb `position: relative` am Rahmen. */
.st-kette,
.ek-form-pillen { position: relative; }
a.st-res {
  text-decoration: none;
  cursor: pointer;
}
a.st-res:hover { filter: brightness(1.12); }
a.st-res:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 1px;
}
.kt-zeiger {
  position: absolute;
  bottom: calc(100% + 6px);
  transform: translateX(-50%);
  z-index: 40;
  display: none;
  max-width: 17rem;
  width: max-content;
  padding: 6px 10px;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  /* Wie `.video-kino` und `.kurve-tooltip`: was ueber dem Inhalt schwebt,
     sagt das im Haus ueber die Hoehenstufe. */
  box-shadow: var(--md-sys-elevation-level2);
  font-size: var(--md-sys-typescale-body-small-size);
  line-height: 1.3;
  /* ⚠️ `.st-trend` setzt `white-space: nowrap`, und der Zeiger haengt darunter.
     Geerbt bedeutet das: der Kasten ist durch `max-width` gedeckelt, der Text
     kann aber nicht umbrechen und laeuft rechts heraus. Gemeldet am
     2026-09-22. Die Regel steht drei Ebenen hoeher und hat mit dem Zeiger
     nichts zu tun - deshalb hier ausdruecklich zurueckgesetzt. */
  white-space: normal;
  /* Kein Ereignis abfangen: sonst nimmt der Zeiger der Kugel den Zeigerfokus
     ab, sobald er sie beruehrt, und blinkt. */
  pointer-events: none;
}
.kt-zeiger.is-auf { display: block; }

/* ── Badges (Container-Farbrollen) ────────────────────────────────────────── */
.badge {
  display: inline-flex;
  padding: 3px 12px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: var(--md-sys-typescale-label-medium-weight);
}
/* Eine Marke, die ein Knopf ist. Seit dem 2026-09-03 traegt „Einladung offen"
   in der Spielerliste die zwei Wege dahinter (erneut senden, zuruecknehmen).

   Ein Knopf bringt Browservorgaben mit, die eine Marke nicht haben darf -
   Rahmen, Systemschrift, eigene Hintergrundfarbe. Die werden hier
   zurueckgenommen; das Erscheinungsbild bleibt die Marke, nur der Zeiger und
   die Tastaturerreichbarkeit kommen dazu. */
.badge-knopf {
  border: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
  font-family: inherit;
  cursor: pointer;
}
.badge-knopf:hover { background: var(--md-sys-color-surface-container-highest); }

/* Freundschaftsspiel: aufgezeichnet, laeuft aber in keine Wertung */
.badge-friendly {
  background: var(--md-sys-color-secondary-container, var(--surface));
  color: var(--md-sys-color-on-secondary-container, var(--text-dim));
}
/* Gast: spielt mit, wird aber nicht aufgezeichnet */
.badge-guest {
  background: var(--md-ext-color-warning-container);
  color: var(--md-ext-color-on-warning-container);
}
.player-card.is-guest { outline: 2px dashed var(--md-ext-color-warning); }

.badge-active   { background: var(--md-ext-color-success-container); color: var(--md-ext-color-on-success-container); }
.badge-finished { background: var(--md-sys-color-surface-container-highest); color: var(--md-sys-color-on-surface-variant); }
.badge-archived { background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
.badge-running  { background: var(--md-ext-color-warning-container); color: var(--md-ext-color-on-warning-container); }
.badge-pending  { background: var(--md-sys-color-surface-container-highest); color: var(--md-sys-color-on-surface-variant); }

/* ── Player link (klickbarer Name/Avatar) ─────────────────────────────────── */
.player-link {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  border-radius: var(--md-sys-shape-corner-extra-small);
  padding: 1px 4px;
  margin: -1px -4px;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard),
              color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}
.player-link:hover {
  background: rgba(var(--md-sys-color-primary-rgb) / 0.12);
  color: var(--md-sys-color-primary);
}

/* ── Status list ──────────────────────────────────────────────────────────── */
/* ⚠️ `flex-start` und nicht `center`, seit dem 2026-09-13. Einzeilige
   Meldungen sahen gleich aus, dreizeilige nicht: bei „Medienserver antwortet
   nicht - ohne ihn gibt es keinen Livestream (die Aufzeichnung laeuft weiter)"
   stand der rote Punkt am Telefon neben der ZWEITEN Zeile und sah aus wie ein
   Aufzaehlungszeichen, das verrutscht ist. Der Punkt gehoert an den Anfang der
   Meldung. Die 6 px setzen ihn auf die Mitte der ersten Zeile (8 px Punkt,
   rund 20 px Zeile). */
.status-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 6px 0;
  font-size: var(--md-sys-typescale-body-medium-size);
}
.status-dot { width: 8px; height: 8px; border-radius: 50%;
              flex-shrink: 0; margin-top: 6px; }
.status-dot.ok  { background: var(--md-ext-color-success); }
.status-dot.err { background: var(--md-sys-color-error); }

/* ── Turnier: Name und Laufzeit an Ort und Stelle ändern ───────────────────
   Ein Stift hinter dem Namen, ein Haken zum Bestätigen. Das Projekt benennt
   sonst per Dialog um; hier war es ausdrücklich gewünscht, und dafür braucht es
   drei kleine Bausteine: einen flachen Symbolknopf, ein Eingabefeld im
   Schriftgrad der Überschrift und eine Zeile für die zwei Datumsfelder. */

/* Flacher Symbolknopf. Bewusst KEIN `.btn`: der brächte Rahmen, Polsterung und
   eine Mindesthöhe von --min-touch mit und machte aus einer Überschrift eine
   Werkzeugleiste. Das Ziel bleibt trotzdem gross genug (36 px plus die
   Zeilenhöhe der Überschrift). */
.t-stift, .t-haken {
  border: none;
  background: none;
  cursor: pointer;
  padding: 4px;
  min-width: 36px;
  min-height: 36px;
  line-height: 1;
  vertical-align: middle;
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-surface-variant);
}
.t-haken { color: var(--md-sys-color-primary); }
.t-stift:hover, .t-haken:hover {
  background: rgba(var(--md-sys-color-on-surface-rgb) / 0.08);
}
.t-stift:focus-visible, .t-haken:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* Das Namensfeld trägt den Schriftgrad der Überschrift: der Name soll beim
   Ändern nicht springen, sonst liest man zweimal hin. */
.t-name-feld {
  display: inline-block;
  width: auto;
  min-width: min(100%, 22ch);
  max-width: 100%;
  font-size: var(--md-sys-typescale-headline-small-size);
  font-weight: 700;
  vertical-align: middle;
}

/* Die Laufzeitzeile unter den Merkmalen. Umbruchfähig, weil sie bei 390 px aus
   fünf Teilen besteht und sonst die Seite breit machen würde. */
.t-laufzeit {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
/* `width: auto` gegen die Grundregel für `.form-input` (100 %): zwei Datumsfelder
   nebeneinander sollen nur so breit sein wie ein Datum. */
.t-datum {
  width: auto;
  min-width: 9.5rem;
  min-height: 40px;
  padding: 6px 10px;
}

/* ── Standings ────────────────────────────────────────────────────────────── */
/* Wertspalte breiter als die Zählspalten: Elo/Glicko sind vierstellig.

   Acht Spuren seit dem 2026-08-22: zwischen Name und Wert steht der ANTEIL an
   allen Punkten im Umlauf. 84 px, weil die Überschrift „Anteil in %“ die
   breiteste Angabe der Spalte ist - die Zelle traegt seit dem 2026-08-24 nur
   noch die Zahl. Die Spalte bleibt leer, wenn die Wertung keinen Nenner hat
   (alles ausser Elo) - ein leeres Feld ist billiger, als zwei Raster zu
   pflegen. */
.standing-row {
  display: grid;
  grid-template-columns: 36px 1fr 84px 74px repeat(4, 52px);
  gap: 8px;
  align-items: center;
  padding: 10px 8px;
  border-radius: var(--md-sys-shape-corner-medium);
}
/* Kopfzeile der Rangliste – muss dasselbe Raster nutzen wie .standing-row */
.standing-head {
  display: grid;
  grid-template-columns: 36px 1fr 84px 74px repeat(4, 52px);
  gap: 8px;
  align-items: center;
  padding: 8px;
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-on-surface-variant);
}
/* Der Anteil: rechtsbuendig unter seinem Titel, und Ziffern gleicher Breite -
   sonst tanzt das Komma von Zeile zu Zeile. */
/* „Anteil in %“ am Bildschirm, nur „%“ am Telefon: dort ist die Spur 44 px
   breit. Die Zahl selbst trägt kein Zeichen mehr – ohne die Überschrift wäre
   sie nicht zu deuten, deshalb die Kurzform statt Weglassen. */
.hdr-kurz { display: none; }

.standing-share {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface-variant);
}
/* Die Platzentwicklung der Startseite - rechtsbuendig unter ihrer Ueberschrift
   (2026-09-17). Die Spur ist `auto` und wird von der Ueberschrift „PLATZ"
   gesetzt (gemessen 36,6 px bei 1280 px), der Wert ist nur 15-17 px breit.
   Linksbuendig klebte er an der Kette daneben: gemessen SECHS Pixel Abstand,
   rechtsbuendig sind es 28.

   ⚠️ Der Stil sitzt auf der ZELLE, nicht auf `.st-trend`. Dieselbe Spanne
   steht im Turnier und in den Statistiken mitten in der Namenszelle - dort
   waere rechtsbuendig sinnlos bis falsch. */
.standing-platz {
  text-align: right;
}
/* Die Angabe, worauf sich die Prozente beziehen. Einmal je Liste, nicht je
   Zeile - sonst wiederholt sich derselbe Nenner acht Mal. */
/* ── Vorschau: die Gegenueberstellung der zwei Ausgaenge ────────────────────
   Zeilen = Spieler, Spalten = "X gewinnt" / "Y gewinnt". Seit dem 2026-09-03;
   vorher standen zwei Listen untereinander, und wer vergleichen wollte, musste
   zwischen ihnen hin und her springen (siehe components/preview.js).

   Die allgemeinen `table`-Regeln des Projekts gelten mit - hier steht nur, was
   diese Tabelle davon unterscheidet. */
.pv-tabelle { width: 100%; }
/* Die Ausgangsspalten gleich breit, die Spielerspalte nimmt, was sie braucht.
   `table-layout` bleibt automatisch: ein langer Name darf die Spalte
   verbreitern, `.table-wrap` faengt den Ueberlauf. */
.pv-tabelle thead th:nth-child(n+2) { width: 40%; text-align: left; }
/* „ABD GEWINNT" fett und in der Farbe des Textes (2026-09-04). Der Betreiber:
   „Ich möchte nur die Überschrift ,Spieler gewinnt' fett angezeigt bekommen. So
   ist das etwas ,blass'."

   Und blass war es wörtlich: die Zelle erbte die allgemeine `th`-Regel, und die
   ist für SPALTENTITEL gemacht - `label-medium`, gedämpfte Farbe. Hier steht
   aber keine Spaltenbeschriftung, sondern die Aussage der ganzen Tabelle.

   ⚠️ Nur `thead th`, und nur ab der zweiten Spalte wäre falsch: die erste ist
   leer (die Zeilenköpfe stehen als `scope="row"` links). Die Regel trifft alle
   drei und lässt die leere unberührt. `text-transform: uppercase` bleibt aus
   der `th`-Regel - GROSS und fett ist die Wirkung, die er gemeint hat. */
.pv-tabelle thead th {
  font-size: var(--md-sys-typescale-title-medium-size);
  font-weight: 700;
  letter-spacing: 0;
  color: var(--md-sys-color-on-surface);
}
/* Abschnitt IM Ergebniskasten statt eigener Karte (2026-09-04). „Es soll
   einfach nur ein Kasten zu sehen sein" - `#pv-result` ist schon eine `.card`,
   und „Diese Begegnung spielen" wie „X herausfordern" hingen ihre eigene
   hinein: Rahmen im Rahmen, Polster im Polster.

   Eine Trennlinie statt eines Rahmens: der Abschnitt bleibt als eigener
   Gedanke erkennbar, ohne ein zweites Mal einzurahmen. */
/* Die Ueberschrift der Gegenueberstellung („2026 · Elo") - seit dem
   2026-09-13 die ERSTE KOPFZELLE der Tabelle und keine eigene Zeile mehr.

   Der Betreiber: „Ich möchte in der Vorschau, die Überschrift der ‚Spieler
   GEWINNT' in einer Höhe wie ‚2026 - Elo' haben. Ich möchte so gerne mehr
   platz sparen." Vorher waren es zwei Zeilen für zwei Überschriften, die
   dasselbe überschreiben; jetzt ist es eine.

   Grösse, Gewicht und Farbe kommen von `.pv-tabelle thead th` daneben - der
   Kopf sieht damit aus wie „X gewinnt" und steht auf derselben Linie, weil er
   in derselben Tabellenzeile steht.

   ⚠️ `text-transform: none` nimmt die Hausregel `th { uppercase }` zurück.
   Die gilt Spaltentiteln; hier steht ein Turniername und der Name eines
   Wertungssystems. „2026 · ELO" wäre ein Eigenname in Versalien.

   ⚠️ `white-space: nowrap`: die erste Spalte ist die schmalste (die zwei
   Wertspalten nehmen je 40 %), und „2026 · Elo" über zwei Zeilen machte die
   Kopfzeile wieder so hoch, wie sie vorher war - genau das, was hier
   eingespart werden sollte. */
.pv-kopf { text-transform: none; white-space: nowrap; }

.pv-abschnitt {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
.pv-spieler {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
/* Der heutige Wert - die Zahl, von der die zwei Ausgaenge abgehen. Gedimmt und
   tabellarisch: er ist der Bezugspunkt und nicht die Aussage. */
.pv-jetzt {
  color: var(--md-sys-color-on-surface-variant);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.pv-zelle { vertical-align: middle; }
/* Die Richtung faerbt die ZELLE und nicht die Zeile - anders als in der Liste
   vorher, wo eine Zeile genau einen Ausgang zeigte. Hier stehen zwei Ausgaenge
   in einer Zeile, und einer davon ist ein Aufstieg und der andere nicht. */
.pv-zelle.is-up   { background: rgba(125, 216, 145, .10); }
.pv-zelle.is-down { background: rgba(255, 180, 171, .12); }
.pv-zelle.is-unrated { opacity: .55; }
/* Die Punktesumme unter jeder Vorschauspalte (2026-09-22).

   HINTER allen bestehenden `.pv-`-Regeln: drei Zusicherungen schneiden mit
   `css.index(".pv-abschnitt {")`, `.pv-tabelle thead th {` und `.pv-kopf {`,
   und eine neue Regel davor zoege ihren Schnitt mit (CLAUDE.md Falle 4).

   Dieselbe Schrift wie `.standing-pool` ueber der echten Rangliste - es ist
   derselbe Satz an einer anderen Stelle, und er soll auch so aussehen.

   ⚠️ Deshalb `label-medium` und nicht `body-small`. Beide liefern heute 12px,
   aber Gewicht (500 gegen 400) und Laufweite (0,5 gegen 0,4px) gehen
   auseinander - der erste Anlauf am 2026-09-22 nahm `body-small` und der
   Kommentar darueber behauptete trotzdem „dieselbe Schrift". Bei der naechsten
   Token-Anpassung waere das lautlos auseinandergelaufen. */
.pv-summe {
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-on-surface-variant);
}

.standing-pool {
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-on-surface-variant);
}
/* Die Speedshot-Rangliste leiht sich `.standing-row` fuer ihr AUSSEHEN und
   fuellt nur vier Zellen. Mit der neuen Anteilsspur waere ihr km/h-Wert in die
   62-px-Spalte gerutscht. Sie sagt deshalb ausdruecklich, wo ihre letzten zwei
   Zellen stehen - kein zweites Raster, nur zwei Platzangaben. */
.sp-row .sp-wert { grid-column: 4; }
.sp-row .sp-zahl { grid-column: 5 / -1; }

/* Trenner vor den nicht gewerteten Zeilen (nur bei der Siegquote) */
.standing-subhead {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 14px 0 2px;
  padding: 8px 8px 6px;
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: 600;
  color: var(--md-sys-color-on-surface-variant);
  border-top: 2px solid var(--md-sys-color-outline-variant);
}
.standing-subhead ~ .standing-row { opacity: .62; }
/* Hinweis hinter dem Namen: "erst 3 Spiele", "±217", "4/10 Spiele" */
.rating-note {
  font-size: var(--md-sys-typescale-label-small-size, .72rem);
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
  flex-shrink: 0;
}
.standing-row:nth-child(odd) { background: rgba(var(--md-sys-color-on-surface-rgb) / 0.04); }
.standing-rank { color: var(--md-sys-color-on-surface-variant); font-weight: 700; text-align: center; }
.standing-pts  { color: var(--md-sys-color-primary); font-weight: 700; text-align: right; }
.standing-hdr  { color: var(--md-sys-color-on-surface-variant); font-size: var(--md-sys-typescale-label-medium-size); text-align: right; }
.standing-name-col {
  text-align: left;
  display: flex;
  align-items: center;
  gap: 6px;
  overflow: hidden;
}

/* ── Kalender ───────────────────────────────────────────────────────────────
   Monatsraster mit den Spielen je Tag. Sieben Spalten, Montag zuerst.
   Kein Inline-Raster in der Ansicht - `tests/test_responsive.py` verbietet das
   in den Komponenten, und zu Recht: die Spaltenzahl gehört ins Stylesheet.   */
.kal-monat {
  font-size: var(--md-sys-typescale-title-medium-size);
  font-weight: 700;
  margin-bottom: 12px;
}
/* Die Aktionszeile UNTER dem Raster. Blättern links, „meine Spiele" rechts -
   der Filter ist eine andere Art von Entscheidung als der Monatswechsel. */
.kal-fuss {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
}
.kal-steuerung { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* Ein Chip ist 32 px hoch (unter --min-touch, siehe die Begründung in
   kalender.js). Die beiden Pfeile bekommen dafür Breite: die Trefffläche wächst
   waagerecht statt senkrecht, und die Zeile bleibt flach. */
.kal-steuerung .chip { padding: 0 14px; }
.kal-steuerung .chip .msym { margin-left: 0; }

.kal-raster {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 4px;
}
.kal-wochentag {
  text-align: center;
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: 700;
  color: var(--md-sys-color-on-surface-variant);
  text-transform: uppercase;
  letter-spacing: .04em;
  padding-bottom: 4px;
}
.kal-leer { min-height: 44px; }
/* Ein Tag OHNE Spiele ist ein `div` und kein `button` - er ist nicht anklickbar,
   und ein Knopf, der nichts tut, ist eine Lüge. Deshalb tragen beide dieselbe
   Klasse und der leere zusätzlich `.is-leer`. */
.kal-tag {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-height: 44px;
  padding: 4px 2px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-on-surface);
  font: inherit;
  cursor: pointer;
  transition: background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
}
.kal-tag.is-leer {
  cursor: default;
  background: transparent;
  border-color: transparent;
  color: var(--md-sys-color-on-surface-variant);
}
.kal-tag:not(.is-leer):hover { background: var(--md-sys-color-secondary-container); }
/* Heute: nur ein Rand. Eine Füllung würde mit „gewählt" konkurrieren, und beides
   kann gleichzeitig zutreffen. */
.kal-tag.is-heute { border-color: var(--md-sys-color-primary); border-width: 2px; }
.kal-tag.is-gewaehlt {
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  border-color: var(--md-sys-color-primary);
}
.kal-zahl { font-size: var(--md-sys-typescale-body-medium-size); font-variant-numeric: tabular-nums; }
.kal-anzahl {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 700;
  line-height: 1;
  padding: 1px 5px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}
.kal-tag.is-gewaehlt .kal-anzahl {
  background: var(--md-sys-color-on-primary);
  color: var(--md-sys-color-primary);
}

/* Der Spielstand nach einem Tor, in beiden Torlisten unter einem Spiel.

   `tabular-nums` ist hier nicht Kosmetik, sondern der Zweck der Spalte: nur so
   stehen die Doppelpunkte untereinander und die Spalte laesst sich von oben
   nach unten lesen. `nowrap`, damit "10:9" nicht in zwei Zeilen bricht.

   Eine eigene Klasse und nicht die der Kalender-Tagesansicht: die gab es
   einmal (`.kal-spiel-stand`), und sie ist am 2026-09-01 verschwunden, als die
   Tagesansicht auf die Zeile der Startseite umgestellt wurde. Genau deshalb
   steht sie hier eigenstaendig - eine geliehene Klasse waere mit ihrer Heimat
   mitgegangen. */
.tor-stand { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Die grosse Zahl einer Kachel - urspruenglich die vier Zahlen der Karte
   „Gesamt" (deren Kacheln seit dem 2026-10-08 entfallen sind, siehe die Kiste
   im Reiter „System", die diese Klasse seit dem 2026-10-07 ebenfalls nutzt).

   Die Grösse hing an einem Inline-Stil (`font-size:1.8rem`) und konnte deshalb
   nicht mitschrumpfen - auf dem Handy sprengte die Zahl ihre Kachel. `clamp`
   statt einer Regel im Responsive-Abschnitt: hier geht es nicht um einen
   Haltepunkt, sondern um ein stufenloses Mitwachsen mit der Kartenbreite. */
.stat-kachel-wert {
  font-size: clamp(1.25rem, 5vw, 1.8rem);
  font-weight: 700;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

/* Eine Spalte der Spielerstatistik: packt ihre Karten eng, statt sie auf die
   Höhe der Nachbarspalte zu strecken. Siehe die Begründung in stats.js. */
/* ⚠️ `.stat-spalte` stand hier bis zum 2026-09-03 und war ein FLEX-WICKEL um
   je zwei Karten. Sein Grund war gut und gilt weiter, nur an anderer Stelle:

     "Das Raster streckt eine Zeile auf die Hoehe ihrer hoechsten Karte.
      'Direkte Duelle' ist bei einem Vielspieler dreimal so hoch wie 'Gesamt',
      und die dritte Karte begann erst unter dieser Hoehe - mit einem Loch von
      mehreren hundert Pixeln unter 'Gesamt'."

   Der Betreiber wollte am 2026-09-03 eine andere Anordnung, und die braucht
   das Strecken sogar:

     Zeile 1   Gesamt | Angstgegner    GESTRECKT - "gleiche Hoehe" war der
                                       ausdrueckliche Wunsch
     Zeile 2   Kurve  | Duelle         NICHT - eine Kurve mit festem
                                       Seitenverhaeltnis unter einer sehr hohen
                                       Liste zu strecken ist genau das alte Loch

   `align-items` gilt fuer den ganzen Behaelter und kann das nicht
   unterscheiden. Deshalb ist die Vorgabe `stretch` (Zeile 1) und die zweite
   Zeile traegt `align-self: start` an ihren beiden Karten (`.stat-oben`).

   Ein eigenes Raster und nicht `.dashboard-grid`: dort ist `stretch` schon die
   Vorgabe, aber `.dashboard-grid > * { min-width: 0 }` haengt an einer Liste
   von Selektoren, und ein zweiter Nutzer mit anderen Regeln waere der Anfang
   von zwei Bedeutungen. */
.stat-raster {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* ⚠️ Eine eigene Variable, weil `js/anordnung.js` DIESELBE Zahl braucht:
     im gepackten Raster ist `row-gap` null, und der senkrechte Abstand steckt
     in der Spanne jeder Kiste. Zweimal 20 waeren die Stelle, an der Stylesheet
     und Javascript auseinanderlaufen.

     ⚠️ Und ueber eine VARIABLE statt ueber `getComputedStyle(...).columnGap`:
     der Rumpf traegt ein CSS-`zoom`, und Laengen kommen von dort gezoomt
     zurueck. Der Wert einer eigenen Eigenschaft nicht - er kommt als
     Zeichenkette, wie er dasteht. */
  --anord-luecke: var(--md-sys-spacing-5);
  gap: var(--anord-luecke);
  align-items: stretch;
}
.stat-raster > * { min-width: 0; }

/* ── Die anpassbare Anordnung (2026-09-13) ──────────────────────────────────

   Der Betreiber: „Ich möchte, dass ein angemeldeter Spieler den
   Statistikbereich anpassen kann … die Positionen der Kisten frei wählen …
   Er soll auch die Breite bestimmen können - halbe Breite oder über die ganze
   Breite."

   ⚠️ Die Klassen sitzen auf der `.card` SELBST und nicht auf einem Wickel.
   `tests/test_spielerstatistik.py` nagelt `.stat-raster > .card + .card
   { margin-top: 0 }` fest - ein `<div>` um jede Karte machte diesen Selektor
   still wirkungslos, und jede Zeile bekäme 16 px Versatz zurück.

   ⚠️ Der Behälter heisst weiter `.stat-raster`. Damit gelten die drei
   gemessenen Regeln von dort unverändert - Spaltenzahl, Abstände und der
   840-px-Umbruch. `.anord` ist nur ein Verhaltenshaken. */

/* ⚠️ `1 / -1` und NICHT `span 2`. `span 2` erzeugt in einem einspaltigen
   Raster eine implizite ZWEITE Spalte: die Kiste stünde am Telefon doppelt so
   breit wie der Rest und schöbe die Seite seitwärts. `1 / -1` heisst „alle
   Spalten, wie viele es auch sind" - die vorhandene Regel im 840er-Block
   erledigt den Rest, ohne dass hier eine Breitenregel dazukommt. */
.anord-kiste.ist-ganz { grid-column: 1 / -1; }

/* Die Höhe kommt als Benutzerdatum inline an der Kiste an (`--kh`). Ohne sie
   gilt `auto` - die Kiste ist so hoch wie ihr Inhalt, wie bisher. */
.anord-kiste { height: var(--kh, auto); overflow: hidden; }
/* ⚠️ Nicht strecken, wo das Verhaeltnis fest ist. Das Raster zieht eine
   Zeile auf die Hoehe ihrer hoechsten Karte - gewollt bei Gesamt neben
   Angstgegner, falsch bei einer Kurve neben einer sehr langen Liste.
   Dieselbe Menge wie bei der Hoehe (App.anordnung.FEST), und aus demselben
   Grund. Loest `.stat-oben` ab, das dasselbe fuer den alten Stapel tat.

   ⚠️ Eine Kiste MIT gesetzter Hoehe wird ohnehin nicht gestreckt: `stretch`
   gilt nur, solange die Quergroesse `auto` ist. Deshalb braucht es hier
   keine zweite Regel fuer den Fall. */
.anord-kiste.anord-oben { align-self: start; }

/* ── Die Kisten rutschen hoch (2026-09-14) ────────────────────────
   Der Betreiber: „wenn ich eine breite Kachel in der Breite habe, rutscht die
   Kachel nicht hoch in den freien Raum. Ich habe quasi eine Treppe in den
   Kacheln. Im Verlauf klappt das wunderbar." — im Verlauf nur deshalb, weil
   dort alle drei Kisten die ganze Breite haben. Wo nur eine je Zeile steht,
   kann kein Loch entstehen.

   Ein Raster zieht jede ZEILE auf die Hoehe ihrer hoechsten Karte, und die
   naechste Zeile beginnt erst darunter. Links kurz, rechts lang: die dritte
   Kiste faengt unter der LANGEN an. Dagegen hilft nur, die Zeile als Mass
   aufzugeben - eine Zeile ist hier ein Pixel, und jede Kiste bekommt von
   `js/anordnung.js` ihre gemessene Hoehe angeschrieben.

   ⚠️ `row-gap: 0` und NICHT `gap: 0`. Der senkrechte Abstand steckt schon in
   der geschriebenen Zahl (Hoehe plus `--anord-luecke`); ein echter `row-gap`
   kaeme zwischen JEDEN der vielen Ein-Pixel-Schritte. Die 20 px zwischen den
   beiden SPALTEN muessen dagegen bleiben - sonst kleben die Karten seitlich
   aneinander.

   ⚠️ Die 1 px sind eine Absprache mit dem Javascript: es rechnet in Pixeln.
   Wer hier 8px hinschreibt, macht aus jeder Hoehe das Achtfache.

   ⚠️ `align-self: start` fuer alle Kisten - UND DAS IST DER PREIS. „Gesamt
   und Angstgegner sollen gleich hoch sein" (Wunsch vom 2026-09-03) gilt in
   diesem Raster NICHT MEHR. Eine kurze Kiste kann nicht gleichzeitig auf die
   Hoehe ihrer Nachbarin gestreckt werden und nach oben rutschen; das ist
   dieselbe Aussage, zweimal verneint. Die Treppe wog schwerer.
   `align-items: stretch` an `.stat-raster` bleibt trotzdem stehen: daran
   haengt der Rueckfall OHNE `.anord` (Modul nicht geladen, nicht angemeldet).

   ⚠️ Kein `grid-auto-flow: dense`. Die knappe Platzierung fuellt das Loch
   bereits - der Setzzeiger steht nach einer hohen Kiste wieder in DEREN
   erster Zeile und laeuft von dort abwaerts. `dense` zoege zusaetzlich eine
   SPAETERE Kiste ueber eine fruehere, und die Reihenfolge ist genau das, was
   der Spieler eingestellt hat (die Pfeile heissen „einen Platz nach vorn").

   ⚠️ `anord-gepackt` setzt das Javascript selbst, vor dem ersten Messen.
   Ohne die Klasse gilt haargenau, was vorher galt - ein Fehler im Packen
   sieht damit aus wie „keine Verbesserung" und nicht wie „alle Karten kleben
   uebereinander".

   Bleibt ein Rest: die unterste Kiste jeder Spalte traegt ihren Abstand mit,
   das Raster ist also 20 px zu hoch. Ein negativer Aussenabstand dagegen
   waere gefaehrlich - `.stat-raster + .card { margin-top: 16px }` ergaebe -4
   und damit eine echte Ueberlappung. */
.stat-raster.anord-gepackt { grid-auto-rows: 1px; row-gap: 0; }
.stat-raster.anord-gepackt > .anord-kiste { align-self: start; }
/* ⚠️ `position: relative` nur im Modus: `.card` hat es nicht, und ausserhalb
   des Modus gibt es nichts zu positionieren. Eine Karte, die dauerhaft einen
   eigenen Positionsrahmen aufmacht, ändert die Bezugsebene für alles darin -
   das ist genau die Art Nebenwirkung, die man erst drei Runden später sieht. */
.anord-modus .anord-kiste { position: relative; cursor: grab; }
.anord-modus .anord-kiste.wird-gezogen { opacity: .45; cursor: grabbing; }
.anord-modus .anord-kiste.ist-ziel {
  outline: 2px dashed var(--md-sys-color-primary);
  outline-offset: -2px;
}

/* Die Leiste oben rechts in der Kiste. */
.anord-leiste {
  position: absolute;
  top: 6px; right: 6px;
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-highest);
  box-shadow: var(--md-sys-elevation-level1);
  z-index: 2;
}
.anord-knopf {
  border: none;
  background: transparent;
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  /* Stufe XS der Hausleiter - dieselbe Untergrenze, die die Sichtprüfung für
     jedes Bedienziel verlangt. */
  min-width: var(--md-sys-comp-size-xs);
  min-height: var(--md-sys-comp-size-xs);
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--md-sys-shape-corner-full);
}
.anord-knopf:hover { background: var(--md-sys-color-surface-container-low); }
.anord-knopf:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: -2px;
}
.anord-knopf .msym { font-size: 1.15rem; }

/* Der Griff an der Unterkante. 10 px sichtbar, aber 24 px hoch zu treffen -
   die Sichtprüfung misst die Trefferfläche, nicht den Strich. */
.anord-griff {
  position: absolute;
  left: 25%; right: 25%;
  bottom: 0;
  height: 24px;
  cursor: ns-resize;
  display: flex; align-items: center; justify-content: center;
  z-index: 2;
}
.anord-griff::before {
  content: '';
  width: 100%;
  height: 4px;
  border-radius: 2px;
  background: var(--md-sys-color-outline-variant);
}
.anord-griff:hover::before { background: var(--md-sys-color-primary); }
/* Die untere Zeile: oben ausgerichtet statt gestreckt. */
.stat-oben { align-self: start; }
/* ⚠️ Zwei Karten in EINER Rasterzelle, gestapelt: die Punktekurve und
   darunter die beliebtesten Tore (Wunsch vom 2026-09-12, „unter dem
   Punkteverlauf, genau in der gleichen Breite").

   Ein eigenes Rasterkind mit `grid-column: 1` waere einfacher und ist auch
   probiert worden - es landet ebenfalls links und gleich breit, aber in der
   naechsten ZEILE, und die beginnt erst unter der hoechsten Karte daneben.
   Gemessen: 790 px Luft, weil „Direkte Duelle" so lang ist.

   `gap` ist dieselbe Zahl wie im Raster - der Abstand soll nicht davon
   abhaengen, ob zwei Karten in einer Zelle stehen oder in zweien. */
.stat-stapel {
  display: flex;
  flex-direction: column;
  gap: 20px;
  align-self: start;
  min-width: 0;
}
/* ⚠️ `.stat-oben` steht an der Punktekurve und heisst im RASTER „nicht in die
   Hoehe strecken". Im Flex-Stapel zeigt die Querachse waagerecht, und
   dieselbe Regel heisst dort „nicht in die BREITE strecken" - die Kurve war
   damit gemessene 342 statt 547 px breit, also schmaler als die Karte
   darunter. Genau das, was der Wunsch „genau in der gleichen Breite"
   ausschliesst. Hier zurueckgenommen statt die Klasse an der Kurve
   anzufassen: im Raster ist sie weiterhin richtig. */
.stat-stapel > .card { align-self: stretch; }

/* ── Angstgegner und Opfer ─────────────────────────────────────────────────── */
/* Zwei Gruppen mit Überschrift, je EINE Zeile: Name links, Bilanz rechts.
   Seit dem 2026-09-02 ein Gegner je Gruppe statt drei. Die Überschrift steht
   weiter ÜBER dem Namen, weil eine Marke daneben bei 900 px die Namensspalte
   auf null quetscht (Begründung in stats.js). */
.duell-gruppe + .duell-gruppe { margin-top: 14px; }
.duell-titel {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  margin-bottom: 2px;
}
.duell-titel .msym { font-size: 18px; }
.duell-zeile {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px 10px;
  padding: 7px 0;
}
.duell-zeile + .duell-zeile { border-top: 1px solid var(--md-sys-color-outline-variant); }
.duell-name {
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.duell-bilanz { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.duell-zahl { font-weight: 700; font-size: 1.15rem; font-variant-numeric: tabular-nums; }

/* ── Das Band der Kiste „Gesamt" (2026-09-22) ──────────────────────────────────

   Der Betreiber: „Beste Serie ever … ist zweizeilig. Die Beschreibung soll
   links stehen und der Wert rechts daneben. … die Beschreibung der Letzten 5
   Spielen Rechts und die Kette links, bitte genau andersherum! Ich bin
   allgemein mit dem Gesamt etwas unzufrieden."

   Der Grund fuer die Unzufriedenheit ist zaehlbar: unter dem Bilanzbalken
   standen DREI verschiedene Zeilenbauarten untereinander - eine Kette mit
   Nachsatz rechts, zwei zweizeilige Emoji-Bloecke nebeneinander, ein
   Fliesstext. Dazu zwei Kachelraster darueber. Vier Bauarten in einer Karte.

   Jetzt ist die untere Haelfte EINE: Beschreibung links, Wert rechts am
   Kartenrand, duenne Trennlinie dazwischen. Die Werte fluchten dadurch
   untereinander - das ist der eigentliche Ordnungsgewinn, nicht die Linie.

   ⚠️ HINTER `.duell-zahl` und nicht davor. Mehrere Zusicherungen schneiden mit
   `css.index(".duell-…")`, und eine neue Regel davor zoege ihren Schnitt mit
   (CLAUDE.md §4 ④).

   ⚠️ Nicht `.duell-zeile` mitbenutzt, obwohl sie fachlich dasselbe tut: sie
   heisst nach den Duellen, und die naechste Aenderung an den Duellen traefe
   dann die Spielerkarte mit. Zwei Namen, zwei Zustaendigkeiten.

   ⚠️ 1.15rem wie `.duell-zahl` und nicht 1.3rem wie die alten Serienzahlen:
   neben einer Beschriftung in einer Zeile ist 1.3 laut. Die grossen Zahlen
   bleiben oben in den Kacheln - „Kacheln laut, Band leise" ist der Punkt.

   ⚠️ ZURUECKGEZOGEN am 2026-10-08: „Kacheln laut, Band leise" galt nur so
   lange, wie es noch Kacheln gab. Die Kiste „Gesamt" hat seitdem gar keine
   mehr - jeder Wert steht im Band, in einer Pille (`.vg-wert`, siehe die
   Uebersteuerung `.ps-band .vg-wert` weiter unten in dieser Datei). Diese
   Basis-Regel bleibt stehen (sie gilt noch fuer `.ps-wert` ausserhalb des
   Bandes), ist aber im Band selbst durch `.ps-band .ps-zeile` ueberschrieben. */
.ps-zeile {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 0;
}
.ps-zeile + .ps-zeile { border-top: 1px solid var(--md-sys-color-outline-variant); }
.ps-band { margin-top: 12px; border-top: 1px solid var(--md-sys-color-outline-variant); }
.ps-wert {
  font-weight: 700;
  font-size: 1.15rem;
  /* Die Zahlen fluchten - dieselbe Begruendung wie bei `.duell-zahl`. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* ── Punkteverlauf: die Kurve ──────────────────────────────────────────────────
   Von Hand als SVG (das Projekt hat kein Diagrammpaket). Farben ausschliesslich
   aus den Tokens, damit sie im hellen UND im dunklen Thema stimmen - ein fester
   Farbwert waere in einem der beiden falsch.                                   */
/* ⚠️ `position: relative` seit dem 2026-09-03: der Tooltip der Kurve ist
   HTML und wird daran ausgerichtet. Er ist bewusst KEIN SVG-Text - ein
   Kaestchen mit Rahmen, Grund und vier Zeilen in SVG von Hand zu setzen heisst
   Breiten selbst zu messen, und Textbreiten misst SVG nicht. */
.kurve-rahmen { margin-top: 6px; position: relative; }
.kurve { width: 100%; height: auto; display: block; overflow: visible; }
.kurve-raster {
  stroke: var(--md-sys-color-outline-variant);
  stroke-width: 1;
}
.kurve-marke {
  fill: var(--md-sys-color-on-surface-variant);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.kurve-flaeche { fill: var(--md-sys-color-primary); opacity: .14; stroke: none; }
.kurve-linie {
  fill: none;
  stroke: var(--md-sys-color-primary);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* Der Ausgang je Spiel als Farbe des Punktes - dieselbe Zuordnung wie in den
   Bilanzbalken darueber (Sieg gruen, Niederlage rot, Remis gelb). */
.kurve-punkt { fill: var(--md-sys-color-primary); }
.kurve-punkt.is-win  { fill: var(--md-ext-color-success); }
.kurve-punkt.is-loss { fill: var(--md-sys-color-error); }
.kurve-punkt.is-draw { fill: var(--md-ext-color-warning); }

/* ── Der Zeiger auf der Kurve ──────────────────────────────────────────────
   Die erste eigene Zeiger-Schicht des Projekts (2026-09-03). Bis dahin stand
   die Auskunft in einem SVG-`<title>` und der Browser zeigte sie als nativen
   Tooltip - das verlangt, dass man den PUNKT trifft, und bei 100 Spielen
   liegen die 7,5 Pixel auseinander.

   `pointer-events: none` an allem hier: die Ereignisse gehoeren dem SVG, und
   ein Element unter dem Zeiger, das sie abfaengt, laesst ihn beim
   Darueberfahren flackern. */
.kurve-zeiger {
  stroke: var(--md-sys-color-outline);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  pointer-events: none;
}
.kurve-treffer {
  fill: var(--md-sys-color-primary);
  stroke: var(--md-sys-color-surface);
  stroke-width: 2;
  pointer-events: none;
}
.kurve-treffer.is-win  { fill: var(--md-ext-color-success); }
.kurve-treffer.is-loss { fill: var(--md-sys-color-error); }
.kurve-treffer.is-draw { fill: var(--md-ext-color-warning); }

/* Das Kaestchen. `left` setzt die Zeigerschicht in Prozent, `translateX(-50%)`
   zentriert es darueber - und `.links` kippt es nach links, sobald es rechts
   nicht mehr passt. */
.kurve-tooltip {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  display: flex;
  gap: 10px;
  align-items: baseline;
  flex-wrap: wrap;
  max-width: 90%;
  padding: 6px 10px;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container-highest);
  border: 1px solid var(--md-sys-color-outline-variant);
  box-shadow: var(--md-sys-elevation-level2);
  font-size: var(--md-sys-typescale-label-medium-size);
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
}
.kurve-tooltip.links { transform: translateX(-100%); }
.kurve-tooltip .kt-summe { font-weight: 700; color: var(--md-sys-color-primary); }
.kurve-tooltip .kt-delta { font-weight: 700; }
.kurve-tooltip .kt-delta.is-win  { color: var(--md-ext-color-success); }
.kurve-tooltip .kt-delta.is-loss { color: var(--md-sys-color-error); }
.kurve-tooltip .kt-delta.is-draw { color: var(--md-ext-color-warning); }
.kurve-tooltip .kt-wann { color: var(--md-sys-color-on-surface-variant); }

/* ── Legende der Bilanz ───────────────────────────────────────────────────────
   Hier standen bis zum 2026-09-03 auch `.stat-bar`, `.stat-bar-seg` und
   `.stat-bar-legend`. Die Balken sind am 2026-09-02 mit den Zahlenspalten aus
   Kiosk und App gefallen (der Balken sagte nichts, was die Zahl daneben nicht
   schon sagt); die Punkte der Legende benutzt `components/stats.js` weiter. */
.legend-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  vertical-align: middle;
  margin-right: 3px;
}

/* ── Avatar images ────────────────────────────────────────────────────────── */
.avatar-circle {
  width: calc(44px * var(--avatar-scale-lg, 1));
  height: calc(44px * var(--avatar-scale-lg, 1));
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--md-sys-color-outline-variant);
}
/* Platzhalter (kein Bild hinterlegt) – exakt so groß wie .avatar-circle */
.avatar-circle.is-placeholder {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface-variant);
  font-size: calc(1.5rem * var(--avatar-scale-lg, 1));
}

/* ── Misc helpers ─────────────────────────────────────────────────────────── */
.flex { display: flex; }
.gap-8  { gap: 8px; }
.gap-12 { gap: 12px; }
.mt-16 { margin-top: 16px; }
.text-dim { color: var(--md-sys-color-on-surface-variant); }
.text-sm  { font-size: var(--md-sys-typescale-body-medium-size); }
.text-center { text-align: center; }
.w-full { width: 100%; }
.hidden { display: none !important; }

/* M3-Spinner (Circular indeterminate) */
.loading-spinner {
  display: flex;
  justify-content: center;
  padding: 60px;
}
.spinner {
  width: 40px; height: 40px;
  border: 4px solid var(--md-sys-color-secondary-container);
  border-top-color: var(--md-sys-color-primary);
  border-radius: 50%;
  animation: spin 0.8s var(--md-sys-motion-easing-linear) infinite;
}
.empty-state {
  text-align: center;
  padding: 60px 20px;
  color: var(--md-sys-color-on-surface-variant);
}
.empty-state .empty-icon { font-size: 3rem; margin-bottom: 12px; }

/* Die Listenzeile. 56 px ist die einzeilige M3-Hoehe (ANALYSE.md §8: 56 / 72
   / 88 fuer eine, zwei, drei Zeilen).

   ⚠️ Nur die 56. Fuer 72 und 88 gibt es hier bewusst KEINE Zusatzklassen: es
   gibt in dieser Anwendung keine zwei- oder dreizeilige Liste, und eine Klasse
   ohne Verbraucher ist genau das, was Paket 1 aus dieser Datei entfernt hat.
   Die Zahlen stehen im Abgleich, wenn eine solche Liste kommt.

   ⚠️ Und es ist ein Zuwachs: mit Polster 14 und einer Textzeile kam die Zeile
   auf rund 48. Das Polster bleibt, `min-height` hebt nur den Boden - eine
   Zeile mit hoeherem Inhalt wird weiter hoeher. */
.list-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 56px;
  padding: 14px 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  gap: 12px;
}
.list-item:last-child { border-bottom: none; }
.list-item-actions { display: flex; gap: 8px; }

/* ── Die Glocke: eine Zeile je Benachrichtigung ──────────────────────────────
   Aufsatz auf `.list-item` und keine eigene Zeilenklasse: Innenabstand, Trenner
   und die Anordnung stimmen dort schon. Hier kommt nur dazu, was eine
   Benachrichtigung von einem Listeneintrag unterscheidet - drei Textzeilen
   uebereinander statt einer, und die Hervorhebung des Ungelesenen. */
/* Ungelesen: ein Balken links, kein farbiger Hintergrund. Der Hintergrund
   haette gegen die Kartenflaeche in beiden Themes anders gewirkt und die
   Trennlinien verschluckt - der Balken traegt dieselbe Auskunft an einer
   Stelle, die kein anderes Element beansprucht. Der `padding-left` steht am
   Balken und nicht an `.is-neu`, damit der Text bei gelesenen Zeilen an
   derselben Stelle bleibt - sonst sprang die Liste beim Lesen. */
/* ── Die Benachrichtigungstabelle (2026-09-10) ───────────────────────────────
   EINE Tabelle statt zweier Karten: die Art ist die Zeile, der Kanal die
   Spalte. Der Betreiber wollte die Eintraege "nicht wie heute Dopppelt je
   Benachrichtungsart" haben.

   ⚠️ `table-layout: fixed` mit FESTEN Kanalspalten, und das ist keine
   Feinheit, sondern der Unterschied zwischen brauchbar und nicht. Gemessen
   am 2026-09-10 ohne diese Zeile, bei 390 px: die Textspalte wurde
   **1224 px** breit, weil die Regel `table { width: auto; min-width: 100% }`
   der schmalen Ansicht die Tabelle an ihrem Inhalt bemisst und die
   Beschreibungen nicht umbrachen. Die Kaestchen lagen damit rechts
   ausserhalb - erreichbar nur durch Wischen, und sie sind der Zweck der
   Tabelle. Mit fester Aufteilung bricht der Text um und die Schalter stehen
   da, wo man sie sieht.

   ⚠️ `.bt-tabelle` schlaegt dabei `table` in der 600er-Umbruchmarke, weil
   eine Klasse hoeher wiegt als ein Elementname. Deshalb steht hier `width`
   noch einmal - ohne das gaebe `width: auto` von dort den Ausschlag.

   ⚠️ `vertical-align: top` in BEIDEN, seit dem 2026-09-13. Der Schalter
   gehoert auf die Hoehe der Ueberschrift der Zeile, nicht auf die Mitte eines
   sechszeiligen Beschreibungstextes - das war schon immer die Absicht, und
   `middle` hat sie erreicht, solange jede Zeile genau eine Zeile hoch war.
   Seit der Text am Telefon umbricht (siehe `white-space` unten), stand der
   Schalter mitten im Absatz und gehoerte optisch zu nichts. */
.bt-tabelle { width: 100%; table-layout: fixed; border-collapse: collapse; }
/* ⚠️ `white-space: normal` NIMMT DIE SCHMALE ANSICHT ZURUECK, und ohne diese
   Zeile war die Tabelle am Telefon unbrauchbar (gemeldet am 2026-09-13: „die
   Beschreibungen reichen in die Schalter und werden von den Schaltern
   verdeckt").

   Die Regel `th, td { white-space: nowrap }` in der 600er-Umbruchmarke haelt
   Tabellenzellen schmal, damit breite Tabellen wischbar bleiben. Diese
   Tabelle wischt aber NICHT - sie hat feste Spalten, genau damit die Schalter
   im Bild stehen (siehe daneben). Das `nowrap` blieb trotzdem und hat den
   Beschreibungstext in EINE Zeile gezwungen: gemessen bei 390 px eine Zeile
   von 727 px in einer 206 px breiten Zelle, gemalt von x=36 bis x=763 - quer
   durch beide Schalterspalten, die bei 238 und 298 beginnen.

   ⚠️ Der Kasten schnitt dabei nichts ab und nichts lief aus dem Fenster;
   darum hat es keine der bestehenden Messungen gesehen. Seit dem 2026-09-13
   misst die Sichtpruefung den GEMALTEN Text gegen die Bedienelemente
   (`TEXT_UNTER_BEDIENELEMENT`). */
.bt-tabelle th, .bt-tabelle td { padding: 12px 8px; text-align: left;
                                 white-space: normal; }
.bt-tabelle thead th {
  font-size: var(--md-sys-typescale-label-large-size);
  color: var(--md-sys-color-on-surface-variant);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  white-space: nowrap;
}
.bt-tabelle tbody tr + tr th, .bt-tabelle tbody tr + tr td {
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
/* ⚠️ `text-transform: none` nimmt die Hausregel `th { text-transform:
   uppercase }` zurueck. Die gilt SPALTENkoepfen - dort sind es ein bis zwei
   Woerter. Hier ist das `th` ein ZEILENkopf (`scope="row"`, damit ein
   Vorleseprogramm am Schalter sagt, wozu er gehoert), und er traegt einen
   ganzen Satz: „WENN DIR JEMAND SCHREIBT. EINE ZEILE JE GESPRAECH, EGAL WIE
   VIELE SAETZE KOMMEN." Versalien lesen sich in dieser Laenge schlecht, und
   gemeldet wurde genau das - „ich moechte, dass alles sauber und klar
   dargestellt wird" (2026-09-13). Die Kopfzeile oben bleibt versal. */
.bt-art { font-weight: normal; vertical-align: top; overflow-wrap: anywhere;
          text-transform: none; }
/* 92 px: der Schalter ist 52 breit, dazu die 8 px Innenabstand beidseits und
   Luft fuer die Kopfzeile "E-Mail" samt Symbol. */
.bt-kanal { width: 92px; white-space: nowrap; vertical-align: top; }
.bt-tabelle thead .bt-kanal { text-align: center; }
.bt-tabelle tbody .bt-kanal { text-align: center; }
.bt-kanal .msym { vertical-align: -4px; margin-right: 2px; }
/* Zwei Kaestchen in einer Zelle - nur beim Platzwechsel. Raster statt Flex,
   damit die zwei Pfeile untereinander in einer Spalte stehen. */
.bt-stapel {
  display: grid;
  grid-template-columns: auto auto;
  gap: 6px 4px;
  align-items: center;
  justify-content: center;
}
.bt-pfeil { color: var(--md-sys-color-on-surface-variant); }

.nb-zeile {
  align-items: flex-start;
  border-left: 3px solid transparent;
  padding-left: 10px;
}
/* `min-width: 0` ist keine Zierde: ohne sie weigert sich ein Flex-Kind, unter
   seine Inhaltsbreite zu schrumpfen, und ein langer Titel schiebt die Knoepfe
   aus der Zeile. Bei 360 px ist das der Normalfall. */
.nb-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.nb-titel { display: flex; align-items: center; gap: 6px; }
.nb-titel .msym { color: var(--md-sys-color-on-surface-variant); }
.nb-zeile.is-neu { border-left-color: var(--md-sys-color-primary); }
.nb-zeile.is-neu .nb-titel { font-weight: 600; }

/* ⚠️ `flex-wrap` und `gap` seit dem dritten Knopf (2026-09-03): bei 360 px
   passen "Alle als gelesen", "Gelesene loeschen" und "Alle loeschen" nicht in
   eine Zeile, und ohne Umbruch schoebe der letzte aus dem Modal heraus.
   Rechtsbuendig bleibt es - die Knoepfe stehen ueber der Liste, nicht in ihr. */
.nb-kopf { display: flex; flex-wrap: wrap; gap: 2px;
           justify-content: flex-end; margin-bottom: 4px; }

/* ── „Was ist neu?": die Stichpunkte je Monat ────────────────────────────────
   ⚠️ DER ERSTE `<ul>` IM PROJEKT. Bis hierher gab es keine einzige Liste im
   Frontend, und deshalb auch kein `list-style` in diesem Stylesheet. Es ist
   trotzdem eine echte Liste geworden und kein Stapel aus `•`-Zeilen: eine
   Aufzählung von Stichpunkten IST eine Liste, ein Vorleseprogramm sagt dann
   „Liste mit vier Einträgen", und ein Stapel behauptet das nur.

   Der globale Reset ganz oben nimmt JEDEM Element `padding` - ohne die
   `padding-left` hier lägen die Punkte am Kartenrand und die zweite Zeile
   eines langen Eintrags unter dem Punkt statt eingerückt. Das ist der ganze
   Grund für diese vier Zeilen; sie sind keine Verschönerung. */
.nk-liste { list-style: disc; padding-left: 20px; margin-bottom: 16px; }
.nk-liste li { margin-bottom: 4px; }
.nk-liste li::marker { color: var(--md-sys-color-primary); }
.nk-liste:last-child { margin-bottom: 0; }

.section-title {
  font-size: var(--md-sys-typescale-label-medium-size);
  font-weight: var(--md-sys-typescale-label-medium-weight);
  letter-spacing: 1px;
  color: var(--md-sys-color-on-surface-variant);
  text-transform: uppercase;
  margin-bottom: 16px;
  margin-top: 32px;
}
.section-title:first-child { margin-top: 0; }
/* Und ebenso direkt hinter einer Seitenkopfzeile. Der Fall entstand, als die
   Ueberschriften aus den Menuezielen fielen: in der Turnieruebersicht steht
   seither ein `page-header` mit nur einem Knopf, danach „AKTIVE TURNIERE".
   `:first-child` greift dort nicht - der Kopf steht davor -, also blieben
   24 px (Kopf) + 32 px (Titel) zwischen Knopf und erster Zeile. Der Kopf
   trennt schon; ein zweiter Abstand trennt nichts mehr. */
.page-header + .section-title { margin-top: 0; }

.divider {
  height: 1px;
  background: var(--md-sys-color-outline-variant);
  border: none;
  margin: 20px 0;
}

/* ── Animations ───────────────────────────────────────────────────────────── */
@keyframes spin    { to { transform: rotate(360deg); } }
@keyframes pulse   { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
@keyframes slideIn { from { transform: translateX(40px); opacity: 0; } }
@keyframes fadeIn  { from { opacity: 0; } }
@keyframes fadeOut { to   { opacity: 0; } }
@keyframes dlg-in    { from { transform: scale(0.85); opacity: 0.4; } }
@keyframes sheet-in  { from { transform: translateY(100%); } }
@keyframes snack-in  { from { transform: translateY(120%); opacity: 0.6; } }
@keyframes banner-in { from { transform: scale(0.8); opacity: 0; } }

/* ── Setup wizard ─────────────────────────────────────────────────────────── */
.setup-wizard {
  max-width: 560px;
  margin: 40px auto;
  text-align: center;
}
.setup-wizard h1 {
  font-size: var(--md-sys-typescale-headline-medium-size);
  line-height: var(--md-sys-typescale-headline-medium-line-height);
  margin-bottom: 8px;
}
.setup-wizard p  { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; }

/* ── Die Betriebsschranke (2026-09-10) ─────────────────────────────────────
   Wartung oder „Ausser Betrieb": statt jeder Ansicht steht diese Meldung da.

   Die Massform ist die des Assistenten darueber - dieselbe Lage auf dem
   Bildschirm fuer dieselbe Sorte Nachricht („hier geht es gerade nicht
   weiter"). Wer beide kennt, erkennt die zweite wieder.

   ⚠️ Das Menueband und die untere Leiste verschwinden ueber eine
   KOERPERKLASSE und nicht, indem `app.js` sie versteckt: die Schranke ist
   umkehrbar, und ein `style.display = 'none'` von Hand muesste beim Aufheben
   von Hand zurueckgenommen werden. Eine Klasse nimmt sich selbst zurueck.
   Genau daran krankt die Anmeldeschranke daneben - die kommt aber nie
   zurueck, dort ist es kein Fehler. */
body.betrieb-zu #navbar,
body.betrieb-zu .bottom-nav,
body.betrieb-zu #readonly-banner { display: none; }

.betrieb-schranke {
  max-width: 560px;
  margin: 40px auto;
  text-align: center;
}
/* Das Symbol traegt die Aussage schon aus einigen Metern - auf einem
   Flurmonitor liest man den Text nicht, man sieht das Schloss. */
.betrieb-symbol .msym {
  font-size: 4rem;
  color: var(--md-sys-color-on-surface-variant);
}
.betrieb-schranke h1 {
  font-size: var(--md-sys-typescale-headline-medium-size);
  line-height: var(--md-sys-typescale-headline-medium-line-height);
  margin: 12px 0 8px;
}
.betrieb-schranke p { color: var(--md-sys-color-on-surface-variant); }
/* Der eigene Satz des Betreibers. Auf der Tonflaeche und nicht bloss fett:
   er ist die einzige Zeile, die NICHT immer dasteht - und damit die einzige,
   die jemand wirklich liest.
   ⚠️ Mit dem Elternselektor davor und NICHT mit `!important`: die Zeile
   darueber faerbt jedes `p` in der Schranke, und M3-RULES Regel 5 laesst genau
   zwei benannte `!important` zu, beide im Kiosk. Spezifitaet ist hier die
   richtige Antwort - 0,2,0 gegen 0,1,1. */
.betrieb-schranke .betrieb-zusatz {
  margin: 20px auto 28px;
  padding: 12px 16px;
  max-width: 34rem;
  border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
}

/* ── Schema update banner ─────────────────────────────────────────────────── */
/* Warnung in der Auto-Update-Karte: Repo-Remote ist HTTPS statt SSH */
.update-remote-warning {
  padding: 14px 16px;
  margin-bottom: 14px;
  background: var(--md-ext-color-warning-container);
  color: var(--md-ext-color-on-warning-container);
  border-radius: var(--md-sys-shape-corner-large);
}
.update-remote-cmd {
  margin: 0;
  padding: 8px 10px;
  border-radius: var(--md-sys-shape-corner-small);
  background: rgba(var(--md-sys-color-on-surface-rgb) / 0.10);
  font-family: monospace;
  font-size: .78rem;
  white-space: pre-wrap;
  word-break: break-all;
  user-select: all;
}
/* Steht statt des Such-Knopfes da, wo gar kein Update laufen kann - auf dem
   Windows-Entwicklungsrechner und auf dem Mac. Bewusst dieselbe Farbrolle wie
   die HTTPS-Warnung darueber: es ist dieselbe Art Aussage ("so wird das
   nichts"), nur eine Stufe harmloser. */
.update-hinweis {
  padding: 12px 14px;
  margin-top: 16px;
  background: var(--md-ext-color-warning-container);
  color: var(--md-ext-color-on-warning-container);
  border-radius: var(--md-sys-shape-corner-large);
  font-size: .86rem;
}
/* Der Update-Verlauf. `max-height` und nicht `height`: meistens stehen dort
   drei Zeilen, und ein leerer Kasten von 300 px waere Zierrat. */
.update-log {
  margin: 12px 0 0;
  padding: 10px 12px;
  max-height: 320px;
  overflow: auto;
  border-radius: var(--md-sys-shape-corner-small);
  background: rgba(var(--md-sys-color-on-surface-rgb) / 0.06);
  font-family: monospace;
  font-size: .76rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Endabrechnung im Sieger-Banner. Feste helle Farben wie .banner-points –
   der Banner liegt immer auf dem dunklen Scrim, unabhängig vom Theme. */
.banner-rating {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* ⚠️ `stretch` und nicht `center`, seit dem 2026-09-04 - und das ist die
     Ursache des gemeldeten Bildes: „Aktuell ist der Name und das Bild des
     Spielers verrueckt. Diese sollen aber in einer Reihe stehen. Der Name des
     Spielers und die Daten des Spielers sollen wie in einer Tabelle
     ausgerichtet angezeigt werden."

     Mit `center` schrumpft JEDE Zeile auf ihren eigenen Inhalt und wird dann
     mittig gesetzt. Zwei Zeilen verschiedener Laenge haben damit verschiedene
     Spaltenbreiten UND verschiedene linke Kanten - Bild und Name des einen
     Spielers stehen versetzt zu denen des anderen. Jede Zeile war fuer sich
     ein Raster, und keine wusste von der anderen.

     `stretch` gibt allen Zeilen dieselbe Breite (die der breitesten), und
     zusammen mit den festen Spurbreiten unten stehen die fuenf Spalten
     tatsaechlich untereinander. */
  align-items: stretch;
  font-size: 1.15rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.banner-rating:empty { display: none; }
/* Raster statt umbrechendem Flex: Bild, Name, Differenz, Wert und Platzwechsel
   stehen dadurch in Spalten untereinander statt zu verrutschen. Der Name ist
   die einzige mitwachsende Spalte.

   ⚠️ FUENF Spalten seit dem 2026-09-03. Die erste war das Kennzeichen
   (GEWINNER/VERLIERER) und ist auf Wunsch des Betreibers entfallen - die
   Begruendung stand hier schon, nur weiter unten und nur fuer schmale
   Fenster: „die Reihenfolge und die Farben tragen dieselbe Information". Das
   war bei jeder Breite wahr. */
.br-row {
  display: grid;
  /* ⚠️ Die letzten zwei Spuren waren `auto auto` und sind seit dem 2026-09-04
     `minmax(...)` mit UNTERGRENZE. `auto` heisst „so breit wie der Inhalt
     DIESER Zeile" - und jede Zeile ist ein eigenes Raster. „1042 ELO" und
     „988 ELO" ergaben damit zwei verschiedene Spaltenbreiten, und alles davor
     verschob sich mit.

     Mit einer Untergrenze treffen beide Zeilen dieselbe Breite: der Wert hat
     drei oder vier Stellen, und das Kuerzel des Bewertungssystems ist in
     beiden Zeilen dasselbe. `auto` als Obergrenze bleibt, damit ein langes
     Kuerzel nicht abgeschnitten wird - dann waechst es in BEIDEN Zeilen
     gleich.

     Kein `subgrid`: die Zeile traegt ihre Einblendung (`br-row-in`) und ihre
     Gewinner-Kennzeichnung, und `display: contents` nimmt ihr beides. */
  grid-template-columns: 1.7em minmax(0, 1fr) 3.4em minmax(6.5em, auto)
                         minmax(5.5em, auto);
  align-items: center;
  gap: 4px 10px;
  /* Gestaffeltes Erscheinen; den Versatz je Zeile setzt
     _renderEndRating in components/game.js inline.
     Dauer und Kurve aus den Motion-Tokens – dadurch greift
     prefers-reduced-motion automatisch (m3-tokens.css schaltet sie auf 1 ms). */
  animation: br-row-in var(--md-sys-motion-spring-fast-spatial-duration)
             var(--md-sys-motion-spring-fast-spatial-easing) both;
}
@keyframes br-row-in {
  from { opacity: 0; transform: translateY(8px); }
}
/* Feste Größe, absichtlich NICHT über --avatar-scale: die Stellschraube steht am
   Kiosk auf 2,6 und würde das Banner sprengen. */
.br-avatar {
  width: 1.7em;
  height: 1.7em;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(255, 255, 255, .35);
}
.br-avatar.is-empty { border: none; }
.br-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
          text-align: left; }
.br-delta { text-align: right; }
.br-delta.is-up   { color: #7DD891; }
.br-delta.is-down { color: #FFB4AB; }
.br-delta.is-flat { opacity: .7; }
.br-val { opacity: .7; font-weight: 500; font-size: .9em; text-align: right; }

/* Platzwechsel „#2 → #1 ↑". Nutzt die .proj-*-Klassen der Live-Ansicht, damit es
   dasselbe Vokabular ist – nur die Farben sind hier fest, weil der Scrim in
   beiden Designs dunkel ist (siehe Kommentar am Anfang dieses Blocks). */
.br-rank {
  display: inline-flex;
  align-items: baseline;
  gap: .3em;
  justify-content: flex-end;
  min-width: 5.5em;
}
.br-rank:empty { min-width: 0; padding: 0; background: none; }
/* Abzeichen: der Platzwechsel ist der Blickfang der Zeile. Grundton nach
   Richtung, damit man ihn auch aus drei Metern Abstand erkennt. */
.br-rank { padding: .12em .55em; border-radius: var(--md-sys-shape-corner-full);
           background: rgba(255, 255, 255, .07); }
.br-rank.is-up   { background: rgba(125, 216, 145, .16); }
.br-rank.is-down { background: rgba(255, 180, 171, .16); }
.br-rank .proj-rank.is-now { font-size: 1.15em; }
.br-rank .proj-dir.is-up   { color: #7DD891; }
.br-rank .proj-dir.is-down { color: #FFB4AB; }
/* Kurzer Pop, wenn die Zahl ihr Ziel erreicht – Muster wie .gt-proj.bump. */
.br-rank .proj-rank { transition: transform var(--md-sys-motion-spring-fast-effects-duration)
                                  var(--md-sys-motion-spring-fast-effects-easing); }
.br-rank .proj-rank.bump { transform: scale(1.35); }

/* Kopf eines beendeten Spiels - in der Anordnung der LAUFENDEN Partie.

   Sieger gross darueber, links und rechts je eine Seite mit grossem Avatar,
   der Stand in der Mitte, und unter jedem Spieler was er gewonnen oder
   verloren hat. Genau das zeigt `.game-score-area` im laufenden Spiel; hier
   dieselben Bausteine (`.team-players-big`, `.gt-proj`) ohne die Torknoepfe.

   Anlass am 2026-08-26: vorher standen Avatare dreimal im Kopf - in der
   Aufstellung, beim Sieger und noch einmal in jeder Zeile der Endabrechnung.

   Kein eigenes grid-template-columns: das verbietet test_responsive.py fuer die
   Komponenten, und hier reicht flex. Das Umklappen auf schmalen Geraeten steht
   im Responsive-Abschnitt weiter unten - dort gehoeren alle Breitenregeln hin,
   und es gibt nur die zwei erlaubten Umbruchpunkte 600 und 840.

   Die Schriftgroesse kommt aus der Typoskala und NICHT aus einer dvh-Rechnung
   mit Division durch die Zoomstufe: die vierzehn solchen Stellen in dieser
   Datei sind produktiv erprobt und von test_responsive.py als Kanarienvogel
   gezaehlt - der Test zaehlt die Zeichenfolge in der GANZEN Datei, also auch
   in Kommentaren wie diesem. Eine fuenfzehnte waere eine Aenderung an genau
   der Rechnung, die den grossen Bildschirm verstellt, und eine Ueberschrift
   braucht sie nicht. */
.fg-kopf { text-align: center; margin-bottom: 16px; }
.fg-sieger {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .4em;
  flex-wrap: wrap;
  font-size: var(--md-sys-typescale-headline-medium-size);
  line-height: var(--md-sys-typescale-headline-medium-line-height);
  font-weight: 700;
  margin: 4px 0 18px;
}
.fg-sieger .msym { font-size: 1.15em; color: var(--yellow, #E9A800); }
.fg-buehne {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(12px, 4vw, 48px);
}
.fg-seite { flex: 1 1 0; min-width: 0; }
/* Die Mittelspalte: Torstand, darunter die Zeitangaben (2026-09-14).

   Der Betreiber: „Das soll aber mittig unter dem Spiel stehen, wie es vorher
   auch war." Und zur Hoehe: „Aber die Kachel des Spiels soll nicht nach unten
   groesser werden - das Datum des Spiels soll dann lieber hoch in den freien
   Raum rutschen."

   Genau das tut dieser Kasten. Die zwei Avatarseiten sind rund 300 px hoch,
   der Stand allein rund 120 - die 180 px darunter waren leer. Die Zeitangaben
   ziehen dort ein, und die Karte wird dabei KUERZER statt laenger, weil der
   Fuss seine rechte Gruppe verliert. */
.fg-mitte {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.fg-stand { margin: 0; padding-top: .2em; }
.fg-stand .sep { margin: 0 .25em; }
/* ⚠️ Eigene Farben statt der geerbten aus `.banner-score` (2026-09-10).

   Die Sichtpruefung hat es gemeldet: Kontrast 1,70 : 1 im HELLEN Thema, noetig
   sind 3 : 1 fuer grosse Schrift. Die Ursache ist eine geliehene Farbrolle -
   `.banner-score .t1` benutzt `primary-fixed-dim`, und die ist fuer einen
   Grund aus `primary-fixed` gemacht. Im Endebanner steht sie darauf und
   stimmt; der Spielkopf hat diese Regel mitgeerbt und setzt sie auf eine
   gewoehnliche Karte - dort ist sie eine blasse Farbe auf hellem Grund.

   `primary` und `tertiary` waren dieselben zwei Rollen, aber die Fassung fuer
   einen normalen Grund. Seit dem 2026-09-30 ist `primary` der neutrale
   Akzent - Team 1 steht deshalb auf seiner eigenen Rolle, dasselbe Paar wie
   in der Liste der letzten Spiele (bis zum 2026-10-16 `.recent-score`).

   ⚠️ Seit dem 2026-10-06 OHNE Teamfarbe, aus demselben Grund wie bei
   `.banner-score`: Schwarz/Weiss als Text ist auf dem jeweils gleichfarbigen
   Grund unleserlich, und die Seite steht schon per Name daneben. */
.fg-stand .t1, .fg-stand .t2 { color: inherit; }
/* Der Fuss der Kopfkarte: die Aktionen des beendeten Spiels.

   ⚠️ Die Geschichte dieser Zeile in zwei Schritten, weil beide Male eine
   Regel umgeworfen wurde und der naechste Leser sonst die erste zurueckholt:

   1. Bis zum 2026-09-13 stand hier nur Text, mittig. Dann wurde sie zur
      AKTIONSZEILE (Daumen, Video, Loeschen), zusammen mit den Zeitangaben in
      einer Zeile - `space-between` hielt beide Gruppen an ihren Raendern.
   2. Seit dem 2026-09-14 sind die Zeitangaben WEG aus dieser Zeile: sie
      stehen unter dem Torstand (`.fg-mitte`). Der Betreiber: „Zusaetzlich
      sind die Informationen Gestartet / Spielzeit jetzt nach ganz rechts
      gerutscht. Das soll aber mittig unter dem Spiel stehen."

   `flex-start` und nicht `center`: die Aktionen bleiben „unten links", wie am
   2026-09-13 bestellt. `align-items: center` bleibt, weil die Gruppe
   umbrechen darf und die Knoepfe dann auf einer Linie sitzen sollen. */
.fg-fuss {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 16px 24px;
  flex-wrap: wrap;
  margin-top: 18px;
}
.fg-aktionen { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Die Zeitangaben unter dem Stand: untereinander und zentriert, nicht
   nebeneinander - die Mittelspalte ist nur so breit wie der Torstand. */
.fg-zeiten {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  text-align: center;
}

/* ⚠️ KLEIN UND FLACH, seit dem 2026-09-14 - und das unterschreitet
   `--min-touch` mit Absicht.

   Der Betreiber: „Die Aktionen in einem abgeschlossenen Spiel werden sauber
   angezeigt. Sie sind nur viel zu gross. Ich haette sie gerne wesentlich
   kleiner. Die Pillen um die Aktionen sind zu hoch." Auf die Rueckfrage nach
   dem Mass: „klein mit Text - orientiere dich da bitte an das was facebook
   macht." Facebook macht an dieser Stelle: flach, grau, ohne Rahmen, kleine
   Schrift, ein schwacher Grund erst beim Ueberfahren.

   ⚠️ EINE REGEL AM ORT statt einer neuen Stufe in der Groessenleiter. Die
   Leiter traegt eine ausdrueckliche Warnung, dass `.btn-sm`/`.btn-xs` nichts
   an einem Ziel zu suchen haben, das am Tisch getroffen wird - und die bleibt
   richtig. Diese Zeile ist keins: sie steht in der ZUSAMMENFASSUNG eines
   beendeten Spiels, die niemand mitten im Spiel bedient.

   ⚠️ Sie trifft auch die zwei Daumen, die aus `_zeigeEcho` kommen und ihre
   Klassen mit der Torkarte teilen. Dort bleiben sie gross - die Regel haengt
   am Behaelter und nicht an `.echo-knopf`, und genau deshalb stimmt der lange
   Kommentar bei `.echo-knopf` weiterhin.

   ⚠️ `.aktion` STEHT DANEBEN, seit dem 2026-09-15 - und der Behaelter bleibt.

   Der Betreiber: „Ich moechte das diese Aktionen Like, Dislike Video ansehen
   und Spiel loeschen als Beispiel fuer alle andere Aktionen in der
   Spielzusammenfassung sind." Dasselbe Aussehen brauchen seither die
   Torliste (ohne Schriftzug), die Sprungmarken und die Torleiste im
   Abspieler - und die stehen alle NICHT in `.fg-aktionen`.

   Warum nicht `.fg-aktionen .btn` durch `.aktion` ERSETZEN: die zwei
   Spiel-Daumen kommen aus `_zeigeEcho`, und dieselbe Vorlage baut die
   GROSSEN Knoepfe der Resonanzkarte. Heute trennt der Behaelter die beiden
   Faelle, ohne dass die Vorlage davon weiss. Auf `.aktion` umzustellen
   hiesse, ihr einen Schalter mitzugeben - eine Fallunterscheidung mehr in
   einer Funktion, die zwei Ansichten bedient.

   Warum trotzdem EINE Regel mit zwei Selektoren und nicht zwei Regeln: das
   Aussehen hatte vor dieser Runde FUENF Schreibstellen (dreimal ein
   Inline-`style` in game.js, `.tor-echo-knopf`, und diese Regel). Genau
   daraus entsteht die Treppe, die der Betreiber gesehen hat.

   ⚠️ Die Knoepfe tragen `class="btn aktion"` und nicht nur `aktion`:
   Grundmasse, Welle und `flex-shrink` kommen aus `.btn`, hier steht nur,
   was flach daran ist. Dieselbe Schichtung wie bei `.fg-aktionen .btn`. */
.fg-aktionen .btn,
.aktion {
  min-height: var(--md-sys-comp-size-xs);
  padding: 4px 12px;
  font-size: var(--md-sys-typescale-label-medium-size);
  border: none;
  color: var(--md-sys-color-on-surface-variant);
  border-radius: var(--md-sys-shape-corner-small);
}
.fg-aktionen .btn:hover:not(:disabled),
.aktion:hover:not(:disabled) {
  background: rgba(var(--md-sys-color-on-surface-rgb) / 0.08);
}
/* ⚠️ Der gedrueckte Daumen behaelt seine Farbe. Ohne diese Zeile gewaenne
   `.fg-aktionen .btn` (0,2,0) gegen `.echo-an` (0,1,0) und faerbte die eigene
   Stimme grau - man saehe nicht mehr, dass man abgestimmt hat.

   ⚠️ Fuer `.aktion` gilt dasselbe aus einem zweiten Grund: `.aktion`
   (0,1,0) und `.echo-an` (0,1,0) sind gleich stark, und `.aktion` steht
   SPAETER in dieser Datei - bei Gleichstand gewinnt die letzte. Ohne die
   Zeile hier waere die eigene Stimme in der Torliste unsichtbar. */
.fg-aktionen .btn.echo-an,
.aktion.echo-an { color: var(--md-sys-color-on-secondary-container); }
/* Nur ein Symbol, kein Schriftzug (Torliste, Sprungmarken-Wiedergabe): dann
   ist das Polster links und rechts gleich, sonst sitzt das Zeichen schief.

   ⚠️ `min-width` und nicht nur Polster. Gemessen von der Sichtpruefung am
   2026-09-15: 8 + 15 + 8 = **31 px** - das Symbol ist keine ganzen 16 px
   breit, und damit lag das Bedienziel einen Pixel unter dem Hausmass. Ein
   Pixel klingt nach nichts und ist genau die Art Abweichung, die man nie
   sieht und die eine Pruefung finden muss. Mit `min-width` steht die Breite
   am selben Token wie die Hoehe und kann nicht mehr auseinanderlaufen. */
.aktion.nur-symbol {
  padding: 4px 8px;
  min-width: var(--md-sys-comp-size-xs);
}
/* ⚠️ DAS „+" IST EINE AKTION IN GRIFFGROESSE (2026-09-21).

   Der Betreiber: „Die Aktionen + die vorher Hinzufügen hießen, sind mir zu
   klein. Das Plus soll deutlich größer sein."

   `.aktion` misst `--md-sys-comp-size-xs` (32 px). Das ist richtig für die
   sieben Symbole einer Spielerzeile, die in einer Reihe nebeneinander sitzen
   - und zu wenig für die EINE Aktion, die allein im Seitenkopf steht und die
   Hauptsache der Seite ist. 56 px ist `--min-touch` und damit kein neues
   Mass, sondern das, was jeder gewöhnliche Knopf im Haus hat.

   ⚠️ EIN MODIFIKATOR UND KEINE AENDERUNG AN `.aktion`. An `.aktion` hängen
   die sieben Zeilenaktionen jeder Spielerzeile, die Daumen der Torliste, der
   Videoknopf und der Burger der Herausforderungen. Wäre das Mass dort
   gewachsen, wäre jede Tabellenzeile mitgewachsen - das genaue Gegenteil von
   Wunsch 5 derselben Runde.

   ⚠️ VERWORFEN: `.btn.ak-knopf` dafür benutzen, das ja schon 56 px setzt.
   Die Klasse ist der Burger der Herausforderungsliste und steht unter einem
   `display: none` oberhalb von 600 px - im Seitenkopf wäre das „+" auf dem
   Schreibtisch unsichtbar gewesen.

   ⚠️ HINTER `.aktion.nur-symbol` (Falle ④): beide Selektoren sind (0,2,0)
   stark, und bei Gleichstand gewinnt die spätere Regel. Davor hätte das
   `min-width: 32px` von dort gewonnen und der Knopf wäre quer geworden.

   Das Symbol wächst mit: 18 px in einem 56er Feld sieht aus wie ein Versehen.

   ⚠️ Hier stand zuerst „24 px ist die Stufe, die `.btn-icon` im Haus
   benutzt". Das ist falsch und die Hausstilpruefung hat es am 2026-09-21
   gefunden: `.btn-icon` steht auf `1.4rem` = 22,4 px. Das Haus hat fuer
   Symbole ueberhaupt keine Leiter - es stehen 15, 18, 20, 1,15rem, 1,4rem,
   1,5rem und 2,2rem nebeneinander, jedes an seinem Bauteil. 24 px ist
   also eine BEGRUENDETE Wahl und keine uebernommene Stufe: es ist das
   naechste runde Mass ueber `.btn-icon`, und dieser Knopf ist mit 56 px
   der groesste Symbolknopf der Seite. */
.btn.aktion-gross {
  min-width: var(--min-touch);
  min-height: var(--min-touch);
}
.btn.aktion-gross .msym { font-size: 24px; }

/* ⚠️ DER BURGER BEHAELT SEIN GRIFFMASS, auch mit der neuen Optik (2026-09-17).

   `.aktion` misst `--md-sys-comp-size-xs` (32 px) - richtig fuer einen Knopf,
   der in einer Reihe mit anderen steht. Der Burger steht in keiner Reihe: er
   erscheint NUR unter 600 px, also am Telefon, und ist dort der einzige
   Bedienweg der eingeklappten Leiste. `--min-touch` ist in diesem Haus
   `--md-sys-comp-size-m` = 56 px, und genau so gross war er vorher durch
   `btn-icon`. Die Optik zu uebernehmen heisst nicht, das Griffmass
   mitzunehmen.

   ⚠️ HINTER `.aktion.nur-symbol` und nicht davor: beide Selektoren sind
   (0,2,0) stark, und bei Gleichstand gewinnt die spaetere Regel (Falle ④).

   ⚠️ Zwei Klassen aus demselben Grund wie oben bei `display` - `.ak-knopf`
   allein (0,1,0) kaeme gegen `.aktion.nur-symbol` nicht an. */
.btn.ak-knopf {
  min-width: var(--min-touch);
  min-height: var(--min-touch);
}

/* Der Kopierknopf der Kanalliste bricht nicht um - „Link kopieren" in zwei
   Zeilen waere neben Kanalname und Adresse unlesbar. Die Eigenschaft kam
   vorher aus einer eigenen Knopfregel, die am 2026-09-15 ersatzlos gefallen
   ist. ⚠️ HIER und nicht weiter oben: eine Uebersteuerung gehoert hinter
   das Bauteil, das sie uebersteuert - sonst zieht sie dessen Schnittmarken
   mit (Falle ④). */
.kanal-aktion .aktion { white-space: nowrap; }

/* ⚠️ `display: contents` an den drei Platzhaltern, und das ist hier genau
   das Richtige: alle werden NACHTRAEGLICH gefuellt (`_zeigeEcho`,
   `_ladeVideoKnoepfe` und `_ladeAnalyseKnopf`), und ihre Knoepfe sollen im selben Abstandsraster
   stehen wie das Loeschen daneben - nicht in einem eigenen Kasten mit
   eigenem Abstand.

   Leer bleiben sie damit ohne jede Wirkung: `display: contents` an einem
   Element ohne Kinder zeichnet nichts und nimmt keinen Platz. Genau deshalb
   braucht es hier kein `:empty` mehr. */
#fg-daumen, #fg-video-knopf,
#fg-analyse,
/* Derselbe Handgriff je Torzeile, seit dem 2026-09-15: der Wiedergabeknopf
   kommt aus `_ladeVideoKnoepfe` und soll trotzdem VORN stehen. */
td[id^="goal-actions-"] > span[id^="goal-play-"] { display: contents; }

.schema-update-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 16px 20px;
  background: var(--md-ext-color-warning-container);
  color: var(--md-ext-color-on-warning-container);
  border: none;
  border-radius: var(--md-sys-shape-corner-large);
  margin-bottom: 20px;
}

/* ── Fireworks ────────────────────────────────────────────────────────────── */
#fireworks-container {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
  z-index: 1;
}
.fw-particle {
  position: absolute;
  bottom: 20%;
  border-radius: 50%;
  opacity: 0;
  animation-name: fw-shoot;
  animation-timing-function: var(--md-sys-motion-easing-emphasized-decelerate);
  animation-fill-mode: forwards;
}
@keyframes fw-shoot {
  0%   { transform: translateY(0) translateX(0) scale(1); opacity: 1; }
  60%  { opacity: 1; }
  100% { transform: translateY(-70vh) translateX(var(--tx, 0px)) scale(0.2); opacity: 0; }
}
#game-end-banner .banner-content { position: relative; z-index: 2; }

.banner-points {
  font-size: 1.4rem;
  font-weight: 700;
  color: #F6D975;   /* Warn-Gold auf dunklem Scrim (beide Modi) */
  margin: 8px 0 16px;
  min-height: 1.6em;
}
/* Ohne Siegesserie (die Regel, erst ab zwei Siegen in Folge) blieb die leere
   Zeile stehen: 1,6 em Mindesthoehe, 24 px Aussenabstand und ein zweiter
   Flex-Abstand - eine Luecke zwischen Stand und Bewertung, die der Betreiber
   am 2026-09-29 auf dem Tisch angestrichen hat. Dasselbe Muster wie
   `.banner-avatars:empty`. */
.banner-points:empty { display: none; }

/* Rueckspiel und neues Spiel im Spielende-Banner (2026-09-29), eine Reihe
   ueber „Zurueck zum Start". `btn-tonal` und nicht `btn-outline`: der
   umrandete Knopf schreibt in `primary`, im hellen Thema dunkelviolett auf
   dem dunklen Schleier. Die tonale Flaeche bringt ihre eigene Schriftrolle mit.
   Aus dem Netz bleibt der Behaelter leer und nimmt keinen Platz. */
.banner-weiter {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
}
.banner-weiter:empty { display: none; }

/* ── Live-Anzeige in der Spielkopfzeile ───────────────────────────────────────
   Macht einen Verbindungsabbruch sichtbar, statt ihn als Stillstand zu tarnen
   (dasselbe Muster wie die Kiosk-Seite).                                      */
.live-pill {
  margin-top: 2px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--md-ext-color-success);
  white-space: nowrap;
}
.live-pill.stale { color: var(--md-sys-color-error); }

/* „Pausiert" im Spielkopf. Ohne diese Anzeige sah man der Ansicht nicht an, dass
   pausiert ist - die Uhr blieb einfach stehen, und das wirkte wie ein Haenger.
   Warnfarbe wie das Halbzeit-Abzeichen im Kiosk, damit dasselbe Signal auch
   dieselbe Farbe hat. */
.game-paused-badge {
  display: inline-block;
  margin-top: 3px;
  padding: 1px 8px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-ext-color-warning-container);
  color: var(--md-ext-color-on-warning-container);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.game-paused-badge.hidden { display: none; }

/* Letzte Update-Pruefung. Gruen, solange sie im Intervall liegt; faellt sie
   deutlich zurueck, laeuft der Cron-Job nicht mehr - und genau das war vorher
   nicht zu sehen, weil die Seite nur den letzten PROTOKOLLIERTEN Lauf zeigte
   (also die letzte Fehlermeldung, tagelang). */
.update-ok   { color: var(--md-ext-color-success); font-weight: 700; }
.update-late { color: var(--md-sys-color-error);   font-weight: 700; }

/* ── Raster-Bausteine ─────────────────────────────────────────────────────────
   Ersetzen die frueher in den JS-Ansichten fest verdrahteten
   grid-template-columns – von dort war sie fuer kein Media-Query erreichbar.
   Spaltenzahl bleibt ausdruecklich (kein auto-fit), damit sich auf dem
   1400 px breiten #content nichts gegenueber vorher verschiebt.

   minmax(0, 1fr) statt 1fr ist der entscheidende Teil: ein Grid-Item hat
   min-width:auto, also mindestens seine min-content-Breite. Eine sechsspaltige
   Tabelle darin schiebt diese Breite nach oben durch und sprengt die Spalte –
   genau deshalb konnte .table-wrap { overflow-x: auto } bisher nie greifen.
   Auf breiten Ansichten ist minmax(0,1fr) mit 1fr deckungsgleich.             */
.grid-2, .grid-3, .grid-4 { display: grid; gap: var(--grid-gap, 16px); }
.grid-2      { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3      { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4      { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-2-wide { display: grid; gap: var(--grid-gap, 16px);
               grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); }
/* Beschriftung + Wert, z. B. die Repo-Angaben in den Einstellungen */
.grid-kv     { display: grid; gap: 6px 14px;
               grid-template-columns: max-content minmax(0, 1fr); }
/* Die Kernzeilen der CPU-Karte (Einstellungen › System): so viele Spalten
   nebeneinander, wie 150px breite Zeilen hergeben - ein Entwicklungsrechner
   mit 22 Kernen stand sonst als 22 Zeilen untereinander. Bei 600px (siehe
   Medienabfrage unten) auf EINE Spalte, wie .grid-2/.grid-3. */
.sys-kerne   { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
               gap: 4px 16px; margin-top: 4px; }
/* Inhalt + Knoepfe am Ende einer Listenzeile */
.row-actions { display: grid; gap: 8px; align-items: center;
               grid-template-columns: minmax(0, 1fr) auto; }

/* ── Listenzeilen, deren Raster fruehe inline in settings.js/dashboard.js stand ── */
/* Sound je Ereignis: Bezeichnung · Auswahl · Lautstaerke · Abspielen */
.sound-event-row {
  display: grid;
  grid-template-columns: 1.6fr 2fr 1.2fr auto;
  gap: 10px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
/* Eigene Sounddatei: Name · Abspielen · Loeschen */
.sound-file-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 8px;
  align-items: center;
  padding: 6px 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
/* Siegesserie auf der Startseite: Anzahl · Spieler · Erlaeuterung */
/* Die Siegesserien-Zeile. Drei Spalten statt einer Flexzeile, damit Platz,
   Avatar, Name und Zahl untereinander stehen - Wunsch des Betreibers am
   2026-09-03.

   ⚠️ Die Kante der Avatare entsteht NICHT hier, sondern in
   `dashboard.js::_avatarOrGap`: `App.avatarHtml` gibt fuer einen Spieler ohne
   Bild einen Leerstring zurueck, und dessen Name rutschte um die Bildbreite
   nach links. Kein Raster kann eine Zelle ausrichten, die es nicht gibt.

   Ein eigenes Raster und NICHT `.standing-row` nachgenutzt: dessen Spaltenzahl
   ist von tests/test_responsive.py auf {8, 4} festgenagelt, und dort haengen
   acht Wertungsspalten dran. Vier Spalten mehr fuer drei Werte waeren eine
   Rangliste, die vorgibt, eine zu sein. */
/* ⚠️ `.streak-row`, `.streak-rank` und `.streak-zahl` sind am 2026-09-17
   entfallen. Sie gehoerten ALLEIN dem Serien-Kasten der Startseite, und den
   gibt es nicht mehr; die TV-Ansicht bringt ihre eigenen Regeln desselben
   Namens mit und laedt diese Datei nicht.

   ⚠️ `.streak-name` bleibt und ist KEIN Rest: die Serienliste unter
   „Statistiken -> Einzelspieler -> Siegesserien" (`stats.js`) traegt sie, und
   die 390-px-Regel `.streak-name img` weiter unten haengt daran. Dasselbe gilt
   fuer `.streak-label` darunter. */
.streak-name { display: flex; align-items: center; gap: 6px;
               overflow: hidden; min-width: 0; }
/* Die Beschriftung am Ende jeder Serienzeile. Am Telefon faellt das Wort
   „Siege" weg - dasselbe Mittel wie bei `.bt-kanal-wort`, und aus demselben
   Grund: der Platz gehoert dem Namen.

   ⚠️ Gemessen bei 390 px: „Siege in Folge" belegte rund 100 px, fuer den Namen
   blieben 49 - „Johannes" wurde zu „Joh…", „King Oemer" zu „Kin…". Mit „in
   Folge" bleiben rund 95 px, und die Namen stehen vollstaendig da. Die
   Aussage geht nicht verloren: darueber steht die Karte „Einzelspieler", und
   der Reiter heisst „Siegesserien". */
.streak-label { white-space: nowrap; }
.sound-event-row > *, .sound-file-row > * { min-width: 0; }

/* ⚠️ UND DER REGLER SELBST MUSS SCHRUMPFEN DUERFEN (2026-09-14).

   Befund der Sichtpruefung: die Prozentzahl lag bei 390 px 15 × 16 px unter
   dem Abspielknopf, bei 840 px 12 × 16 px - und ragte 24 px aus ihrem Kasten.

   Die Ursache ist die automatische Mindestbreite eines Flexkindes. Der Regler
   steht in `<div style="display:flex">` mit `flex:1`, und `flex-shrink` greift
   trotzdem nicht: ein `<input type=range>` ist ein ERSETZTES Element, seine
   `min-width: auto` ist die Eigenbreite (in Chrome 129 px). Der Kasten laeuft
   damit ueber, und die Zahl daneben wird hinausgeschoben - unter den Knopf.

   Die Zeile darueber (`> *`) trifft den Kasten, aber nicht den Regler DARIN.
   Genau eine Ebene zu tief, und genau deshalb war es unsichtbar: alle drei
   Spalten der Zeile waren sauber begrenzt, nur ihr Inhalt nicht.

   ⚠️ Die Zahl behaelt ihre 36 px. Sie zu kuerzen waere die falsche Antwort:
   „100%" braucht sie, und eine Zahl, die manchmal „10…" heisst, ist schlimmer
   als ein etwas kuerzerer Regler. */
.sound-event-row .slider, .sound-file-row .slider { min-width: 0; }
/* ⚠️ Das Kaestchen je Klangereignis (2026-09-11, Mobilpruefung). Das
   `input` selbst ist 13 x 13 px gross - die native Groesse -, aber es liegt
   in einem `<label>`, und das ist die Flaeche, die zaehlt. Gemessen bei
   390 px: 183 x 24 px. Breit genug, aber acht Pixel zu flach fuer die
   Hausleiter. `min-height` statt einer groesseren Schrift: die Zeile soll
   nicht wachsen, nur der Griff. */
.sound-event-row > label { min-height: var(--md-sys-comp-size-xs); }

/* Derselbe Schutz fuer die bestehenden Raster (siehe minmax-Begruendung oben) */
.dashboard-grid > *, .grid-2 > *, .grid-3 > *, .grid-4 > *,
.grid-2-wide > *, .grid-kv > *, .row-actions > * { min-width: 0; }
.table-wrap { min-width: 0; }

/* ── Navigationsleiste unten (nur schmale Geraete) ────────────────────────────
   Erscheint erst im 600-px-Abschnitt weiter unten. Hier nur das Aussehen, damit
   Grund- und Breitenregeln getrennt bleiben.                                  */
#bottom-nav {
  display: none;                      /* Standard: es gibt die obere Leiste */
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 90;                        /* unter Modal (400), ueber dem Inhalt */
  /* Halbdurchsichtig mit Weichzeichner (2026-09-30, Entwurf): der Inhalt
     scheint beim Rollen durch, wie in einer App. ⚠️ Eine BENANNTE Ausnahme
     von M3-RULES §5 („keine Glas-Effekte") - nur hier und nicht im Kiosk,
     wo ein Weichzeichner ueber dem Livebild teuer und verboten ist. */
  background: rgba(var(--md-sys-color-surface-container-rgb) / 0.86);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border-top: 1px solid var(--md-sys-color-outline-variant);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}
.bn-link {
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  gap: 2px;
  padding: 6px 2px;
  border-radius: var(--md-sys-shape-corner-none);
  font-size: var(--md-sys-typescale-label-small-size);
  line-height: 1.1;
  text-align: center;
}
.bn-link .msym {
  font-size: 24px;                       /* --md-sys-layout-icon-size */
  /* Die Indikator-Pille des aktiven Ziels, 64x32 (ANALYSE.md §8). Sie liegt
     HINTER dem Icon und nicht um die ganze Kachel: eine Kachel ist 90 px
     breit, eine Pille darum waere ein Balken.

     ⚠️ Am Icon und nicht am Anker, weil die Beschriftung darunter steht -
     M3 legt die Pille nur hinter das Symbol. Ohne `display: grid` und
     `place-items` sitzt sie nicht mittig. */
  display: grid;
  place-items: center;
  /* ⚠️ `min(64px, 100%)` und NICHT 64px. GEMESSEN: M3 nennt 64, aber unsere
     Leiste hat SECHS Ziele - eine Kachel ist bei 360 px nur 60 px breit und
     bei 320 px 53. Mit festen 64 px ragte die Pille 4 bzw. 11 px in die
     Nachbarkacheln, auf beiden Seiten. M3 rechnet mit drei bis fuenf Zielen;
     die Zahl ist hier eine Obergrenze und keine Breite. */
  width: min(64px, 100%);
  height: 32px;
  border-radius: var(--md-sys-shape-corner-full);
  transition: background-color var(--md-sys-motion-spring-fast-effects-duration)
                               var(--md-sys-motion-spring-fast-effects-easing);
}
.bn-link.active .msym {
  /* Keine farbige Pille mehr (2026-09-30): das aktive Ziel ist dunkler und
     kraeftiger beschriftet. Der Kasten bleibt und zentriert das Symbol. */
  background: transparent;
  color: var(--md-sys-color-on-surface);
}
/* ⚠️ HINTER `.nav-link.active` (gleiche Staerke): sonst bekaeme jede Kachel
   die weisse Pille der oberen Schiene samt Schatten. */
.bn-link.active {
  background: none;
  box-shadow: none;
  color: var(--md-sys-color-on-surface);
}
/* ⚠️ `.bn-text` und NICHT `> span:last-child`, und der Grund ist gemessen
   (2026-09-07): `static/js/ripple.js` haengt bei `pointerdown` ein
   `<span class="ripple-ink">` an den Wirt, und `.nav-link` steht in seinem
   SELECTOR - `.bn-link` traegt beide Klassen. Ab dem `appendChild` ist die
   Welle das letzte Kind, die Beschriftung also nicht mehr `:last-child`, und
   diese Regel fiel fuer die Dauer des Tippens aus: die Beschriftung verlor
   ihre Ellipse und lief ueber. Im Seitenmenue, das die Beschriftung ganz
   verbarg, war derselbe Wegfall der aufblitzende Text, den der Betreiber
   gemeldet hat. */
.bn-link > .bn-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                      max-width: 100%; }
.bn-link.active .bn-text { font-weight: 600; }
/* Zahnrad in der oberen Leiste – bei JEDER Breite sichtbar.

   Es war früher nur auf schmalen Geräten da, weil dort `.nav-links` ausgeblendet
   ist. Seit die Pillen nicht mehr umbrechen, sondern wischbar sind, wäre
   „Einstellungen" als letzte Pille zwischen etwa 601 und 1085 px aber nur noch
   nach seitlichem Wischen erreichbar – und ein Ziel, das man erst finden muss,
   ist kein Ziel. Zwei sichtbare Wege dorthin sind der kleinere Preis.

   Kein dritter Umbruchpunkt dafür: „ab wann reicht der Platz nicht" lässt sich
   in CSS nicht ausdrücken, und die beiden erlaubten Marken (600, 840) treffen
   die Grenze nicht. */
.nav-gear { display: inline-flex; align-items: center; justify-content: center;
            padding: 8px; }
/* ... aber nicht fuer jeden. Aus dem Netz sah das Zahnrad bisher jeder,
   obwohl `App.ui.requireAdmin()` dahinter abweist - ein Ziel, das eine
   Fehlermeldung liefert, ist kein Ziel. Wunsch des Betreibers am 2026-09-03.

   Die Klasse setzt `App.auth.apply()`, und dort steht auch, warum am Tisch
   und in einer Anlage ohne Benutzer nichts verschwindet. */
body:not(.kann-admin) .nav-gear { display: none; }
/* ... und seit dem 2026-09-07 NUR NOCH AM TISCH. Der Betreiber: „Koennen wir
   das Zahnrad nur am (localhost - Tisch) anzeigen lassen?! Ich wuerde gerne
   Remote das anderes geloest haben. Ich moechte, dass bei einem Benutzer der
   Administrator ist, die 'Tischeinstellungen' mit in die Einstellungen wandern
   [...], die erst ueber das Kontextmenues seines Avatars oeffnet."

   Aus dem Netz fuehrt der Weg jetzt ueber den Avatar: die neun globalen Reiter
   stehen dort als „… - Admin" in der Konto-Seite
   (`App.account._globalReiter`).

   ⚠️ `am-tisch` POSITIV und nicht `:not(.nicht-am-tisch)`: die zweite Fassung
   traefe JEDEN Besucher, solange `/api/auth/context` noch nicht geantwortet
   hat - das Zahnrad waere einen Moment da und verschwaende dann. Genau
   deshalb setzt `App.auth.apply()` beide Klassen und nicht nur eine.

   ⚠️ Die ROUTE `#/settings` bleibt erreichbar, und das ist Absicht: Verweise
   darauf und ein aufgerufener Reiter (`#/settings/video`) sollen weiter
   funktionieren. Was verschwindet, ist der Knopf - die Sperre dahinter ist und
   bleibt `App.ui.requireAdmin()`. */
body:not(.am-tisch) .nav-gear { display: none; }
.nav-gear .msym { font-size: 1.4rem; }
/* Der Farbmodus-Knopf, nur am Tisch (2026-09-30). ⚠️ `am-tisch` POSITIV,
   aus demselben Grund wie beim Zahnrad darüber. */
.nav-farbmodus { display: none; align-items: center; justify-content: center;
                 padding: 8px; border: none; background: transparent;
                 color: inherit; border-radius: var(--md-sys-shape-corner-full);
                 min-width: var(--min-touch); min-height: var(--min-touch); cursor: pointer; }
body.am-tisch .nav-farbmodus { display: inline-flex; }
.nav-farbmodus .msym { font-size: 1.4rem; }

/* Höhe der Navigationsleiste unten – von #content und den fixierten Elementen
   gebraucht, damit sie nichts verdecken. */
/* Die Hoehe der unteren Leiste. 80 px ist der M3-Wert (ANALYSE.md §8).

   ⚠️ Hier zieht die Vorgabe in die RICHTIGE Richtung: unsere Kacheln waren
   60 px, M3 nennt 80 - groessere Ziele am Telefon, keine kleineren. Bei den
   Knoepfen war es umgekehrt, deshalb bleibt dort unsere Stufe M (siehe die
   Groessenleiter).

   Der Preis sind 20 px Inhaltshoehe: das Polster von `#content` haengt an
   diesem Wert. Auf einem 844 px hohen Telefon sind das 2,4 Prozent. */
:root { --bn-h: var(--md-sys-layout-navbar-height); }

/* ── Schemabild (Feldstatistik) ───────────────────────────────────────────────
   Zwei Bilder nebeneinander, bei schmalem Fenster untereinander - ueber
   `auto-fit` und nicht ueber einen Umbruchpunkt. Der Responsive-Abschnitt
   weiter unten ist der einzige Ort fuer @media-Regeln
   (tests/test_responsive.py), und hier braucht es keine.

   `minmax(min(100%, 320px), 1fr)`: das `min(100%, ...)` ist noetig, damit die
   Spalte auf einem 360-px-Telefon nicht breiter wird als der Bildschirm - ohne
   das schiebt ein 320-px-Minimum plus Innenabstand die Seite seitlich weg. */
.schemabilder {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: 16px;
}

.schemabild { margin: 0; display: flex; flex-direction: column; gap: 6px; }
.schemabild img, .schemabild svg {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--md-sys-shape-corner-medium, 12px);
}
/* ⚠️ Seit dem 2026-10-07 inline statt <img> (Spurenverlauf). Seit dem
   2026-10-09 traegt JEDE eigene Figur `data-rod` (Kontextmenue), nicht mehr
   nur die mit Treffern - der Zeiger gilt deshalb ueberall auf der eigenen
   Seite. Seit dem 2026-10-12 klickt man sie nicht mehr: Hover/Tipp zeigt
   Tooltip und Ballverfolgung, der Zeiger bleibt ein gewoehnlicher. */
.schemabild svg rect[data-rod] { cursor: default; }
.schemabild figcaption {
  font-size: var(--md-sys-typescale-label-medium-size, .8rem);
  color: var(--text-dim, #7a8a83);
  text-align: center;
}
/* `position: relative`, damit der Tooltip (`.figur-tooltip`,
   `static/components/spielfeldspur.js::_zeiger`) sich an DIESEM Rahmen
   ausrichtet und nicht an der ganzen Seite. */
.sb-bild { position: relative; }
.figur-tooltip {
  position: absolute;
  /* Angesetzt an der OBERKANTE der Figur (JS setzt `top`/`left` genau
     dorthin) - der Y-Anteil hebt das Kaestchen um seine eigene Hoehe plus
     6 px Luft darueber, der X-Anteil zentriert es wie beim Kurven-Tooltip. */
  transform: translate(-50%, calc(-100% - 6px));
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 10px;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container-highest);
  border: 1px solid var(--md-sys-color-outline-variant);
  box-shadow: var(--md-sys-elevation-level2);
  font-size: var(--md-sys-typescale-label-medium-size, .75rem);
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
}
.figur-tooltip.links { transform: translate(calc(-100% + 16px), calc(-100% - 6px)); }

/* 2026-10-12: die Schalterreihe „Ballverlauf"/„Kennzahlen" ueber den
   Schemabildern (App.spur.schalterHtml, `.chip`). Bruch bei schmalem
   Bildschirm statt Seitenlauf. */
.schema-schalter { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }

/* ── Videospieler ─────────────────────────────────────────────────────────────
   Der Spieler liegt in einem Modal, und das ist bei `wide: true` 720 px breit.
   `width: 100%` mit `aspect-ratio` statt einer festen Hoehe: das Kamerabild ist
   16:9 (das Spielfeld hat 1200/680 = 1,765, also fast genau dasselbe), und ein
   schwarzer Grund verhindert das Aufblitzen von Weiss, bevor das erste Bild da
   ist.

   Kein `@media` hier - die Regeln passen sich ueber Prozentwerte selbst an, und
   der Responsive-Abschnitt weiter unten ist der einzige Ort, an dem
   Umbruchpunkte stehen duerfen (tests/test_responsive.py). */
.video-spieler {
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 60vh;
  background: #000;
  border-radius: var(--md-sys-shape-corner-medium, 12px);
  display: block;
}

/* Die eigene Videoseite am Telefon (2026-09-13).

   ⚠️ Sie braucht fast nichts: das Bild liegt im `.video-rahmen` wie in der
   Ueberlagerung, die Sprungleiste und der Ladeknopf sind dieselben
   Bausteine. Der einzige Unterschied ist, dass hier kein Fenster mit
   eigener Hoehengrenze darum liegt - das Bild darf die ganze Breite
   nehmen, und mehr als die Bildschirmhoehe soll es nicht werden.

   `100dvh` und nicht `100vh`: am Telefon faehrt die Adressleiste ein und
   aus, und `vh` rechnet mit der GROESSTEN Hoehe - das Bild waere beim
   Aufsetzen zu hoch und der Ladeknopf darunter unerreichbar. `dvh` folgt
   dem, was gerade sichtbar ist. Der Abzug von 200 px laesst Platz fuer
   Seitenkopf, Sprungleiste und die untere Navigationsleiste. */
.video-seite .video-spieler { max-height: calc(100dvh - 200px); }
.video-seite .video-marken { margin-top: 12px; }

/* Die Stimmen an einer Sprungmarke (2026-09-13): Zahl, dann Daumen.
   ⚠️ Kleiner und blasser als der Text davor - die Marke sagt, WELCHES Tor,
   die Stimmen sagen, wie es ankam. Zwei gleich laute Angaben in einem Knopf
   von 32 px Hoehe machen beide unleserlich. */
.vm-echo {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: 6px;
  color: var(--md-sys-color-on-surface-variant);
  font-variant-numeric: tabular-nums;
}
.vm-echo .msym { font-size: 1em; }

/* Die Ueberschrift ueber dem Video ist die Begegnung (2026-09-13) - in der
   Ueberlagerung wie auf der eigenen Seite am Telefon dieselben Klassen.

   ⚠️ `flex-wrap` und nicht `nowrap`: zwei Namen, ein Torstand und ein
   Zeitpunkt sind an einem 390-px-Bildschirm laenger als eine Zeile. Umbricht
   sie, faellt der Zeitpunkt nach unten - und genau der ist die Angabe, die am
   ehesten warten kann.

   Der Zeitpunkt ist kleiner und blasser: er beantwortet WANN, die Paarung
   beantwortet WAS. Zwei gleich laute Angaben in einer Ueberschrift machen
   beide zur Aufzaehlung. */
.video-titel {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
}
/* Das Abspielsymbol gehoert IN diese Gruppe (siehe game.js) - deshalb hier
   `align-items: center` fuer das Symbol und `baseline` nur zwischen den
   Textteilen: ein Symbol auf der Schriftlinie sitzt sichtbar zu tief. */
.vt-paarung {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
}
.vt-paarung > .msym { align-self: center; }
/* Der Torstand traegt die Ziffernbreite der Tabelle: „10:7" und „3:5"
   sollen an derselben Stelle enden, wenn zwei Fenster nacheinander aufgehen. */
.vt-erg { font-variant-numeric: tabular-nums; }
.vt-zeit { font-size: .8em; font-weight: 400; }

/* ⚠️ Hier standen bis zum 2026-09-13 die Lageregeln fuer den Platz von
   „Video ansehen" UEBER der Begegnung - erst gar keine (der Knopf stand
   links, weil ein Blockfluss links anfaengt), dann rechtsbuendig mit
   Aussenabstand nur bei Inhalt.

   Beides ist entfallen: der Knopf steht jetzt in der Aktionszeile im Fuss
   der Kopfkarte, zwischen den Daumen und dem Loeschen (siehe `.fg-fuss`).
   Die Frage „wo steht er allein" stellt sich damit nicht mehr - er steht
   nicht mehr allein. */

/* ── Der Kinoknopf auf dem Video (2026-09-10) ────────────────────────────────
   Der Betreiber: „Der Kinomodus im Player soll nicht als 'Tag' wie ein Tor
   angezeigt werden. Sondern als Aktion in dem Player selbst zu Verfuegung
   stehen." Vorher stand er unter den Sprungmarken, in derselben Form.

   ⚠️ Der Rahmen ist die Bezugsflaeche und keine Zierde. Ohne ihn haenge der
   Knopf am Modalkasten - und der ist im Kinomodus BREITER als das Video, das
   dort ueber seine Hoehe bemessen und mittig gestellt wird. Der Knopf saesse
   dann neben dem Bild. */
.video-rahmen { position: relative; }
/* ⚠️ Und deshalb schmiegt sich der Rahmen im Kinomodus an das Video: erst
   damit liegt seine rechte Kante auf der rechten Kante des Bildes. */
#modal-box.kino .video-rahmen { width: fit-content; margin: 0 auto; }

/* Oben rechts, weil die nativen Bedienelemente des Browsers unten sitzen -
   ein Knopf daneben waere ein zweiter Bedienstreifen.

   ⚠️ IMMER sichtbar und nicht erst beim Ueberfahren: am Kicker-Touch gibt es
   kein Hover, und ein Knopf, den nur eine Maus findet, ist dort keiner.

   ⚠️ Die Farben sind das INVERSE Paar von M3 - dieselbe Rolle wie bei einer
   Snackbar, die ueber dem Inhalt liegt. Ein fester Grauton waere in einem der
   beiden Themes falsch, und ein Scrim mit eigener Schriftfarbe waere zwei
   Entscheidungen statt einer.

   Stufe S (40) und nicht `--min-touch` (56): der Knopf liegt AUF dem Bild,
   und 56 px verdecken dort spuerbar mehr. Er ist kein primaeres Ziel der
   Seite - genau der Fall, fuer den die Leiter S und XS vorsieht.
   ⚠️ Als px-Wert und NICHT durch `--ui-zoom` geteilt: die Variable ist ein
   CSS-`zoom` auf dem Rumpf, px skalieren ohnehin mit. */
.video-kino {
  position: absolute;
  top: 10px;
  right: 10px;
  width: var(--md-sys-comp-size-s);
  height: var(--md-sys-comp-size-s);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--md-sys-shape-corner-full);
  cursor: pointer;
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  box-shadow: var(--md-sys-elevation-level2);
}
.video-kino .msym { font-size: 1.3rem; }
/* ⚠️ Eigener Tastaturfokus, und der ist hier Pflicht statt Kuer: der
   Hausfokus haengt an `.btn:focus-visible::after` (eine Zustandsflaeche), und
   diesen Knopf traegt bewusst KEIN `.btn` - `.btn` brachte `--min-touch` (56)
   mit und machte aus dem 40er Quadrat einen 40x56-Riegel. Ohne eigene Regel
   bliebe nur der Standardring des Browsers, und der ist auf einem dunklen
   Videobild kaum zu sehen.

   Dieselbe Form wie bei `.t-stift`/`.t-haken` - das vorhandene Vorbild fuer
   ein fokussierbares Element ausserhalb des Knopfsystems. `outline-offset`
   nach AUSSEN, weil der Knopf auf dem Bild liegt und ein Ring nach innen im
   Symbol verschwaende. */
.video-kino:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* ══ Die Torleiste unter dem Video (2026-09-15) ═════════════════════

   Zwischen Video und Sprungmarken: das Tor, das man gerade gesehen hat, seine
   drei Knoepfe, der Weg zum naechsten - und darunter Kommentarfeld und Liste.

   ⚠️ LEER OHNE WIRKUNG. Der Kasten steht immer im Markup und wird
   nachtraeglich gefuellt. Kein `margin` am Kasten selbst, sondern an seiner
   Zeile - sonst schoebe er die Sprungmarken auch dann nach unten, wenn man
   das ganze Spiel von vorn ansieht und gar kein Tor gewaehlt ist. */
.torleiste:empty { display: none; }
.tl-zeile {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  margin-top: 12px;
}
/* Der Name traegt die Zeile - er sagt, WORAUF sich die Knoepfe daneben
   beziehen. Etwas kraeftiger als der Rest, aber keine Ueberschrift: die
   Ueberschrift ist die Begegnung ueber dem Video. */
.tl-name { font-weight: 500; margin-right: 4px; }
/* Der Weg zum naechsten Tor steht am anderen Ende der Zeile. `margin-left:
   auto` und nicht `space-between`: die drei Daumen sollen beieinander bleiben
   und nicht auseinandergezogen werden, wenn die Zeile breit ist. */
.tl-weiter { margin-left: auto; }
.tl-hinweis { margin-top: 10px; }
/* ⚠️ Die Liste steht in einem Kasten, der mitwaechst - und das Video
   darueber behaelt seine Hoehe. Bei zwanzig Kommentaren an einem Tor waere
   die Leiste sonst laenger als der Bildschirm; hier rollt sie fuer sich. */
.torleiste .echo-liste { max-height: 30vh; overflow-y: auto; }

/* Die Sprungmarken unter dem Spieler. Umbrechend, weil ein Spiel auch zwanzig
   Tore haben kann - und dann darf die Leiste wachsen statt zu ueberlaufen.

   ⚠️ FLACH seit dem 2026-09-15, auf ausdrueckliche Ansage: die Marken waren
   `.btn.btn-outline` mit Inline-Massen und damit Chips mit Rahmen. Der
   Betreiber wollte die flache Optik der Aktionen „auch Sprungmarken und
   Herunterladen".

   ⚠️ Und genau deshalb steht hier ein groesserer Abstand als vorher. Ohne
   Rahmen trennt die Kette nichts mehr voneinander - bei zwanzig Toren stuende
   dort eine Wand aus Text. Die Trennung leistet jetzt der WEISSRAUM: 14 px
   waagerecht (statt 6), senkrecht dagegen nur 2, weil die Zeilen einer
   umgebrochenen Leiste zusammengehoeren. */
.video-marken {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  margin-top: 10px;
}

/* ── Kinomodus (2026-09-09) ───────────────────────────────────────────────────
   Der Betreiber: "Ich moechte in den Videos des Spiel ein Anzeigemodi 'Kino'
   wie bei Youtube haben. [...] KEIN VOLLBILD. Aber die gesamten Aktionen
   bleiben erhalten. Nur das Video ist groesser!"

   ⚠️ Die BREITE steht nicht hier, sondern in `App.game._kinoAnwenden`. Sie
   muss dort stehen: `App.ui.showModal` setzt `#modal-box.style.maxWidth`
   INLINE (520 / 720 / 900), und ein Stylesheet kaeme dagegen nur mit
   `!important` an. Modalbreiten liegen ohnehin in einer Hand - in app.js
   (SPEZIFIKATION 10.3), und tests/test_responsive.py besteht darauf, dass
   keine davon ins Stylesheet wandert.

   Was hier steht, ist die HOEHE, und die ist der eigentliche Trick. Ohne sie
   wuerde das Video zwar breiter und sofort wieder flachgedrueckt: `#modal-box`
   deckelt auf 80 dvh, `.video-spieler` zusaetzlich auf 60 vh. Beide Bremsen
   muessen mit.

   Das Video wird ueber die HOEHE bemessen (Breite folgt aus `aspect-ratio`)
   und mittig gestellt. Ueber die Breite bemessen waere es bei 90 vw auf einem
   1080er Schirm 972 px hoch - dann passten Sprungleiste, Download und
   Schliessen nicht mehr ins Bild, und genau die sollen ja bleiben.

   ⚠️ Die Zoomdivision steht an BEIDEN Hoehen, und der erste Entwurf hatte sie
   nur an einer. Gemessen bei Zoom 1,25: der Kasten kam auf 795 px (92 dvh
   geteilt), das Video auf 691 px (64 dvh ungeteilt) - der Inhalt lief um
   147 px ueber, und "Schliessen" stand ausserhalb. Genau der Knopf, der bei
   "KEIN VOLLBILD" bleiben soll. Eine Hoehe, die gegen den Bildschirmzoom
   rechnet, und eine daneben, die es nicht tut, ergibt immer diesen Fehler.

   60 dvh und nicht mehr: unter dem Video stehen Sprungleiste, Kinoknopf,
   Download und Schliessen, zusammen rund 220 px. Gemessen bleibt damit bei
   Zoom 1,25 noch Luft, und bei Zoom 1 reichlich. */
#modal-box.kino {
  padding: 16px 20px;
  max-height: calc(92dvh / var(--ui-zoom, 1));
}
#modal-box.kino .video-spieler {
  max-height: none;
  width: auto;
  height: calc(60dvh / var(--ui-zoom, 1));
  margin: 0 auto;
}
#modal-box.kino .video-marken { justify-content: center; }

/* ── Responsive ───────────────────────────────────────────────────────────────
   EINE Stelle für alle breitenabhängigen Regeln. Außerhalb dieses Abschnitts
   steht kein `@media (max-width: …)` – tests/test_responsive.py prüft das, weil
   genau diese Streuung vorher die Wartungsfalle war.

   Marken nach den Material-3-Window-Size-Classes (m3.material.io), damit sie
   nachschlagbar sind und nicht selbst ausgemessen werden müssen:
     ≤ 600 px   compact   Telefone (360 · 390 · 430)
     ≤ 840 px   medium    Tablet im Hochformat, schmales Fenster

   Warum Media-Queries und nicht mehr das frühere `html[data-vw]`:
   `zoom` auf <body> lässt Media-Queries den UNGEZOOMTEN Viewport sehen. Auf
   Telefonen ist der Zoom seit index.html aber immer 1 – die Serverwerte 1,1/2,6
   stammen vom Kicker-Touchscreen und werden dort verworfen –, roher und
   wirksamer Viewport stimmen also überein. Auf dem Kicker-Touch selbst hätte
   `data-vw` ohnehin nie greifen können (1920/1,5 = 1280). Der zweite Regelsatz
   war damit überflüssig und ist entfallen; `html[data-dev]` bleibt für
   Gerätefragen im JavaScript (Bildschirmtastatur, Bildschirmschoner).         */

/* ══ ≤ 840 px – Tablet im Hochformat, schmales Fenster ══════════════════════ */
@media (max-width: 840px) {
  /* `.stat-raster` mit dabei, seit es die Spielerstatistik traegt
     (2026-09-03). Ohne diese Zeile stehen dort am Telefon zwei Spalten mit je
     der halben Breite - und die Punktekurve ist dann 170 px breit. */
  .dashboard-grid, .stat-raster { grid-template-columns: 1fr; }
  /* Unter 840 px gibt es nichts hochzurutschen: eine Spalte hat kein Loch
     neben sich. Abgeschaltet wird im Javascript (es schreibt hier keine
     Spannen und nimmt `anord-gepackt` wieder ab); diese zwei Zeilen sind die
     Bremse fuer den einen Rahmen dazwischen und fuer den Fall, dass das Modul
     gar nicht laeuft.

     ⚠️ `!important` ist hier kein Holzhammer, sondern der einzige Weg -
     DIESELBE FALLE wie beim `--kh: auto` im 600er-Block: die Spanne steht als
     INLINE-Stil an der Kiste, und ein Inline-Stil schlaegt jede Regel aus
     einem Stylesheet. Ohne das Ausrufezeichen bliebe eine stehengebliebene
     Spanne gueltig, und unter jeder Kachel stuenden ein paar hundert Pixel
     Luft. */
  .stat-raster.anord-gepackt { grid-auto-rows: auto; row-gap: var(--anord-luecke); }
  .stat-raster.anord-gepackt > .anord-kiste { grid-row: auto !important; }
  /* Nachrichten: EINE Spalte, und welche gezeigt wird, entscheidet
     `.zeigt-gespraech` - gesetzt beim Oeffnen, geloest vom Zurueck-Knopf.
     Zwei Spalten zu je 170 px waeren beides unbenutzbar. */
  .nr-raster { grid-template-columns: 1fr; }
  .nr-raster .nr-gespraech { display: none; }
  .nr-raster.zeigt-gespraech .nr-liste { display: none; }
  .nr-raster.zeigt-gespraech .nr-gespraech { display: flex; }
  .nr-zurueck { display: inline-flex; }
  /* Am Telefon nur der Punkt, ohne das Wort „online" (2026-09-25): mit dem
     Wort blieben dem Namen bei 390 px vier Buchstaben („Joha…"). */
  .nr-online-wort { display: none; }
  /* Die Uhr: hier faellt der Wochentag. "Donnerstag den 04.09.2026 -
     00:41:12" braucht mehr Platz, als in einer Leiste mit Zahnrad, Glocke und
     Konto-Knopf uebrig ist. Das Datum bleibt - es ist die Auskunft, die man
     einem Bildschirm im Flur nicht ansieht. */
  .nav-clock { font-size: .95rem; gap: .3em; }
  .nav-clock .uhr-tag { display: none; }
  .grid-3, .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* Die fuenf Aktionen des laufenden Spiels brechen hier um.

     ⚠️ Das behebt einen Befund, der AELTER ist als das Aktionsmenue: der
     Umbruch stand nur im 600er-Block, und zwischen 601 und 839 px gab es
     keinen. Gemessen bei 690 px Fensterbreite: die Leiste war 742 px breit und
     die Seite lief 74 px quer. Betroffen ist genau ein reales Geraet - ein
     Telefon im QUERFORMAT (etwa 740x360) - und ein Tablet im Hochformat.

     ⚠️ Hier und nicht im 600er-Block: dort ist die Leiste seit dem
     2026-09-04 eine Spalte im Aktionsmenue, und `flex-wrap` an einer Spalte
     waere eine Erklaerung fuer nichts. Der Betreiber wollte das Menue
     ausdruecklich "nur fuer die Ansicht auf dem Smartphone" - ein Burger auf
     dem Tablet waere mehr, als bestellt war. Also dort weiter alle Knoepfe,
     nur eben umbrechend. */
  .game-controls { flex-wrap: wrap; }

  /* ⚠️ Der Anfeuer-Verlauf faellt zurueck UNTER die Spieler (2026-09-07).
     Entscheidung des Betreibers, gefragt und beantwortet: "Am Telefon wie
     heute unten" - eine 220 px breite Spalte neben zwei Spielerkisten laesst
     von den Kisten je 85 px uebrig.

     Die Marke ist 840 und keine neue: das Projekt hat GENAU ZWEI
     Umbruchpunkte, und `tests/test_responsive.py` laesst keinen dritten zu.
     Der Grundzustand oben ist deshalb der breite Fall, und dieser Block
     nimmt ihn zurueck - nicht umgekehrt.

     Vier Zeilen, und jede nimmt genau eine der Regeln von oben zurueck: die
     zweite Spalte, die Platzierungen, die Spaltenrichtung der Sendeleiste und
     die freie Hoehe des Verlaufs.

     ⚠️ JEDE TRAEGT `body.nicht-am-tisch`, seit dem 2026-09-08 - und das ist
     keine Kosmetik. Die Regeln oben tragen dieses Praefix seitdem (am Tisch
     steht die Spalte nicht rechts, → Punkt 6 des Betreibers). Ohne dasselbe
     Praefix hier waeren diese Zeilen SCHWAECHER als die, die sie zuruecknehmen
     sollen: `#game-view:has(...)` ist eine ID plus eine Klasse,
     `body.nicht-am-tisch #game-view:has(...)` eine ID plus zwei Klassen. Sie
     stehen zwar spaeter in der Datei, aber Spezifitaet schlaegt Reihenfolge -
     der Verlauf sass am Tablet weiter in Spur 2 von einer.

     ⚠️ Und die Platzierungen werden hier NICHT `initial`: der Grundzustand
     hat seit dem 2026-09-08 FUENF Zeilen und platziert jedes Kind
     ausdruecklich (siehe dort, samt Begruendung). `initial` gaebe die
     automatische Platzierung zurueck, und bei verborgener Spalte fiele das
     Torprotokoll aus dem `1fr`. Zurueckgenommen wird nur die SPALTE. */
  body.nicht-am-tisch #game-view:has(> .rk-spalte) {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto auto 1fr;
  }
  body.nicht-am-tisch .rk-spalte { width: auto; }
  /* Die Kachelspur faellt ebenfalls zurueck - dieselbe Entscheidung wie beim
     Anfeuer-Verlauf eine Zeile darueber und aus demselben Grund: 112 px Spur
     neben zwei Kisten lassen von den Kisten je rund 240 px uebrig, und darin
     steht kein Name mehr.

     ⚠️ MIT demselben `:has()`-Praefix wie die Grundregel. Ohne das waere diese
     Zeile SCHWAECHER als die, die sie zuruecknehmen soll - genau die Lehre,
     die dieser Block ueber den `body.nicht-am-tisch`-Praefixen schon traegt.

     ⚠️ Und die Tortasten bekommen ihre Grundmasse zurueck: sie geben nur
     nach, solange die Spur ihnen Breite nimmt. */
  .game-score-area:has(> .ek-kacheln.ek-links) {
    grid-template-columns: 1fr 1fr;
    --tor-w: 130px;
    --tor-h: 100px;
  }
  .ek-kacheln.ek-links {
    grid-column: 1 / -1;
    grid-row: auto;
    flex-direction: row;
    flex-wrap: wrap;
  }
  .ek-kacheln.ek-links > .ek-kachel { flex: 1 1 140px; }
  body.nicht-am-tisch #game-view:has(> .rk-spalte) > .game-header     { grid-column: initial; grid-row: 1; }
  body.nicht-am-tisch #game-view:has(> .rk-spalte) > .game-score-area { grid-column: initial; grid-row: 2; }
  body.nicht-am-tisch #game-view:has(> .rk-spalte) > .rk-spalte       { grid-column: initial; grid-row: 3; }
  body.nicht-am-tisch #game-view:has(> .rk-spalte) > .game-statusbar  { grid-column: initial; grid-row: 4; }
  body.nicht-am-tisch #game-view:has(> .rk-spalte) > .card            { grid-column: initial; grid-row: 5; }
  /* ⚠️ Und das Feld steht hier wieder oben in der Reihe, nicht unten: in
     einer REIHE ist `margin-top: auto` ohne Sinn, und der Verlauf steht
     darueber mit fester Hoehe. */
  body.nicht-am-tisch .rk-spalte > .rk-leiste {
    flex-direction: row; align-items: center; margin-top: 0;
  }
  /* ⚠️ Und das Feld bekommt seine Mindestbreite zurueck. Ohne diese Zeile
     war es bei 390 px 24 px breit - gemessen, nicht vermutet. In der schmalen
     Spalte darueber ist `min-width: 0` richtig (dort steht es allein in einer
     Spalte), hier nicht: `flex-wrap: wrap` schiebt es mit 12 em in eine eigene
     Zeile, statt es zwischen zwei Knoepfen zu zerdruecken. */
  body.nicht-am-tisch .rk-spalte > .rk-leiste .rk-eingabe-feld { min-width: 12em; }
  body.nicht-am-tisch .rk-spalte > .rk-leiste .rk-name { max-width: 9em; }
  body.nicht-am-tisch .rk-spalte > .rk-feed { height: 4.2em; flex: 0 0 auto; margin-top: 6px; }

}

/* ══ ≤ 600 px – Telefon ════════════════════════════════════════════════════ */
@media (max-width: 600px) {
  /* Die Kommentar-Eingabe untereinander: Feld und Knopf nebeneinander
     lassen dem Text rund 200 px, und darin steht kein Kommentar.
     ⚠️ Hier und nicht neben `.echo-eingabe` - alle Breitenregeln stehen
     in diesem Abschnitt, und test_responsive.py besteht darauf. */
  .echo-eingabe { flex-direction: column; align-items: stretch; }
  /* ⚠️ Am Telefon KEIN `margin-left: auto`. Die Zeile bricht dort, und der
     Knopf stuende als einziges in seiner Zeile ganz rechts - weit weg von
     dem, worauf er sich bezieht. */
  .tl-weiter { margin-left: 0; }

  /* Die Verlaufszeile bricht um: Text oben, Zeitpunkt und Zahlen darunter.

     ⚠️ Gemessen bei 390 px am 2026-09-12: nebeneinander blieben dem Satz
     rund 190 px, und „Tor von Kaan zum 8:8" endete als „Tor von Kaan…" -
     abgeschnitten wurde also genau die Auskunft, um die es in dieser Runde
     geht. Die Zahlen rutschen nach unten; sie sind kurz und vertragen die
     zweite Reihe. */
  .vl-zeile { flex-wrap: wrap; row-gap: 2px; }
  .vl-zeile > .vl-tor { flex: 1 1 100%; }


  /* Die Kanalliste stapelt.

     ⚠️ Hier stand bis zum 2026-09-04 `.kanal-aktion .form-input { width: auto;
     flex: 1; }` mit der Begruendung: „Das Adressfeld behaelt seine Aufgabe
     (lesen und kopieren) nur mit Breite - auf 60 px zusammengedrueckt zeigt es
     drei Zeichen vom Link." Das Feld ist jetzt bei JEDER Breite unsichtbar
     (Wunsch des Betreibers, die Begruendung steht an der Grundregel), also
     gibt es auch nichts mehr breit zu machen. Die Ausnahme faellt mit. */
  .kanal-zeile  { flex-direction: column; align-items: stretch; gap: 10px; }

  /* ── Rahmen ──────────────────────────────────────────────────────────────
     overflow-x zurück auf den Standard: abgeschnittener Inhalt wäre hier
     unerreichbar. Ein sichtbarer Seitwärts-Scroll ist die ehrlichere Anzeige
     und gleichzeitig der Ausweg. Nebeneffekt: <body> ist dann kein
     Scroll-Container mehr, wodurch position:sticky wieder verlässlich am
     Viewport hängt (.page-header.sticky-header).                            */
  body { overflow-x: visible; }

  /* Kopf eines beendeten Spiels: die Bühne klappt untereinander. Der Stand
     kommt dabei ZWISCHEN die beiden Seiten (order), damit die Leserichtung
     „links – Stand – rechts" auch senkrecht erhalten bleibt. Die Avatare
     werden von `.team-players-big` schon kleiner gesetzt. */
  .fg-buehne { flex-wrap: wrap; }
  .fg-buehne .fg-seite { flex-basis: 100%; }
  /* ⚠️ `.fg-mitte` und NICHT mehr `.fg-stand`: seit dem 2026-09-14 liegt
     der Stand in einem Mittelkasten und ist kein Flex-Kind der Buehne mehr.
     Die Regel haette hier stumm nichts mehr getroffen, und der Stand stuende
     am Telefon wieder ueber beiden Spielern statt zwischen ihnen. */
  .fg-buehne .fg-mitte { flex-basis: 100%; order: 1; }
  .fg-buehne .fg-seite:last-child { order: 2; }

  /* Die Anteilsspalte ist hier 44 px breit - dort passt nur das Zeichen.
     Die Zahl selbst traegt keines mehr, deshalb die Kurzform der Ueberschrift
     statt sie weglassen. Basis dazu: .hdr-kurz im Standings-Abschnitt. */
  .hdr-lang { display: none; }
  .hdr-kurz { display: inline; }

  /* Dasselbe fuer die Mini-Rangliste der Startseite: ihr Raster steht inline
     (im Template) und kann keine Medienregel tragen - die Spurbreite kommt
     deshalb aus dieser Variablen. Ohne das nimmt die Anteilsspalte am Telefon
     dem Spielernamen 30 Pixel weg, fuer eine Ueberschrift, die dort nur noch
     ein Zeichen ist. */
  .dashboard-grid { --anteil-spur: 44px; }

  /* ⚠️ Einzeln und NICHT `padding: 0 8px`: die Kurzform setzte den oberen
     Abstand zur Notch zurueck - genau am Telefon, dem einzigen Geraet mit
     einer (2026-09-30). */
  #navbar { padding-left: max(8px, env(safe-area-inset-left, 0px));
            padding-right: max(8px, env(safe-area-inset-right, 0px)); gap: 4px; }
  .nav-links { display: none; }              /* Ersatz: #bottom-nav */
  /* Am Telefon der Seitentitel statt der Uhr (2026-09-30, Betreiber: „Gerne
     mit Seitentitel. so wie in dem Entwurf."). Bis dahin stand hier die
     Uhrzeit allein. */
  .nav-titel { display: block; }
  .nav-clock { display: none; }
  #bottom-nav { display: flex; min-height: var(--bn-h); }

  /* Angedockte Chatfenster am Telefon (2026-09-04).

     Drei Aenderungen, und jede hat einen Grund am Geraet:

       * UEBER der unteren Leiste - sonst liegt das Eingabefeld hinter den
         sechs Menuekacheln, und man tippt auf "Statistik" statt in den Chat.
       * volle Breite - 20 em sind bei 390 px fast alles, und ein Fenster, das
         fast alles ist, soll auch die Kante nutzen.
       * NUR das juengste. Zwei 20-em-Fenster passen bei 390 px nicht
         nebeneinander; sie wuerden sich ueberlappen oder aus dem Bild laufen.
         `:last-child` ist dabei das zuletzt Angehaengte, also das juengste -
         dieselbe Reihenfolge wie am Rechner, nur ohne die Nachbarn. Die
         anderen bleiben im DOM und im localStorage: am Rechner stehen sie
         wieder da. */
  #chat-dock {
    left: 12px; right: 12px;
    bottom: calc(var(--bn-h) + env(safe-area-inset-bottom, 0px));
  }
  #chat-dock > .cd-fenster:not(:last-child) { display: none; }
  .cd-fenster { width: 100%; max-width: none; }
  .cd-verlauf { height: 12em; }
  /* ⚠️ KEIN GRIFF unter 601 px. Das Fenster nimmt hier die ganze Breite
     (`width: 100%`) - es in der Breite zu verkleinern ergibt keinen Sinn, und
     der Griff laege ueber dem Titel, der am Telefon ohnehin am knappsten ist.
     Die gemerkte Groesse bleibt im `localStorage` stehen und gilt am
     Schreibtisch wieder. */
  .cd-griff { display: none; }

  #content {
    padding: 16px 12px;
    /* sonst verdeckt die Leiste unten das letzte Element */
    padding-bottom: calc(var(--bn-h) + 16px + env(safe-area-inset-bottom, 0px));
  }

  /* ⚠️ UND DIE UNTERE LEISTE ABZIEHEN, wo eine Hoehe gegen das FENSTER
     rechnet (2026-09-14).

     Gemessen bei 390 × 800 px: das Nachrichtenraster lief von 80 bis 768, die
     Leiste beginnt bei 720 - die Eingabezeile samt Sendeknopf lag **39 px
     dahinter**. Man konnte die Nachricht schreiben und musste die Seite
     rollen, um sie abzuschicken.

     Die Grundregel zieht `--nav-h` ab, also die Leiste OBEN. Die untere gibt
     es nur hier, und deshalb steht der Abzug auch nur hier - im 840er-Block
     wäre er 80 px geschenkter Platz, denn dort ist die Leiste ausgeblendet.

     ⚠️ Das `padding-bottom` darüber hilft hier NICHT. Es verlängert das
     Dokument, damit man das letzte Element hochrollen kann; ein Element mit
     fester Höhe misst sich aber am Fenster und nicht an seinem Kasten. Zwei
     verschiedene Mechanismen für dasselbe Ziel, und man braucht beide.

     ⚠️ Geteilt wird durch den Zoom, weil die Grundregel es tut. Eine Höhe,
     die gegen den Bildschirmzoom rechnet, und eine daneben, die es nicht tut,
     ergibt immer denselben Fehler (siehe den Kanarienvogel in
     tests/test_responsive.py). */
  .nr-raster {
    height: calc(100dvh / var(--ui-zoom, 1) - var(--nav-h) - var(--bn-h)
                 - env(safe-area-inset-bottom, 0px) - 48px);
  }

  /* Kopfzeile der Seite – die negativen Margins spiegeln das #content-Padding */
  .page-header.sticky-header { flex-wrap: nowrap; padding: 8px 12px; margin: -16px -12px 16px; }
  .sticky-header .page-title {
    font-size: var(--md-sys-typescale-title-large-size);
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* Die Beschriftung war hier verborgen, weil Titel UND Knopf nicht in eine
     Zeile von 360 px passten. Seit dem 2026-09-03 gibt es den Titel nicht mehr
     (→ game.js), der Platz ist da, und aus dem Icon-Knopf wird wieder ein
     lesbarer Knopf. Genau das ist der Wunsch "breiter", nur am kleinsten
     Geraet - dort nimmt der Knopf die ganze Zeile.

     ⚠️ `min-width: 0` muss die 320 px aus dem Grundabschnitt zuruecknehmen:
     bei 320 px Bildbreite bleiben nach dem Kopfpolster 296 px, und die
     Mindestbreite haette die Zeile gesprengt. */
  .page-header.aktion-mitte > .page-actions { width: 100%; }
  /* ⚠️ Am Telefon NICHT die Polsterung der Startseite. Dort steht der Knopf im
     Fluss, hier in einer KLEBENDEN Kopfzeile: 30 px Polster und 1,5 rem ergeben
     rund 116 px, die bei 780 px Bildhoehe dauerhaft oben stehen bleiben. Die
     Breite bleibt voll, nur die Hoehe kommt auf das Mass der Zeile zurueck -
     "genauso gross" war fuer den Bildschirm bestellt, an dem man spielt. */
  .page-header.aktion-mitte .big-start-btn {
    min-width: 0; flex: 1; padding: 14px 18px; font-size: 1.05rem;
  }

  /* ── Raster: alles einspaltig ──────────────────────────────────────────── */
  .grid-2, .grid-3, .grid-2-wide { grid-template-columns: minmax(0, 1fr); }
  .sys-kerne { grid-template-columns: minmax(0, 1fr); }
  /* Angstgegner/Opfer: Bilanz unter den Namen. Bei 320 px blieben für
     „9 Niederlagen aus 29 Spielen" neben dem Namen sonst keine 90 px, und der
     Name verschwand hinter „…". Untereinander ist beides lesbar. */
  .duell-zeile { grid-template-columns: minmax(0, 1fr); }

  /* .grid-4 bleibt ZWEIspaltig, anders als seine Nachbarn.
     Grund ist die Karte „Gesamt" der Spielerstatistik: darin stehen vier
     Zahlen mit je einem kurzen Wort darunter (Punkte, Siege, Niederl.,
     Unentsch.). Einspaltig wurden daraus vier volle Zeilen - die Karte war auf
     dem Handy hoch wie ein halber Bildschirm, und der Betreiber hat genau das
     gemeldet („alles steht untereinander und nicht nebeneinander"). Zwei mal
     zwei passt bei 320 px gemessen ohne Umbruch, weil die Zahl über
     .stat-kachel-wert mitschrumpft. */
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-kv { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
  /* vier Spalten à 2 fr passen nicht in 296 px – die Auswahl mit Dateinamen
     erzwingt ihre eigene Breite. Deshalb zwei Reihen: Bezeichnung + Abspielen,
     darunter Auswahl + Lautstärke. */
  .sound-event-row { grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }

  /* ── Benachrichtigungstabelle: das Wort weicht dem Symbol ───────────────
     Bei 390 px braucht der Beschreibungstext jeden Punkt. Die Kopfzeile sagt
     dann nur noch mit dem Symbol, welche Spalte welche ist - der Name steht
     weiter am Kaestchen selbst (aria-label), also verliert niemand etwas,
     der die Seite nicht sieht.                                             */
  /* ⚠️ 60 px Spalte UND 4 px Innenabstand gehoeren zusammen: der Schalter
     ist 52 px breit, mit den 8 px von oben waeren es 68 - die Zelle liefe
     um 8 px ueber, und der Kasten wischte, obwohl er passt. Gemessen. */
  .bt-tabelle th, .bt-tabelle td { padding: 12px 4px; }
  .bt-kanal { width: 60px; }
  .bt-kanal-wort { display: none; }

  /* ── Siegesserien: das Wort weicht dem Namen ────────────────────────────
     Siehe `.streak-label` im Grundabschnitt. */
  .streak-wort { display: none; }

  /* ── Die anpassbare Anordnung am Telefon ───────────────────────────────
     Die BREITE braucht hier gar keine Regel: die Ganzbreite steht als
     grid-column: 1 / -1 und wird in einem einspaltigen Raster von selbst
     die volle Breite (siehe dort). Nur die HOEHE muss weg - eine Kiste, die
     am Rechner 300 px hoch ist, schnitte am Telefon die Haelfte ihres
     Inhalts ab, weil dort alles untereinander steht.

     Und Breitenknopf und Griff verschwinden mit: sie stellten etwas ein,
     das hier nicht gilt. Die Pfeile bleiben - die Reihenfolge gilt sehr
     wohl, und am Telefon sind sie ohnehin der einzige Weg (HTML5-Ziehen
     gibt es dort nicht). */
  /* ⚠️ HEIGHT und nicht die Variable. Der erste Entwurf setzte hier
     --kh auf auto - wirkungslos: die Hoehe steht als INLINE-Stil an der
     Kiste (sie ist Benutzerdatum, kein Designwert), und ein Inline-Stil
     schlaegt jede Regel aus einem Stylesheet. Die Zusicherung dazu las
     die Zeichenkette und war gruen, waehrend am Telefon die halbe Liste
     abgeschnitten blieb. Gefunden hat es der Funktionstest.

     Diese Regel steht spaeter in der Datei als die mit var(--kh) und
     gewinnt deshalb ohne !important. */
  .anord-kiste { height: auto; }
  .anord-modus .anord-griff { display: none; }
  .anord-modus .anord-knopf:nth-child(3) { display: none; }

  /* ⚠️ Und der Avatar wird gedeckelt. `App.avatarHtml` schreibt seine Groesse
     als INLINE-Stil (`calc(26px * var(--avatar-scale))`), und auf dieser
     Anlage steht die Skala auf 2,6 - aus 26 px werden 68. In einer Zeile aus
     Platz, Zahl, Bild, Name und Beschriftung blieb dem Namen bei 390 px
     danach ein einziger Buchstabe: „J…", „M…", „K…".

     `!important` ist hier kein Holzhammer, sondern die einzige Moeglichkeit:
     ein Inline-Stil schlaegt jedes Stylesheet. Derselbe Weg, den
     `static/kiosk.html` fuer `.lv-p .av` geht, und aus demselben Grund.

     Gedeckelt wird NUR diese Liste. Wo der Avatar die Aussage ist (Vorschau,
     Spielaufbau, Spielerliste), bleibt die Wahl des Betreibers unangetastet -
     hier ist er Beiwerk neben dem Namen. */
  .streak-name img { width: 40px !important; height: 40px !important; }

  /* ⚠️ Und die zweite Medaille faellt weg. Die Zeile trug zwei: links den
     Platz (🥇🥈🥉 oder eine Zahl) und daneben noch ein 🏅 als Schmuck. Am
     Telefon kostete der Schmuck 27 px samt Abstand - und genau die fehlten
     dem Namen. Auf dem Rechner bleibt er stehen, dort ist Platz.

     Der engere Abstand kommt aus derselben Rechnung: fuenf Spalten mal 12 px
     sind 48 px, mal 8 sind es 32. */
  .serien-zeile { gap: 8px !important; }
  .serien-medaille { display: none; }

  /* ── Reiterleisten: enger, aber nicht anders ─────────────────────
     Das Wischen selbst steht seit dem 2026-09-10 in der Grundregel - es
     wurde auf jeder Breite gebraucht (die Einstellungsseite ist mit den
     Symbolen 1422 px breit). Hier bleibt nur, was am Telefon wirklich
     anders ist: weniger Innenabstand und eine Stufe kleinere Schrift,
     damit mehr Reiter gleichzeitig zu sehen sind.                       */
  .tab {
    padding: 12px 12px;
    font-size: var(--md-sys-typescale-label-large-size);
  }

  /* ── Segment-Gruppen: umbrechen statt abschneiden ───────────────────────
     Überzählige Segmente ragten aus der Gruppe heraus und wurden von deren
     `overflow: hidden` unsichtbar gemacht – die 150-%-Stufe der
     Oberflächengröße war deshalb nicht antippbar. Mit `flex-wrap` rutschen sie
     in eine zweite Reihe, statt überzulaufen; `overflow: hidden` bleibt also
     erhalten und schneidet weiterhin die abgerundeten Enden sauber zu.       */
  .seg-group {
    display: flex;
    flex-wrap: wrap;
    width: 100%;
    /* ⚠️ KLEINERER Radius, sobald umgebrochen werden darf (2026-09-11,
       bei der Mobilpruefung gesehen - keine Messung hat es gemeldet, das
       Bild schon).

       `corner-full` sind 9999 px. Bei EINER Reihe ergibt das die Pille, die
       M3 fuer die Segmentgruppe vorsieht. Bei ZWEI Reihen ergibt es eine
       Ellipse: gemessen bei 390 px war die Gruppe 326 x 114 px, und die
       Rundung frass sich in „Geraetevorgabe" oben links und „Sehr gross"
       unten hinein - zusammen mit `overflow: hidden` sah es aus, als seien
       die Beschriftungen abgeschnitten.

       `corner-large` ist die Form, die M3 fuer einen mehrzeiligen Behaelter
       vorsieht. Sie steht hier und nicht in der Grundregel: oberhalb von
       600 px bricht die Gruppe nicht um, und dort ist die Pille richtig. */
    border-radius: var(--md-sys-shape-corner-large);
  }
  .seg-btn { flex: 1 1 auto; padding: 10px 12px; }

  /* ── Tabellen: Kernspalten stehen, der Rest ist wischbar ────────────────
     `width: auto` statt 100 %: nur so darf die Tabelle breiter werden als ihr
     Rahmen und .table-wrap überhaupt scrollen. `nowrap` hält die Zellen
     schmal und lesbar, statt sie in hohe Stapel zu brechen – bei 8 px Polster
     stehen damit die ersten drei Spalten ohne Wischen im Bild.
     border-collapse wird aufgelöst, weil zusammengefasste Rahmen in einem
     Scrollbereich verschwinden; die Trennlinie kommt als inset-Schatten.     */
  .table-wrap { -webkit-overflow-scrolling: touch; }
  table { width: auto; min-width: 100%; border-collapse: separate; border-spacing: 0; }
  th, td {
    padding: 8px 10px;
    white-space: nowrap;
    border-bottom: none;
    box-shadow: inset 0 -1px 0 var(--md-sys-color-outline-variant);
  }
  /* ⚠️ `th` mit, aus demselben Grund wie bei `border-bottom` weiter oben:
     unter 600 px ersetzt der Schatten die Linie, und die Ausnahme fuer die
     letzte Zeile muss beide Zellenarten nennen. Sonst bricht die unterste
     Linie der Vorschau am Telefon genauso ab wie am Rechner. */
  tr:last-child td, tr:last-child th { box-shadow: none; }

  /* ── Ranglisten ────────────────────────────────────────────────────────
     Alle sieben Spalten passen in 326 px (390 − #content − .card), deshalb
     hier kein Wischen: Rang 22 + Wert 40 + 4×26 + 6×4 Abstand = 190 px fest,
     der Name bekommt den Rest. Kopf UND Zeile bekommen dasselbe Raster –
     vorher hatte die Zeile sechs Spalten für sieben Angaben (die letzte
     rutschte in eine zweite Rasterreihe) und der Kopf behielt seine sieben,
     wodurch die Überschriften über den falschen Werten standen.             */
  /* Vier Spuren statt acht. Mit dem ANTEIL waeren es acht in 326 px, je
     Zahlenspalte rund 22 px - „13,2 %" passt dort nicht. Entscheidung des
     Betreibers: der Anteil bleibt, S/N/U/Spiele fallen weg. Die drei stehen in
     der Statistikansicht ausfuehrlicher, und Platz, Anteil und Wert sind, was
     eine Rangliste ausmacht. Rang 22 + Anteil 44 + Wert 40 + 3x4 Abstand =
     118 px fest, der Name bekommt den Rest. */
  .standing-row, .standing-head {
    grid-template-columns: 22px minmax(0, 1fr) 44px 40px;
    gap: 4px;
    padding: 8px 4px;
    font-size: var(--md-sys-typescale-label-medium-size);
  }
  /* Die vier Zaehlspalten verschwinden, statt ihre Zellen umzubrechen: ohne
     `display: none` rutschten sie in eine zweite Rasterreihe. */
  .standing-stat { display: none; }
  /* ⚠️ Am Telefon nur die letzten DREI Spiele - fuenf braeuchten 79 px.
     Ausgeblendet werden die AELTESTEN (`-n+2`), das juengste Spiel steht
     immer. Reines CSS, kein Zweig im JavaScript: dasselbe Markup in beiden
     Lagen - wie `.ek-kacheln.ek-links .ek-form-wer`.

     ⚠️ **Gemessen am 2026-09-15, und das Ergebnis war ueberraschend:** die
     Spaltenbreite bestimmt hier nicht die Kette, sondern die UEBERSCHRIFT.
     "SPIELE" misst 40 px, drei Pillen 47, zwei nur 31 - der Sprung von drei
     auf zwei Pillen brachte dem Spielernamen ganze 2 px (48 -> 50). Deshalb
     bleibt es bei drei: mehr Aussage fuer fast nichts.
     Der Preis steht trotzdem fest: der Name hat bei 390 px 48 px statt 55
     wie vorher, und "Johannes" braucht 56. Er war also schon vorher knapp
     gekuerzt und ist es jetzt etwas mehr. Wer die Namen zurueck will, muss
     die Anteilsspalte (44 px) am Telefon opfern - das waere eine eigene
     Entscheidung. */
  .st-kette .st-res:nth-child(-n+2) { display: none; }
  /* ⚠️ Und genau HIER die Ausnahme, nicht weiter oben: `css.index(".st-res {")`
     schneidet an der ERSTEN Fundstelle, und eine Uebersteuerung gehoert hinter
     die Regel, die sie uebersteuert (CLAUDE.md §4 ④).

     In der Spielerkarte gilt der Grund oben nicht. Dort steht die Kette nicht
     in einer Tabellenspalte unter einer Ueberschrift, sondern frei in der
     Kiste „Gesamt": gemessen am 2026-09-22 bei 390 px ist die Zeile 293 px
     breit, die ganze Kette braucht 79. Bestellt waren „die 'Letzten 5'" -
     drei davon waeren die falsche Antwort auf ein Platzproblem, das es an
     dieser Stelle nicht gibt. */
  .ps-kette .st-kette .st-res:nth-child(-n+2) { display: inline-flex; }
  /* Und die Speedshot-Zellen brauchen ihre Platzangaben hier nicht - im
     Vier-Spuren-Raster stehen sie ohnehin an Position 3 und 4. */
  .sp-row .sp-wert, .sp-row .sp-zahl { grid-column: auto; }
  /* nowrap + flex-shrink:0 – der Zusatz sprengt sonst die Namensspalte */
  .rating-note { display: none; }

  /* ── Das Aktionsmenue (2026-09-04) ─────────────────────────────────────
     Hier wird der Knopf ueberhaupt erst sichtbar; der Grundabschnitt haelt ihn
     auf `display: none` (dort steht die Begruendung). Die Leiste dahinter
     verschwindet und kommt als Tafel zurueck, sobald der Knopf offen steht.

     ⚠️ Der Geschwisterselektor `+` und kein Rahmen um beide: die Leiste behaelt
     damit ihr Klassenattribut unveraendert. Drei Zusicherungen greifen die
     woertliche Zeichenkette `<div class="game-controls">` bzw.
     `<div class="nb-kopf">` (tests/test_pause_and_fit.py,
     tests/test_benachrichtigungen.py) und brechen mit ValueError ab, wenn dort
     etwas dazukommt - nicht mit einer Meldung, sondern mit einem Absturz. */
  /* ⚠️ Zwei Klassen, weil der Grundzustand oben auch zwei traegt
     (`.btn.ak-knopf`) - eine einzelne kaeme dagegen nicht an. */
  .btn.ak-knopf { display: inline-flex; }

  /* ⚠️ Die Leisten stehen mit ihren EIGENEN Klassennamen im Selektor, und es
     gibt bewusst keine gemeinsame Mechanikklasse. Genau daran haengt, dass ihr
     Klassenattribut byte-gleich bleibt: `tests/test_pause_and_fit.py` sucht
     `<div class="game-controls">` und `tests/test_benachrichtigungen.py`
     sucht `<div class="nb-kopf">` - woertlich, mit `str.index`. Ein
     zusaetzliches Klassenwort in diesen Attributen haette beide mit
     ValueError abbrechen lassen, nicht mit einer Meldung.

     Preis: die naechste Leiste braucht hier eine Zeile mit. Dafuer sagt der
     Selektor ausdruecklich, welche Leisten so gebaut sind. Und er greift nur,
     wo wirklich ein Knopf davorsteht - eine `.game-controls` ohne
     Aktionsmenue bleibt unberuehrt. */
  .ak-knopf + .game-controls,
  .ak-knopf + .nb-kopf { display: none; }
  .ak-knopf[aria-expanded="true"] + .game-controls,
  .ak-knopf[aria-expanded="true"] + .nb-kopf {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }

  /* Die Tafel der SPIELANSICHT schwebt unter der Kopfzeile.

     ⚠️ Und die der Benachrichtigungen tut das ausdruecklich NICHT - sie bleibt
     im Fluss und schiebt die Liste nach unten. Der Grund ist gemessen:
     `#modal-box` traegt `overflow: hidden`, eine schwebende Tafel wuerde dort
     abgeschnitten. Hier dagegen wuerden fuenf Knoepfe (5x56 + 4x8 + 12 =
     324 px) im Fluss den Spielstand aus dem Bild schieben - und der ist das,
     was man ansieht. Ein Unterschied in der Deklaration, EINE Mechanik.

     Bezug ist `.game-header` (`position: relative` im Grundabschnitt). */
  .ak-knopf[aria-expanded="true"] + .game-controls {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    min-width: 220px;
    z-index: 90;
    padding: 6px;
    background: var(--md-sys-color-surface-container-high);
    border: 1px solid var(--md-sys-color-outline-variant);
    border-radius: var(--md-sys-shape-corner-medium);
    box-shadow: var(--md-sys-elevation-level2);
  }

  /* Ein Knopf braucht kein Menue.

     Aus dem Netz OHNE Adminrecht bleibt in der Spielleiste nur "Zurueck":
     Pause und Seiten tauschen verschwinden ganz (`data-requires-table="hide"`),
     Beenden und Abbrechen nur ohne `kann-admin` - beide Regeln stehen weiter
     oben in dieser Datei.

     ⚠️ `:has(> .btn:only-child)` kann das NICHT loesen, und das ist der Punkt:
     alle fuenf Knoepfe stehen IMMER im Markup, nur ihr `display` unterscheidet
     sich. CSS hat kein Praedikat "sichtbares Kind" - `:only-child` und
     `:nth-child` zaehlen Knoten und liefern hier immer fuenf. Die Sichtbarkeit
     ist aber im Stylesheet schon entschieden, also wird dieselbe Bedingung
     gespiegelt.

     ⚠️ WORTGLEICH mit der Bedingung an "Beenden"/"Abbrechen" weiter oben. Wer
     die eine aendert, muss die andere aendern; eine Zusicherung haelt die zwei
     Zeichenketten zusammen.

     ⚠️ SEIT DEM 2026-09-07 SIND ES NULL KNOEPFE UND NICHT EINER. "Zurueck"
     war der letzte, der aus dem Netz ohne Adminrecht sichtbar blieb, und er
     ist auf Wunsch des Betreibers entfallen (die Begruendung steht am Markup
     in components/game.js). Deshalb steht hier jetzt `display: none` und
     nicht mehr "flach statt Klapptafel": eine leere Leiste unter einem
     Burgerknopf waere schlechter als beides.

     ⚠️ Die Spezifitaet ist tragend:
     `body.nicht-am-tisch:not(.kann-admin) .ak-knopf + .game-controls` ist
     (0,4,0) und schlaegt `.ak-knopf[aria-expanded="true"] + .game-controls`
     mit (0,2,1). Noetig, weil die Koerperklassen erst mit
     /api/auth/context kommen - wer in diesem Fenster den Knopf tippt,
     hinterlaesst `aria-expanded="true"` und haette danach eine schwebende
     Einzeltafel.

     ⚠️ Die Benachrichtigungen brauchen das nicht: ihre Leiste entsteht nur im
     Zweig `if (zeilen.length)`, also ist `ungelesene || gelesene` immer wahr -
     es sind dort immer zwei oder drei Knoepfe, niemals einer. */
  body.nicht-am-tisch:not(.kann-admin) .btn.ak-knopf { display: none; }
  body.nicht-am-tisch:not(.kann-admin) .ak-knopf + .game-controls { display: none; }
  /* … und dann traegt der Livestream-Knopf die automatische Aussenkante,
     weil er das einzige Kind der rechten Gruppe ist (Begruendung oben an
     `.game-header > .game-controls`). */
  body.nicht-am-tisch:not(.kann-admin) .gv-livestream { margin-left: auto; }

  /* ── Laufendes Spiel ───────────────────────────────────────────────────
     ⚠️ `#game-view { height: auto }` stand hier bis zum 2026-09-10 und ist
     ENTFALLEN, weil es jetzt in der Grundregel steht: das Torprotokoll soll
     auf jeder Breite weiterlaufen (Wunsch des Betreibers). Die Ausnahme des
     Telefons ist damit zur Regel geworden - eine Fassung statt dreier.

     Beide Teams bleiben nebeneinander: beide Punktestände gleichzeitig zu
     sehen ist der Sinn dieser Ansicht.                                      */
  .game-header { gap: 8px; }
  .game-timer { font-size: 1.6rem; min-width: 70px; }
  .game-score-area { gap: 8px; }
  .team-panel { padding: 10px 6px; }
  .team-score { font-size: 3.5rem; }
  /* 2 × 64 + 8 = 136 px bei ~144 px Innenraum je Panel.
     ⚠️ Seit dem 2026-09-13 stehen Breite und Höhe als Variablen an der
     Spielstandsfläche: der Eigentor-Knopf (halb so hoch) und der
     Like-Knopf (genauso gross) rechnen mit derselben Zahl. Die
     Schriftgrösse bleibt an der Taste - sie gilt nur für „+“ und „−“.
     Ein zweites `width: 64px` am Knopf wäre ein zweiter Ort für
     dieselbe Zahl. */
  .game-score-area { --tor-w: 64px; --tor-h: 64px; }
  /* ⚠️ EINE Zeile mit dem staerkeren Selektor. `:has()` zaehlt wie eine Klasse
     und schlaegt die Zeile darueber sonst - die Tortasten blieben am Telefon
     bei 76 px statt 64. Die Kacheln selbst brauchen hier nichts: bei 390 px
     ergibt `flex: 1 1 140px` von selbst zwei je Reihe. */
  .game-score-area:has(> .ek-kacheln.ek-links) { --tor-w: 64px; --tor-h: 64px; }
  .goal-btn { font-size: 2rem; }
  /* ⚠️ AM TELEFON BLEIBT DER ZAEHLER UEBER DEN KNOEPFEN, und das ist ein
     gemessenes Zugestaendnis, keine Bequemlichkeit.

     Der Wunsch war eine Zeile: Minus, Zaehler, Plus. Bei 390 px hat eine
     Spielerkiste innen rund 194 px, und zwei Knoepfe von je 64 px lassen
     davon 58 px fuer den Zaehler. Ein zweistelliger Stand ("10") braucht bei
     3,2 rem genau so viel - gemessen sprengte die linke Kiste damit ihre
     Rasterspalte (206 px gegen 177 px rechts), und die SEITE lief quer. Bei
     320 px passen die zwei Knoepfe allein schon nicht mehr neben einen
     Zaehler.

     Die Knoepfe schmaler zu machen war die Alternative und ist verworfen: die
     64 px sind eine gemessene Treffflaeche (tests/test_responsive.py haelt sie
     fest), und ein Ziel, das man am Tisch unter Zeitdruck trifft, soll nicht
     kleiner werden, weil jemand das Fenster verkleinert.

     Also ein Raster statt einer Zeile: der Zaehler nimmt beide Spalten, die
     Knoepfe stehen darunter - Minus an der linken, Plus an der rechten Kante.
     Die BUENDIGKEIT bleibt damit erhalten, nur die Reihe ist eine andere.

     💡 Und es trifft ohnehin fast nie zu: die Knoepfe erscheinen NUR am
     Kickertisch. Wer am Telefon zusieht, sieht den Zaehler allein - und der
     ist in beiden Fassungen mittig. */
  .team-punkte {
    display: grid;
    /* ⚠️ BENANNTE Felder und nicht nur `grid-column`. Die Reihenfolge im
       Markup ist Minus, Zaehler, Plus - die automatische Platzierung setzt
       damit Minus in Zeile 1 Spalte 1, schiebt den Zaehler (der beide Spalten
       will) in Zeile 2 und das Plus in Zeile 3 Spalte 1. Gemessen: Plus lag
       bei [38, 102] und damit UEBER dem Minus bei [18, 82]. Mit Feldnamen
       steht jedes Kind da, wo es hingehoert, unabhaengig von der
       Markup-Reihenfolge. */
    grid-template-columns: 1fr 1fr;
    grid-template-areas: "score score"
                         "minus plus";
    gap: 8px 0;
  }
  .team-punkte .team-mitte { grid-area: score; }
  .team-punkte .goal-btn-minus { grid-area: minus; justify-self: start; }
  .team-punkte .goal-btn-plus-t1,
  .team-punkte .goal-btn-plus-t2 { grid-area: plus; justify-self: end; }
  /* ⚠️ Der Knopf steht IN der Mittelspalte und braucht keine eigene
     Rasterflaeche - die dritte Zeile des ersten Entwurfs ist damit
     entfallen. */
  .team-score { font-size: 3.2rem; }
  .team-players-big { --gt-av-size: clamp(48px, 14vw, 72px); }

  /* ── Dialoge: eine Stelle für alle zwölf ───────────────────────────────── */
  /* Dialoge als Blatt von unten (2026-09-30, App-Gefuehl): volle Breite,
     oben gerundet, unten ueber dem Home-Indikator. `max-width: none`, weil
     die 520 px der Grundregel sonst zwischen 521 und 600 px ein schmaleres
     Blatt ergaeben. Weiter genau ZWEI Zoom-Divisionen in diesem Block. */
  #modal-overlay { align-items: flex-end; }
  #modal-box {
    padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    width: calc(100vw / var(--ui-zoom, 1));
    max-width: none;
    max-height: calc(92dvh / var(--ui-zoom, 1));
    border-radius: var(--md-sys-shape-corner-extra-large) var(--md-sys-shape-corner-extra-large) 0 0;
    animation: sheet-in var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-emphasized-decelerate);
  }
  .modal-close { top: 6px; right: 6px; }

  /* ── Über der Leiste unten bleiben ─────────────────────────────────────── */
  #toast-container {
    left: 12px; right: 12px;
    bottom: calc(var(--bn-h) + 12px + env(safe-area-inset-bottom, 0px));
  }
  .toast { max-width: none; min-width: 0; }
  .readonly-banner {
    left: 12px; right: 12px; max-width: none;
    bottom: calc(var(--bn-h) + 12px + env(safe-area-inset-bottom, 0px));
  }

  /* ── Zeilen, die sonst gedrückt werden ─────────────────────────────────── */
  .list-item,
  .page-actions { flex-wrap: wrap; }
  /* ⚠️ `.game-controls` stand hier bis zum 2026-09-04 mit. Sie ist seit
     demselben Tag am Telefon eine SPALTE im Aktionsmenue (weiter oben), und
     `flex-wrap` an einer Spalte, die ohnehin jedes Kind auf eigene Zeile
     setzt, ist eine Erklaerung fuer nichts - dasselbe Argument wie bei
     `.streak-row` darunter. Der Umbruch war die Notloesung, die es jetzt nicht
     mehr braucht: bei 360 px standen die fuenf Knoepfe in DREI Reihen ueber
     dem Spielstand.
     ⚠️ `.page-actions` bleibt stehen - die Leisten dort tragen 0 bis 1 Knopf
     und sind kein Menue. */
  /* ⚠️ `.streak-row` stand hier bis zum 2026-09-03 mit. Sie ist seit demselben
     Tag ein RASTER, und `flex-wrap` an einem Raster ist wirkungslos - die
     Angabe waere eine Erklaerung fuer nichts. Schmal umbrechen muss sie auch
     nicht: drei Spalten (32px, 1fr, auto) passen bei 360 px. */
  /* .list-item-actions bricht ausdrücklich NICHT um: die Klasse sitzt in einer
     Tabellenzelle, und die Knöpfe würden dort untereinander rutschen und jede
     Zeile dreifach hoch machen. Sie bleibt in einer Reihe und macht die Tabelle
     stattdessen wischbar – so war die Entscheidung. In einer .list-item-Zeile
     schadet das nichts, weil die Zeile selbst umbricht.
     (Hier stand bis zum 2026-09-03 auch `.admin-actions` – eine Klasse, die
     kein Element im Projekt trägt.) */
  .update-remote-cmd { white-space: pre-wrap; word-break: break-all; }
  .banner-content { padding: 24px 16px; }
  /* Fuenf Spalten passen nicht in 390 px: Bild und Name oben, Differenz, Wert
     und Platzwechsel darunter.

     ⚠️ Hier stand bis zum 2026-09-03 zusaetzlich `.br-tag { display: none; }`
     mit der Begruendung „die Reihenfolge und die Farben tragen dieselbe
     Information". Genau diese Begruendung hat den Betreiber ueberzeugt, das
     Kennzeichen bei JEDER Breite weglassen zu koennen - es gibt es nicht mehr,
     also auch nichts mehr zu verbergen. */
  .br-row {
    grid-template-columns: 1.7em minmax(0, 1fr) auto;
    gap: 2px 8px;
    padding-bottom: 4px;
  }
  .br-delta { grid-column: 2; text-align: left; }
  .br-val   { grid-column: 3; }
  .br-rank  { grid-column: 2 / -1; justify-content: flex-start; min-width: 0; }
}

/* ══ Flaches Fenster – Telefon im Querformat (932 × 430) ═══════════════════
   Dort ist nicht die Breite das Problem, sondern die Höhe. Bewusst nur für die
   Spielansicht, kein allgemeiner Umbruch (Muster wie max-height: 900px oben). */
@media (max-height: 500px) {
  /* ⚠️ `#game-view { height: auto }` stand auch hier und ist am 2026-09-10
     entfallen - dasselbe wie im 600er-Block: es gilt jetzt immer. */
  .team-players-big { --gt-av-size: clamp(44px, 9dvh, 72px); }
  .team-score { font-size: 2.6rem; }
  /* Nur die Höhe fällt - siehe die Variablen an `.game-score-area`. */
  .game-score-area { --tor-w: 64px; --tor-h: 52px; }
  .goal-btn { font-size: 1.7rem; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Benutzerverwaltung – Anmeldung, Profil, Benutzer und Gruppen
   Stufe 1, siehe docs/KONZEPT_BENUTZER.md

   Absichtlich knapp: alles, was das Projekt schon hat, wird weiterverwendet
   (.card, .btn, .form-group.field, .chip, .badge, .table-wrap, .row-actions,
   .text-dim, .page-header). Hier stehen nur die Bausteine, die es vorher
   nirgends gab. Farben und Radien ausschliesslich ueber M3-Tokens, damit die
   eigenen Themes aus der Datenbank ebenfalls greifen.
   ═════════════════════════════════════════════════════════════════════════ */

/* Fehlermeldung im Formular. Bewusst neben dem Knopf und nicht als Toast: sie
   gehoert zur Eingabe und soll stehen bleiben, bis sie behoben ist. */
.form-error {
  margin: 8px 0;
  padding: 10px 12px;
  border-radius: var(--md-sys-shape-corner-small, 8px);
  background: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container);
  font-size: 0.9rem;
}

/* Knopfreihe am Ende eines Dialogs. */
.form-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 16px;
  flex-wrap: wrap;
}

/* Ankreuzzeile: das ganze Zeilenfeld ist klickbar, nicht nur das Kaestchen –
   auf einem Touchscreen ist ein 16-px-Kaestchen kein brauchbares Ziel. */
.check-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 4px;
  min-height: var(--min-touch, 44px);
  cursor: pointer;
  user-select: none;
}
.check-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  accent-color: var(--md-sys-color-primary);
}

/* Rechte-Liste als Chips. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 0;
}

/* Eine Gruppe in der Uebersicht. */
/* Eine Zeile in der Gruppenliste (linke Spalte der Meister-Detail-Ansicht).

   Auswahlzustand im Farbrezept von `.chip.selected` - das ist im Projekt der
   durchgehende Weg, Gewaehltes zu zeigen (seg-btn und player-card
   machen es genauso). Ein eigenes Rezept waere ein fuenftes. */
.group-item {
  padding: 10px 12px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium);
  margin-bottom: 8px;
  cursor: pointer;
  min-height: var(--min-touch, 44px);
  transition: background var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
}
.group-item:hover { background: var(--md-sys-color-surface-container-highest); }
.group-item.selected {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  border-color: transparent;
}
.group-item.selected .text-dim { color: var(--md-sys-color-on-secondary-container); }

/* Gesperrtes Konto: gedaempft, aber lesbar. Nicht ausblenden – man muss es
   ja wiederfinden, um es zu entsperren. */
.row-inactive { opacity: 0.55; }
.row-inactive td { text-decoration: none; }

/* Aktionsspalte der Benutzertabelle. Ohne feste Breite und nowrap brach die
   Reihe aus Stift/Schluessel/Muelleimer um, und die Symbole standen versetzt
   unter den falschen Spalten. */
.table-wrap td.user-actions {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}
.table-wrap td.user-actions .row-actions {
  display: inline-flex;
  gap: 4px;
  justify-content: flex-end;
}

/* Aktionsspalte der Spielerliste - rechtsbuendig, Kopf und Zelle gemeinsam.

   Warum eine eigene Klasse und nicht `.list-item-actions` selbst: dieselbe
   Klasse traegt in components/challenges.js eine Flex-Zeile ganz ohne Tabelle.
   Dort haette „rechtsbuendig" die Knoepfe vom Text weggerissen. Die Ausrichtung
   ist eine Eigenschaft der SPALTE, also gehoert sie an die Spalte.

   Der Kopf „AKTIONEN" wandert mit: eine linksbuendige Ueberschrift ueber
   rechtsbuendigen Knoepfen waere halb umgesetzt. Vorbild ist `td.user-actions`
   darueber, samt `width: 1%` und `nowrap` - so zieht die Spalte nur so viel
   Breite, wie die Knoepfe brauchen, und die Reihe bricht nicht um. */
.table-wrap th.spalte-aktionen,
.table-wrap td.spalte-aktionen {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}
.table-wrap td.spalte-aktionen .list-item-actions { justify-content: flex-end; }

/* Zugänge (Stufen 2–5): Zustandsfarben für Token, Mail und SSO.
   Über die Fehler-/Erfolgsrollen der M3-Tokens, damit eigene Themes greifen. */
.ac-warn { color: var(--md-sys-color-error); }
.ac-ok   { color: var(--md-sys-color-primary); }
.ac-warn strong { color: var(--md-sys-color-error); }

/* Abstand zwischen aufeinanderfolgenden Karten.

   Bisher setzte jede Ansicht ihn einzeln als Inline-Style (21 Stellen
   `margin-bottom:16px`). Die Ansichten der Benutzerverwaltung taten das nicht –
   dort stiessen die Karten aneinander.

   Als `+`-Regel und nicht als `margin-bottom` auf `.card`: so entsteht KEIN
   Abstand nach der letzten Karte, und bei den bestehenden Ansichten kollabiert
   der neue `margin-top` mit deren `margin-bottom` zu 16 px (in `#content`
   herrscht normaler Blockfluss, kein Flex/Grid – dort würden Margins sich
   addieren). Die 21 Inline-Styles bleiben damit wirkungsgleich. */
.card + .card { margin-top: 16px; }
/* ⚠️ DER GROSSE SPEICHERN-KNOPF BRAUCHT DENSELBEN ABSTAND (2026-09-13).
   Der Betreiber: „In den Einstellungen hat der Kasten ,Hinweis im Browser'
   keinen Abstand zur Aktion ,Einstellungen speichern'.“

   Nicht die Karte ist schuld: `.card + .card` greift nur zwischen Karten, und
   `#btn-save-settings` ist keine. Zu ihm gab es in dieser Datei bis heute
   überhaupt keine Regel.

   Warum es trotzdem meistens gut aussah: der EIGENE Knopf der
   Benachrichtigungsseite bringt den Abstand inline mit (`#nb-save`). Genau
   dieser Knopf wird Administratoren entfernt, sobald der große gemeinsame da
   ist (components/account.js) - und der brachte nichts mit. Der Fehler traf
   also NUR Administratoren, und auf `#/settings` selbst fiel er weniger auf,
   weil die Reiter unterschiedlich enden.

   ⚠️ Benannte Regel und nicht inline am Knopf: derselbe Bausatz
   (`App.settings._seiteHtml`) bedient ZWEI Ansichten, und der Defekt steckte
   in beiden. Eine Stelle heilt beide.

   ⚠️ `display: flex` GEHÖRT DAZU, und das ist gemessen. `.btn` ist
   `inline-flex`, und ein Element auf Zeilenebene lässt seine Aussenabstände
   NICHT kollabieren. Mit `margin-top` allein stand der Knopf auf
   `#/settings` 35 px unter der letzten Karte statt 16: deren
   `margin-bottom: 16px` und die neuen 16 addierten sich, plus 3 px
   Zeilenkasten. Als Block kollabieren beide zu 16 - genau die Rechnung,
   die in der Begründung zu `.card + .card` darüber steht. Die Breite
   ändert sich nicht: der Knopf trägt ohnehin `w-full`.

   ⚠️ Kein doppelter Abstand: `#nb-save` ist ein anderes Element, und wo der
   große Knopf steht, ist der eigene weg. */
#btn-save-settings { display: flex; margin-top: 16px; }
/* Ein Raster ist im Blockfluss von `#content` ein Block wie eine Karte und
   braucht denselben Abstand. `.card + .card` deckte das nicht ab: dazwischen
   steht kein `.card`, sondern der Rastercontainer. In der Begegnungs-Vorschau
   (Karte -> Raster -> Karte) beruehrten sich die Bloecke deshalb auf den Pixel
   genau - gemessen: Karte endet bei 304, Raster beginnt bei 304. Mit runden
   Ecken und Rahmen sieht das aus wie eine Ueberlappung.

   Die Startseite war nicht betroffen: dort traegt der Startknopf davor einen
   `margin-bottom`, und die Karte danach einen inline gesetzten `margin-top`. */
.card + .dashboard-grid, .card + .grid-2, .card + .grid-3,
.card + .grid-4, .card + .grid-2-wide, .card + .stat-raster,
.dashboard-grid + .card, .grid-2 + .card, .grid-3 + .card,
.grid-4 + .card, .grid-2-wide + .card, .stat-raster + .card { margin-top: 16px; }
/* ... aber NICHT in einem Raster. Dort erledigt `gap` den Abstand, und der
   `margin-top` kommt oben drauf: die zweite Karte rutscht 16 px nach unten, und
   weil `align-items: stretch` die Reihenhoehe am Randbereich der Karte
   ausrichtet, wird die ERSTE 16 px hoeher. Beide enden auf derselben Linie und
   beginnen versetzt - im Browser gemessen (Seite 1: oben 304, hoch 524; Seite 2:
   oben 320, hoch 508, beide endeten bei 828).

   Der Kommentar oben nennt `#content` als Ort des Blockflusses und hat die
   Rasterfaelle deshalb nicht bedacht. Es traf jede Ansicht mit zwei Karten
   nebeneinander, auch die Startseite.

   Dieselbe Aufzaehlung wie bei `min-width: 0` weiter unten - ein `:is()` waere
   kuerzer, kommt in dieser Datei aber nirgends vor. */
.dashboard-grid > .card + .card,
.grid-2 > .card + .card,
.grid-3 > .card + .card,
.grid-4 > .card + .card,
.grid-2-wide > .card + .card,
.stat-raster > .card + .card { margin-top: 0; }

/* Und jede Karte nur so hoch wie ihr Inhalt.

   `align-items: stretch` ist die Vorgabe im Raster: beide Karten bekommen die
   Hoehe der HOEHEREN. Bei "Aktive Turniere" (acht Spieler) neben "Letzte Spiele"
   (fuenf Partien) hiess das ein Handbreit Leerraum unter der kuerzeren.

   `align-self` an den KARTEN und nicht `align-items` am Container: dieselben
   Raster tragen an anderen Stellen Formularfelder, und dort soll sich nichts
   aendern. Der gerade Abschluss unten geht dafuer verloren - bei stark
   verschiedenem Inhalt enden die Karten auf verschiedener Hoehe.

   `:not(.card-fill)` ist der Ausgleich mit der aelteren, umgekehrten Antwort auf
   dieselbe Klage. Eine Karte mit `.card-fill` WILL gestreckt werden: sie
   verteilt die geschenkte Hoehe auf ihren Inhalt, statt sie unten leer zu
   lassen (siehe die Regel dort). Ohne die Ausnahme waere `.card-fill` still zu
   einer Regel ohne Wirkung geworden - die Karte waere inhaltshoch, und es gaebe
   nichts mehr zu verteilen. Beides bleibt also gueltig, und die Wahl liegt
   sichtbar an der Karte: mit `.card-fill` streckt sie, ohne schrumpft sie. */
.dashboard-grid > .card:not(.card-fill),
.grid-2 > .card:not(.card-fill),
.grid-3 > .card:not(.card-fill),
.grid-4 > .card:not(.card-fill),
.grid-2-wide > .card:not(.card-fill) { align-self: start; }
/* ⚠️ `.stat-raster` steht hier ABSICHTLICH NICHT mit drin, und das ist der
   ueberraschende Teil an der Familie oben: sie zeigt, dass ein Raster in
   diesem Projekt seine Karten NICHT streckt - `align-self: start` ist die
   Vorgabe, nicht `stretch`.

   Das Statistikraster braucht beides. Seine erste Zeile (Gesamt neben
   Angstgegner) SOLL gleich hoch sein, das war der Wunsch des Betreibers am
   2026-09-03; die zweite (Kurve neben Duellen) nicht. Deshalb setzt
   `.stat-raster` `align-items: stretch` selbst und die zwei Karten der unteren
   Zeile tragen `.stat-oben`. Wer es hier eintraegt, macht die erste Zeile
   wieder ungleich hoch - und die Regel waere staerker (0,2,1 gegen 0,1,0). */


/* ── Mit mir vergleichen (2026-09-25) ──────────────────────────────────────────
   Eine Tabelle: links die Beschriftung, daneben eine Spalte je Spieler, ich
   zuerst - bis zu fuenf. Der Betreiber hat die Tabelle am 2026-09-25 gewaehlt
   („Tabelle, Beschriftung links"); die erste Fassung fuer zwei Spieler hatte
   die Beschriftung in der Mitte, und die traegt keine dritte Spalte.

   ⚠️ Die BESCHRIFTUNG KLEBT links (`position: sticky`). Am Telefon passen
   keine fuenf Spalten nebeneinander; `.table-wrap` wischt seitlich, und ohne
   die klebende Spalte stuende nach dem Wischen eine Zahlenreihe ohne Namen
   da. Sie braucht deshalb einen DECKENDEN Hintergrund in der Farbe der
   Karte - sonst liefen die Zahlen sichtbar unter ihr durch.

   ⚠️ Die Spalten sind GLEICH BREIT (`table-layout: fixed`, siehe unten)
   und nicht so breit wie ihr Inhalt: sonst stuende ein langer Name
   breiter da als ein kurzer, und die Werte fluchteten nicht.

   ⚠️ Markiert wird der HOECHSTE Wert jeder Zeile, mit `primary-container` -
   dieselbe Rolle wie ein gewaehlter Chip. Sie hat in Hell UND Dunkel ihr
   eigenes `on-`-Paar, eine Farbe von Hand haette in einem der beiden Themen
   keinen Kontrast. Keine Siegfarbe (gruen/rot): die Markierung sagt „mehr",
   nicht „besser" - bei Niederlagen ist mehr schlechter. */
/* ⚠️ Hoechstens 760 px breit und mittig. Gemessen am 2026-09-25 bei 1280 px:
   ohne Grenze standen bei zwei Spielern die Werte einer Zeile rund 790 px
   auseinander - man liest dann nicht mehr „Wert gegen Wert". */
.vg-karte { max-width: 760px; margin: 0 auto; }
/* ⚠️ `table-layout: fixed` teilt die Breite gleich auf, und `min-width` an
   einer Tabellenzelle gilt nicht. Die Untergrenze steht deshalb an der
   TABELLE: 104 px Beschriftung plus 88 px je Spieler. Ist das Fenster
   schmaler, wischt `.table-wrap` seitlich; `--vg-n` setzt die Ansicht. */
.vg-tabelle { table-layout: fixed; min-width: calc(104px + var(--vg-n, 2) * 88px); }
.vg-tabelle th,
.vg-tabelle td {
  padding: 8px 6px;
  text-align: center;
  vertical-align: middle;
}
.vg-tabelle tr:hover td { background: none; }
/* Die Koepfe OBEN ausgerichtet: urspruenglich, weil meiner keinen Knopf zum
   Entfernen hatte und mittig ausgerichtet um die halbe Knopfhoehe tiefer
   stand als die der anderen (Bild vom 2026-09-25: „Martin" eine Zeile unter
   „Abd"). Seit dem 2026-10-06 hat jede Spalte zwei Knoepfe (ersetzen,
   entfernen) - die Regel bleibt richtig, weil jetzt alle Koepfe gleich hoch
   sind und "oben" keinen mehr verschiebt. */
.vg-tabelle thead th { vertical-align: top; }
.vg-tabelle th.vg-name,
.vg-tabelle td.vg-ecke {
  position: sticky;
  left: 0;
  z-index: 1;
  /* Die Flaeche des umgebenden Kastens, in Hell UND Dunkel - dasselbe
     Mittel, mit dem die schwebende Feldbeschriftung ihren Rahmen abdeckt.
     Gemessen am 2026-09-25: mit `surface-container-lowest` fest stand die
     Spalte im dunklen Thema als dunklerer Streifen (15,13,23) auf der Karte
     (32,30,41) - dort ist `.card` `surface-container`. */
  background: var(--field-surface);
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  width: 104px;
}
.vg-seite {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-width: 0;
  text-transform: none;
  letter-spacing: normal;
}
/* ⚠️ Das Seitenverhaeltnis HALTEN. `App.avatarHtml` setzt Breite und Hoehe
   inline (mal `--avatar-scale`), und `img { max-width: 100% }` ganz oben im
   Stylesheet klemmt nur die Breite. Gemessen am 2026-09-25 bei 390 px und
   grosser Avatarstufe: 91 px breit, 140 px hoch - ein Oval. `!important`, weil
   nur so ein Inline-Stil zu schlagen ist; dasselbe Mittel wie bei
   `.streak-name img` und in M3-RULES.md §5 als Ausnahme benannt (es
   ueberschreibt die Groesse aus `App.avatarHtml`, keinen Token). Der Rand
   rechts (inline 4 px) faellt weg, sonst sitzt das Bild neben der Mitte.
   Verworfen: `App.avatarHtml` einen Schalter „ohne Hoehe" geben - ein
   zweites Verhalten an einem Baustein mit 33 Aufrufstellen (gezaehlt am
   2026-09-25) fuer eine einzige. */
.vg-seite img {
  height: auto !important;
  aspect-ratio: 1 / 1;
  margin-right: 0 !important;
}
/* Die Auswahl „Spieler hinzufuegen": 28 px FEST, wie in „Wen anschreiben?"
   (dort baut `App.nachrichten._avatar` das Bild ohne `--avatar-scale`).
   Gemessen am 2026-09-25 im Bild des Betreibers: mit `App.avatarHtml` und
   grosser Avatarstufe wurden die 28 px zum Mehrfachen, und die Bilder lagen
   in der 48 px hohen Zeile uebereinander. Dasselbe `!important` wie oben und
   aus demselben Grund (M3-RULES.md §5). */
.vg-wahl img {
  width: 28px !important;
  height: 28px !important;
  margin-right: 0 !important;
}
.vg-spieler {
  font-weight: 700;
  color: var(--md-sys-color-on-surface);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vg-tabelle th.vg-name {
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 400;
}
.vg-direkt {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
.vg-gegen-zeile + .vg-gegen-zeile { margin-top: 8px; }
.vg-wert {
  padding: 2px 10px;
  border-radius: var(--md-sys-shape-corner-full);
  font-weight: 700;
  font-size: 1.15rem;
  /* Die Zahlen fluchten - dieselbe Begruendung und dieselbe Groesse wie bei
     `.ps-wert` und `.duell-zahl`. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
}
.vg-wert.ist-mehr {
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
}
/* Die Chip-Zeile der Kennzahlauswahl (2026-10-06), ueber der Tabelle - nur
   der Abstand ist eigen, die Chips selbst sind `.chip-wahl-reihe` aus dem
   Formulardialog „Spieler anlegen" (Gruppenwahl, players.js), keine zweite
   Fassung. */
#vg-chips { margin-bottom: 14px; }
/* ══ MESSENGER: BEARBEITEN, ZURUECKRUFEN, ONLINE (2026-09-25) ══════════════════
   Am Ende der Datei und nicht bei `.nr-*` oben: eine Uebersteuerung gehoert
   HINTER die Regel, die sie uebersteuert (CLAUDE.md Falle ④) - hier
   `.status-dot` mit seinem `margin-top: 6px` fuer die Verbindungsliste. */

/* Der Punkt hinter dem Namen. In der Liste steht er im Namen, im Kopf des
   Gespraechs mit dem Wort „online" daneben. Er schrumpft nie mit: der Name
   kuerzt mit Ellipse, der Punkt bleibt zu sehen. */
.nr-online {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 6px;
  flex-shrink: 0;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
  vertical-align: middle;
}
.nr-online .status-dot,
.cd-kopf .status-dot { margin-top: 0; }
.cd-kopf .cd-online { margin-right: 4px; }

/* Die zwei Symbole unter der eigenen, noch unbeantworteten Blase. Rechts
   buendig wie die Zeit darueber; das negative Polster zieht die Flaeche der
   Knoepfe an den Rand der Blase, damit sie nicht hoeher wird als noetig. */
.nr-n-aktionen {
  display: flex;
  justify-content: flex-end;
  gap: 2px;
  margin: 2px -6px -4px;
}
.nr-n.is-meins .nr-n-aktionen .aktion { color: inherit; }
/* Die Blase, die gerade im Feld unten bearbeitet wird. */
.nr-n.is-aendern { outline: 2px solid var(--md-sys-color-primary); }

/* Zurueckgerufen: dieselbe Stelle, aber ohne Flaeche - kein Beitrag mehr,
   nur noch der Hinweis, dass hier einer war. */
.nr-n.is-zurueck,
.nr-n.is-meins.is-zurueck {
  background: transparent;
  border: 1px dashed var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface-variant);
}
.nr-n.is-zurueck .nr-n-text { font-style: italic; }

/* Der Streifen ueber dem Feld, solange bearbeitet wird. */
.nr-aendern {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 0 12px;
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-primary);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
.nr-aendern .aktion { margin-left: auto; }

/* Dasselbe im angedockten Fenster. */
.cd-n-aktionen { display: flex; gap: 2px; }
.cd-n.is-zurueck .cd-text,
.cd-n.ich.is-zurueck .cd-text {
  background: transparent;
  border: 1px dashed var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface-variant);
  font-style: italic;
}
.cd-bearbeitet { font-size: var(--md-sys-typescale-label-small-size); }
.cd-aendern {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 0 10px;
  font-size: var(--md-sys-typescale-label-medium-size);
  color: var(--md-sys-color-primary);
}
.cd-aendern .aktion { margin-left: auto; }

/* ── Zitieren (2026-09-25) ────────────────────────────────────────────────────
   Ebenfalls am Ende: `.nr-n`, `.cd-n` und `.cd-verlauf` werden hier ERGAENZT,
   und eine Ergaenzung gehoert hinter die Regel (Falle ④).

   Der Knopf steht NEBEN der Blase im freien Rand, unten buendig - bei der
   fremden rechts, bei der eigenen links (so gewaehlt). Eine Blase hat
   hoechstens 80 % Breite; der Rand daneben traegt den 32-px-Knopf, ohne dass
   eine Nachricht hoeher wird. */
.nr-verlauf,
.cd-verlauf { position: relative; }
.nr-n,
.cd-n { position: relative; }
.nr-n-zitieren,
.cd-n-zitieren {
  position: absolute;
  bottom: 0;
  left: calc(100% + 2px);
}
.nr-n.is-meins .nr-n-zitieren,
.cd-n.ich .cd-n-zitieren {
  left: auto;
  right: calc(100% + 2px);
}
/* Im Fenster ist der Rand schmaler (12 % von rund 300 px) - dort bekommt die
   Blase ausdruecklich Platz fuer den Knopf. */
.cd-n { max-width: calc(100% - 40px); }

/* Das Zitat oben in der Blase: eine Tonstufe und kein Rahmen (M3-RULES Regel
   3), mit eigener Textrolle, damit es auf der kraeftigen eigenen Blase so
   lesbar bleibt wie auf der fremden (Regel 2). Kleinere Ecke als die Blase
   (Regel 4). Hoechstens zwei Zeilen. */
.nr-zitat,
.cd-zitat {
  display: block;
  width: 100%;
  margin: 2px 0 4px;
  padding: 4px 8px;
  border: none;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface);
  font: inherit;
  text-align: left;
}
button.nr-zitat,
button.cd-zitat { cursor: pointer; }
.nr-zitat-von,
.cd-zitat-von {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 700;
  color: var(--md-sys-color-primary);
}
.nr-zitat-text,
.cd-zitat-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--md-sys-typescale-body-small-size);
}
.nr-zitat-text.is-zurueck,
.cd-zitat-text.is-zurueck {
  font-style: italic;
  color: var(--md-sys-color-on-surface-variant);
}
/* Der Streifen ueber dem Feld traegt dieselbe Bauart wie „Nachricht
   bearbeiten" (`.nr-aendern`); der Kopf ist einzeilig. */
.nr-zitat-kopf,
.cd-zitat-kopf {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--md-sys-color-on-surface);
}
/* Die angesprungene Nachricht leuchtet kurz auf - Bewegung als Antwort auf
   einen Klick (M3-RULES Regel 5). */
.nr-n.is-angesprungen,
.cd-n.is-angesprungen { outline: 2px solid var(--md-sys-color-primary); }

/* ══ Form-Punkte statt Buchstabenpillen (Neugestaltung, 2026-09-30) ═══════
   Der freigegebene Entwurf: ● Sieg, ○ Niederlage, – Unentschieden, in
   Textfarbe statt Gruen und Rot. Die FORM traegt die Auskunft und nicht die
   Farbe - dieselbe Bedingung, unter der bisher die Buchstaben standen.

   ⚠️ Der Buchstabe BLEIBT im Markup und wird nur durchsichtig: er traegt den
   Zwillingstest zwischen app.js und kiosk.html, und `title` bzw. `data-kt`
   sagen ohnehin das ganze Wort. Die Groesse haengt am `em` des Kontexts
   (Kette, Kachel, Einzelpille) - deren gemessene Schriftgroessen bleiben.

   ⚠️ AM ENDE und als Uebersteuerung mit zwei Klassen (Falle ④): mehrere
   Zusicherungen schneiden ab der ersten `.st-res`-Regel, und die Regeln der
   Kette und der Kachel haben dieselbe Staerke - sie verlieren hier nur, weil
   sie weiter oben stehen. Der Platzhalter `.is-leer` (drei Klassen) bleibt
   der blasse Kreis, den der Betreiber bestellt hat.

   Verworfen: die Buchstaben zu ersetzen. Das haette `standingTrend` und
   seinen Zwilling im Kiosk umgeschrieben, damit auch jeden Tooltip - fuer
   eine Frage, die nur das Aussehen betrifft. */
.st-res.is-win, .st-res.is-loss, .st-res.is-draw {
  box-sizing: border-box;
  width: 1em;
  min-width: 1em;
  height: 1em;
  padding: 0;
  /* `visible` und nicht `hidden`: der Buchstabe ist durchsichtig, zu
     beschneiden gibt es nichts - und ein beschneidender Kasten mit groesserem
     Inhalt ist genau das, was die Sichtpruefung meldet (am Ring der
     Niederlage 2 px, mit der Schrift wachsend). */
  overflow: visible;
  color: transparent;
  /* Ohne Zeilenhoehe traegt der unsichtbare Buchstabe nichts auf: sonst waere
     er hoeher als der Punkt, und die Sichtpruefung meldete ihn - zu Recht
     nach ihrer Regel - als abgeschnitten (134 weiche Befunde im Lauf vom
     2026-09-30). Der Strich des Unentschiedens ist aus demselben Grund KEIN
     3 px hoher Kasten, sondern ein 1-em-Kasten mit beschnittener Fuellung
     (`background-clip: content-box`) - gemessen: ein 3-px-Kasten meldete
     trotz `line-height: 0` 7 px Inhalt. */
  line-height: 0;
  border-radius: var(--md-sys-shape-corner-full);
}
.st-res.is-win  { background: var(--md-sys-color-on-surface); }
.st-res.is-loss { background: transparent;
                  border: .17em solid var(--md-sys-color-on-surface-variant); }
.st-res.is-draw { padding: .37em 0; background: var(--md-sys-color-on-surface-variant);
                  background-clip: content-box; }
.st-kette, .ek-form-pillen { gap: .3em; }

/* ══ Neugestaltung · die Huelle aus dem Netz (2026-09-30) ═══════════════════
   Der Betreiber nach dem ersten Bau: „Ich will das komplette neue desgine.
   Die alten Entscheidungen sind hinfaellig!!!!! … Es gelten keine Regel! Es
   ist nur wichtig, dass die Unterscheidung der Aktionen am Tisch (localhost)
   und remote behaelt."

   Damit gilt der freigegebene Entwurf (WebStart, WebStatistik … 1440 px):
   links eine Seitenleiste von 248 px mit Marke und acht Zielen, oben rechts
   im Inhalt Titel, Glocke und Konto. ⚠️ Verworfen ist damit die Rueckbestellung
   vom 2026-09-07 („Das Menue soll nicht mehr links am Rand sein") - sie steht
   in tests/test_kopfzeile_menue.py als Geschichte.

   ⚠️ NUR AUS DEM NETZ. `html[data-ort]` setzt der Server vor dem ersten Bild
   (routers/settings.py::ui_boot_snippet); der Tisch behaelt seine Leiste oben
   (Entwurf Main.dc.html), das Telefon seine untere Leiste. Deshalb stehen die
   Regeln in `min-width`-Bloecken: unter 601 px greifen sie gar nicht erst,
   und es muss nichts zurueckgesetzt werden. Die Marken bleiben 600/840.

   Zwischen 601 und 840 px (Tablet hochkant, halbes Fenster) steht die Leiste
   als SCHIENE aus Symbolen, 76 px: 248 px liessen dort dem Inhalt zu wenig. */
.nav-marke, .nav-mehr { display: none; }
.nav-kuerzel { font-size: 13px; font-weight: 600; letter-spacing: .02em; }

@media (min-width: 601px) {
  html[data-ort="netz"] #navbar {
    top: 0; bottom: 0; left: 0; right: auto;
    width: 248px; height: auto;
    flex-direction: column; align-items: stretch; gap: 4px;
    padding: 24px 16px;
    border-bottom: none;
    border-right: 1px solid var(--md-sys-color-outline-variant);
    background: var(--md-sys-color-surface);
  }
  html[data-ort="netz"] #navbar.is-gescrollt {
    background: var(--md-sys-color-surface);
    border-bottom: none;
  }
  /* Das Band oben rechts traegt Titel, Glocke und Konto. Ein Pseudoelement der
     festen Leiste und kein eigenes Element: es gibt dieses Band nur hier. */
  html[data-ort="netz"] #navbar::after {
    content: ''; position: fixed; z-index: -1;
    top: 0; left: 248px; right: 0; height: 88px;
    background: var(--md-sys-color-surface);
  }
  html[data-ort="netz"] .nav-marke {
    display: flex; align-items: center; gap: 10px;
    padding: 0 10px 20px;
    font-size: 18px; font-weight: 600; letter-spacing: -0.01em;
    color: var(--md-sys-color-on-surface); text-decoration: none;
  }
  html[data-ort="netz"] .nav-marke-bild {
    position: relative; flex: 0 0 28px; height: 28px; border-radius: 8px;
    background: var(--md-sys-color-primary);
  }
  html[data-ort="netz"] .nav-marke-bild::after {
    content: ''; position: absolute; left: 50%; top: 50%;
    width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; border-radius: 50%;
    background: var(--md-sys-color-on-primary);
  }
  html[data-ort="netz"] .nav-links,
  html[data-ort="netz"] .nav-mehr {
    display: flex; flex-direction: column; gap: 4px;
    flex: 0 0 auto; padding: 0; overflow: visible;
    background: none; border-radius: 0;
  }
  html[data-ort="netz"] .nav-mehr { margin-top: 4px; }
  html[data-ort="netz"] body.chat-aus .nav-nachrichten { display: none; }
  html[data-ort="netz"] #navbar .nav-link {
    height: 42px; min-height: 42px; padding: 0 12px; gap: 12px;
    border-radius: 12px; box-shadow: none;
    font-size: 15px; font-weight: 500; letter-spacing: 0;
    color: var(--md-sys-color-on-surface-variant);
  }
  html[data-ort="netz"] #navbar .nav-link .msym { font-size: 20px; }
  html[data-ort="netz"] #navbar .nav-link:hover {
    background: var(--md-sys-color-surface-container);
  }
  html[data-ort="netz"] #navbar .nav-link.active {
    background: var(--md-sys-color-surface-container-high);
    color: var(--md-sys-color-on-surface); font-weight: 600; box-shadow: none;
  }
  html[data-ort="netz"] .nav-clock { display: none; }
  /* Titel, Glocke und Konto: oben rechts im Inhalt, wie im Entwurf. */
  html[data-ort="netz"] .nav-titel {
    display: block; position: fixed; top: 24px; left: 284px; right: 160px;
    margin: 0;
    font-size: 28px; line-height: 40px; font-weight: 600; letter-spacing: -0.01em;
  }
  html[data-ort="netz"] .nav-bell,
  html[data-ort="netz"] #account-btn {
    position: fixed; top: 24px; width: 40px; height: 40px;
  }
  html[data-ort="netz"] .nav-bell {
    right: 88px; border-radius: 12px;
    border: 1px solid var(--md-sys-color-outline-variant);
    background: var(--md-sys-color-surface-container-lowest);
    color: var(--md-sys-color-on-surface);
  }
  html[data-ort="netz"] #account-btn {
    right: 36px; border: none; border-radius: 50%;
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    overflow: hidden;
  }
  html[data-ort="netz"] #account-btn .nav-avatar { width: 40px; height: 40px; }
  /* Der Kopf ist hier 88 px hoch (24 + 40 + 24). Ueber `--nav-h` und nicht
     als eigene Zahl: Inhaltsabstand, Mindesthoehe und die klebenden
     Kopfzeilen rechnen alle schon damit - und eine neue Division durch
     `--ui-zoom` waere gezaehlt (test_zoom_math_untouched). */
  html[data-ort="netz"] { --nav-h: 88px; }
  html[data-ort="netz"] #content {
    max-width: none; margin-left: 248px; margin-right: 0;
    padding: 0 36px 28px;
  }
  /* Die Ueberschrift der Ansicht steht jetzt im Kopf (App.ui.titelAusAnsicht). */
  html[data-ort="netz"] #content .page-title { display: none; }
  html[data-ort="netz"] #readonly-banner { margin-left: 248px; }
}

@media (min-width: 601px) and (max-width: 840px) {
  html[data-ort="netz"] #navbar { width: 76px; padding: 20px 12px; }
  html[data-ort="netz"] #navbar::after { left: 76px; }
  html[data-ort="netz"] .nav-marke { padding: 0 0 16px 12px; }
  html[data-ort="netz"] .nav-marke-text { display: none; }
  html[data-ort="netz"] #navbar .nav-link {
    justify-content: center; padding: 0; font-size: 0; gap: 0;
  }
  html[data-ort="netz"] #navbar .nav-link .msym { font-size: 22px; }
  html[data-ort="netz"] .nav-titel { left: 100px; font-size: 24px; }
  html[data-ort="netz"] #content { margin-left: 76px; padding: 0 20px 24px; }
  html[data-ort="netz"] #readonly-banner { margin-left: 76px; }
}

/* ── Die Glocke: ein roter Punkt statt der Zahl (Entwurf) ──────────────────
   Die Zahl bleibt im `title` und `aria-label` (App.auth.apply). Der Punkt hat
   einen Ring in der Flaeche des Knopfes, damit er sich auch ohne Farbsinn vom
   Knopf abhebt. */
.nav-bell[data-count]:not([data-count=""])::after {
  content: ''; top: 7px; right: 8px;
  min-width: 0; width: 8px; height: 8px; padding: 0;
  border-radius: 50%;
  border: 2px solid var(--md-sys-color-surface-container-lowest);
  box-sizing: content-box;
}

/* ── Das Kontomenue und das Spielermenue (Entwurf WebNeuigkeiten) ────────── */
.konto-menu {
  min-width: 240px; padding: 6px;
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: 18px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, .18);
}
html[data-theme="dark"] .konto-menu { background: var(--md-sys-color-surface-container); }
.konto-menu .km-punkt {
  min-height: 42px; gap: 12px; padding: 0 12px;
  border-radius: 12px; font-size: 14px;
}
.konto-menu .km-punkt:hover { background: var(--md-sys-color-surface-container-high); }
.konto-menu .km-punkt .msym { font-size: 18px; color: var(--md-sys-color-on-surface-variant); }
.konto-menu .km-punkt.km-aus { color: var(--md-sys-color-error); }
.konto-menu .km-punkt.km-aus .msym { color: inherit; }
.konto-menu.spieler-menue { min-width: 230px; border-radius: 16px; }
.konto-menu.spieler-menue .km-punkt { border-radius: 10px; }
/* Am Tisch bleibt die Trefferflaeche gross - dort bedient ein Finger. */
body.am-tisch .konto-menu .km-punkt { min-height: var(--min-touch); }

/* ── Die Benachrichtigungen als Blatt am rechten Rand (Entwurf WebGlocke) ──
   Die Klasse `glocke-blatt` setzt App.challenges.panel am vorhandenen Modal;
   am Telefon (unter 601 px) bleibt es das Blatt von unten. */
@media (min-width: 601px) {
  #modal-overlay.glocke-blatt {
    justify-content: flex-end; align-items: stretch; padding: 0;
    background: rgba(20, 20, 22, .45);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  html[data-theme="dark"] #modal-overlay.glocke-blatt { background: rgba(0, 0, 0, .62); }
  #modal-overlay.glocke-blatt #modal-box {
    width: 460px; max-width: 460px !important; max-height: none; height: 100%;
    margin: 0; border-radius: 0;
    border: none; border-left: 1px solid var(--md-sys-color-outline-variant);
    padding: 22px 18px;
    background: var(--md-sys-color-surface-container-lowest);
    box-shadow: -12px 0 40px rgba(0, 0, 0, .18);
    animation: blatt-rechts var(--md-sys-motion-duration-medium2)
               var(--md-sys-motion-easing-emphasized-decelerate);
  }
  html[data-theme="dark"] #modal-overlay.glocke-blatt #modal-box {
    background: var(--md-sys-color-surface-container);
  }
}
#modal-overlay.glocke-blatt .modal-close { display: none; }
@keyframes blatt-rechts { from { transform: translateX(100%); } }
.nb-blattkopf { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.nb-blattkopf h2 { margin: 0; flex: 1; font-size: 22px; font-weight: 600; }
.nb-zu {
  width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%;
  border: 1px solid var(--md-sys-color-outline-variant);
  background: transparent; color: var(--md-sys-color-on-surface);
  display: grid; place-items: center; cursor: pointer;
}
.nb-zu .msym { font-size: 18px; }

/* Die Zeilen des Blattes (Entwurf WebGlocke). Hinter den alten Regeln von
   `.nb-zeile` und `.nb-kopf` (Falle ④): der 3-px-Streifen links weicht dem
   Grund und dem roten Punkt am Symbol. */
.nb-kopf { justify-content: flex-start; gap: 6px; margin-bottom: 10px; }
.nb-kopf .aktion, .nb-ziele .aktion {
  height: 34px; min-height: 34px; padding: 0 14px; gap: 8px;
  border-radius: 12px; font-size: 14px; font-weight: 500;
  color: var(--md-sys-color-on-surface-variant); white-space: nowrap;
}
.aktion.nb-tonal { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); font-weight: 600; }
/* ⚠️ Der Rand der Umriss-Pille steht seit dem 2026-10-06 in `outline` und nicht
   in `outline-variant`. Gemessen im Glockenblatt: `outline-variant` gegen die
   Zeile war 1,2 : 1 - „Ablehnen" war ein Wort ohne erkennbare Pille. Dieselbe
   Entscheidung wie bei den Tortasten am Tisch (1 px, dunkler). */
.aktion.nb-umriss { border: 1px solid var(--md-sys-color-outline); background: transparent; color: var(--md-sys-color-on-surface); }
.aktion.nb-voll { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); font-weight: 600; }
/* ⚠️ Das Ueberfahren darf der Pille die Flaeche nicht nehmen (2026-10-06). Der
   Betreiber: eine der zwei Aktionen in der Glocke „ist eine komplett schwarze
   Pille und man erkennt nichts". Gemessen im Glockenblatt am Tisch: normal ist
   „Annehmen" lesbar (hell 18,4 : 1, dunkel 15,8 : 1), aber
   `.aktion:hover:not(:disabled)` (0,3,0) legt einen Schleier von 8 % ueber die
   Flaeche und schlaegt `.aktion.nb-voll` (0,2,0), waehrend die Schrift
   `on-primary` bleibt: hell weiss auf hellgrau (1,3 : 1), dunkel dunkel auf
   dunkel (1,12 : 1). Am Touchtisch bleibt `:hover` nach dem Tippen haengen.
   Hier (0,4,0) und HINTER der Regel darueber (Falle ④): die Flaeche bleibt. */
.aktion.nb-voll:hover:not(:disabled) { background: var(--md-sys-color-primary); }
.nb-zeile {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 10px; margin: 0 0 2px; border: none; border-left: none;
  border-radius: 16px; background: transparent; min-height: 0;
}
.nb-zeile.is-neu { background: var(--md-sys-color-surface); border-left: none; }
.nb-symbol {
  position: relative; flex: 0 0 38px; height: 38px; border-radius: 50%;
  background: var(--md-sys-color-surface-container-high);
  display: grid; place-items: center; color: var(--md-sys-color-on-surface);
}
.nb-symbol .msym { font-size: 18px; }
.nb-punkt {
  display: none; position: absolute; left: -2px; top: -2px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--md-sys-color-error);
  border: 2px solid var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"] .nb-punkt { border-color: var(--md-sys-color-surface-container); }
.nb-zeile.is-neu .nb-punkt { display: block; }
.nb-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.nb-titel { font-size: 15px; font-weight: 500; color: var(--md-sys-color-on-surface); }
.nb-zeile.is-neu .nb-titel { font-weight: 600; }
.nb-koerper { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
.nb-meta { font-size: 12px; color: var(--md-sys-color-on-surface-variant); }
.nb-ziele { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.nb-werkzeug { display: flex; gap: 4px; align-items: center; flex: 0 0 auto; }
.aktion.nb-rund {
  width: 34px; height: 34px; min-width: 34px; min-height: 34px; padding: 0;
  border-radius: 50%; border: 1px solid var(--md-sys-color-outline-variant);
  background: transparent; color: var(--md-sys-color-on-surface-variant);
}
.aktion.nb-rund .msym { font-size: 17px; }

/* ══ Neugestaltung · die Bauteile aus dem Netz (2026-09-30) ═════════════════
   Die Masse der freigegebenen Entwuerfe (WebEinst*, WebStart, WebGlocke …):
   Knoepfe 40 px mit 12 px Ecke statt 56-px-Pillen, Felder 44 px mit der
   Beschriftung UEBER dem Feld, Reiter 40 px mit 2-px-Strich in Textfarbe,
   Segmente als 12-px-Schiene, Schalter 44 x 26, Karten mit Polster 20/22 und
   ohne Symbol im Titel.

   ⚠️ NUR AUS DEM NETZ (`html[data-ort="netz"]`, Browser und Telefon). Am
   Tisch bedient ein Finger, und dort bleiben die grossen Trefferflaechen
   (`--min-touch`, 56 px) samt den Pillen - der Tisch hat seine eigenen
   Entwuerfe (Main, Tisch*).

   ⚠️ Alles HINTER den Grundregeln (Falle ④): mehrere Zusicherungen lesen die
   erste Fundstelle von `.btn`, `.tab`, `.seg-btn`, `.card-title`. Diese
   Regeln tragen ein Praefix und sind damit andere Bloecke. */

/* Knoepfe */
html[data-ort="netz"] .btn {
  min-height: 40px; padding: 0 14px; gap: 8px;
  border-radius: 12px;
  font-size: 14px; font-weight: 500; letter-spacing: 0;
  color: var(--md-sys-color-on-surface);
}
html[data-ort="netz"] .btn:active:not(:disabled) { border-radius: 12px; transform: none; }
html[data-ort="netz"] .btn .msym { font-size: 18px; }
html[data-ort="netz"] .btn-primary,
html[data-ort="netz"] .btn-filled { color: var(--md-sys-color-on-primary); font-weight: 600; }
html[data-ort="netz"] .btn-tonal { font-weight: 600; }
html[data-ort="netz"] .btn-outline,
html[data-ort="netz"] .btn-outlined {
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-outline-variant);
}
html[data-theme="dark"][data-ort="netz"] .btn-outline,
html[data-theme="dark"][data-ort="netz"] .btn-outlined { background: var(--md-sys-color-surface-container); }
html[data-ort="netz"] .btn-text { color: var(--md-sys-color-on-surface-variant); padding: 0 10px; }
/* Gefahr: umrandet mit roter Schrift - der Entwurf entschaerft die gefuellten
   roten Knoepfe. Die Rueckfrage (App.ui.frage) bleibt, wo sie war. */
html[data-ort="netz"] .btn-danger {
  background: transparent; color: var(--md-sys-color-error); font-weight: 600;
  border: 1px solid var(--md-sys-color-outline-variant);
}
html[data-ort="netz"] .btn-sm { min-height: 34px; padding: 0 12px; }
html[data-ort="netz"] .btn-xs { min-height: 28px; padding: 0 10px; font-size: 13px; }
html[data-ort="netz"] .btn-lg { min-height: 46px; padding: 0 18px; font-size: 15px; }
html[data-ort="netz"] .btn-xl { min-height: 48px; padding: 0 20px; font-size: 15px; border-radius: 12px; }
html[data-ort="netz"] .btn-xl:active:not(:disabled) { border-radius: 12px; }
html[data-ort="netz"] .btn-icon {
  width: 40px; height: 40px; min-height: 40px; padding: 0;
  border-radius: 12px; font-size: 18px;
  border: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"][data-ort="netz"] .btn-icon { background: var(--md-sys-color-surface-container); }
html[data-ort="netz"] .btn-icon.btn-sm { width: 34px; height: 34px; min-height: 34px; }

/* Felder: die Beschriftung steht UEBER dem Feld (Entwurf), auch beim
   schwebenden Hausmuster `.form-group.field` - das Markup bleibt, die
   Beschriftung wird per `order` nach oben gestellt. */
html[data-ort="netz"] .form-label {
  font-size: 13px; font-weight: 500; color: var(--md-sys-color-on-surface-variant);
  margin-bottom: 6px;
}
html[data-ort="netz"] .form-input {
  min-height: 44px; padding: 0 14px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: 12px; font-size: 15px;
  background: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"][data-ort="netz"] .form-input { background: var(--md-sys-color-surface-container); }
html[data-ort="netz"] textarea.form-input { padding: 10px 14px; }
html[data-ort="netz"] .form-input:focus {
  outline: 2px solid var(--md-sys-color-on-surface); outline-offset: -1px;
  border-color: transparent;
}
html[data-ort="netz"] .form-group.field { display: flex; flex-direction: column; gap: 6px; }
html[data-ort="netz"] .field > .form-input,
html[data-theme="dark"][data-ort="netz"] .field > .form-input {
  background: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"][data-ort="netz"] .field > .form-input { background: var(--md-sys-color-surface-container); }
html[data-ort="netz"] .field > .form-input + .form-label,
html[data-ort="netz"] .field > .form-input:focus + .form-label,
html[data-ort="netz"] .field > .form-input:not(:placeholder-shown) + .form-label,
html[data-ort="netz"] .field > select.form-input + .form-label {
  position: static; order: -1; transform: none;
  margin: 0; padding: 0; background: none;
  font-size: 13px; font-weight: 500; white-space: normal;
  color: var(--md-sys-color-on-surface-variant);
}
html[data-ort="netz"] .form-group > .text-dim.text-sm { font-size: 12px; }

/* Schalter 44 x 26: an = Textfarbe, der Knopf rechts; aus = Flaeche 2 mit
   Haarlinie, der Knopf etwas kleiner links. */
html[data-ort="netz"] .toggle { width: 44px; height: 26px; }
html[data-ort="netz"] .toggle-slider {
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
}
html[data-ort="netz"] .toggle-slider::before {
  width: 18px; height: 18px; left: 3px; margin-top: -9px;
  background: var(--md-sys-color-on-surface-variant);
}
html[data-ort="netz"] .toggle input:checked + .toggle-slider {
  background: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary);
}
html[data-ort="netz"] .toggle input:checked + .toggle-slider::before,
html[data-ort="netz"] .toggle input:active + .toggle-slider::before {
  width: 20px; height: 20px; margin-top: -10px;
  transform: translateX(17px);
  background: var(--md-sys-color-on-primary);
}
html[data-ort="netz"] .toggle input:active:not(:checked) + .toggle-slider::before {
  width: 18px; height: 18px; margin-top: -9px; transform: none;
  background: var(--md-sys-color-on-surface-variant);
}
/* Die Schalterzeile: Haarlinie darunter, Titel 15/500. */
html[data-ort="netz"] .toggle-row {
  min-height: 52px; padding: 6px 0; gap: 16px;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
html[data-ort="netz"] .toggle-row:last-child { border-bottom: none; }
html[data-ort="netz"] .toggle-label strong { font-size: 15px; font-weight: 500; }
html[data-ort="netz"] .toggle-label .text-dim { font-size: 13px; }

/* Reiter: 40 px, 14 px, Strich 2 px in Textfarbe ueber die ganze Breite,
   ohne Symbol. */
html[data-ort="netz"] .tabs { gap: 2px; }
html[data-ort="netz"] .tab {
  min-height: 0; padding: 10px 12px; border-radius: 0;
  font-size: 14px; font-weight: 400; letter-spacing: 0;
  color: var(--md-sys-color-on-surface-variant);
}
html[data-ort="netz"] .tab > .msym:first-child { display: none; }
html[data-ort="netz"] .tab.active { color: var(--md-sys-color-on-surface); font-weight: 600; }
html[data-ort="netz"] .tab::before {
  left: 0; right: 0; height: 2px; border-radius: 0;
  background: var(--md-sys-color-on-surface);
}

/* Segmente: 12-px-Schiene mit 3 px Polster, Segment 38 px mit 9 px Ecke. */
html[data-ort="netz"] .seg-group {
  padding: 3px; gap: 0; border-radius: 12px; width: fit-content; max-width: 100%;
}
html[data-ort="netz"] .seg-btn {
  min-height: 34px; padding: 0 14px; border-radius: 9px;
  font-size: 14px; font-weight: 500; letter-spacing: 0;
  color: var(--md-sys-color-on-surface-variant);
}
html[data-ort="netz"] .seg-btn.selected {
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface); font-weight: 600;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
}
html[data-theme="dark"][data-ort="netz"] .seg-btn.selected { background: var(--md-sys-color-surface-container); }

/* Karten: Polster 20/22, Radius 22, Titel 17/600 ohne Symbol. */
html[data-ort="netz"] .card { padding: 20px 22px; border-radius: 22px; }
html[data-ort="netz"] .card-title { font-size: 17px; font-weight: 600; margin-bottom: 12px; gap: 10px; }
html[data-ort="netz"] .card-title > .msym:first-child,
.card-title > span:first-child > .msym:first-child { display: none; }
html[data-ort="netz"] .card > h3 { font-size: 17px; font-weight: 600; }
html[data-ort="netz"] .card > h3 > .msym:first-child { display: none; }

/* Chips und Marken */
html[data-ort="netz"] .chip {
  height: 32px; padding: 0 12px; font-size: 13px; font-weight: 500;
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
}
html[data-theme="dark"][data-ort="netz"] .chip { background: var(--md-sys-color-surface-container); }
html[data-ort="netz"] .chip.selected {
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); font-weight: 600;
}
html[data-ort="netz"] .badge {
  display: inline-flex; align-items: center; height: 22px; padding: 0 8px;
  font-size: 12px; font-weight: 500; letter-spacing: 0;
}
html[data-ort="netz"] .section-title {
  font-size: 13px; font-weight: 600; text-transform: none; letter-spacing: 0;
  color: var(--md-sys-color-on-surface-variant); margin-top: 10px;
}

/* Regler: Spur 6 px, Griff 22 px rund mit 2-px-Rand in Textfarbe. */
html[data-ort="netz"] .slider { height: 28px; }
html[data-ort="netz"] .slider::-webkit-slider-runnable-track {
  height: 6px; margin-block: 11px; border-radius: 3px;
  background: linear-gradient(to right, var(--md-sys-color-primary) var(--fill, 50%),
                              var(--md-sys-color-surface-container-high) var(--fill, 50%));
}
html[data-ort="netz"] .slider::-webkit-slider-thumb {
  width: 22px; height: 22px; margin-top: -8px; border-radius: 50%;
  background: var(--md-sys-color-surface-container-lowest);
  border: 2px solid var(--md-sys-color-primary);
  box-shadow: none;
}

/* Die Tabellenkoepfe: 12/500 in Nebentext, ohne Versalien. */
html[data-ort="netz"] th {
  font-size: 12px; font-weight: 500; text-transform: none; letter-spacing: 0;
  color: var(--md-sys-color-on-surface-variant);
}

/* ══ Neugestaltung · die Startseite (2026-09-30) ════════════════════════════
   Nach den Entwuerfen Main (Tisch, 1536 px) und WebStart (Netz, 1440 px).
   EIN Renderer fuer beide Orte (dashboard.js); die Masse staffelt das
   Stylesheet ueber Variablen und `html[data-ort]`.

   Rechts eine Spalte (`.dash-rechts`): am Tisch der Startknopf als dunkler
   Kasten ueber „Letzte Spiele", aus dem Netz die LIVE-Karte, wenn ein Spiel
   laeuft. ⚠️ Der Startknopf stand bis dahin ueber der ganzen Breite - der
   Entwurf setzt ihn neben die Rangliste. */
.dashboard-grid { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 20px; }
/* 1,6 zu 1 seit dem 2026-10-30 - das Verhaeltnis der TV-Ansicht (`#kiosk-standings`
   flex 1.6, `#kiosk-sidebar` flex 1). Vorher 1,5 zu 1 (Entwurf WebStart). */
html[data-ort="netz"] .dashboard-grid { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
.dash-rechts { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
.dash-rechts > .rg-karte { flex: 1 1 auto; }

/* Der Startknopf: 96 px, Ecke 24, Kreis mit Spielsymbol, zweizeilig. */
.dash-rechts .big-start-btn {
  width: 100%; min-height: 96px; margin: 0; padding: 0 28px; gap: 18px;
  justify-content: flex-start; text-align: left;
  border-radius: 24px;
}
.dash-rechts .big-start-btn .bs-kreis {
  flex: 0 0 52px; height: 52px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--md-sys-color-on-primary); color: var(--md-sys-color-primary);
}
.dash-rechts .big-start-btn .bs-kreis .msym { font-size: 26px; }
.dash-rechts .big-start-btn .bs-text { display: flex; flex-direction: column; gap: 2px; }
.dash-rechts .big-start-btn .bs-text strong { font-size: 24px; font-weight: 600; }
.dash-rechts .big-start-btn .bs-text span { font-size: 15px; font-weight: 400; opacity: .7; }

/* Die Rangliste: Kopf auf einer Grundlinie, Zeilen mit Haarlinie. */
.card-title.rl-kopf { align-items: baseline; justify-content: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.rl-kopf > span:first-child { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
.rl-kopf > span:first-child > .msym { display: none; }
.rl-zusatz { font-size: 15px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
.rl-karte .mini-rang {
  --rl-platz: 44px; --rl-form: 110px; --rl-trend: 64px; --anteil-spur: 84px; --rl-wert: 80px;
  gap: 0 !important; flex: 0 0 auto !important;
}
.rl-karte .mini-rang > span { padding-right: 10px; }
.rl-karte .mini-rang > span:nth-last-child(-n+1),
.rl-karte .mini-rang > .standing-pts { padding-right: 0; }
html[data-ort="netz"] .rl-karte { --avatar-scale: 2.3; }   /* 14 x 2,3 = 32 px wie im Entwurf */
html[data-ort="netz"] .rl-karte .mini-rang {
  --rl-platz: 36px; --rl-form: 96px; --rl-trend: 56px; --anteil-spur: 70px; --rl-wert: 64px;
}
.rl-karte .mini-rang > span {
  min-height: 59px; display: flex; align-items: center;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
html[data-ort="netz"] .rl-karte .mini-rang > span { min-height: 56px; }
.rl-karte .mini-rang > .mini-head {
  min-height: 0; padding-bottom: 8px;
  font-size: 13px; font-weight: 500; text-transform: none; letter-spacing: 0;
  color: var(--md-sys-color-on-surface-variant);
}
html[data-ort="netz"] .rl-karte .mini-rang > .mini-head { font-size: 12px; }
.rl-karte .mini-rang > .mini-head[style*="right"],
.rl-karte .standing-platz, .rl-karte .standing-share, .rl-karte .standing-pts { justify-content: flex-end; }
.rl-platz { font-size: 16px; color: var(--md-sys-color-on-surface-variant); font-variant-numeric: tabular-nums; }
html[data-ort="netz"] .rl-platz { font-size: 15px; }
.rl-spieler { gap: 14px; overflow: hidden; font-size: 18px; font-weight: 500; }
html[data-ort="netz"] .rl-spieler { gap: 12px; font-size: 15px; }
.rl-spieler > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rl-karte .standing-share { font-size: 16px; color: var(--md-sys-color-on-surface-variant); }
.rl-karte .standing-pts { font-size: 19px; font-weight: 600; }
html[data-ort="netz"] .rl-karte .standing-share { font-size: 15px; }
html[data-ort="netz"] .rl-karte .standing-pts { font-size: 15px; }
/* Der Trend ohne Farbe - „↑ 1" in Nebentext (Entwurf). */
.rl-karte .st-move.is-up, .rl-karte .st-move.is-down, .rl-karte .st-move.is-flat {
  color: var(--md-sys-color-on-surface-variant); opacity: 1; font-weight: 400; font-size: 15px;
}
html[data-ort="netz"] .rl-karte .st-move { font-size: 13px; }
/* ⚠️ Die Groesse der Punkte am `.st-res` innerhalb der Karte und NICHT als
   `.st-kette .st-res`: dieser Selektor ist die Schnittmarke der Form-Punkte
   (test_die_form_steht_als_punkt_in_app_und_kiosk, Falle ④). */
.rl-karte .st-kette { gap: 6px; }
html[data-ort="netz"] .rl-karte .st-kette { gap: 5px; }
.rl-karte .st-res { font-size: 8px; }
html[data-ort="netz"] .rl-karte .st-res { font-size: 7px; }
/* Die Legende nur am Tisch (Entwurf Main; WebStart hat keine). */
.rl-legende {
  display: flex; gap: 18px; padding-top: 12px;
  font-size: 13px; color: var(--md-sys-color-on-surface-variant);
}
.rl-legende > span { display: inline-flex; align-items: center; gap: 6px; }
.rl-leg { display: inline-block; width: 8px; height: 8px; border-radius: 50%; box-sizing: border-box; }
.rl-leg-s { background: var(--md-sys-color-on-surface); }
.rl-leg-n { border: 1.5px solid var(--md-sys-color-on-surface-variant); }
.rl-leg-u { height: 2px; border-radius: 1px; background: var(--md-sys-color-on-surface-variant); }
html[data-ort="netz"] .rl-legende { display: none; }

/* Ohne Bild: Initialen im Kreis (Entwurf) statt einer Luecke. */
.av-kuerzel {
  display: inline-grid; place-items: center; flex-shrink: 0; border-radius: 50%;
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
  font-size: 11px; font-weight: 600; letter-spacing: 0;
}

.rg-karte .card-title { font-size: 18px; }
.rg-karte .card-title > .msym { display: none; }

/* Die LIVE-Karte (nur aus dem Netz, siehe dashboard.js). */
.live-karte {
  display: flex; flex-direction: column; gap: 12px;
  padding: 18px 22px; text-decoration: none; color: var(--md-sys-color-on-surface);
}
.lk-kopf { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.lk-marke {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; font-weight: 600; letter-spacing: .06em; color: var(--md-sys-color-error);
}
.lk-punkt { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.lk-weiter { font-size: 13px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); }
/* 2026-10-12: „Livestream" ist ein eigener Link (die Karte ist kein <a> mehr);
   Bedienziel mindestens 32 px, dafuer Polster statt Hoehe der Zeile. */
a.lk-weiter {
  text-decoration: none; padding: 6px 4px; margin: -6px -4px; min-height: 32px;
  display: inline-flex; align-items: center;
}
a.lk-weiter:hover { color: var(--md-sys-color-on-surface); }
.lk-stand { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 10px; }
a.lk-stand { text-decoration: none; color: inherit; }
.lk-seite { display: inline-flex; align-items: center; gap: 8px; font-size: 18px; font-weight: 600; min-width: 0; }
.lk-rechts { justify-content: flex-end; }
.lk-farbe { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
/* Figurenfarbe + Kontur-Ring, siehe `.sd-punkt` oben. */
.lk-farbe.t1, .lk-farbe.t2 { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.lk-farbe.t1 { background: var(--md-ext-color-team1); }
.lk-farbe.t2 { background: var(--md-ext-color-team2); }
.lk-tore { font-size: 36px; font-weight: 600; font-variant-numeric: tabular-nums; }
/* Seit dem 2026-10-06 OHNE Teamfarbe am Text - `.lk-farbe` daneben traegt
   die Zuordnung (siehe die Begruendung bei `.fg-stand` weiter oben). */
.lk-tore .t1, .lk-tore .t2 { color: inherit; }
.lk-trenner { color: var(--md-sys-color-on-surface-variant); margin: 0 6px; }

/* ══ Neugestaltung · die Leiste am Tisch (Entwurf Main, 2026-09-30) ═════════
   Marke „Kicker", die sechs Ziele in einer grauen Schiene (auch TV und
   Turnier - die Entscheidung vom 2026-09-03, sie am Tisch zu verbergen, ist
   mit der Neugestaltung hinfaellig), rechts die Uhr „Mi, 30.09.  11:28" und
   zwei runde Knoepfe: Darstellung und Einstellungen. 88 px hoch. */
html[data-ort="tisch"] { --nav-h: 88px; }
html[data-ort="tisch"] #navbar {
  gap: 28px;
  padding-left: max(32px, env(safe-area-inset-left, 0px));
  padding-right: max(32px, env(safe-area-inset-right, 0px));
}
html[data-ort="tisch"] .nav-marke {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  font-size: 19px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--md-sys-color-on-surface); text-decoration: none;
}
html[data-ort="tisch"] .nav-marke-bild {
  position: relative; width: 30px; height: 30px; border-radius: 9px;
  background: var(--md-sys-color-primary);
}
html[data-ort="tisch"] .nav-marke-bild::after {
  content: ''; position: absolute; left: 10px; top: 10px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--md-sys-color-on-primary);
}
html[data-ort="tisch"] .nav-links { gap: 2px; padding: 4px; }
html[data-ort="tisch"] .nav-links .nav-link {
  min-height: 0; padding: 10px 18px; gap: 8px;
  font-size: 15px; font-weight: 500; letter-spacing: 0;
  color: var(--md-sys-color-on-surface-variant);
}
html[data-ort="tisch"] .nav-links .nav-link .msym { font-size: 20px; }
html[data-ort="tisch"] .nav-links .nav-link.active {
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface); font-weight: 600;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
}
html[data-theme="dark"][data-ort="tisch"] .nav-links .nav-link.active { background: var(--md-sys-color-surface-container); }
html[data-ort="tisch"] body.am-tisch .nav-links .nav-link[data-route="/tv"],
html[data-ort="tisch"] body.am-tisch.kann-turniere .nav-links .nav-link[data-route="/tournaments"] { display: flex; }
html[data-ort="tisch"] .nav-clock { gap: 10px; align-items: baseline; }
html[data-ort="tisch"] .nav-clock .uhr-tag,
html[data-ort="tisch"] .nav-clock .uhr-datum { font-size: 16px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
html[data-ort="tisch"] .nav-clock .uhr-tag { margin-right: -5px; }
html[data-ort="tisch"] .nav-clock .uhr-zeit { font-size: 22px; font-weight: 600; }
html[data-ort="tisch"] .nav-farbmodus,
html[data-ort="tisch"] .nav-gear {
  width: 48px; height: 48px; min-width: 48px; min-height: 48px; padding: 0;
  border-radius: 50%; border: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface);
}
html[data-theme="dark"][data-ort="tisch"] .nav-farbmodus,
html[data-theme="dark"][data-ort="tisch"] .nav-gear { background: var(--md-sys-color-surface-container); }
html[data-ort="tisch"] .nav-farbmodus .msym,
html[data-ort="tisch"] .nav-gear .msym { font-size: 22px; }
/* Der Schriftzug am Zahnrad steht nur in der Seitenleiste (2026-10-01). */
.nav-gear-text { display: none; }
/* „Messenger" im Kontomenue nur am Telefon (2026-10-01). Der Betreiber: „Bitte
   entferne den. Der ist jetzt links im Menue zu sehen." Ab 601 px steht aus
   dem Netz „Nachrichten" in der Seitenleiste - zwei Wege zum selben Ziel. Am
   Telefon traegt die untere Leiste keine Nachrichten, dort bleibt er. */
@media (min-width: 601px) {
  html[data-ort="netz"] .km-nachrichten { display: none; }
}

/* ══ Die Seitenleiste auch am Tisch (2026-10-01) ═══════════════════════════
   Der Betreiber: „Das Menueleiste (Pille) ist am Kickertisch oben. Die
   Einstellungen ist als Kugel oben rechts. Koennen wir das Menue genauso
   links anordnen wie es ueberall (remote) ist?" Damit ist die Leiste oben
   (Entwurf TischStart, Block darueber) ab 601 px abgeloest; sie steht als
   Geschichte dort, die Regeln hier gewinnen, weil sie dahinter stehen.

   Wie aus dem Netz: 248 px, Marke, die sechs Ziele - und unten
   „Einstellungen" als Eintrag (die globalen Reiter hinter der PIN, wie zuvor
   der Kreis). „Nachrichten" bleibt am Tisch weg: dort ist meist niemand
   angemeldet. Entschieden am 2026-10-01: Uhr und Mond bleiben oben rechts
   im Kopfband; im LAUFENDEN Spiel klappt die Leiste zur Schiene (76 px) ein,
   damit die Spielansicht kaum Breite verliert.

   ⚠️ Eintraege 52 px hoch statt 42 wie im Netz: hier bedient ein Finger.
   ⚠️ Keine neue Zoomdivision - alles in px, `zoom` am Rumpf skaliert mit. */
@media (min-width: 601px) {
  html[data-ort="tisch"] #navbar {
    top: 0; bottom: 0; left: 0; right: auto;
    width: 248px; height: auto;
    flex-direction: column; align-items: stretch; gap: 4px;
    padding: 24px 16px;
    border-bottom: none;
    border-right: 1px solid var(--md-sys-color-outline-variant);
    background: var(--md-sys-color-surface);
  }
  html[data-ort="tisch"] #navbar.is-gescrollt {
    background: var(--md-sys-color-surface);
    border-bottom: none;
  }
  /* Das Kopfband rechts der Leiste traegt Titel, Uhr und Mond. */
  html[data-ort="tisch"] #navbar::after {
    content: ''; position: fixed; z-index: -1;
    top: 0; left: 248px; right: 0; height: 88px;
    background: var(--md-sys-color-surface);
  }
  html[data-ort="tisch"] .nav-marke { padding: 0 10px 20px; }
  html[data-ort="tisch"] .nav-links {
    flex-direction: column; gap: 4px; flex: 0 0 auto;
    padding: 0; overflow: visible; background: none; border-radius: 0;
  }
  html[data-ort="tisch"] #navbar .nav-link,
  html[data-ort="tisch"] #navbar .nav-gear {
    width: auto; height: 52px; min-height: 52px; padding: 0 14px; gap: 12px;
    justify-content: flex-start;
    border: none; border-radius: 12px; box-shadow: none; background: none;
    font-size: 16px; font-weight: 500; letter-spacing: 0;
    color: var(--md-sys-color-on-surface-variant);
  }
  html[data-ort="tisch"] #navbar .nav-link .msym { font-size: 22px; }
  html[data-ort="tisch"] #navbar .nav-link:hover { background: var(--md-sys-color-surface-container); }
  html[data-ort="tisch"] #navbar .nav-link.active {
    background: var(--md-sys-color-surface-container-high);
    color: var(--md-sys-color-on-surface); font-weight: 600; box-shadow: none;
  }
  html[data-ort="tisch"] #navbar .nav-gear { margin-top: 4px; }
  html[data-ort="tisch"] .nav-gear-text { display: inline; }
  html[data-ort="tisch"] .nav-titel {
    display: block; position: fixed; top: 24px; left: 284px; right: 440px;
    margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 28px; line-height: 40px; font-weight: 600; letter-spacing: -0.01em;
  }
  html[data-ort="tisch"] .nav-clock {
    position: fixed; top: 20px; right: 104px; height: 48px;
    display: flex; align-items: center; margin: 0;
  }
  html[data-ort="tisch"] .nav-farbmodus { position: fixed; top: 20px; right: 36px; }
  /* Angemeldet am Tisch: Glocke und Konto wie aus dem Netz ganz rechts, Mond
     und Uhr ruecken davor. ⚠️ Ohne diese Lage standen beide im Fluss der
     Leiste, und das Kontomenue ging aus dem Fenster (Sichtpruefung
     2026-10-01, z-kontomenue: 12 harte Befunde je Breite). */
  html[data-ort="tisch"] .nav-bell,
  html[data-ort="tisch"] #account-btn { position: fixed; top: 24px; width: 40px; height: 40px; }
  html[data-ort="tisch"] .nav-bell { right: 88px; }
  html[data-ort="tisch"] #account-btn { right: 36px; overflow: hidden; }
  html[data-ort="tisch"] #account-btn .nav-avatar { width: 40px; height: 40px; }
  html[data-ort="tisch"] body.signed-in .nav-farbmodus { right: 140px; }
  html[data-ort="tisch"] body.signed-in .nav-clock { right: 208px; }
  html[data-ort="tisch"] #content {
    max-width: none; margin-left: 248px; margin-right: 0;
    padding: 0 36px 28px;
  }
  html[data-ort="tisch"] #content .page-title { display: none; }
  html[data-ort="tisch"] #readonly-banner { margin-left: 248px; }

}
/* Zwischen 601 und 840 px immer die Schiene, wie aus dem Netz. */
@media (min-width: 601px) and (max-width: 840px) {
  html[data-ort="tisch"] #navbar { width: 76px; padding: 20px 12px; }
  html[data-ort="tisch"] #navbar::after { left: 76px; }
  html[data-ort="tisch"] .nav-marke { padding: 0 0 16px 12px; }
  html[data-ort="tisch"] .nav-marke-text,
  html[data-ort="tisch"] .nav-gear-text { display: none; }
  html[data-ort="tisch"] #navbar .nav-link {
    justify-content: center; padding: 0; font-size: 0; gap: 0;
  }
  html[data-ort="tisch"] .nav-titel { left: 100px; right: 300px; font-size: 24px; }
  html[data-ort="tisch"] body.signed-in .nav-titel { right: 360px; }
  html[data-ort="tisch"] .nav-clock .uhr-tag { display: none; }
  html[data-ort="tisch"] #content { margin-left: 76px; padding: 0 20px 24px; }
  html[data-ort="tisch"] #readonly-banner { margin-left: 76px; }
}
/* ⚠️ Beim Neuen Spiel und im laufenden Spiel KEINE Leiste (2026-10-01). Der
   Betreiber: „Laeuft ein Spiel (am Tisch), sehe ich links noch die
   Einstellungen. Das will ich nicht mehr sehen" - und dasselbe beim Neuen
   Spiel. Die Menuepunkte waren dort schon verborgen (Entwuerfe
   TischNeuesSpiel, TischSpiel: kein Hauptmenue); uebrig blieb eine Leiste mit
   Marke und „Einstellungen". Sie verschwindet ganz, die Seite nimmt die
   Breite; Uhr und Mond bleiben im Kopfband. Das ersetzt die Schiene im Spiel
   vom Vormittag. HINTER beiden Leistenbloecken, damit es auch bis 840 px
   gilt. */
@media (min-width: 601px) {
  html[data-ort="tisch"]:is(:has(#game-view), :has(#content .setup-seiten)) #navbar {
    width: 0; padding: 0; border-right: none;
  }
  html[data-ort="tisch"]:is(:has(#game-view), :has(#content .setup-seiten)) :is(.nav-marke, .nav-links, .nav-gear) {
    display: none;
  }
  html[data-ort="tisch"]:is(:has(#game-view), :has(#content .setup-seiten)) #navbar::after { left: 0; }
  html[data-ort="tisch"]:is(:has(#game-view), :has(#content .setup-seiten)) #content { margin-left: 0; }
  html[data-ort="tisch"]:is(:has(#game-view), :has(#content .setup-seiten)) #readonly-banner { margin-left: 0; }
  html[data-ort="tisch"]:has(#content .setup-seiten) .nav-titel { left: 36px; }
  html[data-ort="tisch"]:has(#game-view) .nav-titel { display: none; }
}

/* Kartentitel ohne Symbol - in keinem Entwurf steht eins, auch am Tisch nicht. */
.card-title > .msym:first-child,
.card-title > span:first-child > .msym:first-child { display: none; }

/* „Aktuelle Serien" (Tisch und Netz, dashboard.js::_serienKarte). Ein
   Dreierraster wie `#streaks-list` im TV: bis zu fuenf Kacheln, die vierte
   beginnt eine zweite Reihe. Vorher eine Flex-Reihe fuer drei. */
.serien-karte { padding: 20px 24px; }
.serien-karte .card-title { font-size: 18px; margin-bottom: 12px; }
.serien-kacheln { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.serie {
  min-width: 0; display: flex; flex-direction: column; gap: 2px;
  padding: 12px 14px; border-radius: 16px;
  background: var(--md-sys-color-surface-container-high);
}
.serie strong { font-size: 24px; font-weight: 600; }
.serie-name { font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.serie-seit { font-size: 12px; color: var(--md-sys-color-on-surface-variant); }

/* ══ Neugestaltung · Statistiken (Entwurf WebStatistik, 2026-09-30) ════════ */
.st-paar { --grid-gap: 16px; align-items: start; }
.serien-zeile {
  display: flex; align-items: center; gap: 10px; min-height: 52px;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.sz-platz { width: 22px; flex: 0 0 22px; font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-variant-numeric: tabular-nums; }
.serien-zeile .streak-name { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
.sz-name { font-size: 15px; font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.serien-zeile .streak-label { font-size: 14px; font-weight: 600; color: var(--md-sys-color-on-surface); white-space: nowrap; }
html[data-ort="netz"] #streaks-content { --avatar-scale: 2.3; }   /* 14 x 2,3 = 32 px */
.av-kuerzel:not([style]) { width: 32px; height: 32px; }
.sb-top { display: grid; grid-template-columns: 36px minmax(0, 1fr) 70px 80px; align-items: center; }
.sb-top > span { min-height: 52px; display: flex; align-items: center; border-bottom: 1px solid var(--md-sys-color-outline-variant); font-size: 14px; }
.sb-top > .sb-kopf { min-height: 0; padding-bottom: 8px; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); }
.sb-spieler .player-link { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.sb-spieler .av-kuerzel:not([style]) { width: 28px; height: 28px; }
.sb-wert { font-weight: 700; }
.sb-platz, .sb-datum { color: var(--md-sys-color-on-surface-variant); }
.sb-datum { color: var(--md-sys-color-on-surface); }

/* ══ Neugestaltung · Seitenaktionen in der Titelzeile (2026-09-30) ══════════
   Der Entwurf setzt die Aktion einer Seite („+ Spieler", „Neues Turnier" …)
   rechts in die Titelzeile, vor Glocke und Konto. Eine KLASSE am Kopf der
   Ansicht und keine Regel fuer jeden ersten Kopf: manche Koepfe tragen Filter
   und gehoeren in den Inhalt. Nur aus dem Netz und ueber 600 px - dort steht
   die Titelzeile; darunter bleibt der Kopf im Fluss. */
@media (min-width: 601px) {
  html[data-ort="netz"] .page-header.kopf-aktionen {
    position: fixed; top: 24px; right: 140px; z-index: 102;
    margin: 0; min-height: 40px;
  }
}

/* ══ Neugestaltung · Spieler (Entwurf WebSpieler, 2026-09-30) ══════════════ */
html[data-ort="netz"] .sp-tabelle { --avatar-scale: 1.5; }   /* 24 x 1,5 = 36 px */
.sp-tabelle .sp-name { display: inline-flex; align-items: center; gap: 12px; }
.sp-tabelle .sp-name .player-link { display: flex; flex-direction: column; }
.sp-name-haupt { font-size: 15px; font-weight: 500; }
.sp-name-zusatz { font-size: 12px; color: var(--md-sys-color-on-surface-variant); }
.badge.badge-art, .badge.badge-guest {
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
}
.sp-tabelle td { font-size: 14px; }
.sp-tabelle .anm-zeit { font-size: 12px; }
.av-kuerzel[style] { font-size: 12px; }

/* Feste Spalten wie im Entwurf: Spieler und Anmeldung teilen sich den Rest,
   Art 130, Gruppen 140, Aktionen 250 (sieben Knoepfe zu 34 px ohne Abstand).
   Ohne Anmeldespalten (keine Benutzerverwaltung) sind es drei Spalten. */
html[data-ort="netz"] .sp-tabelle { table-layout: fixed; width: 100%; }
html[data-ort="netz"] .sp-tabelle th:nth-child(2) { width: 130px; }
html[data-ort="netz"] .sp-tabelle th:nth-child(4) { width: 140px; }
html[data-ort="netz"] .sp-tabelle th.spalte-aktionen { width: 270px; }
html[data-ort="netz"] .sp-tabelle td { vertical-align: middle; padding-top: 12px; padding-bottom: 12px; }
html[data-ort="netz"] .sp-tabelle .spalte-aktionen .list-item-actions { gap: 0; justify-content: flex-end; flex-wrap: nowrap; }
html[data-ort="netz"] .sp-tabelle .aktion.nur-symbol {
  width: 34px; height: 34px; min-width: 34px; min-height: 34px; padding: 0;
  border: none; background: transparent; color: var(--md-sys-color-on-surface);
}
html[data-ort="netz"] .sp-tabelle .aktion.nur-symbol .msym { font-size: 18px; }
.sp-tabelle .badge { white-space: nowrap; }
html[data-ort="netz"] .sp-tabelle td > div:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
html[data-ort="netz"] .sp-tabelle td:nth-child(4) { font-size: 13px; color: var(--md-sys-color-on-surface-variant); }

/* ══ Neugestaltung · die Spielerkarte (Entwurf Spielerkarte, 2026-09-30) ═══ */
.pk-kopf { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
.pk-kopf > img { width: 56px !important; height: 56px !important; margin: 0 !important; }
.pk-kuerzel { width: 56px !important; height: 56px !important; font-size: 18px !important;
              background: var(--md-sys-color-primary) !important; color: var(--md-sys-color-on-primary) !important; }
.pk-titel { flex: 1 1 160px; min-width: 0; }
.pk-name { font-size: 22px; font-weight: 600; }
.pk-unter { font-size: 13px; color: var(--md-sys-color-on-surface-variant); }
.pk-zu {
  width: 38px; height: 38px; flex: 0 0 38px; border-radius: 50%;
  border: 1px solid var(--md-sys-color-outline-variant); background: transparent;
  color: var(--md-sys-color-on-surface); display: grid; place-items: center; cursor: pointer;
}
.pk-zu .msym { font-size: 18px; }
#modal-box:has(.pk-kopf) > .modal-close { display: none; }
#modal-box:has(.pk-kopf) { border-radius: 28px; padding: 22px; }
#player-overlay-inner .card { padding: 16px 18px; }

/* ⚠️ Die Kacheln der Kiste „Gesamt" (`.sk-kacheln`/`.sk-kachel`/`.sk-l`) sind
   seit dem 2026-10-08 ENTFALLEN - die Kiste zeichnet ihre Werte seitdem als
   Pillenband (`.ps-band .vg-wert` weiter unten). `.bilanz` bleibt: `_statBar`
   zeichnet damit weiterhin den Bilanzbalken im Direktvergleich
   (`_vergleichHtml`, dort gegen einen ANDEREN Spieler - fuer einen Einzelnen
   gibt es nichts zu bilanzieren). */
.bilanz { display: flex; gap: 2px; height: 10px; margin: 0 0 12px; }
.bilanz > span:first-child { border-radius: var(--md-sys-shape-corner-extra-small) 0 0 var(--md-sys-shape-corner-extra-small); }
.bilanz > span:last-child  { border-radius: 0 var(--md-sys-shape-corner-extra-small) var(--md-sys-shape-corner-extra-small) 0; }
.bilanz > span:only-child  { border-radius: var(--md-sys-shape-corner-extra-small); }
.bilanz .b-sieg  { background: var(--md-sys-color-on-surface); }
.bilanz .b-nied  { background: var(--md-sys-color-on-surface-variant); }
.bilanz .b-remis { background: var(--md-sys-color-outline-variant); }
/* Das Band als EINE Reihe. */
.ps-band { display: flex; flex-wrap: wrap; gap: 16px 28px; border: none; padding: 0; }
.ps-band .ps-zeile { display: flex; border: none; padding: 0; min-height: 0; }
/* „Letzte 5" GEMESSEN am 2026-10-08: ohne eigene Hoehe blieb die Pille bei
   15 px statt der 28 px jeder Zahlenpille - die Kugeln sind kuerzer als eine
   Textzeile. 12px statt 9px, damit sie die groessere Pille auch fuellen und
   nicht darin verloren wirken. */
.ps-band .st-res { font-size: 12px; }
.ps-band .st-kette { gap: 6px; }
/* ⚠️ Seit dem 2026-10-08 steht JEDER Wert der Kiste „Gesamt" in einer Pille
   (`.vg-wert`, derselbe Baustein wie im Direktvergleich) statt im schlichten
   `.ps-wert`. `.vg-wert` traegt fuer sich genommen aber KEINEN Grund - nur
   `.ist-mehr` bekommt dort einen, wenn eine Spalte mehr hat als eine andere.
   Die Kiste „Gesamt" zeigt immer nur EINEN Spieler; `_vergleichZeilen`s
   `hoechste()` markiert bei weniger als zwei Werten nie etwas (nichts zum
   Vergleichen da) - ohne diese Regel waere hier also NIE eine Pille zu sehen,
   nur eingerueckter Fettdruck. Eigener, unbedingter Grund nur hier im Band:
   die Vergleichstabelle (`.vg-tabelle`) behaelt ihr Verhalten unveraendert.

   ⚠️ Dritter Anlauf (2026-10-08, Betreiber nach dem dritten Bild): „Wie vor
   dem grossen Umbau soll die Ueberschrift MIT in der Pille stehen." Die
   Pille ist seitdem selbst eine Spalte (`flex-direction: column`): oben die
   Beschriftung (klein, gedaempft - siehe `.ps-band .vg-wert > .text-dim`
   weiter unten), darunter der Wert. Vorher stand die Beschriftung AUSSERHALB
   der Pille, als Geschwister in `.ps-zeile` - das Problem, die Pille auf die
   Breite ihrer Beschriftung zu strecken (voriger Anlauf, `align-items:
   stretch` auf `.ps-zeile`), loest sich damit von selbst: Beschriftung und
   Wert sind jetzt EIN Inhalt derselben Pille, keine zwei Geschwister mehr,
   die man aufeinander abstimmen muesste.

   GEMESSEN: jede Pille mit eingebauter Beschriftung braucht mehr Hoehe als
   eine blosse Zahl - `min-height` von 34 auf 48 px angehoben, damit
   Beschriftungszeile und Wertzeile beide Luft haben.

   ⚠️ Vierter Anlauf (2026-10-08, Betreiber mit einem Referenzbild): „Mir
   gefallen die Pillen... noch immer nicht. Nimm bitte die Groesse und
   Positionieren von dem Screenshot als Vorlage." Zwei Aenderungen gegen die
   Form, NICHT gegen die Struktur (Beschriftung bleibt IN der Pille, siehe
   oben):
   - `border-radius` von der vollen Stadionform (`--md-sys-shape-corner-full`,
     geerbt von der Basisregel in der Vergleichstabelle) auf
     `--md-sys-shape-corner-medium` (12px) - ein abgerundetes Rechteck statt
     einer Pille im engeren Sinn. Nur hier ueberschrieben: die
     Vergleichstabelle (`.vg-tabelle .vg-wert`) behaelt ihre Stadionform.
   - `align-items` von `center` auf `flex-start` - linksbuendiger Text wie im
     Referenzbild, statt zentriert.
   - Grosszuegigeres Polster und eine groessere Mindestbreite, am Referenzbild
     nachgemessen (Screenshot-Vergleich, nicht nur Augenmass). */
.ps-band .vg-wert { flex-direction: column; align-items: flex-start; gap: 4px;
                     font-size: 16px; font-weight: 600; padding: 12px 18px;
                     min-height: 56px; min-width: 104px; box-sizing: border-box;
                     justify-content: center; border-radius: var(--md-sys-shape-corner-medium);
                     color: var(--md-sys-color-on-surface);
                     background: var(--md-sys-color-surface-container-high); }
.ps-band .vg-wert > .text-dim { font-size: 13px; font-weight: 400; }
/* Angstgegner und Opfer: zwei Spalten, Name gross. */
.duell-paar { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.duell-titel { font-size: 12px; color: var(--md-sys-color-on-surface-variant); margin-bottom: 4px; font-weight: 400; }
.duell-zeile { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 0; border: none; }
.duell-name { font-size: 17px; font-weight: 600; }
.duell-bilanz { font-size: 12px; color: var(--md-sys-color-on-surface-variant); }
/* Direkte Duelle: Name 70, Balken, Ergebnis 48. */
.h2h-liste { display: flex; flex-direction: column; gap: 10px; }
.h2h-zeile { display: flex; align-items: center; gap: 10px; }
.h2h-name { width: 70px; flex: 0 0 70px; font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.h2h-balken { flex: 1 1 auto; display: flex; height: 8px; border-radius: 4px; background: var(--md-sys-color-surface-container-high); overflow: hidden; }
.h2h-balken > span { border-radius: 4px; background: var(--md-sys-color-on-surface); }
.h2h-erg { width: 48px; flex: 0 0 48px; text-align: right; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }

/* Reiter "System" (2026-10-07): CPU-Kerne, RAM/Swap, Festplatte, Verzeichnis-
   Drilldown. Eigener Name und nicht .h2h-balken direkt wiederverwendet -
   eine spaetere Anpassung fuer Spielerkarten soll diesen Reiter nicht
   versehentlich mittreffen. Werte sind eine Kopie von .h2h-balken. */
.sys-balken { flex: 1 1 auto; display: flex; height: 8px; border-radius: 4px; background: var(--md-sys-color-surface-container-high); overflow: hidden; }
.sys-balken > span { border-radius: 4px; background: var(--md-sys-color-on-surface); }
/* Der Punkteverlauf als Saeulen. `padding-top` reserviert Platz FUER die
   Zahlen ueber Tief/Hoch/Jetzt (2026-10-08) - er zieht sich von der
   Hoehenformel in spielerkarte.js ab (globales `box-sizing: border-box`,
   Zeile 11), die Prozent-Rechnung selbst bleibt unangetastet (die pruefen
   Tests woertlich). Ohne das Polster saesse die Zahl der hoechsten Saeule
   (90 % Hoehe) auf der Kante der Karte. */
.ps-saeulen { display: flex; align-items: flex-end; gap: 3px; height: 110px; margin-bottom: 8px; padding-top: 18px; }
.ps-saeule { position: relative; flex: 1 1 0; border-radius: 4px; background: var(--md-sys-color-surface-container-high); }
.ps-saeule.ist-jetzt { background: var(--md-sys-color-on-surface); }
/* ⚠️ NICHT `.ps-wert` - die Klasse gibt es schon (Kiste "Gesamt", Zeile
   5858), mit 700er Gewicht und 1.15rem fuer eine ganz andere Stelle. Eine
   zweite Bedeutung unter demselben Namen waere hier eine Falle fuer sich. */
/* ⚠️ `on-surface`, NICHT `on-surface-variant` (2026-10-08, Sichtpruefung):
   gemessener Kontrast von `on-surface-variant` gegen die Kartenflaeche war
   2.81:1 hell / 2.29:1 dunkel - unter den geforderten 4.5:1. */
.ps-saeule-zahl { position: absolute; left: 50%; bottom: 100%; transform: translateX(-50%);
                  margin-bottom: 2px; font-size: 10px; font-weight: 600; white-space: nowrap;
                  color: var(--md-sys-color-on-surface); }
.ps-unterschrift { font-size: 12px; color: var(--md-sys-color-on-surface-variant); }
.kt-spanne { display: none; }

.duell-paar .duell-titel { text-transform: none; letter-spacing: 0; }
.duell-paar .duell-gruppe { margin: 0; padding: 0; border: none; }
#modal-box:has(.pk-kopf) { background: var(--md-sys-color-surface-container-lowest); }
html[data-theme="dark"] #modal-box:has(.pk-kopf) { background: var(--md-sys-color-surface-container); }
#modal-box:has(.pk-kopf) .card { border: 1px solid var(--md-sys-color-outline-variant); }

/* ══ Neugestaltung · Turniere (Entwurf WebTurniere/WebTurnierDetail) ═══════ */
.t-zeile {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  max-width: 980px; margin-bottom: 10px; padding: 14px 16px;
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 16px;
  background: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"] .t-zeile { background: var(--md-sys-color-surface-container); }
.t-pokal .msym { font-size: 18px; }
.t-zeile-name { font-size: 16px; font-weight: 600; color: var(--md-sys-color-on-surface); text-decoration: none; }
.t-luecke { flex: 1 1 auto; }
.badge.t-marke { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant); }
.badge.t-marke-aktiv { background: transparent; color: var(--md-ext-color-success); border: 1px solid currentColor; }
.badge.t-marke-standard { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); font-weight: 600; }
.t-kopfzeile { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.t-kopfzeile .t-laufzeit { margin: 0; }
/* Der Schalter „Aktiv" im Kopf (2026-10-01): Beschriftung und Schalter in einer
   Zeile, ohne den oberen Abstand, den `.toggle-label` in einer Formularzeile hat. */
.t-aktiv { display: inline-flex; align-items: center; gap: 10px; }
.t-aktiv .toggle-label { padding-top: 0; }
/* Zurueck vor dem Titel: aus dem Netz steht der Titel oben im Kopf, der Knopf
   gehoert davor - der Titel rueckt dann um 52 px. */
@media (min-width: 601px) {
  html[data-ort="netz"] .kopf-zurueck { position: fixed; top: 24px; left: 284px; z-index: 102; }
  /* Ein Kopf, der nur noch Titel und den festen Zurueck-Knopf traegt, nimmt
     keinen Platz (derselbe Gedanke wie beim Kopf nur mit Titel). */
  html[data-ort="netz"] #content .page-header:has(> .kopf-zurueck):not(:has(> :not(.page-title):not(.kopf-zurueck))) {
    height: 0; min-height: 0; margin: 0; padding: 0; border: none;
  }
  html[data-ort="netz"]:has(#content .kopf-zurueck) .nav-titel { left: 336px; }
}
@media (min-width: 601px) and (max-width: 840px) {
  html[data-ort="netz"] .kopf-zurueck { left: 100px; }
  html[data-ort="netz"]:has(#content .kopf-zurueck) .nav-titel { left: 152px; }
}
.t-bew-system { display: flex; flex-direction: column; gap: 6px; max-width: 360px; }
.t-bewertung .pv-abschnitt { margin-top: 14px; }
.t-bewertung .pv-abschnitt > div:last-child .btn-primary { margin-left: auto; }
.t-spieler { font-size: 15px; font-weight: 500; }
#tab-standings .standing-rank { color: var(--md-sys-color-on-surface-variant); font-weight: 400; }
#tab-standings .standing-pts { font-weight: 600; }
#tab-standings .standing-share { color: var(--md-sys-color-on-surface-variant); }

/* ══ Neugestaltung · Mein Profil (Entwurf WebProfil, 2026-09-30) ══════════ */
.pf-raster { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
@media (min-width: 841px) { .pf-raster { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.pf-spalte { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.pf-spalte > .card { margin: 0; }
.pf-bild-knoepfe { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pf-bild-hinweis { margin-top: 10px; font-size: 13px; }
#pf-avatar.avatar-circle, #pf-avatar-wrap img.avatar-circle { width: 88px; height: 88px; }
.pf-paar { --grid-gap: 14px; }
.pf-fuss { display: flex; justify-content: flex-end; margin-top: 4px; }
.pf-sitzung { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.pf-sitzung p { margin: 0; flex: 1 1 200px; }

/* ══ Neugestaltung · Kalender (Entwurf WebKalender, 2026-09-30) ═══════════ */
.kal-kopf { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.kal-kopf .kal-monat { flex: 1 1 auto; margin: 0; font-size: 17px; font-weight: 600; }
.kal-kopf .kal-fuss { margin: 0; gap: 6px; }
.kal-kopf .kal-steuerung { gap: 6px; }
.kal-kopf .chip { height: 32px; }
.kal-kopf .kal-steuerung .chip:first-child, .kal-kopf .kal-steuerung .chip:last-child { width: 32px; padding: 0; justify-content: center; border-radius: 50%; }
.kal-raster { gap: 6px; }
.kal-wochentag { text-align: left; padding: 0 10px 4px; font-size: 12px; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--md-sys-color-on-surface-variant); }
.kal-tag, .kal-leer {
  min-height: 74px; padding: 10px; border-radius: 14px; border: none;
  display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between;
  text-align: left; font: inherit;
}
.kal-tag { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }
.kal-tag.is-leer { background: transparent; }
.kal-tag .kal-zahl { font-size: 15px; font-weight: 600; }
.kal-tag.is-leer .kal-zahl { font-weight: 400; }
.kal-tag .kal-anzahl {
  background: none; color: var(--md-sys-color-on-surface-variant); min-width: 0; height: auto; padding: 0;
  font-size: 11px; font-weight: 400; border-radius: 0;
}
.kal-tag.is-heute { box-shadow: inset 0 0 0 1px var(--md-sys-color-on-surface); border: none; }
.kal-tag.is-gewaehlt { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.kal-tag.is-gewaehlt .kal-anzahl { color: var(--md-sys-color-on-primary); font-weight: 600; background: none; }
/* Ein Kopf, der nur die (im Netz ausgeblendete) Ueberschrift traegt, nimmt
   keinen Platz mehr - der Titel steht im Kopf der Seite (2026-09-30). */
@media (min-width: 601px) {
  html[data-ort="netz"] #content .page-header:not(:has(> :not(.page-title))) { display: none; }
}
/* ══ Neugestaltung · Was ist neu? (Entwurf WebNeuigkeiten) ═══════════════ */
#content:has(.nk-liste) .card { max-width: 860px; }
.nk-liste { margin: 0 0 14px; padding-left: 18px; font-size: 14px; }
.nk-liste li { margin: 0 0 6px; }
#content:has(.nk-liste) .card .card-title { font-size: 13px; font-weight: 600; margin: 10px 0 8px; }
#content:has(.nk-liste) .card > h3 { font-size: 17px; margin-bottom: 8px; }

/* ══ Neugestaltung · TV (Entwurf WebTV, 2026-09-30) ═══════════════════════ */
.card-unterzeile { margin: -8px 0 12px; font-size: 13px; color: var(--md-sys-color-on-surface-variant); }
.kanal-raster { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; margin-bottom: 12px; }
.kanal-raster .kanal-zeile {
  display: flex; flex-direction: column; align-items: stretch; gap: 12px;
  padding: 14px 16px; border: 1px solid var(--md-sys-color-outline-variant); border-radius: 16px;
}
.kanal-raster .kanal-zeile + .kanal-zeile { border-top: 1px solid var(--md-sys-color-outline-variant); }
.kanal-raster .kanal-name { font-size: 16px; font-weight: 600; gap: 8px; }
.kanal-raster .kanal-aktion { display: flex; gap: 10px; align-items: stretch; }
/* Die Adresse steht wieder sichtbar da (Entwurf WebTV) - ausgeblendet war
   sie seit dem 2026-09-04 („die URLs … sollen nicht angezeigt werden"),
   hinfaellig mit der Neugestaltung. */
.kanal-raster .kanal-aktion .form-input {
  position: static; width: auto; height: auto; min-height: 44px; padding: 0 14px;
  opacity: 1; pointer-events: auto;
  display: block; flex: 1 1 auto; min-width: 0; border: none; border-radius: 12px;
  background: var(--md-sys-color-surface-container-high);
  font-family: var(--md-ref-typeface-mono); font-size: 14px;
}
.kanal-raster .kanal-aktion .aktion { border: 1px solid var(--md-sys-color-outline-variant); border-radius: 12px; }
.tv-highlights { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
.tv-hl {
  display: flex; flex-direction: column; padding: 0; overflow: hidden;
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 16px;
  background: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"] .tv-hl { background: var(--md-sys-color-surface-container); }
.tv-hl-ziel { display: flex; flex-direction: column; gap: 2px; text-decoration: none; color: var(--md-sys-color-on-surface); }
.tv-hl-bild {
  position: relative; height: 120px; margin-bottom: 12px;
  background: #2D5A3A;
  display: grid; place-items: center;
}
.tv-hl-bild::after { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: rgba(255, 255, 255, .25); }
.tv-hl .tv-hl-titel {
  position: absolute; left: 10px; top: 10px; z-index: 1;
  padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 600;
  text-transform: none; letter-spacing: 0;
  background: rgba(0, 0, 0, .7); color: #fff;
}
.tv-hl-play {
  position: relative; z-index: 1; width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center; background: rgba(0, 0, 0, .6); color: #fff;
}
.tv-hl-play .msym { font-size: 20px; }
.tv-hl-tat { padding: 0 14px; font-size: 15px; font-weight: 600; color: var(--md-sys-color-on-surface); }
.tv-hl .tv-hl-name { padding: 0 14px; font-size: 12px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
.tv-hl-aktionen { display: flex; gap: 6px; padding: 12px 14px 14px; }
html .tv-hl-aktionen .aktion.btn {
  min-height: 28px; height: 28px; padding: 0 10px; border-radius: 999px;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
  font-size: 12px;
}
html .tv-hl-aktionen .aktion.btn.echo-an { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }

/* ══ Neugestaltung · Vorschau (Entwurf WebVorschau, 2026-09-30) ═══════════
   Kleine, umrandete Spielerkacheln, fuenf je Reihe; die gewaehlte in der
   Farbe ihrer Tischseite; ein Farbpunkt vor „Spieler 1" / „Spieler 2". Nur
   hier und nur aus dem Netz - `.player-card` teilt sich die Vorschau mit dem
   Spielaufbau am Tisch, und der hat seinen eigenen Entwurf. */
#pv-t1-titel::before, #pv-t2-titel::before {
  content: ''; display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 4px;
}
/* Figurenfarbe + Kontur-Ring, siehe `.sd-punkt` oben. */
#pv-t1-titel::before, #pv-t2-titel::before { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
#pv-t1-titel::before { background: var(--md-ext-color-team1); }
#pv-t2-titel::before { background: var(--md-ext-color-team2); }
html[data-ort="netz"] [id^="pv-grid-"].player-grid {
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 8px; --avatar-scale: 1.4;
}
html[data-ort="netz"] [id^="pv-grid-"] .player-card {
  min-height: 0; padding: 12px 6px 10px; gap: 6px;
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 14px;
  background: var(--md-sys-color-surface-container-lowest); box-shadow: none;
}
html[data-theme="dark"][data-ort="netz"] [id^="pv-grid-"] .player-card { background: var(--md-sys-color-surface-container); }
html[data-ort="netz"] [id^="pv-grid-"] .player-card .player-name { font-size: 14px; font-weight: 500; }
html[data-ort="netz"] #pv-grid-t1 .player-card.selected {
  border-color: var(--md-ext-color-team1); background: var(--md-ext-color-team1-container);
  box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset;
}
html[data-ort="netz"] #pv-grid-t2 .player-card.selected {
  border-color: var(--md-ext-color-team2); background: var(--md-ext-color-team2-container);
  box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset;
}

/* ══ Neugestaltung · Verlauf (Entwurf WebVerlauf, 2026-09-30) ═════════════ */
.vl-reihe { min-height: 58px; }
.vl-paarung { font-size: 15px; font-weight: 500; }
.vl-tat { font-size: 12px; }
html .vl-aktionen .aktion.btn {
  min-height: 28px; height: 28px; padding: 0 9px; gap: 4px; border-radius: 999px;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); font-size: 12px;
}
html .vl-aktionen .aktion.btn .msym { font-size: 14px; }
html .vl-aktionen .aktion.btn.vl-spielen {
  width: 30px; min-width: 30px; height: 30px; padding: 0; border-radius: 50%;
  background: transparent; border: 1px solid var(--md-sys-color-outline-variant);
}
html .vl-aktionen .aktion.btn.echo-an { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }

/* Symbolknoepfe in der Titelzeile (Anordnen …): 40 px, Ecke 12, Haarlinie. */
html .kopf-aktionen .aktion.btn.nur-symbol {
  width: 40px; min-width: 40px; height: 40px; min-height: 40px; padding: 0; border-radius: 12px;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
}
html[data-theme="dark"] .kopf-aktionen .aktion.btn.nur-symbol { background: var(--md-sys-color-surface-container); }
html .kopf-aktionen .aktion.btn.nur-symbol .msym { font-size: 18px; }

/* ══ Neugestaltung · Einstellungen (Entwuerfe WebEinst*, 2026-09-30) ═══════
   Aufs Stylesheet gelegt und nicht ins Markup: die zwoelf Reiter tragen
   Kennungen, Sammelwege (`_gatherCfg`) und Einmal-Sperren, an denen
   Dutzende Zusicherungen haengen. Verbindlich ist das Aussehen.

   * Die Karte wird eine umbrechende Reihe: Titel, Absaetze, Schalterzeilen
     stehen voll breit, die SCHMALEN Felder (die bisher `max-width: 280px`
     trugen) paarweise nebeneinander - das Zweierraster des Entwurfs.
   * Die Erklaerung unter dem Titel ist die kleine Unterzeile des Entwurfs
     (13 px, Nebentext); gekuerzt wird nicht - sie traegt die Einschraenkungen
     („nur am Tisch", „unwiderruflich").
   * Keine Symbole vor Beschriftungen und Schalterzeilen.
   * „Einstellungen speichern" rechts, nicht ueber die volle Breite.
   * Die Gefahrenzone mit rotem Rahmen, der Titel in Textfarbe. */
#content:has(#settings-tabs) .card,
#content:has(#konto-tabs) .card { display: flex; flex-wrap: wrap; column-gap: 14px; align-items: flex-start; }
#content:has(#settings-tabs) .card > *,
#content:has(#konto-tabs) .card > * { flex: 1 1 100%; min-width: 0; }
#content:has(#settings-tabs) .card > .form-group.field[style*="max-width"],
#content:has(#konto-tabs) .card > .form-group.field[style*="max-width"] {
  flex: 1 1 calc(50% - 7px); max-width: none !important;
}
#content:has(#settings-tabs) .card > .card-title + .text-dim,
#content:has(#settings-tabs) .card > .card-title + p.text-dim,
#content:has(#konto-tabs) .card > .card-title + .text-dim,
#content:has(#konto-tabs) .card > .card-title + p.text-dim {
  margin-top: -8px; margin-bottom: 14px; font-size: 13px;
}
#content:has(#settings-tabs) .toggle-label > .msym,
#content:has(#settings-tabs) .form-label > .msym,
#content:has(#konto-tabs) .toggle-label > .msym,
#content:has(#konto-tabs) .form-label > .msym { display: none; }
#btn-save-settings { display: flex; width: auto; margin: 4px 0 0 auto; min-height: 46px; }
#btn-save-settings > .msym { display: none; }
#btn-save-settings::before { content: 'check'; font-family: 'Material Symbols'; font-size: 18px; }
#content:has(#settings-tabs) .card[style*="var(--red)"] { border-color: var(--md-sys-color-error) !important; }
#content:has(#settings-tabs) .card[style*="var(--red)"] > .card-title { color: var(--md-sys-color-on-surface) !important; }
#content:has(#settings-tabs) .card > .text-dim, #content:has(#settings-tabs) .card > p.text-dim,
#content:has(#konto-tabs) .card > .text-dim, #content:has(#konto-tabs) .card > p.text-dim { font-size: 13px; }

/* ══ Neugestaltung · Messenger (Entwurf WebNachrichten, 2026-09-30) ═════════
   Keine Karten mehr: Liste und Gespraech sind Spalten, getrennt durch eine
   Haarlinie; der Titel „Messenger" steht in der Liste. Die Klassen bleiben -
   an `.nr-raster`, `.nr-n` und `.nr-online` haengen Zusicherungen.

   Aus dem Netz nimmt die Ansicht die volle Fensterhoehe (der Entwurf hat
   hier keinen Seitenkopf): Titel, Glocke und Konto blenden sich aus, die
   Glocke bleibt auf jeder anderen Seite. ⚠️ Die Hoehe ueber ein ortsfestes
   `--nav-h: -48px` und keine eigene Rechnung: die Grundregel zieht
   `--nav-h` und 48 px ab, und jede neue Division durch `--ui-zoom` waere
   gezaehlt (test_zoom_math_untouched). -48 + 48 = volle Hoehe. */
.nr-raster { gap: 0; }
.nr-liste, .nr-gespraech { border: none; border-radius: 0; background: transparent; }
.nr-liste { border-right: 1px solid var(--md-sys-color-outline-variant); }
.nr-kopf {
  display: flex; align-items: center; gap: 10px;
  padding: 24px 20px 12px; border-bottom: none;
}
.nr-titel { margin: 0; flex: 1; font-size: 26px; font-weight: 600; letter-spacing: -0.01em; }
.nr-kopf .btn.nr-neu { min-height: 38px; height: 38px; padding: 0 14px; border-radius: 12px; font-weight: 600; }
.nr-eintraege { display: flex; flex-direction: column; gap: 2px; padding: 0 12px 12px; }
.nr-eintrag { padding: 10px; gap: 12px; border-radius: 14px; border-bottom: none; }
.nr-eintrag:hover { background: var(--md-sys-color-surface-container); }
.nr-eintrag.is-offen { background: var(--md-sys-color-surface-container-high); }
.nr-av { position: relative; flex: 0 0 auto; display: inline-flex; }
.nr-avatar.is-platzhalter {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  border-radius: 50%; font-weight: 600; letter-spacing: .02em;
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
}
.nr-avatar.nr-gruppe .msym { font-size: 20px; }
/* Der Punkt sitzt AUF dem Bild, unten rechts, mit einem Ring in der Farbe
   des Grundes (Entwurf). */
.nr-av .nr-online { position: absolute; right: -1px; bottom: -1px; margin: 0; }
.nr-av .nr-online .status-dot {
  width: 11px; height: 11px; border: 2px solid var(--md-sys-color-surface);
  box-sizing: content-box; background: var(--md-ext-color-success);
}
.nr-e-z1, .nr-e-z2 { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
.nr-e-z2 { margin-top: 2px; }
.nr-e-name { font-size: 15px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nr-e-zeit { flex: 0 0 auto; font-size: 12px; color: var(--md-sys-color-on-surface-variant); }
.nr-e-letzt { flex: 1; min-width: 0; font-size: 13px; color: var(--md-sys-color-on-surface-variant); }
.nr-punkt { font-size: 11px; font-weight: 600; }

.nr-g-kopf { padding: 18px 24px; gap: 12px; }
.nr-g-kopf .nr-g-bild, .nr-g-kopf .nr-avatar { width: 40px; height: 40px; }
.nr-g-titelblock { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.nr-g-titel { font-size: 17px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nr-g-unter { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--md-sys-color-on-surface-variant); }
.nr-g-unter .nr-online { display: inline-flex; align-items: center; gap: 4px; color: var(--md-ext-color-success); }
.nr-g-unter .nr-online::before { content: '·'; color: var(--md-sys-color-on-surface-variant); margin-right: 2px; }
.nr-g-aktionen { gap: 8px; }
.nr-g-aktionen .btn {
  width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0;
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 12px;
  background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface);
}
.nr-g-aktionen .btn .msym { font-size: 18px; }

.nr-verlauf { padding: 20px 28px; gap: 10px; }
.nr-system {
  padding: 4px 12px; border-radius: 999px; font-size: 12px;
  background: var(--md-sys-color-surface-container-high);
}
.nr-reihe { display: flex; align-items: flex-start; gap: 10px; align-self: flex-start; max-width: 70%; }
.nr-reihe > .nr-avatar { margin-top: 18px; }
.nr-reihe > .nr-n { max-width: none; min-width: 0; }
.nr-n {
  max-width: 70%; padding: 0; border-radius: 0;
  background: none; color: inherit;
}
.nr-n.is-meins { background: none; color: inherit; }
.nr-n-von { font-size: 12px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); margin: 0 0 3px 4px; }
.nr-n-blase {
  display: block; padding: 10px 14px; font-size: 15px; line-height: 1.4;
  border-radius: 18px 18px 18px 6px;
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-outline-variant);
}
html[data-theme="dark"] .nr-n-blase { background: var(--md-sys-color-surface-container); }
.nr-n.is-meins .nr-n-blase {
  border-radius: 18px 18px 6px 18px; border-color: transparent;
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
}
.nr-n-zeit { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin: 3px 4px 0; font-size: 11px; text-align: left; }
.nr-n.is-meins .nr-n-zeit { justify-content: flex-end; }
.nr-n-zeit .nr-n-aktionen { display: inline-flex; gap: 0; position: static; margin: 0; }
html .nr-n-zeit .aktion.btn.nr-meta-aktion {
  min-height: 0; height: auto; min-width: 0; padding: 0; gap: 0;
  border: none; border-radius: 0; background: none; box-shadow: none;
  font-size: 11px; font-weight: 400; color: var(--md-sys-color-on-surface-variant);
}
html .nr-n-zeit .aktion.btn.nr-meta-aktion .msym { display: none; }
html .nr-n-zeit .aktion.btn.nr-meta-aktion::before { content: '·'; margin: 0 4px; }
html .nr-n-zeit .aktion.btn.nr-meta-aktion:hover { color: var(--md-sys-color-on-surface); text-decoration: underline; }
.nr-n.is-zurueck .nr-n-text {
  display: block; padding: 10px 14px; border-radius: 18px;
  border: 1px dashed var(--md-sys-color-outline-variant);
  font-size: 14px; font-style: italic; color: var(--md-sys-color-on-surface-variant);
}
.nr-n.is-zurueck, .nr-n.is-meins.is-zurueck { background: none; border: none; }
.nr-n-blase .nr-zitat {
  margin-bottom: 6px; padding: 6px 10px; border-radius: 10px; border: none;
  background: rgba(127, 127, 127, .22); color: inherit; font-size: 13px;
}
.nr-n-zitieren {
  padding: 3px; border-radius: 12px;
  border: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-lowest);
}
.nr-n-zitieren .aktion.btn { width: 30px; height: 30px; min-height: 30px; border-radius: 9px; }
/* Neben der Blase, mittig zu ihr (Entwurf) - und am Rechner erst beim
   Drueberfahren oder mit dem Fokus; ohne Zeiger (Telefon, Tisch) immer. */
.nr-n > .nr-n-zitieren { bottom: auto; top: 50%; transform: translateY(-50%); left: calc(100% + 8px); }
.nr-n.is-meins > .nr-n-zitieren { left: auto; right: calc(100% + 8px); }
@media (hover: hover) {
  .nr-n > .nr-n-zitieren { opacity: 0; transition: opacity .12s; }
  .nr-n:hover > .nr-n-zitieren, .nr-n:focus-within > .nr-n-zitieren { opacity: 1; }
}
.nr-senden #nr-eingabe { border: none; outline: none; background: transparent; box-shadow: none; }

.nr-eingabe { padding: 0 24px 20px; }
.nr-eingabe .nr-aendern {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 8px; padding: 8px 12px; border: none; border-radius: 12px;
  background: var(--md-sys-color-surface-container-high);
  font-size: 13px; color: var(--md-sys-color-on-surface);
}
.nr-senden {
  align-items: center; gap: 8px;
  padding: 6px 6px 6px 14px;
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 18px;
  background: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"] .nr-senden { background: var(--md-sys-color-surface-container); }
.nr-senden .form-input {
  border: none; background: transparent; box-shadow: none;
  min-height: 38px; height: 38px; padding: 0; font-size: 15px;
}
.nr-senden .em-knopf { min-width: 28px; width: 28px; height: 28px; min-height: 28px; padding: 0; color: var(--md-sys-color-on-surface-variant); }
.nr-senden .em-knopf .msym { font-size: 20px; }
.nr-senden .btn-primary.nr-senden-knopf {
  min-height: 38px; height: 38px; padding: 0 16px; gap: 8px;
  border-radius: 13px; font-weight: 600;
}
.nr-senden-knopf .msym { font-size: 16px; }

@media (min-width: 601px) {
  html[data-ort="netz"] #content:has(> .nr-raster) { margin-top: 0; padding: 0; }
  html[data-ort="netz"] #content > .nr-raster { --nav-h: -48px; grid-template-columns: 340px minmax(0, 1fr); }
  html[data-ort="netz"]:has(#content > .nr-raster) .nav-titel,
  html[data-ort="netz"]:has(#content > .nr-raster) .nav-bell,
  html[data-ort="netz"]:has(#content > .nr-raster) #account-btn,
  html[data-ort="netz"]:has(#content > .nr-raster) #navbar::after { display: none; }
}
/* Am Telefon der runde Senden-Knopf. Die Worte „Bearbeiten · Zurückrufen"
   bleiben - der Entwurf MobilChat zeigt sie auch bei 390 px, und sie passen,
   weil sie in der Zeitzeile unter der Blase stehen und nicht in ihr. */
@media (max-width: 600px) {
  .nr-senden-wort { display: none; }
  .nr-senden .btn-primary.nr-senden-knopf { width: 38px; padding: 0; border-radius: 50%; }
  .nr-kopf { padding: 12px 8px 8px; }
  .nr-titel { font-size: 22px; }
  .nr-eintraege { padding: 0 0 12px; }
  .nr-g-kopf { padding: 10px 4px; }
  .nr-verlauf { padding: 12px 4px; }
  .nr-eingabe { padding: 0 0 8px; }
  .nr-reihe { max-width: 85%; }
  .nr-n { max-width: 85%; }
}

/* ══ Neugestaltung · Chatfenster (Entwurf WebChatfenster, 2026-09-30) ══════
   Nur das Aussehen; Griff, Minimieren, Maximieren, Zitieren und Bearbeiten
   bleiben - der Entwurf zeigt den Ruhezustand. Der Kopf traegt die Flaeche
   des Fensters und eine Trennlinie statt der Tonflaeche; der Online-Punkt
   steht VOR dem Namen. Das Polster des Kopfes bleibt 4 px (siehe oben), die
   48 px kommen ueber die Mindesthoehe. Offen 340 px, eingeklappt 240 px. */
.cd-fenster {
  width: clamp(16em, var(--cd-b, 340px), min(40em, calc(100vw - 40px)));
  background: var(--md-sys-color-surface-container-lowest);
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -6px 24px rgba(0, 0, 0, .14);
}
html[data-theme="dark"] .cd-fenster { background: var(--md-sys-color-surface-container); }
.cd-fenster.is-min { width: 240px; box-shadow: 0 -6px 24px rgba(0, 0, 0, .12); }
.cd-kopf {
  min-height: 48px; gap: 8px; padding: 4px 8px 4px 32px;
  background: transparent; color: var(--md-sys-color-on-surface);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.cd-fenster.is-min .cd-kopf { border-bottom: none; padding-left: 14px; }
.cd-kopf .cd-online { margin-right: 0; }
.cd-titel { font-size: 14px; font-weight: 600; }
.cd-knopf { color: var(--md-sys-color-on-surface-variant); }
.cd-verlauf { height: clamp(8em, var(--cd-h, 250px), 34em); padding: 12px; }
.cd-von { margin-left: 4px; }
.cd-text { padding: 8px 12px; font-size: 14px; border-radius: 16px 16px 16px 5px; }
.cd-n.ich .cd-text { border-radius: 16px 16px 5px 16px; }
.cd-senden { gap: 6px; border-top: 1px solid var(--md-sys-color-outline-variant); }
.cd-senden .form-input,
.cd-senden input.form-input:focus {
  border: none; outline: none; background: transparent; box-shadow: none;
  min-height: 34px; padding: 0 4px; font-size: 13px;
}
.cd-senden .em-knopf .msym { font-size: 18px; }

/* ══ Neugestaltung · Dialoge, Toasts, Nur-Lese-Hinweis (Entwurf WebDialoge,
   2026-09-30) ═════════════════════════════════════════════════════════════
   Der Dialog in der Kartenflaeche, 24/26 Polster, ein tiefer Schatten; der
   Hintergrund dunkler und OHNE Weichzeichner (hell .45, dunkel .62) - der
   Weichzeichner kostete ausserdem am Tisch Bildrate.
   Die Rueckfrage (`App.ui.frage`) wird ueber ihre Knopfkennungen gefunden
   und nicht ueber eine neue Klasse: `app.js` liegt im Eager-Budget. Sie hat
   kein Kreuz - Escape und der Klick daneben bleiben „Nein". */
#modal-overlay { background: rgba(20, 20, 22, .45); backdrop-filter: none; }
html[data-theme="dark"] #modal-overlay { background: rgba(0, 0, 0, .62); }
#modal-box {
  background: var(--md-sys-color-surface-container-lowest);
  padding: 24px 26px; max-width: 560px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, .28);
  --field-surface: var(--md-sys-color-surface-container-lowest);
}
html[data-theme="dark"] #modal-box {
  background: var(--md-sys-color-surface-container);
  --field-surface: var(--md-sys-color-surface-container);
}
.modal-title {
  display: flex; align-items: center; gap: 10px;
  font-size: 20px; line-height: 28px; font-weight: 600; margin-bottom: 16px;
  padding-right: 40px;
}
.modal-title .msym { font-size: 22px; }
.modal-close {
  top: 20px; right: 20px; width: 36px; height: 36px;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
}
#modal-box .btn { border-radius: 12px; }
#modal-box:has(#fr-ja) { max-width: 460px; }
#modal-box:has(#fr-ja) > .modal-close { display: none; }
#modal-box:has(#fr-ja) .modal-title { margin-bottom: 0; padding-right: 0; }
#modal-box:has(#fr-ja) .modal-title + div:not(:has(#fr-ja)) {
  margin: 16px 0 20px !important; font-size: 15px;
  color: var(--md-sys-color-on-surface-variant);
}
#modal-box div:has(> #fr-ja) {
  display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px !important;
  margin-top: 16px;
}
#modal-box div:has(> #fr-ja) .btn { min-height: 48px; height: 48px; }
#modal-box div:has(> #fr-ja) #fr-ja { font-weight: 600; }
/* Die Gefahr gefuellt in Rot (Entwurf: „Ja, löschen" auf --live). */
#modal-box div:has(> #fr-ja) #fr-ja.btn-danger {
  background: var(--md-sys-color-error); color: var(--md-sys-color-on-error); border-color: transparent;
}

.toast {
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
  border-left: none; border-radius: 16px;
  min-height: 52px; padding: 0 18px; gap: 12px; font-size: 15px;
  max-width: min(380px, calc(100vw - 40px));
  box-shadow: 0 10px 30px rgba(0, 0, 0, .2);
}
.toast .msym { font-size: 20px; flex: 0 0 auto; }
.toast.success, .toast.error, .toast.warning,
html[data-theme="dark"] .toast.success, html[data-theme="dark"] .toast.error,
html[data-theme="dark"] .toast.warning { border-left: none; }

.readonly-banner {
  padding: 12px 18px; gap: 12px; border-radius: 16px;
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline-variant);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .12);
  font-size: 14px;
}
html[data-theme="dark"] .readonly-banner { background: var(--md-sys-color-surface-container); }
.readonly-banner .btn { min-height: 36px; height: 36px; padding: 0 14px; border-radius: 12px; }
.readonly-banner .banner-close {
  width: 36px; height: 36px; border-radius: 50%; border: none;
  background: transparent; color: var(--md-sys-color-on-surface-variant);
}

/* ══ Neugestaltung · Formulardialog (Entwurf WebDialoge, 2026-09-30) ══════
   Beschriftung ueber dem Feld (44 px, Ecke 12), zwei Felder je Zeile,
   Gruppen als Umschaltchips, der Hauptknopf rechts unten. */
#modal-box .form-group:not(.field) > .form-label {
  position: static; display: block; margin: 0 0 6px; transform: none;
  font-size: 13px; font-weight: 500; color: var(--md-sys-color-on-surface-variant);
}
#modal-box .form-group:not(.field) > .form-input {
  min-height: 44px; height: 44px; border-radius: 12px; font-size: 15px;
  border: 1px solid var(--md-sys-color-outline-variant);
}
.form-zeile2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.chip-wahl-reihe { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-wahl {
  display: inline-flex; align-items: center; height: 32px; padding: 0 14px;
  border-radius: 999px; border: 1px solid var(--md-sys-color-outline-variant);
  font-size: 14px; font-weight: 500; cursor: pointer; user-select: none;
}
.chip-wahl > input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.chip-wahl:has(> input:checked) {
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
  border-color: transparent; font-weight: 600;
}
.chip-wahl:has(> input:focus-visible) { outline: 2px solid var(--md-sys-color-primary); outline-offset: 2px; }
.dlg-fuss { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; }
.dlg-fuss .btn { min-height: 46px; height: 46px; padding: 0 18px; gap: 8px; font-weight: 600; }
#modal-box .text-dim.text-sm { font-size: 13px; }
@media (max-width: 600px) { .form-zeile2 { grid-template-columns: 1fr; gap: 0; } }

/* ══ Neugestaltung · das beendete Spiel (Entwurf WebSpielDetail, 2026-09-30)
   Die Kopfkarte wird flach: eine kleine Siegerzeile, die Seiten als ZEILE
   (Name und Abrechnung neben einem 72-px-Bild in der Farbe der Seite), der
   Stand 64 px, darunter Beginn und Spielzeit in einer Zeile; die Aktionen
   unter einer Haarlinie, „Spiel löschen" rechts. Die Groesse haengt an
   `.fg-seite` und nicht an `.team-players-big` - die laufende Partie teilt
   sich den Baustein und behaelt ihre grossen Bilder. */
.fg-kopf { padding: 24px 28px 18px; border-radius: 24px; }
.fg-sieger {
  margin: 0; gap: .25em; font-size: 15px; line-height: 22px; font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
}
.fg-sieger strong { color: var(--md-sys-color-on-surface); font-weight: 600; }
.fg-buehne {
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: 0; margin-top: 14px;
}
.fg-mitte { padding: 0 40px; gap: 6px; }
.fg-kopf .fg-stand { font-size: 64px; line-height: 1; font-weight: 600; letter-spacing: -.03em; padding: 0; }
.fg-kopf .fg-stand .sep { color: var(--md-sys-color-on-surface-variant); margin: 0 .12em; }
.fg-zeiten {
  flex-direction: row; gap: 0; font-size: 13px;
  color: var(--md-sys-color-on-surface-variant);
}
.fg-zeiten > span + span::before { content: ' · '; white-space: pre; }
.fg-seite .team-players-big { --gt-av-size: 72px; flex-wrap: nowrap; }
.fg-seite .gt-p {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto;
  column-gap: 16px; row-gap: 2px; align-items: center;
}
.fg-seite .gt-avwrap { grid-column: 2; grid-row: 1 / span 2; }
.fg-seite .gt-name { grid-column: 1; grid-row: 1; align-self: end; text-align: right; font-size: 24px; font-weight: 600; }
.fg-seite .gt-proj { grid-column: 1; grid-row: 2; align-self: start; text-align: right; font-size: 13px; line-height: 18px; }
.fg-seite .gt-proj * { font-size: inherit !important; }
.fg-buehne > .fg-seite:first-child .team-players-big { justify-content: flex-end; }
.fg-buehne > .fg-seite:last-child .team-players-big { justify-content: flex-start; }
.fg-buehne > .fg-seite:last-child .gt-p { grid-template-columns: auto minmax(0, 1fr); }
.fg-buehne > .fg-seite:last-child .gt-avwrap { grid-column: 1; }
.fg-buehne > .fg-seite:last-child .gt-name,
.fg-buehne > .fg-seite:last-child .gt-proj { grid-column: 2; text-align: left; }
.fg-seite .gt-av { border: none; }
/* Figurenfarbe statt Blau/Orange (2026-10-06); `on-team1/2` statt fest
   #fff, dazu ein Kontur-Ring (siehe `.sd-punkt` oben). */
.fg-buehne > .fg-seite:first-child .gt-av-empty,
.fg-buehne > .fg-seite:last-child .gt-av-empty { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.fg-buehne > .fg-seite:first-child .gt-av-empty { background: var(--md-ext-color-team1); color: var(--md-ext-color-on-team1); }
.fg-buehne > .fg-seite:last-child .gt-av-empty { background: var(--md-ext-color-team2); color: var(--md-ext-color-on-team2); }
.fg-seite .gt-av-empty .msym { font-size: 32px; }
.fg-fuss {
  margin-top: 18px; padding-top: 14px;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
.fg-aktionen { flex: 1; }
.fg-aktionen .btn { min-height: 38px; padding: 0 14px; gap: 8px; border-radius: 999px; font-size: 14px; color: var(--md-sys-color-on-surface); }
.fg-aktionen .echo-knopf, .fg-aktionen #fg-video-knopf .btn, .fg-aktionen #fg-analyse-knopf { border: 1px solid var(--md-sys-color-outline-variant); }
.fg-aktionen .btn.echo-knopf.echo-an {
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); border-color: transparent;
}
/* „Spiel löschen" ist der letzte Knopf der Zeile und steht rechts. Ohne
   eigene Klasse: der Knopf hat einen Zwilling in speedshot.js, und beide
   sollen gleich bleiben (tests/test_rechte_sichtbarkeit.py). */
.fg-aktionen > button.btn:last-child { margin-left: auto; color: var(--md-sys-color-on-surface-variant); }

/* Eine umbrechende Reihe und keine neue Breitenmarke (die Marken sind 600
   und 840, tests/test_einblicke_ui.py): nebeneinander, solange beide ihre
   Mindestbreite haben, sonst untereinander. Am Telefon stehen die
   Kommentare zuerst (Dokumentreihenfolge), darueber die Tore. */
.sd-raster { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 18px; margin-bottom: 18px; }
.sd-raster > .card, .sd-raster > .sd-komm > .card { margin: 0; }
.sd-raster > .sd-tore { flex: 1.45 1 560px; min-width: 0; }
.sd-raster > .sd-komm { flex: 1 1 340px; min-width: 0; }
.sd-raster > .sd-komm:empty { display: none; }
/* Die Kommentare wandern mit, wenn die Torliste lang ist - sonst stuende
   die Eingabe bei zwanzig Toren einen Bildschirm tiefer. */
@media (min-width: 601px) {
  .sd-raster > .sd-tore { order: -1; }
  .sd-raster > .sd-komm { position: sticky; top: 24px; }
}
.sd-tore .card-title, .sd-kommkarte .card-title { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: 600; }
.sd-tore .card-title .msym, .sd-kommkarte .card-title .msym { display: none; }
.sd-hinweis { margin-left: auto; font-size: 13px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
.sd-bereich {
  margin-left: auto; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 500;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant);
}
.sd-tore table { width: 100%; border-collapse: separate; border-spacing: 0; }
.sd-tore th {
  padding: 6px 6px 8px; font-size: 12px; font-weight: 500; text-align: left;
  color: var(--md-sys-color-on-surface-variant); background: none;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.sd-tore td {
  height: 54px; padding: 0 6px; vertical-align: middle;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  font-variant-numeric: tabular-nums; font-size: 14px;
}
.sd-tore tr.sd-neuestes td { background: var(--md-sys-color-surface-container-high); border-bottom-color: transparent; }
.sd-tore tr.sd-neuestes td:first-child { border-radius: 12px 0 0 12px; }
.sd-tore tr.sd-neuestes td:last-child { border-radius: 0 12px 12px 0; }
.sd-tore tr.sd-storniert { opacity: .4; }
.sd-nr, .sd-min, .sd-kmh { color: var(--md-sys-color-on-surface-variant); }
.sd-nr { font-size: 13px !important; width: 34px; }
.sd-tore .tor-stand { font-size: 15px; font-weight: 600; white-space: nowrap; }
.sd-wer { display: flex; align-items: center; gap: 10px; min-width: 0; }
.sd-wer-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.sd-name { font-size: 15px; font-weight: 500; }
/* Ohne Bild in der Torzeile (Entwurf); `!important` gegen den Inline-Stil
   von App.avatarHtml. */
.sd-name img { display: none !important; }
.sd-quelle { font-size: 12px; color: var(--md-sys-color-on-surface-variant); }
.sd-punkt { flex: 0 0 9px; width: 9px; height: 9px; border-radius: 50%; background: var(--md-sys-color-outline-variant); }
/* Seit dem 2026-10-06 die Figurenfarbe statt Blau/Orange, mit einem
   Kontur-Ring (`box-shadow`, kein Layout-Einfluss) - sonst verschwindet eine
   weisse Marke auf hellem bzw. eine schwarze auf dunklem Grund. */
.sd-punkt.t1, .sd-punkt.t2 { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.sd-punkt.t1 { background: var(--md-ext-color-team1); }
.sd-punkt.t2 { background: var(--md-ext-color-team2); }
.sd-tore .tor-echo .aktion {
  min-height: 26px; height: 26px; padding: 0 8px; gap: 4px; border-radius: 999px;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); font-size: 13px;
}
.sd-tore .tor-echo .aktion .msym { font-size: 14px; }
.sd-tore .tor-echo .aktion.echo-an { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.sd-tore td[id^="goal-actions-"] { white-space: nowrap; text-align: right; }
.sd-tore td[id^="goal-actions-"] .aktion { width: 32px; min-width: 32px; height: 32px; padding: 0; border-radius: 50%; }

.sd-kommkarte { display: flex; flex-direction: column; gap: 12px; }
.sd-kommkarte .card-title { margin-bottom: 0; }
.sd-kommkarte .echo-liste { display: flex; flex-direction: column; gap: 14px; margin: 0; }
.sd-kommkarte .echo-zeile {
  display: grid; grid-template-columns: 34px minmax(0, 1fr); column-gap: 12px; row-gap: 2px;
  padding: 0; border: none; background: none;
}
.echo-kuerzel { display: none; }
.sd-kommkarte .echo-kuerzel {
  display: inline-flex; align-items: center; justify-content: center;
  grid-column: 1; grid-row: 1 / span 3; width: 34px; height: 34px; border-radius: 50%;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant);
  font-size: 12px; font-weight: 600;
}
.sd-kommkarte .echo-zeile > :not(.echo-kuerzel) { grid-column: 2; }
.sd-kommkarte .echo-kopf { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
.sd-kommkarte .echo-kopf strong { font-size: 14px; font-weight: 600; }
.sd-kommkarte .echo-kopf .text-sm { font-size: 12px; }
.sd-kommkarte .echo-text { font-size: 15px; line-height: 1.45; }
.sd-kommkarte .echo-aktionen .aktion { min-height: 24px; height: 24px; padding: 0 6px; font-size: 12px; }
.sd-kommkarte .echo-aktionen .aktion .msym { font-size: 14px; }
.sd-kommkarte .echo-eingabe {
  margin-top: 4px; align-items: center; gap: 8px;
  padding: 6px 6px 6px 14px; border: 1px solid var(--md-sys-color-outline-variant); border-radius: 16px;
  background: var(--md-sys-color-surface);
}
.sd-kommkarte .echo-eingabe textarea {
  border: none; background: transparent; box-shadow: none; resize: none;
  min-height: 36px; padding: 8px 0; font-size: 14px;
}
.sd-kommkarte .echo-eingabe textarea:focus { outline: none; }
.sd-kommkarte .echo-eingabe { display: flex; flex-direction: row; flex-wrap: nowrap; }
.sd-kommkarte .echo-eingabe textarea { flex: 1 1 auto; min-width: 0; height: 36px; overflow: hidden; }
.sd-kommkarte .echo-eingabe .em-knopf { flex: 0 0 28px; width: 28px; min-width: 28px; height: 28px; min-height: 28px; padding: 0; }
.sd-kommkarte .echo-eingabe .btn-primary { min-height: 36px; height: 36px; padding: 0 14px; border-radius: 12px; font-weight: 600; }
.sd-kommkarte .echo-eingabe .btn-primary .msym { display: none; }

.sd-bildkarte .schemabilder { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.sd-bildkarte .schemabild { margin: 0; }
.sd-bildkarte .schemabild img, .sd-bildkarte .schemabild svg {
  display: block; width: 100%; border-radius: 14px;
  background: var(--md-sys-color-surface-container-high);
}
.sd-bildkarte .schemabild figcaption { margin-top: 8px; font-size: 13px; text-align: left; color: var(--md-sys-color-on-surface-variant); }

@media (max-width: 840px) {
  .fg-buehne { grid-template-columns: minmax(0, 1fr); row-gap: 14px; }
  .fg-mitte { padding: 0; order: -1; }
  .fg-buehne > .fg-seite .team-players-big { justify-content: center; }
  .fg-kopf .fg-stand { font-size: 52px; }
}
@media (max-width: 600px) {
  .fg-kopf { padding: 18px 16px 14px; }
  .sd-tore td[id^="goal-actions-"] .aktion { width: 28px; min-width: 28px; height: 28px; }
  .sd-bildkarte .schemabilder { grid-template-columns: minmax(0, 1fr); }
}

/* ══ Neugestaltung · der Videobetrachter (Entwurf WebVideo, 2026-09-30) ═════
   Kopf mit Begegnung und Herunterladen, darunter links das Bild mit einem
   Streifen der Tormarken und der Torleiste, rechts die Sprungmarken als
   Liste (300 px). Nebeneinander nur im Kinomodus - schmal (Kino aus) stehen
   die Marken wie bisher unter dem Bild. Die Hoehenbremsen des Kinomodus
   bleiben, wie sie sind (gezaehlt in test_zoom_math_untouched). */
#modal-box:has(.vb-raster) { border-radius: 28px; padding: 18px 20px 20px; box-shadow: 0 24px 64px rgba(0, 0, 0, .3); }
#modal-box.kino:has(.vb-raster) { width: min(1280px, calc(100vw - 48px)); }
#modal-box:has(.vb-raster) > .modal-close { top: 16px; right: 18px; }
.vb-kopf { display: flex; align-items: center; gap: 12px; padding: 0 52px 0 4px; margin-bottom: 14px; }
.vb-kopf .video-titel { flex: 1; min-width: 0; margin: 0; font-size: 18px; font-weight: 600; }
.vb-kopf .aktion {
  min-height: 36px; height: 36px; padding: 0 12px; gap: 6px; border-radius: 12px;
  border: 1px solid var(--md-sys-color-outline-variant); color: var(--md-sys-color-on-surface);
  font-size: 13px; font-weight: 500; white-space: nowrap;
}
.vb-raster { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; min-height: 0; }
#modal-box.kino .vb-raster { grid-template-columns: minmax(0, 1fr) 300px; }
.vb-links { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.vb-links .video-rahmen { border-radius: 18px; overflow: hidden; background: #1A1B1D; }
#modal-box.kino .vb-links .video-rahmen { width: auto; margin: 0; }
.vb-links .video-spieler { border-radius: 0; }
#modal-box.kino .vb-links .video-spieler { width: 100%; height: auto; max-height: none; }
.vb-links .video-kino {
  width: 38px; height: 38px; border-radius: 12px; box-shadow: none;
  background: rgba(14, 15, 17, .6); color: #F2F2F4;
}
.vb-links .torleiste:not(:empty) { padding: 10px 12px; border-radius: 16px; background: var(--md-sys-color-surface-container-high); }
.vb-links .tl-zeile { margin-top: 0; gap: 8px 10px; }
.vb-links .tl-name { font-size: 15px; font-weight: 600; }
.vb-links .tl-zeile .aktion {
  min-height: 34px; height: 34px; padding: 0 12px; gap: 6px; border-radius: 999px;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface); font-size: 13px;
}
.vb-links .tl-zeile .aktion.echo-an { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); border-color: transparent; }
.vb-links .tl-weiter { font-weight: 500; }
.vb-links .tl-weiter .msym { order: 1; }
.vm-punkt { flex: 0 0 8px; width: 8px; height: 8px; border-radius: 50%; }
/* Figurenfarbe + Kontur-Ring, siehe `.sd-punkt` oben. */
.vm-punkt.t1, .vm-punkt.t2 { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.vm-punkt.t1 { background: var(--md-ext-color-team1); }
.vm-punkt.t2 { background: var(--md-ext-color-team2); }
.vm-zeit { display: none; }
.vb-marken { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.vb-marken-kopf { padding: 4px 6px 8px; font-size: 13px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); }
.vb-marken .video-marken { display: flex; flex-direction: column; flex-wrap: nowrap; gap: 2px; margin: 0; overflow-y: auto; justify-content: flex-start; }
#modal-box.kino .vb-marken .video-marken { justify-content: flex-start; max-height: calc(100% - 30px); }
.vb-marken .vm-marke {
  display: flex; align-items: center; justify-content: flex-start; gap: 10px;
  min-height: 40px; height: 40px; padding: 0 10px; border-radius: 12px;
  font-size: 14px; color: var(--md-sys-color-on-surface); text-align: left;
}
.vb-marken .vm-marke .vm-zeit { display: inline; order: -2; width: 40px; flex: 0 0 40px; font-size: 12px; font-variant-numeric: tabular-nums; opacity: .7; }
.vb-marken .vm-marke .vm-punkt { order: -1; }
.vb-marken .vm-marke .vm-echo { margin-left: auto; font-size: 12px; color: inherit; opacity: .75; }
.vb-marken .vm-marke.echo-an { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.vb-marken .vm-marke.echo-an .vm-punkt { box-shadow: 0 0 0 2px var(--md-sys-color-on-primary); }
#modal-box:not(.kino) .vb-marken .video-marken { flex-direction: row; flex-wrap: wrap; }
#modal-box:not(.kino) .vb-marken .vm-marke { height: 34px; min-height: 34px; }
.video-seite .vm-zeit { display: none; }

/* Die Eingabe unter dem Tor im Betrachter wie die der Kommentarkarte. */
.vb-links .echo-eingabe {
  display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 8px;
  margin-top: 10px; padding: 6px 6px 6px 14px;
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 16px;
  background: var(--md-sys-color-surface-container-lowest);
}
.vb-links .echo-eingabe textarea {
  flex: 1 1 auto; min-width: 0; height: 36px; min-height: 36px; padding: 8px 0; overflow: hidden;
  border: none; background: transparent; box-shadow: none; resize: none; font-size: 14px;
}
.vb-links .echo-eingabe textarea:focus { outline: none; }
.vb-links .echo-eingabe .em-knopf { flex: 0 0 28px; width: 28px; min-width: 28px; height: 28px; min-height: 28px; padding: 0; }
.vb-links .echo-eingabe .btn-primary { min-height: 36px; height: 36px; padding: 0 14px; border-radius: 12px; font-weight: 600; }
.vb-links .echo-eingabe .btn-primary .msym { display: none; }

/* ══ Die eigene Steuerleiste im Bild (Entwurf WebVideo/MobilVideo, 2026-09-30)
   Unten im Rahmen ueber einem Verlauf: Zeitleiste mit den Toren darin,
   darunter Abspielen, Naechstes Tor, Zeit, Tempo, Vollbild. IMMER sichtbar,
   wie der Kinoknopf - am Kicker-Touch gibt es kein Hover.

   ⚠️ Die Farben sind die ROHEN des Entwurfs fuer eine dunkle Bildflaeche,
   wie beim Kinoknopf im Betrachter darueber: das Video ist in beiden
   Farbmodi dunkel, ein Farbrollen-Paar kippte im hellen Modus auf helle
   Schrift ueber hellem Bild. Die Striche nehmen die `fixed-dim`-Toene -
   die kippen im Dunkelmodus nicht und sind auf Schwarz lesbar (der Entwurf
   nennt #FFA86B, das Token ist #FFB77C).

   ⚠️ Trefferflaechen mindestens 32 px (`--md-sys-comp-size-xs`) - die
   Zeitleiste ist 3 px SICHTBAR und 32 px GREIFBAR. Die Sichtpruefung kennt
   `role="slider"` nicht; gemessen wird sie deshalb eigens.

   ⚠️ Keine Zoomdivision: Breite, Griff und Striche stehen in Prozent. */
.vb-steuer {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  display: flex; flex-direction: column; gap: 2px;
  padding: 26px 16px 10px; color: #F2F2F4;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, .62));
}
.vb-zeitleiste {
  position: relative; height: var(--md-sys-comp-size-xs); cursor: pointer;
  touch-action: none; outline: none;
}
.vb-zeitleiste[aria-disabled="true"] { cursor: default; }
.vb-spur {
  position: absolute; left: 0; right: 0; top: 50%; height: 3px; margin-top: -1.5px;
  border-radius: 2px; background: rgba(255, 255, 255, .28); overflow: hidden;
}
.vb-gespielt { display: block; width: 0; height: 100%; background: #F2F2F4; }
/* Spur und Striche mit 2 bzw. 1 px Radius und OHNE Token: die kleinste Stufe
   (`extra-small`, 4 px) runden eine 3-px-Spur zur Pille und einen 2-px-Strich
   zum Punkt. */
.vb-zeitleiste.zieht { cursor: grabbing; }
.vb-zeitleiste .vb-schiene { position: absolute; left: 0; right: 0; top: 50%; height: 0; pointer-events: none; }
.vb-strich { position: absolute; top: -5px; width: 2px; height: 10px; margin-left: -1px; border-radius: 1px; }
/* Figurenfarbe + Kontur-Ring, siehe `.sd-punkt` oben - bei 2 px Breite ist
   der Ring sogar das Auffaelligere. */
.vb-strich.t1, .vb-strich.t2 { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.vb-strich.t1 { background: var(--md-ext-color-team1-fixed-dim); }
.vb-strich.t2 { background: var(--md-ext-color-team2-fixed-dim); }
.vb-strich.ist-aktiv { top: -7px; width: 4px; height: 14px; margin-left: -2px; }
.vb-griff {
  position: absolute; top: 50%; left: 0; width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; background: #FFFFFF; box-shadow: 0 1px 4px rgba(0, 0, 0, .45);
  pointer-events: none;
}
.vb-zeitleiste[aria-disabled="true"] .vb-griff { opacity: 0; }
/* Eigener Fokus, wie beim Kinoknopf: kein `.btn`, also kein Hausfokus. */
.vb-zeitleiste:focus-visible .vb-griff { outline: 2px solid var(--md-sys-color-primary); outline-offset: 2px; }
/* ⚠️ Die Zeile laesst Zeiger DURCH, nur ihre Kinder fangen sie: so darf sie
   am Telefon in die untere Haelfte der 32-px-Trefferflaeche der Zeitleiste
   ragen, ohne dass ein Tipp zwischen zwei Knoepfen verloren geht. */
.vb-zeile { display: flex; align-items: center; gap: 4px; min-width: 0; pointer-events: none; }
.vb-zeile > * { pointer-events: auto; }
.vb-knopf {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--md-sys-comp-size-xs); height: var(--md-sys-comp-size-xs); padding: 0;
  border: none; border-radius: var(--md-sys-shape-corner-small); background: transparent; color: inherit;
  font: inherit; cursor: pointer;
}
.vb-knopf .msym { font-size: 22px; }
.vb-knopf:hover { background: rgba(255, 255, 255, .12); }
.vb-knopf:focus-visible { outline: 2px solid var(--md-sys-color-primary); outline-offset: 2px; }
.vb-knopf:disabled { opacity: .4; cursor: default; background: transparent; }
.vb-knopf[hidden] { display: none; }
/* Die Zeit weicht als Erstes, wenn es eng wird (Ueberlagerung 601-840 px). */
.vb-zeit {
  flex: 0 1 auto; min-width: 0; margin-left: 4px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.vb-luft { flex: 1 1 auto; }
/* Die Pille ist 24 px SICHTBAR in einem 32-px-Ziel: die Flaeche liegt als
   `::before` darin, `isolation` haelt sie hinter dem Text. */
.vb-tempo {
  position: relative; isolation: isolate; min-width: 44px; padding: 0 9px;
  font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.vb-tempo::before {
  content: ''; position: absolute; left: 0; right: 0; top: 4px; bottom: 4px; z-index: -1;
  border-radius: var(--md-sys-shape-corner-small); background: rgba(255, 255, 255, .16);
}
.vb-tempo:hover { background: transparent; }
.vb-tempo.ist-an { color: #1A1B1D; }
.vb-tempo.ist-an::before { background: #F2F2F4; }
.vb-gesperrt .vb-zeitleiste, .vb-gesperrt .vb-knopf { pointer-events: none; opacity: .4; }
/* Das Qualitaetsmenue (2026-10-16): eine Liste ueber dem Zahnrad, im Bild.
   Dieselben ROHEN Farben wie die Leiste darunter und aus demselben Grund (das
   Video ist in beiden Farbmodi dunkel; eine Farbrolle kippte im hellen Modus).
   Liegt im `.vb-steuer` und damit im Rahmen: unten rechts, ueber der Zeile.
   Trefferflaeche 40 px hoch - die Leiste selbst ist 32 px. Kein `@media`: die
   Breite bestimmt der Inhalt, am Telefon passt sie auch. */
.vb-menue {
  position: absolute; right: 16px; bottom: 58px; z-index: 2; min-width: 160px; padding: 6px 0;
  border-radius: var(--md-sys-shape-corner-medium); color: #F2F2F4;
  background: rgba(28, 28, 30, .94); box-shadow: var(--md-sys-elevation-level2);
}
.vb-menue[hidden] { display: none; }
.vb-menue button {
  display: flex; align-items: center; gap: 12px; width: 100%; min-height: 40px; padding: 0 16px;
  border: none; background: transparent; color: inherit; font: inherit; font-size: 14px;
  cursor: pointer; text-align: left;
}
.vb-menue button:hover { background: rgba(255, 255, 255, .12); }
.vb-menue button:focus-visible { outline: 2px solid var(--md-sys-color-primary); outline-offset: -2px; }
.vb-menue .msym { font-size: 20px; visibility: hidden; }
.vb-menue button[aria-checked="true"] .msym { visibility: visible; }
/* Die eigene Seite am Telefon (nur bis 600 px - deshalb kein @media). Der
   Entwurf MobilVideo zeigt dort KEIN „Naechstes Tor" in der Leiste; es steht
   in der Torkarte darunter. */
/* Schwarz wie `.video-spieler`: die Leiste liegt auf dem Rahmen und nicht im
   Video - ohne eigene Flaeche stuende ihre helle Schrift vor dem ersten Bild
   auf der hellen Seite (Sichtpruefung 2026-09-30: Kontrast 1,02:1). */
.video-seite .video-rahmen { overflow: hidden; border-radius: var(--md-sys-shape-corner-medium, 12px); background: #000; }
.video-seite .video-spieler { border-radius: 0; }
.video-seite .vb-weiter { display: none; }
/* Vollbild bekommt der RAHMEN (game.js `_vollbildUm`). ⚠️ `:is(#modal-box,
   .video-seite)` hebt die Regel ueber `#modal-box.kino .vb-links
   .video-spieler` (1,3,0) - sonst behielte das Video im Vollbild seine
   Kinohoehe. */
.video-rahmen:fullscreen { display: flex; align-items: center; justify-content: center; background: #000; border-radius: 0; }
:is(#modal-box, .video-seite) .video-rahmen:fullscreen .video-spieler {
  width: 100%; height: 100%; max-height: none; aspect-ratio: auto; object-fit: contain; border-radius: 0;
}
/* ⚠️ Auf einem SCHMALEN Bild flacher - am Bild gemessen und nicht am
   Fenster, deshalb `@container` und keine neue Breitenmarke. Gemessen am
   2026-09-30: am Telefon (Bild 358 px) verdeckte die volle Leiste 88 von
   201 px (44 %), so sind es 62 px (31 %). Wenig Polster, und die Zeile ragt
   10 px in die Trefferflaeche der Zeitleiste (siehe `.vb-zeile`).
   Der Rahmen ist ohnehin ein Block voller Breite - die Groessenbindung an
   die Breite aendert dort nichts. */
.vb-links .video-rahmen, .video-seite .video-rahmen { container: vbild / inline-size; }
@container vbild (max-width: 480px) {
  .vb-steuer { padding: 6px 6px 2px; gap: 0; }
  .vb-zeitleiste { margin-bottom: -10px; }
  /* Das Qualitaetsmenue im schmalen Bild: am Telefon ist das Video nur rund
     200 px hoch, und die volle Liste (4 x 40 px) ragte oben aus dem Rahmen -
     `.video-seite .video-rahmen` schneidet dort ab (gemessen 2026-10-16, die
     Zeile „Original“ fehlte). Zeilen auf die 32 px der Leiste, und naeher an
     die Zeile. */
  .vb-menue { right: 6px; bottom: 40px; padding: 2px 0; }
  .vb-menue button { min-height: var(--md-sys-comp-size-xs); }
}
/* ⚠️ Und im engen Fenster rutschen die Sprungmarken auch im Kinomodus UNTER
   das Bild. Gemessen am 2026-09-30: bei 700 px Fenster liess die 300-px-
   Spalte dem Bild 296 x 167 px, bei 601 px 197 x 111 - die Leiste verdeckte
   dort 61 bzw. 92 %. Nebeneinander erst ab 760 px Inhalt: dann bleiben dem
   Bild mindestens rund 440 px (gerechnet: 760 - 300 - 16). */
#modal-box:has(.vb-raster) #modal-content { container: vbfenster / inline-size; }
@container vbfenster (max-width: 759px) {
  #modal-box.kino .vb-raster { grid-template-columns: minmax(0, 1fr); }
  #modal-box.kino .vb-marken .video-marken { flex-direction: row; flex-wrap: wrap; max-height: none; }
  #modal-box.kino .vb-marken .vm-marke { height: 34px; min-height: 34px; }
}

/* ══ Neugestaltung · die Huelle am Telefon (Entwuerfe Mobil*, 2026-09-30) ═══
   Der Kopf ist eine Zeile aus Titel und 40-px-Kreisknoepfen, 8 px unter der
   Notch; die untere Leiste 58 px ohne Weichzeichner und ohne Pille. Eine
   Detailansicht (sie traegt `.kopf-zurueck`) zeigt den Zurueck-Kreis links,
   den Titel klein in der Mitte und weder Glocke noch Konto (Entwurf Form B).
   Die Ueberschrift der Ansicht steht genau EINMAL - im Kopf. */
@media (max-width: 600px) {
  :root { --nav-h: calc(64px + env(safe-area-inset-top, 0px)); --bn-h: 58px; }
  #navbar {
    padding-left: max(20px, env(safe-area-inset-left, 0px));
    padding-right: max(20px, env(safe-area-inset-right, 0px));
    gap: 10px; border-bottom: none; background: var(--md-sys-color-surface);
  }
  #navbar.is-gescrollt { background: var(--md-sys-color-surface); }
  .nav-titel { font-size: 30px; line-height: 36px; font-weight: 700; letter-spacing: -0.02em; }
  .nav-bell, #account-btn {
    width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0;
    border: none; border-radius: 50%;
  }
  .nav-bell { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }
  .nav-bell[data-count]:not([data-count=""])::after { top: 8px; right: 9px; border-color: var(--md-sys-color-surface-container-high); }
  #account-btn { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); overflow: hidden; }
  #account-btn .nav-avatar { width: 40px; height: 40px; }

  /* Form B - die Detailansicht. */
  .kopf-zurueck {
    position: fixed; z-index: 102;
    top: calc(env(safe-area-inset-top, 0px) + 12px); left: max(16px, env(safe-area-inset-left, 0px));
    width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0;
    border: none; border-radius: 50%;
    background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
  }
  .kopf-zurueck .msym { font-size: 20px; }
  body:has(#content .kopf-zurueck) .nav-bell,
  body:has(#content .kopf-zurueck) #account-btn { display: none; }
  body:has(#content .kopf-zurueck) .nav-titel {
    margin: 0 44px; text-align: center;
    font-size: 16px; line-height: 22px; font-weight: 600; letter-spacing: 0;
  }

  /* Die Ueberschrift nur im Kopf. Ein Kopf, der danach nichts mehr traegt
     (nur Titel und der feste Zurueck-Knopf), faellt ganz weg. Die
     Spielerkarte nimmt sich aus: ihre Profilzeile ist im Entwurf gewollt. */
  #content .page-header > .page-title:not(.sk-profilzeile) { display: none; }
  /* Kein `display: none` fuer den Kopf selbst: der feste Zurueck-Knopf
     steckt darin und verschwaende mit ihm. */
  #content .page-header:not(:has(> :not(.page-title):not(.kopf-zurueck))):not(:has(> .sk-profilzeile)) {
    height: 0; min-height: 0; margin: 0; padding: 0; border: none; overflow: visible;
  }
  /* Die Kopfaktionen („+ Spieler", „+ Turnier", Umbenennen) als Kreis im
     Kopf, links neben der Glocke - wie das „+" im Entwurf MobilSpieler. */
  #content .page-header.kopf-aktionen {
    position: fixed; z-index: 102; top: calc(env(safe-area-inset-top, 0px) + 12px);
    right: max(120px, calc(env(safe-area-inset-right, 0px) + 120px));
    height: 40px; margin: 0; padding: 0; background: none; border: none;
  }
  body:has(#content .kopf-zurueck) #content .page-header.kopf-aktionen { right: max(16px, env(safe-area-inset-right, 0px)); }
  #content .page-header.kopf-aktionen .page-actions { display: flex; gap: 8px; margin: 0; }
  #content .page-header.kopf-aktionen .btn {
    width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0; gap: 0;
    border: none; border-radius: 50%; font-size: 0;
    background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
  }
  #content .page-header.kopf-aktionen .btn .msym { font-size: 20px; }
  #content .page-header:has(> .page-title) { margin-bottom: 8px; }

  #content { padding: 16px; padding-bottom: calc(var(--bn-h) + 16px + env(safe-area-inset-bottom, 0px)); }
  .card { border-radius: 20px; }

  /* Die untere Leiste. */
  #bottom-nav {
    padding-top: 8px;
    background: rgba(255, 255, 255, .92);
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  .bn-link { gap: 3px; padding: 0 2px; font-size: 10.5px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); }
  .bn-link .msym { font-size: 22px; height: 24px; }
  .bn-link.active { color: var(--md-sys-color-on-surface); }

  /* Spieler: die Tabelle wird eine Liste - Name oben, Art, Anmeldung und
     Gruppen als kleine Unterzeile, die Aktionen darunter rechts. Keine
     Spalte geht verloren, nur ihre Anordnung. */
  html[data-ort] .sp-tabelle, .sp-tabelle tbody { display: block; width: 100%; table-layout: auto; }
  .sp-tabelle thead { display: none; }
  .sp-tabelle tr {
    display: flex; flex-wrap: wrap; align-items: center; column-gap: 8px; row-gap: 2px;
    padding: 10px 0; border-bottom: 1px solid var(--md-sys-color-outline-variant);
  }
  .sp-tabelle tr:last-child { border-bottom: none; }
  /* Die Zeilenlinie der Tabelle ist ein innerer Schatten der Zelle (gemessen
     2026-09-30) - in der Liste zieht sie die Zeile, nicht jede Zelle. */
  html[data-ort] .sp-tabelle td { display: block; padding: 0; border: none !important; box-shadow: none !important; font-size: 12px; }
  html[data-ort] .sp-tabelle td:first-child { flex: 1 0 100%; font-size: 16px; }
  html[data-ort] .sp-tabelle td:not(:first-child):not(.spalte-aktionen) { color: var(--md-sys-color-on-surface-variant); padding-left: 48px; }
  html[data-ort] .sp-tabelle td:not(:first-child):not(.spalte-aktionen) + td:not(.spalte-aktionen) { padding-left: 0; }
  html[data-ort] .sp-tabelle td > div:first-child { white-space: normal; }
  html[data-ort] .sp-tabelle td.spalte-aktionen { flex: 1 0 100%; }
  html[data-ort] .sp-tabelle .spalte-aktionen .list-item-actions { justify-content: flex-end; flex-wrap: wrap; gap: 2px; }
  html[data-ort] .sp-tabelle .aktion.nur-symbol { width: 36px; height: 36px; min-width: 36px; border-radius: 50%; }
  .sp-tabelle .badge { padding: 1px 8px; font-size: 11px; }
}
@media (max-width: 600px) {
  html[data-theme="dark"] #bottom-nav { background: rgba(var(--md-sys-color-surface-container-rgb) / .92); }
}

/* ══ Neugestaltung · das beendete Spiel am Telefon (Entwurf MobilSpielDetail)
   Die Buehne bleibt dreispaltig (Bild ueber dem Namen, Stand dazwischen),
   die Tortabelle wird eine Liste: Name, Stand und Knoepfe oben, Minute,
   Tempo und Resonanz klein darunter. */
@media (max-width: 600px) {
  html .kopf-zurueck.btn { border: none; border-radius: 50%; background: var(--md-sys-color-surface-container-high); }
  .fg-buehne { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); column-gap: 6px; }
  /* Die Mitte loest sich auf: der Stand steht zwischen den Seiten, Beginn
     und Spielzeit darunter ueber die volle Breite. */
  .fg-mitte { display: contents; }
  .fg-buehne > .fg-seite:first-child { grid-column: 1; grid-row: 1; }
  .fg-buehne .fg-stand { grid-column: 2; grid-row: 1; align-self: center; }
  .fg-buehne > .fg-seite:last-child { grid-column: 3; grid-row: 1; }
  .fg-buehne .fg-zeiten { grid-column: 1 / -1; grid-row: 2; margin-top: 10px; }
  .fg-kopf .fg-stand { font-size: 44px; }
  .fg-seite .team-players-big { --gt-av-size: 56px; justify-content: center !important; }
  .fg-seite .gt-p, .fg-buehne > .fg-seite:last-child .gt-p { display: flex; flex-direction: column; align-items: center; gap: 4px; }
  .fg-seite .gt-name, .fg-buehne > .fg-seite:last-child .gt-name { font-size: 15px; text-align: center; align-self: center; }
  .fg-seite .gt-proj, .fg-buehne > .fg-seite:last-child .gt-proj { align-self: center; }
  .fg-seite .gt-proj, .fg-buehne > .fg-seite:last-child .gt-proj { font-size: 12px; text-align: center; }
  .fg-zeiten { flex-wrap: wrap; justify-content: center; }
  .sd-tore .table-wrap { overflow: visible; }
  .sd-tore table, .sd-tore tbody { display: block; }
  .sd-tore thead { display: none; }
  .sd-tore tr { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 10px; padding: 10px 0; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
  .sd-tore tr:last-child { border-bottom: none; }
  .sd-tore tr::after { content: ''; order: 4; flex: 0 0 100%; height: 0; }
  .sd-tore td { display: block; height: auto; padding: 0; border: none !important; box-shadow: none !important; }
  .sd-tore td.sd-nr { display: none; }
  .sd-tore td:nth-child(2) { order: 1; flex: 1 1 auto; min-width: 0; }
  .sd-tore td.tor-stand { order: 2; font-size: 16px; }
  .sd-tore td[id^="goal-actions-"] { order: 3; }
  .sd-tore td.sd-min { order: 5; margin-left: 19px; font-size: 12px; }
  .sd-tore td.sd-kmh { order: 6; font-size: 12px; }
  .sd-tore td.tor-echo { order: 7; margin-left: auto; }
  .sd-tore tr.sd-neuestes { padding: 10px; margin: 0 -10px; border-radius: 12px; background: var(--md-sys-color-surface-container-high); }
  .sd-tore tr.sd-neuestes td { background: none; }
  .sd-quelle { display: none; }
  .sd-kommkarte .echo-eingabe textarea, .vb-links .echo-eingabe textarea {
    min-height: 36px !important; max-height: 36px; line-height: 20px; white-space: nowrap;
  }
}

/* ══ Neugestaltung · Messenger am Telefon (Entwuerfe MobilNachrichten,
   MobilChat, 2026-09-30) ═════════════════════════════════════════════════
   Die Liste: der Titel steht im Kopf, „Neu" als Kreis daneben. Das offene
   Gespraech nimmt den ganzen Schirm - ohne obere und untere Leiste; sein
   eigener Kopf ist die Detailleiste des Entwurfs (Zurueck, Name mittig,
   Werkzeuge rechts). Die Hoehe wieder ueber ortsfeste Variablen und ohne
   neue Division: `--nav-h` = die Notch, `--bn-h` = -32 px hebt die 48 px der
   Grundregel bis auf das Polster von 16 px auf. */
@media (max-width: 600px) {
  .nr-liste { border-right: none; }
  .nr-kopf { padding: 0; min-height: 0; }
  .nr-kopf .nr-titel { display: none; }
  .nr-kopf .btn.nr-neu {
    position: fixed; z-index: 102; top: calc(env(safe-area-inset-top, 0px) + 12px);
    right: max(120px, calc(env(safe-area-inset-right, 0px) + 120px));
    width: 40px; height: 40px; min-height: 40px; padding: 0; gap: 0; border-radius: 50%; font-size: 0;
  }
  .nr-kopf .btn.nr-neu .msym { font-size: 20px; }
  body:has(.nr-raster.zeigt-gespraech) #navbar,
  body:has(.nr-raster.zeigt-gespraech) #bottom-nav,
  .nr-raster.zeigt-gespraech .nr-kopf .btn.nr-neu { display: none; }
  #content:has(> .nr-raster.zeigt-gespraech) {
    margin-top: env(safe-area-inset-top, 0px); padding-bottom: 0; min-height: 0;
  }
  #content > .nr-raster.zeigt-gespraech { --nav-h: env(safe-area-inset-top, 0px); --bn-h: -32px; }
  .nr-raster.zeigt-gespraech .nr-g-kopf { padding: 0 0 10px; gap: 8px; border-bottom: none; }
  .nr-raster.zeigt-gespraech .nr-zurueck {
    display: inline-flex; width: 40px; height: 40px; min-width: 40px; min-height: 40px; padding: 0;
    border-radius: 50%; background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
  }
  .nr-g-kopf > .nr-avatar, .nr-g-kopf > .nr-g-bild { display: none; }
  .nr-g-kopf .nr-g-titelblock { flex: 1; min-width: 0; align-items: center; text-align: center; }
  .nr-g-kopf .nr-g-titel { font-size: 16px; max-width: 100%; }
  .nr-g-kopf .nr-g-unter { font-size: 12px; }
  .nr-g-aktionen { gap: 4px; }
  .nr-g-aktionen .btn {
    width: 36px; height: 36px; min-width: 36px; min-height: 36px;
    border: none; border-radius: 50%; background: var(--md-sys-color-surface-container-high);
  }
  .nr-raster.zeigt-gespraech .nr-verlauf { padding: 8px 0 12px; }
  .nr-raster.zeigt-gespraech .nr-eingabe { padding: 0 0 max(8px, env(safe-area-inset-bottom, 0px)); }
  .nr-senden { border-radius: 22px; }
}

/* ══ Neugestaltung · Start am Telefon (Entwurf MobilStart, 2026-09-30) ═════
   ⚠️ Das Zweierraster der Startseite (weiter oben, ohne Medienabfrage) stand
   HINTER der 600er-Regel „eine Spalte" und schlug sie - am Telefon lagen
   Rangliste und Live-Karte nebeneinander (Pruefbild 2026-09-30). Deshalb hier
   noch einmal, hinter beiden. Die Rangliste nimmt die Spalten des Entwurfs:
   Platz, Spieler mit Form, Wert; Trend klein, der Anteil faellt weg. */
@media (max-width: 600px) {
  .dashboard-grid, html[data-ort="netz"] .dashboard-grid { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  /* Die Live-Karte zuerst (Entwurf), die letzten Spiele unter der Rangliste. */
  .dash-rechts { display: contents; }
  .dashboard-grid > .dash-rechts > .live-karte { order: -1; }
  .dashboard-grid > .dash-rechts > * { margin: 0; }
  html[data-ort] .rl-karte .mini-rang, .rl-karte .mini-rang {
    --rl-platz: 20px; --rl-form: 62px; --rl-trend: 34px; --anteil-spur: 0px; --rl-wert: 52px;
    column-gap: 8px !important;
  }
  .rl-karte .standing-share,
  .rl-karte .mini-rang > .mini-head:nth-child(5):not(:last-child) { overflow: hidden; font-size: 0; }
  .rl-karte .mini-rang > span { min-height: 52px; padding-right: 0; }
  .rl-karte .standing-pts { font-size: 16px; }
  .rl-karte .st-move { font-size: 12px; }
}

/* Reiter am Telefon als Filterchips (Entwurf MobilStatistik, 2026-09-30):
   34 px, Pille, gewaehlt gefuellt. Wischbar bleibt die Leiste wie ueberall. */
@media (max-width: 600px) {
  html[data-ort] .tabs { gap: 8px; border-bottom: none; padding: 0 0 2px; }
  html[data-ort] .tab {
    flex: 0 0 auto; min-height: 34px; height: 34px; padding: 0 14px; border-radius: 999px;
    border: 1px solid var(--md-sys-color-outline-variant);
    background: var(--md-sys-color-surface-container-lowest);
    font-size: 14px; font-weight: 500; color: var(--md-sys-color-on-surface);
  }
  html[data-theme="dark"] .tab { background: var(--md-sys-color-surface-container); }
  html[data-ort] .tab.active {
    background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
    border-color: transparent; font-weight: 600;
  }
  html[data-ort] .tab::before { display: none; }
  .tabs-pfeil { display: none !important; }
}

/* ══ Neugestaltung · Neues Spiel (Entwurf TischNeuesSpiel, 2026-09-30) ═════
   Oben Zurueck und Titel; am Tisch ohne die Menueschiene (der Entwurf zeigt
   hier kein Hauptmenue - der Weg hinaus ist der Zurueck-Kreis). Die Seiten
   nebeneinander mit fuenf Kacheln je Reihe, darunter eine Karte mit vier
   Spalten, die Schalter in einer Reihe, unten klebend die Fussleiste. Die
   Auswahllisten tragen die Werte und sind unsichtbar. */
html[data-ort="tisch"] body:has(#content .setup-seiten) .nav-links { visibility: hidden; }
.setup-kopf { display: flex; align-items: center; justify-content: flex-start; gap: 16px; margin-bottom: 12px; }
.setup-kopf .page-title { flex: 0 1 auto; font-size: 26px; font-weight: 600; margin: 0; }
.setup-kopf .kopf-zurueck {
  width: 48px; height: 48px; min-width: 48px; min-height: 48px; padding: 0; border-radius: 50%;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
}
.setup-seiten { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
.setup-seiten > .card.setup-seite { margin: 0; padding: 20px 22px 22px; border-radius: 24px; }
.seite-titel { display: flex; align-items: baseline; gap: 10px; font-size: 18px; font-weight: 600; margin-bottom: 16px; }
.seite-punkt { display: inline-block; flex: 0 0 12px; width: 12px; height: 12px; border-radius: 50%; align-self: center; }
/* Figurenfarbe + Kontur-Ring, siehe `.sd-punkt` oben. */
.seite-punkt.t1, .seite-punkt.t2 { box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.seite-punkt.t1 { background: var(--md-ext-color-team1); }
.seite-punkt.t2 { background: var(--md-ext-color-team2); }
.seite-wahl { font-size: 15px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
.setup-seite .player-grid, .setup-ss .player-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
.setup-seite .player-card, .setup-ss .player-card {
  min-height: 112px; padding: 14px 6px 12px; gap: 10px; border-radius: 18px;
  border: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-lowest); box-shadow: none;
}
html[data-theme="dark"] .setup-seite .player-card, html[data-theme="dark"] .setup-ss .player-card { background: var(--md-sys-color-surface-container); }
.setup-seite .player-avatar, .setup-ss .player-avatar { width: 46px; height: 46px; }
.pc-kuerzel {
  display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; border-radius: 50%;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant);
  font-size: 14px; font-weight: 600;
}
.setup-seite .player-name { font-size: 15px; font-weight: 500; }
.setup-seite .player-card.selected {
  border: 2px solid var(--md-ext-color-team1); box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset; background: var(--md-ext-color-team1-container);
}
.setup-seite .player-card.selected-team2 {
  border: 2px solid var(--md-ext-color-team2); box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset; background: var(--md-ext-color-team2-container);
}
.setup-seite .player-card.selected .pc-kuerzel { background: var(--md-ext-color-team1); color: var(--md-ext-color-on-team1); box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.setup-seite .player-card.selected-team2 .pc-kuerzel { background: var(--md-ext-color-team2); color: var(--md-ext-color-on-team2); box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur); }
.setup-seite .player-card.ist-drueben { opacity: .35; }
.setup-ss { border-radius: 24px; }

.setup-karte { padding: 20px 22px; border-radius: 24px; margin-bottom: 16px; }
.setup-optionen { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }
.setup-feld { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.setup-feld.hidden { display: none; }
.setup-label { font-size: 13px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); }
.setup-feld .seg-group { display: flex; width: 100%; padding: 4px; border-radius: 14px; }
html .setup-feld .seg-btn {
  flex: 1; justify-content: center; min-height: 44px; height: 44px; padding: 0 10px;
  border-radius: 10px; font-size: 15px; white-space: nowrap;
}
.setup-stepper {
  display: flex; align-items: center; gap: 4px; height: 52px; padding: 4px; border-radius: 14px;
  background: var(--md-sys-color-surface-container-high);
}
.setup-stepper .btn {
  width: 44px; min-width: 44px; height: 44px; min-height: 44px; padding: 0; border-radius: 10px;
  background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface); border: none;
}
.setup-stepper-wert { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 18px; font-weight: 600; }
.setup-stepper-wert .form-input {
  width: 2.4em; min-height: 0; height: 36px; padding: 0; border: none; background: transparent;
  text-align: right; font-size: 18px; font-weight: 600; -moz-appearance: textfield; box-shadow: none;
}
.setup-stepper-wert .form-input::-webkit-outer-spin-button,
.setup-stepper-wert .form-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#tournament-container .form-group { margin: 0; }
#tournament-container .form-input { min-height: 52px; border-radius: 14px; }
.setup-listen { display: none; }
.setup-hinweis { margin-top: 10px; }
.setup-schalter {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 28px;
  margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--md-sys-color-outline-variant);
}
.setup-schalter .toggle-row { display: flex; align-items: center; gap: 10px; padding: 0; border: none; min-height: 0; }
.setup-schalter .toggle-label { font-size: 15px; }
.setup-timer { display: flex; align-items: center; gap: 12px 28px; flex-wrap: wrap; }
.setup-timer.hidden { display: none; }
.setup-timer .form-group { display: flex; align-items: center; gap: 8px; margin: 0; }
.setup-timer .form-label { position: static; transform: none; margin: 0; font-size: 13px; }
.setup-fuss {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; gap: 20px; margin: 8px -4px 0; padding: 16px 4px 20px;
  background: var(--md-sys-color-surface);
}
.setup-zusammenfassung { flex: 1; min-width: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 18px; }
.setup-zusammenfassung strong { font-weight: 600; }
.setup-zusammenfassung .sz-gegen, .setup-zusammenfassung .sz-rest { color: var(--md-sys-color-on-surface-variant); }
.setup-zusammenfassung .sz-rest { font-size: 15px; }
.setup-fuss .big-start-btn {
  width: auto; min-height: 64px; height: 64px; padding: 0 40px; gap: 10px;
  border-radius: 20px; font-size: 20px; font-weight: 600;
}
@media (max-width: 840px) {
  .setup-seiten { grid-template-columns: minmax(0, 1fr); }
  .setup-optionen { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 600px) {
  .setup-optionen { grid-template-columns: minmax(0, 1fr); }
  .setup-seite .player-grid, .setup-ss .player-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .setup-fuss { flex-direction: column; align-items: stretch; bottom: calc(var(--bn-h) + env(safe-area-inset-bottom, 0px)); }
  .setup-fuss .big-start-btn { width: 100%; }
}

/* ══ Neugestaltung · das laufende Spiel (Entwurf TischSpiel, 2026-09-30) ═══
   Die Seiten als weisse Karten mit Haarlinie; oben Bild, Name und Prognose
   als ZEILE (rechts gespiegelt), der Stand gross in der Farbe der Seite,
   darunter „+ Tor" breit und gefuellt (beide Seiten gleich, seit dem
   2026-10-01 in `primary`), „Eigentor" und das
   Zuruecknehmen („−", `_cancelGoal`) als Umrissknoepfe. Am Tisch loest sich
   die Kopfzeile auf (display: contents): LIVE links, die Uhr als Pille in
   der Mitte, die Aktionen unten in einer Fussleiste, „Spiel beenden"
   gefuellt. Die Menueschiene tritt am Tisch zurueck wie beim Spielaufbau. */
.game-meta { display: flex; align-items: center; gap: 10px; min-width: 0; font-size: 15px; color: var(--md-sys-color-on-surface-variant); }
.gm-live { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 600; letter-spacing: .06em; color: var(--md-sys-color-error); }
.gm-live::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
/* Am Tisch kein LIVE (2026-10-01). Der Betreiber: „Am Tisch ist das Spiel
   immer live bzw. aktiv." Aus dem Netz bleibt es eine Auskunft. */
body.am-tisch .gm-live { display: none; }
/* Die Kopfzeile der eingeklappten Spieloptionen (2026-10-01). */
.setup-karte > .setup-klapp {
  display: flex; align-items: center; gap: 14px; min-height: var(--min-touch);
  cursor: pointer; list-style: none; font-weight: 600; font-size: 16px;
}
.setup-karte > .setup-klapp::-webkit-details-marker { display: none; }
.setup-klapp-wahl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                    font-weight: 400; font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
.setup-klapp > .msym { flex: 0 0 auto; transition: transform var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard); }
.setup-karte[open] > .setup-klapp > .msym { transform: rotate(180deg); }
.setup-karte[open] > .setup-klapp { margin-bottom: 16px; }
.setup-karte[open] .setup-klapp-wahl { visibility: hidden; }
.gm-was { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#game-view .team-panel, #game-view .team-panel.team1, #game-view .team-panel.team2 {
  position: relative; align-items: stretch;
  background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline-variant); border-radius: 28px;
}
html[data-theme="dark"] #game-view .team-panel { background: var(--md-sys-color-surface-container); }
body.am-tisch #game-view .team-panel { padding: 22px 26px 24px; gap: 10px; }
/* `!important` an der Variablen: die Groessenregeln der Spielansicht (mit und
   ohne Prognose, am Tisch) setzen sie mit hoeherer Staerke. */
#game-view .team-players-big { --gt-av-size: 52px !important; justify-content: flex-start; flex-wrap: nowrap; }
#game-view .team2 .team-players-big { justify-content: flex-end; }
#game-view .team-players-big .gt-p {
  display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 14px; row-gap: 0; align-items: center;
}
#game-view .team-players-big .gt-avwrap { grid-row: 1 / span 2; }
#game-view .team-players-big .gt-name { font-size: 28px; font-weight: 600; text-align: left; align-self: end; }
#game-view .team-players-big .gt-proj { font-size: 14px; text-align: left; align-self: start; }
#game-view .team-players-big .gt-proj * { font-size: inherit !important; }
#game-view .team2 .team-players-big .gt-p { grid-template-columns: minmax(0, 1fr) auto; }
#game-view .team2 .team-players-big .gt-avwrap { grid-column: 2; }
#game-view .team2 .team-players-big .gt-name,
#game-view .team2 .team-players-big .gt-proj { grid-column: 1; text-align: right; }
#game-view .team1 .gt-av-empty, #game-view .team2 .gt-av-empty {
  border: none; box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur);
}
#game-view .team1 .gt-av-empty { background: var(--md-ext-color-team1); color: var(--md-ext-color-on-team1); }
#game-view .team2 .gt-av-empty { background: var(--md-ext-color-team2); color: var(--md-ext-color-on-team2); }
#game-view .gt-av { border: none; }
/* Der Daumen fuer das letzte Tor sitzt in der Ecke gegenueber dem Bild. */
#game-view .tor-like { position: absolute; top: 18px; z-index: 1; }
#game-view .team1 .tor-like { right: 22px; }
#game-view .team2 .tor-like { left: 22px; }
#game-view .tor-like .btn { width: 48px; height: 48px; min-width: 48px; min-height: 48px; padding: 0; border-radius: 50%; }
#game-view .tor-like .msym { font-size: 22px; }
/* Der Daumen zieht mit (2026-10-01, Betreiber: „Ja"): derselbe kraeftige
   Umriss wie Eigentor und „−". Nur, solange es ein Tor zu liken gibt - der
   gesperrte Daumen behaelt den blassen Rand von `.btn-outline:disabled`, und
   der gelikte steht gefuellt wie jede Reaktion (`.echo-an`). */
#game-view .tor-like .btn:not(:disabled) { border-color: var(--md-sys-color-outline); color: var(--md-sys-color-on-surface); }
#game-view .tor-like .btn.echo-an:not(:disabled) {
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); border-color: transparent;
}
/* Stand und Knoepfe in einem Raster; die Mitte (Stand + Eigentor) loest sich
   darin auf. Die Platzhalter des Anfeuerns braucht das Raster nicht. */
#game-view .team-punkte {
  display: grid; align-items: stretch; column-gap: 12px; row-gap: 8px; flex: 1;
  grid-template-columns: minmax(0, 1fr) 150px 88px;
  grid-template-areas: "score score score" "plus eigen minus" "anf anf anf";
  grid-template-rows: minmax(0, 1fr) auto auto;
}
#game-view .team2 .team-punkte {
  grid-template-columns: 88px 150px minmax(0, 1fr);
  grid-template-areas: "score score score" "minus eigen plus" "anf anf anf";
}
#game-view .team-mitte { display: contents; }
#game-view .team-punkte .team-score {
  grid-area: score; align-self: center; justify-self: center;
  font-weight: 600; letter-spacing: -.04em; line-height: 1;
}
/* Seit dem 2026-10-06 OHNE Teamfarbe (siehe `.fg-stand` oben). */
#game-view .team1 .team-score, #game-view .team2 .team-score { color: inherit; }
#game-view .team-punkte .goal-btn-plus-t1, #game-view .team-punkte .goal-btn-plus-t2 {
  grid-area: plus; width: auto; min-width: 0; height: 88px; border-radius: 22px; box-shadow: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; font-weight: 600; gap: 10px;
  /* ⚠️ Kraeftig und auf BEIDEN Seiten gleich (2026-10-01). Der Betreiber:
     „Am Tisch lassen die sich nicht gut erkennen ... Die Farben der Aktionen
     fuer das + bei beiden Spielern darf gerne gleich sein." Die Tonflaeche
     der Seite (team1-container / tertiary-container) stand gerechnet mit
     1,17 : 1 gegen die weisse Karte - der Knopf hatte keinen Umriss. Jetzt
     gefuellt in `primary` wie „Spiel beenden": hell schwarz, dunkel fast
     weiss. Die Seite zeigt weiter der Stand darueber. Verworfen: Team-1-Blau
     fuer beide (behauptet auf Seite 2 die falsche Seite), mittleres Grau
     (wirkt neben dem Nur-Lese-Zustand ausgegraut). */
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
}
/* „Tor" hinter dem Plus ueber ::before mit order - ::after gehoert der
   Welle beim Druecken (Ripple, oben im Stylesheet). */
#game-view .team-punkte .goal-btn-plus-t1::before, #game-view .team-punkte .goal-btn-plus-t2::before {
  content: 'Tor'; order: 1;
}
#game-view .team-punkte .goal-btn-minus {
  grid-area: minus; width: auto; min-width: 0; height: 88px; border-radius: 22px;
  border: 1px solid var(--md-sys-color-outline); background: transparent;
  color: var(--md-sys-color-on-surface);
}
/* Eigentor und „−" mit dem KRAEFTIGEN Umriss (2026-10-01): `outline` statt
   `outline-variant` (Rand gegen die Karte gerechnet 1,29 -> 4,5 : 1 hell,
   1,27 -> 5,0 : 1 dunkel), die Schrift in `on-surface`. Der Grund ist
   durchsichtig: `surface-container-lowest` ist dunkel #08090A und damit
   DUNKLER als die Karte - die Knoepfe sassen wie in einem Loch. */
#game-view .team-punkte .gv-eigentor {
  grid-area: eigen; width: auto; min-width: 0; height: 88px; min-height: 88px; border-radius: 22px;
  border: 1px solid var(--md-sys-color-outline); background: transparent;
  color: var(--md-sys-color-on-surface); font-size: 16px; font-weight: 500; margin: 0;
}
#game-view .team-punkte .rk-anfeuern { grid-area: anf; justify-self: center; }
#game-view .team-punkte .rk-anfeuern-platz { display: none; }
/* Die Einblicke als weisse Kacheln. */
#game-view .ek-kachel {
  background: var(--md-sys-color-surface-container-lowest); border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: 20px; padding: 14px 18px; box-shadow: none;
}
html[data-theme="dark"] #game-view .ek-kachel { background: var(--md-sys-color-surface-container); }
#game-view .ek-kachel .msym { display: none; }

@media (min-width: 841px) {
  html[data-ort="tisch"] body:has(#game-view) .nav-links { visibility: hidden; }
  body.am-tisch #game-view {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); column-gap: 16px;
    grid-template-rows: auto minmax(0, 1fr) auto auto auto;
  }
  body.am-tisch #game-view > .game-score-area { grid-template-rows: minmax(0, 1fr); align-content: stretch; }
  body.am-tisch #game-view > .game-header { display: contents; }
  body.am-tisch #game-view .game-meta { grid-column: 1; grid-row: 1; align-self: center; }
  body.am-tisch #game-view .game-uhr { grid-column: 2; grid-row: 1; justify-self: center; }
  body.am-tisch #game-view .gv-livestream { grid-column: 3; grid-row: 1; justify-self: end; }
  body.am-tisch #game-view > .game-score-area { grid-column: 1 / -1; grid-row: 2; position: static; margin-top: 0; padding-top: 0; }
  body.am-tisch #game-view .game-controls {
    grid-column: 1 / -1; grid-row: 3; margin: 0; display: flex; align-items: center; gap: 12px;
  }
  body.am-tisch #game-view > .game-statusbar { grid-column: 1 / -1; grid-row: 4; }
  body.am-tisch #game-view > .rk-spalte { grid-column: 1 / -1; }
  body.am-tisch #game-view > .card { grid-column: 1 / -1; grid-row: 5; }
  body.am-tisch #game-view .game-uhr-zeile {
    display: flex; align-items: center; gap: 10px; padding: 8px 18px; border-radius: 999px;
    border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
  }
  html[data-theme="dark"] body.am-tisch #game-view .game-uhr-zeile { background: var(--md-sys-color-surface-container); }
  body.am-tisch #game-view .game-uhr-zeile::before { content: 'timer'; font-family: 'Material Symbols'; font-size: 18px; }
  body.am-tisch #game-view .game-timer { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
  body.am-tisch #game-view .game-controls .btn {
    min-height: 56px; height: 56px; padding: 0 20px; gap: 10px; border-radius: 16px; font-size: 16px; font-weight: 500;
    border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
    color: var(--md-sys-color-on-surface);
  }
  /* Beenden zuletzt und gefuellt, Abbrechen davor als Textknopf (Entwurf). */
  body.am-tisch #game-view .game-controls .btn[onclick*="_abbrechen"] {
    order: 1; margin-left: auto; border-color: transparent; background: transparent; color: var(--md-sys-color-on-surface-variant);
  }
  body.am-tisch #game-view .game-controls .btn[onclick*="_endGame"] {
    order: 2; background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); border-color: transparent; font-weight: 600;
  }
  body.am-tisch #game-view .game-controls .btn[onclick*="_abbrechen"] .msym,
  body.am-tisch #game-view .game-controls .btn[onclick*="_endGame"] .msym { display: none; }
}

/* Das laufende Spiel am Telefon: dieselbe Ordnung, kleinere Masse - die
   150/88-px-Spalten des Tisches sprengten bei 390 px die Karte (Pruefbild
   2026-09-30: Namen brachen buchstabenweise um). */
@media (max-width: 600px) {
  body.am-tisch #game-view .team-panel, #game-view .team-panel { padding: 12px; }
  #game-view .team-players-big { --gt-av-size: 36px !important; }
  #game-view .team-players-big .gt-p { column-gap: 8px; }
  #game-view .team-players-big .gt-name { font-size: 17px; overflow-wrap: normal; word-break: normal; }
  #game-view .team-players-big .gt-proj { font-size: 11px; }
  #game-view .tor-like { position: static; align-self: flex-end; }
  #game-view .team2 .tor-like { align-self: flex-start; }
  .nav-marke { display: none !important; }
  #game-view .tor-like .btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; }
  #game-view .team-punkte, #game-view .team2 .team-punkte {
    column-gap: 6px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "score score" "plus plus" "eigen minus" "anf anf";
  }
  #game-view .team2 .team-punkte { grid-template-areas: "score score" "plus plus" "minus eigen" "anf anf"; }
  #game-view .team-punkte .goal-btn-plus-t1, #game-view .team-punkte .goal-btn-plus-t2,
  #game-view .team-punkte .goal-btn-minus, #game-view .team-punkte .gv-eigentor {
    width: 100%; max-width: none; justify-self: stretch;
    height: 56px; min-height: 56px; border-radius: 16px; font-size: 15px;
  }
  #game-view .team-punkte .goal-btn-plus-t1, #game-view .team-punkte .goal-btn-plus-t2 { font-size: 18px; }
}

/* ══ Neugestaltung · Halbzeit und Spielende (Entwuerfe TischHalbzeit,
   TischSieger, 2026-09-30) ════════════════════════════════════════════════
   Die Halbzeit deckt die ganze Flaeche in der Grundfarbe (kein Scrim), das
   Spielende ist eine Karte ueber dem weichgezeichneten Spiel. */
#halftime-banner { background: var(--md-sys-color-surface); backdrop-filter: none; -webkit-backdrop-filter: none; }
#halftime-banner .banner-content { color: var(--md-sys-color-on-surface); gap: 18px; }
.banner-kreis {
  display: grid; place-items: center; width: 88px; height: 88px; margin: 0 auto; border-radius: 50%;
  background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface);
}
#halftime-banner .banner-kreis .msym { font-size: 40px; }
.banner-marke { font-size: 18px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--md-sys-color-on-surface-variant); }
#halftime-banner h1 { margin: 0; font-size: 56px; font-weight: 600; color: var(--md-sys-color-on-surface); letter-spacing: -.01em; }
#halftime-score { display: flex; align-items: center; justify-content: center; gap: 28px; font-size: 72px; font-weight: 600; }
#halftime-score .hs-name { display: inline-flex; align-items: center; gap: 10px; font-size: 28px; color: var(--md-sys-color-on-surface); }
#halftime-score .hs-name .seite-punkt { width: 14px; height: 14px; flex-basis: 14px; }
/* Seit dem 2026-10-06 OHNE Teamfarbe (siehe `.fg-stand` oben); der
   `.seite-punkt` daneben traegt die Figurenfarbe weiterhin. */
#halftime-score .t1, #halftime-score .t2 { color: inherit; }
#halftime-score .sep { color: var(--md-sys-color-on-surface-variant); margin: 0 -18px; }
#halftime-banner .btn-xl { min-height: 72px; padding: 0 40px; border-radius: 22px; font-size: 20px; font-weight: 600; }

#game-end-banner { background: rgba(20, 20, 22, .45); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
html[data-theme="dark"] #game-end-banner { background: rgba(0, 0, 0, .62); }
#game-end-banner .banner-content {
  width: min(760px, calc(100vw - 48px)); padding: 36px 40px 32px; gap: 6px; border-radius: 32px;
  background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface);
  box-shadow: 0 24px 64px rgba(0, 0, 0, .25);
}
html[data-theme="dark"] #game-end-banner .banner-content { background: var(--md-sys-color-surface-container); }
#game-end-banner .banner-avatars { display: none; }
#game-end-banner .banner-kreis { width: 64px; height: 64px; margin-bottom: 8px; }
#game-end-banner .banner-kreis .msym { font-size: 30px; }
#end-kreis[data-seite="team1"] { background: var(--md-ext-color-team1-container); color: var(--md-ext-color-team1); box-shadow: 0 0 0 2px var(--md-ext-color-figur-kontur) inset; }
#end-kreis[data-seite="team2"] { background: var(--md-ext-color-team2-container); color: var(--md-ext-color-team2); box-shadow: 0 0 0 2px var(--md-ext-color-figur-kontur) inset; }
#game-end-banner .banner-marke { font-size: 15px; font-weight: 500; letter-spacing: 0; text-transform: none; }
#game-end-banner #end-headline { margin: 0; font-size: 52px; font-weight: 600; color: var(--md-sys-color-on-surface); }
#game-end-banner #end-score { font-size: 22px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
#game-end-banner #end-score .t1, #game-end-banner #end-score .t2 { color: inherit; }
#game-end-banner #end-score .sep { margin: 0 .2em; }
#game-end-banner .banner-points { display: none; }
.end-kacheln { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; width: 100%; margin: 22px 0; }
.end-kacheln:empty { display: none; }
.end-kachel {
  display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 14px 16px; border-radius: 18px;
  background: var(--md-sys-color-surface-container-high);
}
.end-kachel strong { font-size: 22px; font-weight: 600; }
.end-kachel span { font-size: 13px; color: var(--md-sys-color-on-surface-variant); }
#game-end-banner:has(#end-kacheln:not(:empty)) .br-row.is-winner { display: none; }
#game-end-banner .br-row { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
#game-end-banner .banner-weiter { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; width: 100%; }
#game-end-banner .banner-weiter .btn {
  flex-direction: column; gap: 2px; min-height: 92px; height: auto; padding: 12px; border-radius: 20px;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
}
#game-end-banner .banner-weiter .btn .msym { font-size: 22px; }
.wk-titel { font-size: 16px; font-weight: 600; }
.wk-unter { font-size: 13px; font-weight: 400; color: var(--md-sys-color-on-surface-variant); }
#game-end-banner .btn-xxl {
  width: 100%; min-height: 60px; height: 60px; margin-top: 10px; padding: 0 20px; border-radius: 20px; font-size: 16px; font-weight: 600;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface);
}
#game-end-banner #end-undo { min-height: 48px; border-radius: 14px; }
@media (max-width: 600px) {
  #halftime-banner h1 { font-size: 34px; }
  #halftime-score { font-size: 48px; gap: 14px; }
  #halftime-score .hs-name { font-size: 16px; }
  #game-end-banner .banner-content { padding: 24px 18px; }
  #game-end-banner #end-headline { font-size: 34px; }
  .end-kacheln, #game-end-banner .banner-weiter { grid-template-columns: minmax(0, 1fr); }
}

/* ══ Neugestaltung · PIN (Entwurf TischPIN, 2026-09-30) ════════════════════
   Acht Punkte, Tasten 76 px hoch (nicht quadratisch) mit Haarlinie in der
   Kartenflaeche, alle gleich neutral - auch das Loeschen (bis dahin rot).
   „Mindestens 4 Ziffern" steht, solange kein Fehler dort steht. */
#modal-box:has(#numpad) { max-width: 460px; }
#modal-box:has(#numpad) .pin-display { gap: 12px; padding: 10px 0; margin-bottom: 16px; }
#modal-box:has(#numpad) .pin-dot { width: 16px; height: 16px; border: 2px solid var(--md-sys-color-outline-variant); background: transparent; }
#modal-box:has(#numpad) .pin-dot.filled { background: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
#modal-box:has(#numpad) .pin-numpad { max-width: none; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
#modal-box:has(#numpad) .pin-key, #modal-box:has(#numpad) .pin-key.del {
  aspect-ratio: auto; width: auto; height: 76px; min-height: 76px; border-radius: 20px;
  border: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-lowest); color: var(--md-sys-color-on-surface);
  font-size: 28px; font-weight: 500;
}
html[data-theme="dark"] #modal-box:has(#numpad) .pin-key { background: var(--md-sys-color-surface-container); }
#modal-box:has(#numpad) #pin-error:empty::before { content: 'Mindestens 4 Ziffern'; }
#modal-box:has(#numpad) #pin-error { font-size: 13px; }

/* ══ Neugestaltung · Schnellster Ball (Entwurf TischSchnellsterBall,
   2026-09-30) ══════════════════════════════════════════════════════════════
   Die Zahl in Textfarbe statt Gelb, 150 px; zwei Spalten. */
.ss-kopf { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; }
#content .ss-kopf .ss-titel { display: block; margin: 0; font-size: 26px; font-weight: 600; }
.ss-marke { padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant); }
.ss-luft { flex: 1; }
.ss-beenden { min-height: 48px; border-radius: 14px; color: var(--md-sys-color-error); font-weight: 600; }
.ss-raster { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 16px; }
.ss-raster > .card { margin: 0; border-radius: 22px; }
.ss-buehne { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 28px; text-align: center; }
.ss-l { font-size: 15px; color: var(--md-sys-color-on-surface-variant); }
.ss-spieler { display: flex; align-items: center; gap: 10px; font-size: 44px; font-weight: 600; }
.ss-tempo { font-size: 150px; line-height: 1; font-weight: 600; letter-spacing: -.03em; font-variant-numeric: tabular-nums; min-height: 150px; transition: all .3s; }
.ss-rang { font-size: 16px; font-weight: 500; }
.ss-steuer { display: flex; align-items: center; gap: 12px; align-self: stretch; margin-top: 8px; }
.ss-steuer .btn { min-height: 56px; border-radius: 16px; }
.ss-steuer .sensor-indicator { display: flex; align-items: center; gap: 8px; }
.ss-manuell { display: flex; gap: 8px; justify-content: center; }
.ss-manuell .form-input { max-width: 120px; text-align: center; }
.ss-top { padding: 20px 22px; }
.ss-top .card-title { font-size: 17px; font-weight: 600; }
@media (max-width: 840px) {
  .ss-raster { grid-template-columns: minmax(0, 1fr); }
  .ss-tempo { font-size: 96px; min-height: 96px; }
  .ss-spieler { font-size: 30px; }
}

.ss-einheit { margin-left: 12px; font-size: 36px; color: var(--md-sys-color-on-surface-variant); letter-spacing: 0; }
.ss-top .sp-row {
  display: grid; grid-template-columns: 24px minmax(0, 1fr) auto auto; align-items: center; gap: 10px;
  min-height: 46px; padding: 0 8px;
}
.ss-top .sp-row > * { min-width: 0; }
.ss-top .standing-rank { font-size: 15px; color: var(--md-sys-color-on-surface-variant); }
.ss-top .standing-name-col { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 500; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.ss-top .sp-wert { white-space: nowrap; }
.ss-top .standing-row-highlight { background: var(--md-sys-color-surface-container-high); border-radius: 10px; }
.ss-top .sp-wert { font-size: 17px; font-weight: 600; color: var(--md-sys-color-on-surface); }

/* Die Vorschau am Tisch wie aus dem Netz (Entwurf TischVorschau, 2026-09-30):
   fuenf Kacheln je Reihe, 96 px, Kuerzel statt Personensymbol bleibt Sache
   der Kachel; gewaehlt in der Farbe der Seite. */
html[data-ort="tisch"] [id^="pv-grid-"].player-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
html[data-ort="tisch"] [id^="pv-grid-"] .player-card {
  min-height: 96px; padding: 12px 6px 10px; gap: 6px; border-radius: 16px;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest); box-shadow: none;
}
html[data-theme="dark"][data-ort="tisch"] [id^="pv-grid-"] .player-card { background: var(--md-sys-color-surface-container); }
html[data-ort="tisch"] [id^="pv-grid-"] .player-avatar { width: 40px; height: 40px; }
html[data-ort="tisch"] [id^="pv-grid-"] .player-name { font-size: 14px; font-weight: 500; }
html[data-ort="tisch"] #pv-grid-t1 .player-card.selected { border: 2px solid var(--md-ext-color-team1); background: var(--md-ext-color-team1-container); }
html[data-ort="tisch"] #pv-grid-t2 .player-card.selected { border: 2px solid var(--md-ext-color-team2); background: var(--md-ext-color-team2-container); box-shadow: 0 0 0 1px var(--md-ext-color-figur-kontur) inset; }

/* ══ Neugestaltung · Zugänge und Vergleich (Entwuerfe WebZugaenge,
   WebVergleich, 2026-09-30) ═══════════════════════════════════════════════ */
.card > .page-header > h3 { margin: 0; font-size: 18px; font-weight: 600; }
.card > .page-header { margin-bottom: 12px; }
html .kopf-aktionen > .aktion.btn:not(.nur-symbol) {
  min-height: 40px; height: 40px; padding: 0 14px; gap: 8px; border-radius: 12px;
  border: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface); font-size: 14px;
}
.vg-tabelle th, .vg-tabelle td { padding: 12px 8px; border-bottom: 1px solid var(--md-sys-color-outline-variant); font-size: 14px; }
.vg-tabelle th { font-weight: 600; }
.vg-tabelle td:nth-child(2) { font-weight: 600; }

/* ══ Nachbesserungen aus der Sichtpruefung aus dem Netz (2026-09-30:
   354 Laeufe, 286 harte Befunde) ═══════════════════════════════════════════
   * Start bei 601-840 px (Schiene): zwei Spalten liessen „Letzte Spiele" auf
     die Namen laufen - dort eine Spalte.
   * Die Spielerliste bei 601-840 px wie am Telefon (die festen Spalten
     liessen Name und Marke ueberlappen).
   * Segmente duerfen umbrechen („Schnellster Ball", „Freundschaft" wurden
     abgeschnitten).
   * TV: die Kanalkacheln mit einer Mindestbreite, die am Telefon passt.
   * Kalender am Telefon: „6 Spiele" bricht in der Zelle um.
   * Das Zitat in der eigenen (dunklen) Blase erbt die Textfarbe. */
@media (max-width: 840px) {
  .dashboard-grid, html[data-ort="netz"] .dashboard-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (min-width: 601px) and (max-width: 840px) {
  /* Spieler: die Tabelle wird eine Liste - Name oben, Art, Anmeldung und
     Gruppen als kleine Unterzeile, die Aktionen darunter rechts. Keine
     Spalte geht verloren, nur ihre Anordnung. */
  html[data-ort] .sp-tabelle, .sp-tabelle tbody { display: block; width: 100%; table-layout: auto; }
  .sp-tabelle thead { display: none; }
  .sp-tabelle tr {
    display: flex; flex-wrap: wrap; align-items: center; column-gap: 8px; row-gap: 2px;
    padding: 10px 0; border-bottom: 1px solid var(--md-sys-color-outline-variant);
  }
  .sp-tabelle tr:last-child { border-bottom: none; }
  /* Die Zeilenlinie der Tabelle ist ein innerer Schatten der Zelle (gemessen
     2026-09-30) - in der Liste zieht sie die Zeile, nicht jede Zelle. */
  html[data-ort] .sp-tabelle td { display: block; padding: 0; border: none !important; box-shadow: none !important; font-size: 12px; }
  html[data-ort] .sp-tabelle td:first-child { flex: 1 0 100%; font-size: 16px; }
  html[data-ort] .sp-tabelle td:not(:first-child):not(.spalte-aktionen) { color: var(--md-sys-color-on-surface-variant); padding-left: 48px; }
  html[data-ort] .sp-tabelle td:not(:first-child):not(.spalte-aktionen) + td:not(.spalte-aktionen) { padding-left: 0; }
  html[data-ort] .sp-tabelle td > div:first-child { white-space: normal; }
  html[data-ort] .sp-tabelle td.spalte-aktionen { flex: 1 0 100%; }
  html[data-ort] .sp-tabelle .spalte-aktionen .list-item-actions { justify-content: flex-end; flex-wrap: wrap; gap: 2px; }
  html[data-ort] .sp-tabelle .aktion.nur-symbol { width: 36px; height: 36px; min-width: 36px; border-radius: 50%; }
  .sp-tabelle .badge { padding: 1px 8px; font-size: 11px; }
}
html .setup-feld .seg-btn { white-space: normal; height: auto; min-height: 44px; line-height: 1.2; padding: 6px 8px; }
.kanal-raster { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
.kanal-raster .kanal-zeile { min-width: 0; }
@media (max-width: 600px) {
  .kal-tag .kal-anzahl { white-space: normal; font-size: 9px; line-height: 1.1; overflow-wrap: anywhere; }
}
.nr-n.is-meins .nr-zitat-von, .nr-n.is-meins .nr-zitat-text { color: inherit; }

/* Zweiter Durchgang der Sichtpruefung (2026-09-30): netz 18, Tisch 30 harte
   Befunde - die drei Ursachen hier. */
.setup-optionen { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
@media (min-width: 601px) and (max-width: 840px) {
  #game-view .team-punkte { grid-template-columns: minmax(0, 1fr) 104px 64px; }
  #game-view .team2 .team-punkte { grid-template-columns: 64px 104px minmax(0, 1fr); }
  #game-view .team-punkte .goal-btn-plus-t1, #game-view .team-punkte .goal-btn-plus-t2 { font-size: 18px; gap: 6px; }
}
/* ⚠️ Ist die Karte zu schmal, steht „+ Tor" UEBER Eigentor und „−"
   (2026-10-01). Eigentor und „−" haben feste Spuren (150 + 88, zwischen 601
   und 840 px 104 + 64), „+ Tor" bekam nur den Rest - gemessen am Tisch bei
   841 px 20 px, bei 700 px 42 px: ein Streifen mit „To". Seit „+ Tor"
   schwarz gefuellt ist (Betreiber: „Kraeftiger"), sprang das ins Auge; der
   Betreiber: „Ja, mit beheben". An der KARTE abgefragt, nicht am Fenster -
   ihre Breite haengt an Einblicken, Seitenleiste und Groesse am Tisch.
   420 px Inhalt = 150 + 88 + 2 x 12 Abstand + 158 fuer „+ Tor". Erst ab
   601 px: darunter hat der 600er Block dieselbe Anordnung schon, mit den
   Massen des Telefons. Die Karte als Container ist gefahrlos: ihre Breite
   kommt vom Raster, nie vom Inhalt (siehe `.team-panel` oben). */
#game-view .team-panel { container: torkarte / inline-size; }
@media (min-width: 601px) {
  @container torkarte (max-width: 419px) {
    #game-view .team-punkte, #game-view .team2 .team-punkte {
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      grid-template-areas: "score score" "plus plus" "eigen minus" "anf anf";
      grid-template-rows: minmax(0, 1fr) auto auto auto;
    }
    #game-view .team2 .team-punkte { grid-template-areas: "score score" "plus plus" "minus eigen" "anf anf"; }
    /* `max-width: none`: die Grundregel deckelt Eigentor bei der Breite der
       Tortaste (130) - gemessen stand es bei 1000 px neben einem „−" von
       171 px. Gestapelt sind beide halbe Breite. */
    #game-view .team-punkte .goal-btn-minus, #game-view .team-punkte .gv-eigentor {
      height: 64px; min-height: 64px; max-width: none; justify-self: stretch;
    }
  }
}
html .setup-feld .seg-group { width: 100%; max-width: none; }
html .setup-feld .seg-btn { font-size: 14px; }

/* Das Menue „Mehr" an der Torzeile: eine schwebende Liste wie das
   Spielermenue (Entwurf WebSpielDetail, 2026-09-30). */
.konto-menu.tor-menue { z-index: 500; min-width: 200px; right: auto; border-radius: 16px; }
.konto-menu.tor-menue .km-punkt { border-radius: 10px; }
html .sd-tore td[id^="goal-actions-"] .aktion.tor-mehr[aria-expanded="true"] { background: var(--md-sys-color-surface-container-high); }

/* Die Filterchips der Glocke (Entwurf MobilGlocke, 2026-09-30): nur am
   Telefon - der Rechner-Entwurf WebGlocke hat keine. */
.nb-filter { display: none; }
@media (max-width: 600px) {
  .nb-filter { display: flex; gap: 6px; flex-wrap: nowrap; overflow-x: auto; margin: 4px 0 10px; }
  .nb-filter .chip { flex: 0 0 auto; height: 32px; min-height: 32px; font-size: 13px; }
  .nb-filter-leer { padding: 24px 0; }
}

/* Die Zahl am Menuepunkt „Nachrichten" (Entwurf WebNachrichten,
   2026-09-30): eine Pille rechts im Punkt, auf der Schiene (601-840) auf dem
   Symbol. Eigener Selektor HINTER der Glockenregel - die sucht ihre erste
   Fundstelle (tests/test_challenges_ui.py). */
.nav-nachrichten[data-count]:not([data-count=""])::after {
  content: attr(data-count); margin-left: auto;
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
  font-size: 11px; font-weight: 600; line-height: 1;
}
.km-zahl {
  margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);
  font-size: 11px; font-weight: 600;
}
@media (min-width: 601px) and (max-width: 840px) {
  html[data-ort="netz"] #navbar .nav-nachrichten { position: relative; }
  html[data-ort="netz"] #navbar .nav-nachrichten[data-count]:not([data-count=""])::after {
    position: absolute; top: 4px; right: 8px; min-width: 16px; height: 16px; font-size: 10px;
  }
}

/* Die Mitglieder einer Gruppe im Kopf (Entwurf WebNachrichten, 2026-09-30):
   die Namen laufen mit Auslassung aus, die Online-Zahl bleibt immer ganz.
   Am Telefon zeigt der Entwurf MobilChat im Gruppenkopf keine Unterzeile. */
.nr-g-unter .nr-g-namen { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nr-g-unter .nr-g-zahl { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 600px) {
  .nr-g-unter:has(.nr-g-namen) { display: none; }
}

/* „Aufzeichnung laeuft" im Kopf des laufenden Spiels (Entwurf TischSpiel,
   2026-09-30): rechts, im Nebenton; am Telefon nur ein Punkt, weil die
   Kopfzeile bei 390 px schon umbricht. */
.gv-aufnahme { margin-left: auto; font-size: 15px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; }
.gv-aufnahme.hidden { display: none; }
@media (min-width: 841px) {
  body.am-tisch #game-view .gv-aufnahme { grid-column: 3; grid-row: 1; justify-self: end; align-self: center; margin: 0; }
}
@media (max-width: 600px) {
  .gv-aufnahme { font-size: 0; width: 8px; height: 8px; border-radius: 50%; background: var(--md-sys-color-error); }
}

/* Hausmass 32 px fuer jedes Bedienziel (Sichtpruefung 2026-09-30: die
   Resonanz-Pillen der Tortabelle waren 26 px hoch, das ⋯ und der Smiley am
   Telefon 28 - je Torzeile ein weicher Befund, 1584 statt 176 am Tisch). */
html .sd-tore .tor-echo .aktion { min-height: 32px; height: 32px; }
html .sd-tore td[id^="goal-actions-"] .aktion { width: 32px; min-width: 32px; height: 32px; min-height: 32px; }
html .nr-senden .em-knopf, html .cd-senden .em-knopf, html .sd-kommkarte .echo-eingabe .em-knopf,
html .vb-links .echo-eingabe .em-knopf { width: 32px; min-width: 32px; height: 32px; min-height: 32px; flex-basis: 32px; }

/* ── Suche in den Einstellungen (2026-10-06) ──────────────────────────────
   Das Feld steht im normalen Fluss, die Trefferliste direkt darunter - KEIN
   `position: fixed` wie bei der @-Erwaehnung (js/erwaehnung.js): dort muss
   die Liste einem Textfeld folgen, das irgendwo auf der Seite stehen kann;
   hier gibt es genau einen festen Platz, und `fixed` bräuchte nur das
   Nachziehen bei Scrollen und Tastatur, das `js/erwaehnung.js` dafür extra
   betreiben muss. Im Fluss erledigt der Browser das von selbst. */
.es-wirt { min-height: 56px; margin-bottom: 16px; }
.es-feld { max-width: 420px; }
.es-liste {
  margin-top: 6px; max-width: 420px;
  background: var(--md-sys-color-surface-container-low);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-small);
  overflow-y: auto; max-height: 50vh;
}
.es-treffer {
  display: flex; flex-direction: column; gap: 2px; width: 100%;
  min-height: var(--min-touch); padding: 8px 14px; text-align: left;
  background: none; border: none; border-bottom: 1px solid var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface); cursor: pointer;
}
.es-liste .es-treffer:last-child { border-bottom: none; }
.es-treffer:hover, .es-treffer.aktiv { background: var(--md-sys-color-surface-container-high); }
.es-treffer-pfad { font-size: 13px; color: var(--md-sys-color-on-surface-variant); }
.es-leer { padding: 10px 14px; font-size: 14px; color: var(--md-sys-color-on-surface-variant); }

/* Vier Sekunden Hervorhebung nach dem Sprung - Vorbild `tr.tor-gesprungen`
   (game.js `_torHervorheben`). `outline` statt `background`: das Ziel ist
   mal eine Tabellenzeile, mal eine ganze Karte, und eine Kontur passt zu
   beiden, ohne deren eigene Fläche zu überdecken. */
.es-gefunden {
  outline: 2px solid var(--md-sys-color-secondary); outline-offset: 2px;
  border-radius: var(--md-sys-shape-corner-extra-small);
}

/* ══ Die schmale Menueleiste (2026-10-17) ══════════════════════════════════
   Der Betreiber: „die Menueseite schmaler, die Icons mit kleinem Text
   darunter, ein kleiner Pfeil klappt das Menue ein, dann bleibt links nur
   eine Lasche - Vorlage MS Teams, dazu eine schoene Animation."

   Aus 248 px (Symbol neben Text) und der Schiene von 76 px wird EINE Breite:
   84 px, Symbol ueber Beschriftung. 84 und nicht 76, weil „Einstellungen" in
   11 px gemessen rund 70 px braucht und in 76 px (Kachel 68) abgeschnitten
   haette - gemessen wird in der Sichtpruefung, nicht gerechnet.
   Zwei Zustaende, am <html> als `data-menue`: fehlt es, steht die Leiste;
   `zu` nimmt sie ganz weg (Merker `bss.menue`, Kopfskript in index.html).

   ⚠️ GANZ AM ENDE und nur uebersteuernd: die Bloecke fuer 248/76 px oben
   bleiben stehen (Geschichte, und Tests lesen sie woertlich). Gleiche
   Staerke, spaeter im Text - und im Spiel am Tisch gewinnt weiter die
   `:is(:has(#game-view)…)`-Regel mit `width: 0`, weil sie staerker ist.
   ⚠️ Die Leiste selbst wird NIE per `transform` verschoben: Titel, Uhr, Glocke
   und Konto sind `position: fixed` und Kinder von `#navbar` - ein Transform am
   Elternteil machte sie zu dessen Kindern und risse sie mit. Stattdessen
   schrumpft die Breite, und die Kacheln gleiten einzeln heraus.
   Verworfen: `margin-left` des Inhalts ohne Uebergang (der Inhalt spraenge).
   ⚠️ Keine neue Zoomdivision (test_zoom_math_untouched): alles in px. */
.nav-zu, .nav-lasche { display: none; }

@media (min-width: 601px) {
  html[data-ort] { --nav-breite: 84px; }
  html[data-ort][data-menue="zu"] { --nav-breite: 0px; }

  html[data-ort] #navbar {
    width: var(--nav-breite);
    padding: 16px 0 12px;
    align-items: center; gap: 4px;
    overflow-y: auto; scrollbar-width: none;
    transition: width var(--md-sys-motion-duration-medium1)
                      var(--md-sys-motion-easing-emphasized),
                border-color var(--md-sys-motion-duration-short4)
                             var(--md-sys-motion-easing-standard);
  }
  html[data-ort] #navbar::-webkit-scrollbar { display: none; }
  html[data-ort][data-menue="zu"] #navbar { border-right-color: transparent; }
  html[data-ort] #navbar::after {
    left: var(--nav-breite);
    transition: left var(--md-sys-motion-duration-medium1)
                     var(--md-sys-motion-easing-emphasized);
  }
  html[data-ort] #content {
    margin-left: var(--nav-breite);
    transition: margin-left var(--md-sys-motion-duration-medium1)
                            var(--md-sys-motion-easing-emphasized);
  }
  html[data-ort][data-menue="zu"] #content { padding-left: 36px; }
  html[data-ort] #readonly-banner { margin-left: var(--nav-breite); }
  html[data-ort] .nav-titel { left: calc(var(--nav-breite) + 36px); }
  html[data-ort="netz"] .kopf-zurueck { left: calc(var(--nav-breite) + 36px); }
  html[data-ort="netz"]:has(#content .kopf-zurueck) .nav-titel { left: calc(var(--nav-breite) + 88px); }

  /* Marke, Ziele und Pfeil: gemeinsames Heraus- und Hineingleiten. */
  html[data-ort] :is(.nav-marke, .nav-links, .nav-mehr, .nav-gear, .nav-zu) {
    transition: transform var(--md-sys-motion-duration-medium1)
                          var(--md-sys-motion-easing-emphasized-decelerate),
                opacity var(--md-sys-motion-duration-short4) linear;
  }
  html[data-ort][data-menue="zu"] :is(.nav-marke, .nav-links, .nav-mehr, .nav-gear, .nav-zu) {
    transform: translateX(-100%); opacity: 0; visibility: hidden; pointer-events: none;
    transition: transform var(--md-sys-motion-duration-medium1)
                          var(--md-sys-motion-easing-emphasized-accelerate),
                opacity var(--md-sys-motion-duration-short4) linear,
                visibility 0s linear var(--md-sys-motion-duration-medium1);
  }

  html[data-ort] .nav-marke { padding: 0 0 12px; justify-content: center; }
  html[data-ort] .nav-marke-text { display: none; }
  html[data-ort] :is(.nav-links, .nav-mehr) {
    width: 100%; align-items: center; flex-direction: column; gap: 4px;
  }

  /* Die Kachel: Symbol in einer Kapsel, die Beschriftung darunter. Der Text
     traegt eine KLASSE (`nav-text`), nie `:last-child` - die Ripple-Welle
     haengt ein <span> an (tests/test_kopfzeile_menue.py). */
  html[data-ort] #navbar :is(.nav-link, .nav-gear) {
    flex-direction: column; justify-content: flex-start; align-items: center;
    width: 76px; height: auto; min-height: 56px; padding: 4px 0 0; gap: 2px;
    border-radius: var(--md-sys-shape-corner-medium); background: none; box-shadow: none;
    font-size: 11px; line-height: 14px; font-weight: 500; letter-spacing: 0;
    text-align: center;
    color: var(--md-sys-color-on-surface-variant);
  }
  html[data-ort] #navbar :is(.nav-link, .nav-gear) .msym {
    display: grid; place-items: center; width: 52px; height: 30px;
    border-radius: var(--md-sys-shape-corner-full); font-size: 22px;
    transition: background-color var(--md-sys-motion-duration-short3) linear;
  }
  html[data-ort] #navbar :is(.nav-link, .nav-gear) :is(.nav-text, .nav-gear-text) {
    display: block; max-width: 100%; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
  }
  html[data-ort] #navbar :is(.nav-link, .nav-gear):hover { background: none; }
  html[data-ort] #navbar :is(.nav-link, .nav-gear):hover .msym {
    background: var(--md-sys-color-surface-container);
  }
  html[data-ort] #navbar .nav-link.active {
    background: none; box-shadow: none; font-weight: 700;
    color: var(--md-sys-color-on-surface);
  }
  html[data-ort] #navbar .nav-link.active .msym {
    background: var(--md-sys-color-secondary-container);
    color: var(--md-sys-color-on-secondary-container);
    animation: nav-kapsel var(--md-sys-motion-duration-short3)
               var(--md-sys-motion-easing-standard);
  }
  @keyframes nav-kapsel {
    from { transform: scaleX(.6); opacity: .4; }
    to   { transform: none; opacity: 1; }
  }

  /* Pfeil unten in der Leiste. */
  .nav-zu {
    display: inline-grid; place-items: center; flex: 0 0 auto;
    width: 44px; height: 40px; margin-top: auto; padding: 0;
    border: none; border-radius: var(--md-sys-shape-corner-medium); background: none; cursor: pointer;
    color: var(--md-sys-color-on-surface-variant);
  }
  .nav-zu:hover { background: var(--md-sys-color-surface-container); color: var(--md-sys-color-on-surface); }
  .nav-zu .msym { font-size: 22px; }

  /* Die Lasche am linken Rand: nur sichtbar, wenn die Leiste zu ist. */
  .nav-lasche {
    display: grid; place-items: center; position: fixed; z-index: 101;
    left: 0; top: 50%; margin-top: -28px; width: 18px; height: 56px; padding: 0;
    border: 1px solid var(--md-sys-color-outline-variant); border-left: none;
    border-radius: 0 var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) 0; cursor: pointer;
    background: var(--md-sys-color-surface);
    color: var(--md-sys-color-on-surface-variant);
    box-shadow: var(--md-sys-elevation-level1);
    opacity: 0; visibility: hidden; pointer-events: none;
    transform: translateX(-100%);
    transition: transform var(--md-sys-motion-duration-short4)
                          var(--md-sys-motion-easing-emphasized-accelerate),
                opacity var(--md-sys-motion-duration-short3) linear,
                width var(--md-sys-motion-duration-short3)
                      var(--md-sys-motion-easing-standard),
                visibility 0s linear var(--md-sys-motion-duration-short4);
  }
  html[data-ort][data-menue="zu"] .nav-lasche {
    opacity: 1; visibility: visible; pointer-events: auto; transform: none;
    transition: transform var(--md-sys-motion-duration-medium1)
                          var(--md-sys-motion-easing-emphasized-decelerate) var(--md-sys-motion-duration-short2),
                opacity var(--md-sys-motion-duration-short4) linear var(--md-sys-motion-duration-short2),
                width var(--md-sys-motion-duration-short3)
                      var(--md-sys-motion-easing-standard);
  }
  .nav-lasche:hover, .nav-lasche:focus-visible { width: 24px; color: var(--md-sys-color-primary); }
  .nav-lasche .msym { font-size: 20px; }

  /* Im Spiel und beim Neuen Spiel am Tisch gibt es keine Leiste, also auch
     weder Pfeil noch Lasche. */
  html[data-ort="tisch"]:is(:has(#game-view), :has(#content .setup-seiten)) :is(.nav-zu, .nav-lasche) {
    display: none;
  }
}

@media (min-width: 601px) and (max-width: 840px) {
  html[data-ort] .nav-titel { left: calc(var(--nav-breite) + 24px); }
  html[data-ort="netz"] .kopf-zurueck { left: calc(var(--nav-breite) + 24px); }
  html[data-ort="netz"]:has(#content .kopf-zurueck) .nav-titel { left: calc(var(--nav-breite) + 76px); }
}

/* ══ Die Spielkachel (2026-10-17) ═══════════════════════════════════════════
   Der Betreiber: die Spielliste „in allen Ansichten (Kiosk, Startseite,
   Spieler Statistiken) auf die zweireihige Kachelansicht umstellen … genau so
   aussehen wie auf den Screenshots" (docs/Vorlage), und die Ueberschriften
   durch das Datum ersetzen.

   Ersetzt die fuenfspurige Tabellenzeile (`.rg-tabelle`, `subgrid`, drei
   ortsabhaengige Uebersteuerungen) samt `.recent-game-row`, `.recent-score`
   und dem Videoknopf `.video-marke`. Markup: `App.spiele.liste` (dashboard.js).

   Die Spaltenzahl entscheidet eine Frage an den WICKEL `.sp-liste` und nicht
   an das Fenster: dieselbe Liste ist je nach Layout 409 oder 800 px breit.
   Die Schwelle (700 px) ist gerechnet und am 2026-10-17 gemessen
   nachgezogen: bei 600 px (je Kachel 300) war „Jonas Hartmann" gekuerzt, eine
   Kachel braucht fuer Name 120 + Tore 20 + Pfeil 12 + Abstaende 24 + rechte
   Spalte 118 + Rand 40 rund 350 px. */
.rg-spieler {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.rg-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sp-liste { container: sp-liste / inline-size; }
.sp-tag {
  padding: 12px 20px;
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-title-small-size);
  font-weight: 600;
}
.sp-raster { display: grid; grid-template-columns: minmax(0, 1fr); }
.sp-kachel {
  text-decoration: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 14px 20px;
  min-width: 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  color: var(--md-sys-color-on-surface);
  cursor: pointer;
}
.sp-paar { display: grid; gap: 8px; min-width: 0; }
.sp-zeile {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 12px;
  align-items: center;
  gap: 12px;
  font-size: var(--md-sys-typescale-body-large-size);
  color: var(--md-sys-color-on-surface-variant);
}
.sp-tore { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; }
/* Die Avatare der Spielkachel am Tisch (2026-10-30, Betreiber: „viel zu grosse
   Avatare in der Spielhistorie, nur am Tisch"). `_teamLabelHtml` zeichnet sie
   mit `30 px * var(--avatar-scale)`, und der Faktor ist am Tisch die Wahl der
   Anzeige (hier 3,8): gemessen bei 1920 x 1080 waren es 114 x 114 px, die
   Kachel 265 px hoch - fuer zwei Namen und einen Stand. Die Rangliste und die
   Tabellen im Netz deckeln ihren Faktor schon selbst (`.rl-karte`,
   `.sp-tabelle`); die Kachel hatte keinen. 1,2 x 30 = 36 px, so gross wie die
   Rundbilder der Rangliste im Entwurf (32-36). Nur am Tisch: im Netz zeigt die
   Kachel den Faktor der eigenen Anzeigewahl, und danach hat niemand gefragt.
   Eigene Regel HINTER den Kachelregeln (Falle 4). */
html[data-ort="tisch"] .sp-liste { --avatar-scale: 1.2; }
/* Sieger und Gleichstand hell, der Verlierer leise. Beim Gleichstand fehlt die
   Klasse `rg-sieg1/2` - dann sind beide hell und es gibt keinen Pfeil. */
.sp-kachel:not(.rg-sieg1):not(.rg-sieg2) .sp-zeile,
.sp-kachel.rg-sieg1 .sp-s1,
.sp-kachel.rg-sieg2 .sp-s2 { color: var(--md-sys-color-on-surface); }
.sp-kachel.rg-sieg1 .sp-s1::after,
.sp-kachel.rg-sieg2 .sp-s2::after {
  content: "";
  width: 0; height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-right: 7px solid currentColor;
}
.sp-rechts {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 4px;
  width: 118px;
  padding-left: 14px;
  border-left: 1px solid var(--md-sys-color-outline-variant);
  text-align: center;
}
.sp-lage { font-size: var(--md-sys-typescale-body-medium-size); }
.sp-laeuft .sp-lage { color: var(--md-sys-color-primary); font-weight: 600; }
.sp-zeit { font-size: var(--md-sys-typescale-body-small-size); }
.sp-rechts .badge {
  margin-top: 2px; padding: 2px 8px;
  font-size: var(--md-sys-typescale-label-small-size);
}
.sp-video {
  position: relative;
  display: block;
  width: 96px; height: 54px;
  margin-top: 4px;
  overflow: hidden;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container-highest);
  cursor: pointer;
}
.sp-video img { display: block; width: 100%; height: 100%; object-fit: cover; }
.sp-video:focus-visible { outline: 2px solid var(--md-sys-color-primary); outline-offset: 2px; }
/* Dreieck und Laenge sitzen auf je einem Plaettchen in Flaechenfarbe und nicht
   auf dem Bild: es gibt keine Farbe, die auf JEDEM Standbild und in beiden
   Themen lesbar waere (`inverse-on-surface` ist im dunklen Thema dunkel,
   Weiss waere ein roher Wert). */
.sp-play, .sp-dauer {
  position: absolute; bottom: 4px;
  border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface);
}
.sp-play { left: 4px; display: grid; place-items: center; padding: 0 2px; }
.sp-play .msym { font-size: 20px; }
.sp-dauer {
  right: 4px;
  padding: 1px 6px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-variant-numeric: tabular-nums;
}

@container sp-liste (min-width: 700px) {
  .sp-raster { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sp-kachel:nth-child(odd) { border-right: 1px solid var(--md-sys-color-outline-variant); }
}

/* Schmale Wickel (Telefon, schmale Karte): gemessen bei 390 px kuerzte die
   Kachel „Tobias Brandt" zu „Tobias Bra…" - rechte Spalte 118 px, Rand 2 × 20.
   Hier ruecken Rand, Abstand und rechte Spalte zusammen; das Videofeld folgt. */
@container sp-liste (max-width: 440px) {
  .sp-kachel { padding: 12px 12px; gap: 10px; }
  .sp-zeile {
    gap: 8px; grid-template-columns: minmax(0, 1fr) auto 10px;
    font-size: var(--md-sys-typescale-body-medium-size);
  }
  .sp-rechts { width: 100px; padding-left: 8px; }
  .sp-video { width: 80px; height: 45px; }
  .sp-tag { padding: 10px 12px; }
}
/* Noch schmaler (die rechte Spalte der Startseite bei 841 bis 1000 px, ein
   Telefon von 360 px): gemessen bei 860 px ist die Kachel 228 px breit und dem
   Namen bleiben 49 px - alle Namen gekuerzt. Dann steht die rechte Spalte UNTER
   dem Paar, wie es die Tabellenzeile bis zum 2026-10-16 bei 380 px tat. */
@container sp-liste (max-width: 330px) {
  .sp-kachel { grid-template-columns: minmax(0, 1fr); }
  .sp-rechts {
    width: auto; padding: 8px 0 0; border-left: none;
    border-top: 1px solid var(--md-sys-color-outline-variant);
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
    gap: 4px 10px;
  }
  .sp-rechts .badge { margin-top: 0; }
  .sp-video { margin-top: 0; }
}

/* ══ Die Startseite anpassen (2026-10-17) ═══════════════════════════════════
   Der Betreiber: die Rangliste in der Breite ziehen, die Karten rechts werden
   breiter; „Letzte Spiele" in der Hoehe. Siehe `App.anordnung.startAnbringen`
   (static/js/anordnung.js). Gespeichert sind ZWEI Zahlen: `data-links` (die
   Zwoelftel der Rangliste, 3 bis 9) am Raster und `--kh` an „Letzte Spiele".
   Die Breite steht als `--lf: 3fr` und `--rf: 9fr`: `fr` ist in `calc()` nicht
   erlaubt, eine Zahl mal `1fr` machte die Regel zur Laufzeit ungueltig.
   Ohne beides bleibt die Seite wie vorher (1,55 zu 1, Netz 1,5 zu 1).

   ⚠️ Eine Rangliste, die man schmal ziehen kann, braucht Spalten, die sich nach
   IHRER Breite richten. Die Verkleinerung stand bisher nur im 600er-Block, am
   Fenster - schmal gezogen blieb dem Namen fast nichts. Jetzt fragt die Karte
   sich selbst (`container`), in zwei Stufen. Die Schwellen sind aus den festen
   Spuren GERECHNET (382 px Spuren + Name 100 px + Polster) und nachzumessen;
   ⚠️ `html[data-ort]` davor, sonst schlaegt die Netzregel
   `html[data-ort="netz"] .rl-karte .mini-rang` sie bei gleicher Staerke. */
.rl-karte { container: rl-karte / inline-size; position: relative; }
@container rl-karte (max-width: 539px) {
  html[data-ort] .rl-karte .mini-rang { --anteil-spur: 0px; }
  /* ⚠️ `:has(+ .mini-head)` und NICHT `:not(:last-child)` wie im 600er-Block:
     die fuenfte Ueberschrift ist nur dann der Anteil, wenn noch eine
     Ueberschrift (der Wert) folgt. Ohne Anteil (Klassisch, kein Pool) ist sie
     der Wert selbst - und `:not(:last-child)` blendete „Pkt" mit aus (gesehen
     bei 452 px Kartenbreite). */
  .rl-karte .standing-share,
  .rl-karte .mini-rang > .mini-head:nth-child(5):has(+ .mini-head) { overflow: hidden; font-size: 0; }
}
@container rl-karte (max-width: 439px) {
  html[data-ort] .rl-karte .mini-rang {
    --rl-platz: 20px; --rl-form: 62px; --rl-trend: 34px; --anteil-spur: 0px; --rl-wert: 52px;
    /* `!important` wie im 600er-Block (gleicher Grund): das Raster traegt
       seinen Abstand als Inline-Stil. */
    column-gap: 8px !important;
  }
  .rl-karte .mini-rang > span { padding-right: 0; }
  .rl-karte .standing-pts { font-size: 16px; }
  .rl-karte .st-move { font-size: 12px; }
}

@media (min-width: 841px) {
  /* `html` davor: die Netzregel `html[data-ort="netz"] .dashboard-grid` hat
     (0,2,1). Die rechte Spalte behaelt mindestens die Breite des Startknopfs
     (gerechnet, nachzumessen). Die Rangliste behaelt mindestens 340 px: gemessen
     bei 3 Zwoelfteln und 1280 px (276 px) blieben dem Namen 40 px. */
  html .dashboard-grid[data-links] {
    grid-template-columns:
      minmax(var(--links-min, 340px), var(--lf))
      minmax(var(--rechts-min, 360px), var(--rf));
  }
  .dash-rechts > .rg-karte[style*="--kh"] {
    flex: 0 0 auto; height: var(--kh); overflow: hidden; position: relative;
  }
  .anord-start-modus .rl-karte, .anord-start-modus .rg-karte {
    outline: 2px dashed var(--md-sys-color-outline-variant); outline-offset: -2px;
  }
  .rg-karte { position: relative; }
}
.anord-teiler {
  position: absolute; top: 0; bottom: 0; right: 0; width: 18px;
  display: flex; align-items: center; justify-content: center;
  cursor: col-resize; z-index: 2;
}
.anord-teiler::before {
  content: ''; width: 4px; height: 72px; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-outline-variant);
}
.anord-teiler:hover::before, .anord-teiler:focus-visible::before {
  background: var(--md-sys-color-primary);
}
.anord-teiler:focus-visible {
  outline: 2px solid var(--md-sys-color-primary); outline-offset: -2px;
}
/* Ohne `touch-action: none` uebernimmt der Browser den Finger fuer das
   Scrollen, es kommt `pointercancel`, und das Ziehen stirbt nach wenigen
   Pixeln. Gilt auch fuer den Hoehengriff der Statistik. */
.anord-teiler, .anord-griff { touch-action: none; }
.rl-kopf > #anord-schalter { margin-left: auto; }
@media (max-width: 840px) {
  .dashboard-grid #anord-schalter, .dashboard-grid #anord-zurueck,
  .dashboard-grid .anord-teiler, .dashboard-grid .anord-griff { display: none; }
}

/* ── GIF-Avatare passen GANZ in den Kreis ──────────────────────────────────────
   Ein Foto soll beschnitten werden: `cover` fuellt den Kreis, und ein Gesicht
   sitzt in der Mitte. Ein GIF ist aber meist eine FIGUR. Gemessen an einem
   67x131-GIF im 44-px-Kreis: `cover` skaliert auf 44x86 und zeigt davon 44x44 -
   51 % der Hoehe, Kopf und Fuesse weg.

   Die Unterscheidung steht in der ADRESSE (`&typ=gif`, gebaut in
   routers/players.py::avatar_url). Warum dort und nicht in einer Klasse: die
   Adresse liegt in `Players.avatar_url` und wird an rund 84 Stellen direkt
   selektiert; eine Klasse haette an jeder der acht Anzeigestellen gesetzt
   werden muessen.

   EINE Regel statt acht geaenderter: die zwei Attributbedingungen ergeben die
   Trefferstaerke (0,2,1) und schlagen damit jede Klassenregel darunter - und
   sie koennen nichts anderes treffen als einen Avatar. Deshalb steht die Regel
   auch am ENDE der Datei (seit dem 2026-10-17 wirklich: hinter ihr stand
   `.sp-video img { object-fit: cover }`): gegen `.banner-avatars img` (0,1,1) entscheidet sonst
   die Reihenfolge.

   Die einzige Stelle, die diese Regel NICHT erreicht, ist der Inline-Stil in
   app.js::avatarHtml - inline schlaegt jedes Stylesheet. Dort entscheidet
   dieselbe Pruefung an der Adresse. `!important` waere die Alternative und die
   schlechtere: es hebelt spaeter auch jede beabsichtigte Ausnahme aus. */
img[src*="/avatar/image"][src*="typ=gif"] { object-fit: contain; }
