/* LernPuls Dashboard — Design "hell" (Variante A, 2026-07-07)
   Marke: lernpuls.de (Sora + Inter, Orange/Coral auf warmem Hell)
   Status-Farben folgen Solmaz' Excel-Semantik: Orange/Amber = offen, Gruen = bezahlt. */

@font-face { font-family: "Sora"; font-style: normal; font-weight: 600; font-display: swap; src: url("../fonts/sora-600-latin-latinext.41126530349d.woff2") format("woff2"); }
@font-face { font-family: "Sora"; font-style: normal; font-weight: 700; font-display: swap; src: url("../fonts/sora-700-latin-latinext.535034c3df79.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("../fonts/inter-400-latin.260c81a4759b.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("../fonts/inter-400-latin-ext.1ad231aac0a8.woff2") format("woff2"); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("../fonts/inter-500-latin.260c81a4759b.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("../fonts/inter-500-latin-ext.1ad231aac0a8.woff2") format("woff2"); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("../fonts/inter-600-latin.260c81a4759b.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122; }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("../fonts/inter-600-latin-ext.1ad231aac0a8.woff2") format("woff2"); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF; }

:root {
  --bg: #FAF8F6;
  --surface: #FFFFFF;
  --text: #2F2B29;
  --muted: #6E6560;
  --border: #EAE3DC;
  /* Rand eines EINGABEFELDS -- bewusst ein zweites Token und nicht ein
     dunkleres --border. --border zieht jede Trennlinie zwischen zwei Flaechen
     (Tabellenzeile, Kartenkante, Topbar); dafuer ist 1.27:1 auf Weiss richtig
     und soll ruhig bleiben. Die KONTUR EINES BEDIENELEMENTS ist etwas anderes:
     sie ist das einzige Signal, dass dort etwas eingetippt werden kann, und
     WCAG 2.2 (1.4.11 Non-text Contrast) verlangt dafuer 3:1.
     Nachgerechnet (relative Luminanz nach WCAG):
       #EAE3DC auf --surface #FFFFFF = 1.27:1  (durchgefallen)
       #EAE3DC auf --bg      #FAF8F6 = 1.20:1  (durchgefallen)
       #928E87 auf --surface #FFFFFF = 3.26:1
       #928E87 auf --bg      #FAF8F6 = 3.08:1
     Beide Nachbarflaechen kommen wirklich vor: das Formularfeld steht auf
     Weiss, das Feld im Auftrags-Register auf --bg. Der Ton bleibt in der
     warmen Familie von --border (R = G+7, B = G-7) -- ein neutrales Grau
     faellt auf dem Creme-Grund als Fremdkoerper auf, siehe Schatten-Leiter. */
  --border-field: #928E87;
  --brand: #F26716;
  --brand-ink: #BC3F06;
  /* Schrift AUF der Brand-Flaeche. Weiss ergibt auf #F26716 nur 3.12:1 und faellt
     als Text durch -- dieses Braun kommt auf 4.93:1. Gilt fuer jeden gefuellten
     Orange-Grund; Icons duerfen weiss bleiben (UI-Schwelle 3:1). */
  --brand-on: #3B1D06;
  --danger: #B91C1C;

  /* Dieselben zwei Farben als Zahlentripel, fuer rgba(...) mit Deckkraft.
     Ohne sie stand "242, 103, 22" 39x und "47, 43, 41" 13x im Bestand -- ein
     Markenwechsel waere eine Suchen-und-Ersetzen-Aktion ueber 17 Dateien
     gewesen, bei der genau eine Stelle uebrig bleibt. */
  --brand-rgb: 242, 103, 22;
  --text-rgb: 47, 43, 41;

  /* ── Deckkraft-Leiter (DESIGN.md Regel 1a) ──────────────────────────────
     Vorher standen 14 Alpha-Werte im Code (.06 .1 .12 .13 .15 .18 .25 .28 .3
     .35 .4 .45 .5 .55) fuer eine Handvoll wiederkehrender Aufgaben: der Rahmen
     einer Karte unter dem Zeiger war im Chat .35, im Kalender .45, in der
     Chat-Liste .5. Denselben Unterschied sieht kein Mensch -- aber jeder neue
     Screen hat sich wieder einen eigenen Wert ausgedacht.

     Benannt ist nach AUFGABE, nicht nach Zahl. Zwei Paare tragen heute denselben
     Wert und sind trotzdem getrennt, weil sie getrennt regelbar sein muessen: */

  /* Rueckmeldung auf eine Bedienhandlung -- Icon-Knopf unter dem Zeiger. */
  --brand-a-hover: .06;
  /* Reine Deko: Verlauf, der nach transparent auslaeuft. Wer den Hover-Effekt
     kraeftiger macht, soll nicht jede Verlaufsecke der App mitverdunkeln. */
  --brand-a-schleier: .06;

  /* Fokus-Ring am Eingabefeld. Die Felder setzen outline:none -- dieser Ring
     IST die Tastatur-Rueckmeldung, kein Schmuck. */
  --brand-a-fokus: .18;
  /* Zierlinie am runden Avatar. Getrennt vom Fokus-Ring, damit ein "der Ring am
     Avatar ist mir zu hart" nicht stillschweigend die Tastaturbedienung der
     ganzen App schwaecht. */
  --brand-a-kontur: .18;

  /* Sichtbare Umrandung eines Bedienelements, ruhend oder unter dem Zeiger. */
  --brand-a-rahmen: .35;
  /* Rahmen, der einen ZUSTAND meldet: ungelesen, aktiv, laeuft gerade.
     .5 und nicht .4: bei .4 waere der Abstand zu --brand-a-rahmen nur .05, und
     das ist auf einem 1px-Rand nicht zu unterscheiden -- die Stufe haette ihren
     eigenen Zweck verfehlt. */
  --brand-a-signal: .5;

  /* Neutrale, entsaettigte Flaeche aus der Textfarbe (erledigt, abgesagt). */
  --text-a-toenung: .08;

  /* OBERGRENZE: ueber --brand-a-signal gibt es keine Alpha-Stufe. Wer eine
     kraeftige Markenkontur oder -flaeche braucht, nimmt var(--brand) bzw.
     var(--brand-ink) DECKEND -- so macht es der Bestand an jeder Stelle, an der
     es kraeftig sein muss (.cal-cell--today, .top-bell-dot, .auftrag-check:hover).
     Ohne diese Zeile schreibt der naechste Screen .6, weil die Leiter aufhoert.

     Die Stufen sind fuer 1px-Striche kalibriert. Ab 1.5px handeln Alpha und
     Strichstaerke gegeneinander (2px bei .25 wiegt etwa wie 1px bei .5) --
     dort einzeln pruefen, nicht die Stufe einsetzen. */

  /* Markenschatten MIT Geometrie, nicht nur als Alpha: Tiefe wird in diesem
     Projekt ueber die --shadow-Leiter geregelt. Ein reines Alpha-Token haette
     eine zweite Leiter fuer dieselbe Sache aufgemacht -- und die Geometrie
     waere 9x dupliziert geblieben. */
  --shadow-brand: 0 2px 8px rgba(var(--brand-rgb), .28);
  --shadow-brand-flat: 0 1px 2px rgba(var(--brand-rgb), .28);

  /* Weiche Markenflaeche: Zaehler-Pillen, aktive Chips, Hinweisstreifen.
     Stand 11x als #FCEADD direkt im Code. */
  --brand-bg: #FCEADD;

  /* Zwei getoente Weiss-Stufen, die in Chat-Liste und Startkarten dieselbe
     Aufgabe hatten und trotzdem doppelt im Code standen:
     --surface-hover  = Karte unter dem Zeiger
     --surface-akzent = Karte, die etwas will (ungelesen, Hinweis, heute) */
  --surface-hover: #FFFCFA;
  --surface-akzent: #FFF7F1;
  --status-open-ink: #9A4508; --status-open-bg: #FBEEDF;
  --status-paid-ink: #166534; --status-paid-bg: #E8F5EC;
  --status-covered-ink: #44546A; --status-covered-bg: #ECF1F7;
  --status-credited-ink: #0F766E; --status-credited-bg: #E7F4F2;

  /* Bestaetigung OHNE Zahlungsbezug (Push aktiv, Notiz erledigt, Erfolgs-Toast).
     Teilt heute die Werte mit --status-paid, ist aber bewusst ein eigenes Token:
     die vier Status-Farben sind Solmaz' Excel-Semantik und duerfen nicht als
     allgemeine Deko mitwandern. Aendert sich die Zahlungs-Semantik, bleibt die
     Bestaetigung, wo sie ist. */
  --ok-ink: #166534; --ok-bg: #E8F5EC;

  /* Flaechen ohne eigene Bedeutung: aktiver Nav-Tab, aktive Glocke. Frueher lieh
     sich der Tab --status-open-bg ("offen"-Gelb) -- eine Zahlungsfarbe als
     Navigations-Deko. */
  --nav-active-bg: #F3EDE7;
  /* Gesperrt heisst grau, nie gruen: --status-paid-bg liess einen toten Button
     wie eine Erfolgsmeldung aussehen. */
  --disabled-ink: #665D57; --disabled-bg: #F0EBE6;

  --font-display: "Sora", "Inter", system-ui, sans-serif;
  --font-body: "Inter", system-ui, sans-serif;


  /* Tiefe als Leiter, nicht ad hoc. Alle Toene warm (47,43,41 = --text);
     kaltes rgba(0,0,0) faellt auf dem Creme-Grund als Fremdkoerper auf. */
  --shadow-card: 0 1px 2px rgba(var(--text-rgb), .05), 0 4px 14px rgba(var(--text-rgb), .06);
  --shadow-flat: 0 1px 2px rgba(var(--text-rgb), .05);
  --shadow-raised: 0 8px 24px rgba(var(--text-rgb), .10);
  --shadow-overlay: 0 12px 34px rgba(var(--text-rgb), .18);

  /* Touch-Mindestmasse. LernPuls laeuft primaer als iPhone-PWA; die Buttons lagen
     bei 28.8px (primaer) und 27.6px (sekundaer) -- unter jeder Touch-Empfehlung
     (Apple HIG 44pt, Material 48dp). */
  --tap-primary: 42px;
  --tap-secondary: 36px;
  --tap-icon: 40px;

  /* Mindest-TREFFERFLAECHE, unsichtbar. Streng getrennt von den drei Massen
     darueber: die messen die SICHTBARE Knopfgroesse und sind damit eine
     Gestaltungsfrage (deshalb steht --tap-primary bei 42px und nicht bei 44 --
     ein groesserer Knopf kostet Platz auf jedem Screen). Diese Flaeche kostet
     kein Pixel Layout, weil sie ueber ein absolut positioniertes ::after
     gelegt wird -- unter der Empfehlung zu bleiben gaebe es hier also keinen
     Grund. 44px = Apple HIG 44pt (Material 48dp liegt darueber).
     Vorsicht bei Nachbarn: die Flaeche ragt ueber den Knopf hinaus. Stehen
     zwei Bedienelemente dichter beieinander als die Ueberstaende zusammen
     breit sind, ueberlappen die Trefferflaechen und der zuletzt gemalte Knopf
     gewinnt. Dort waechst der Knopf selbst (siehe .zug-auge). */
  --tap-hit: 44px;

  /* Freiraum unter dem Seiteninhalt, damit die letzte Zeile nicht hinter der fest
     stehenden Tabbar verschwindet. Gehoert zu den Touch-Massen, nicht zur
     Abstands-Skala: es misst ein konkretes Element, es taktet keinen Rhythmus.

     Am 01.08. nachgemessen und als Rechnung geschrieben statt als eine Zahl:
     die Leiste ist 58px hoch (gemessen bei 390px, ohne den Geraeterand), dazu
     --sp-5 Luft und der Geraeterand selbst. Die alte 104 stammte von der
     hoeheren Leiste mit zwei Zeilen Innenabstand und liess seither zu viel
     Platz stehen -- ein festes Mass altert still mit, wenn das Element sich
     aendert. */
  --tabbar-clearance: calc(58px + var(--sp-5) + env(safe-area-inset-bottom));

  /* Eine Beschleunigung fuer alles. Nacktes "ease" startet zu traege und endet
     zu abrupt; diese Kurve zieht sofort an und laeuft weich aus. */
  --ease: cubic-bezier(.2, .6, .3, 1);

  --content-max: 680px;
  --sidebar-w: 15rem;

  /* ── Skalen (DESIGN.md Regeln 3, 6, 7) ──────────────────────────────────
     Warum es die vorher nicht gab und was das anrichtete: fuer Farben,
     Schatten, Touch-Masse und Beschleunigung gab es Tokens -- fuer Schrift,
     Abstand und Radius NICHT. Es gab nichts zum Danach-Greifen, also hat jeder
     Screen eigene Werte gegriffen. Gemessen am 2026-07-18: 32 Schriftgroessen
     (Budget 6), 54 Abstaende (Budget 7), 17 Radien (Budget 2).

     Die Stufen sind aus der HAEUFIGKEIT im Bestand abgeleitet, nicht aus einem
     Lehrbuch -- die App soll beim Umstellen optisch stehen bleiben.

     ACHTUNG: der Bestand ist noch NICHT umgestellt. Neuer Code benutzt diese
     Tokens; die Migration laeuft in Stationen (DESIGN.md, letzter Abschnitt). */

  /* Spruenge sichtbar (Faktor 1,14-1,55) statt 0,3-px-Brei wie bisher. */
  --fs-xs: 0.70rem;    /* Labels, Versalien-Spaltenkoepfe, Kleingedrucktes */
  --fs-s: 0.80rem;     /* Sekundaertext, Meta, Pillen */
  --fs-base: 0.90rem;  /* Fliesstext, Tabellenzellen, Formularfelder */
  --fs-m: 1.00rem;     /* betonter Fliesstext, Karten-Titel */
  --fs-l: 1.30rem;     /* Abschnitts-Ueberschriften */
  --fs-xl: 2.00rem;    /* Seitentitel, grosse Kennzahl */

  /* Alle Vielfache von 4 -- krumme Werte wie 11.2 oder 17.6 sind Versehen.
     --sp-0 ist die eine begruendete Ausnahme vom 4er-Raster: Text, der INNERHALB
     eines Bedienelements auf Text sitzt (Tab-Icon ueber Tab-Label, Wochentag
     ueber Datum). Dort war der Bestand bei .05-.18rem, also 0.8-2.9px -- auf 4px
     gehoben reisst die Zeile sichtbar auseinander und das Element waechst ueber
     sein Touch-Mass hinaus. Eine Stufe fuer "haarfein gestapelt", nicht mehr. */
  --sp-0: 2px;
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;

  /* Drei Stufen plus Pille. Die Pille ist keine Zwischengroesse, sondern eine
     eigene Aussage: vollrund heisst "Zustand", eckig heisst "Flaeche".

     --radius-xs traegt Elemente UNTER etwa 16px Kantenlaenge: Diagrammbalken
     (11-15px breit), Legenden-Kaestchen (10x10px), Inline-Marker. Auf 8px
     gehoben wird ein 10px-Kaestchen fast rund und ein 11px-Balken zur Kapsel --
     der Radius wuerde die Form uebernehmen statt sie zu brechen.

     --radius-m ist 16px und nicht 12px: das war schon --radius-card und sitzt
     auf jeder Karte der App. 12px waere eine sichtbare Aenderung an jeder
     Flaeche gewesen -- eine Geschmacksentscheidung, die Mustafa trifft, nicht
     ein Nebeneffekt der Aufraeumarbeit. (DESIGN.md Regel 3, korrigiert.) */
  --radius-xs: 4px;
  --radius-s: 8px;
  --radius-m: 16px;
  --radius-pill: 999px;
}

* { box-sizing: border-box; }
/* background schon hier (nicht erst auf body): fuellt die Flaeche beim ersten
   Paint und zwischen zwei Seiten mit der App-Farbe statt Weiss -- gegen den
   "erst weisse Flaeche, dann App"-Blitz (Mustafa 23.07.). */
html { color-scheme: light; background: var(--bg); }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font-family: var(--font-body); font-size: var(--fs-base); line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* Standalone-PWA: der Rubber-Band-Bounce des Dokuments soll nicht die ganze
     App mitziehen, wenn ein innerer Scroller (Chat, Liste) am Rand ist. */
  overscroll-behavior-y: contain;
}

/* Tap-Feedback: interaktive Flaechen reagieren sichtbar auf Beruehrung -- der
   auffaelligste Unterschied zwischen "fuehlt sich wie Web an" und "wie App".
   touch-action: manipulation nimmt den 300ms-Rest-Delay auf Touch, und
   tap-highlight-color: transparent ersetzt das graue iOS-Kaestchen durch unser
   eigenes :active-Feedback. Buttons mit eigener transition behalten sie (hoehere
   Spezifitaet); alle anderen bekommen das leichte Einrasten. */
button, [role="button"], a, label, summary {
  -webkit-tap-highlight-color: transparent;
}
button, [role="button"] {
  touch-action: manipulation;
  transition: transform .08s var(--ease);
}
button:active, [role="button"]:active {
  transform: scale(.97);
}
@media (prefers-reduced-motion: reduce) {
  button, [role="button"] { transition: none; }
  button:active, [role="button"]:active { transform: none; }
}

/* iOS Safari zoomt beim Fokus in ein Formularfeld mit Schriftgroesse UNTER 16px
   automatisch hinein und verschiebt dabei die ganze Ansicht -- danach muss man
   von Hand rauszoomen. Auf Touch-Geraeten heben wir jedes fokussierbare
   Textfeld auf 16px (--fs-m) an: der Auto-Zoom entfaellt, das Pinch-Zoom des
   Nutzers bleibt (kein user-scalable=no, WCAG 1.4.4 gewahrt). Desktop behaelt
   die dichtere --fs-base-Groesse. !important, weil die Feld-Groessen ueber viele
   Komponenten-Regeln verstreut sind (auch inline in Templates) -- ein zentraler
   Riegel trifft sie alle, auch kuenftige Felder. */
@media (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select,
  textarea {
    font-size: var(--fs-m) !important;
  }
}

/* Topbar */
.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-5) var(--sp-3);
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.wordmark { display: inline-flex; align-items: center; text-decoration: none; }
.wordmark .logo-img { height: 27px; width: auto; display: block; }
.topbar-date { font-size: var(--fs-s); color: var(--muted); font-weight: 500; }

/* Navigation: mobil unten fix, ab 768px oben.

   KEIN overflow-x mehr (01.08.). Die Leiste scrollte seitwaerts, weil alle
   Punkte hineingezwungen wurden -- Solmaz hatte am Handy zwoelf in einer
   Zeile. Jetzt stehen hoechstens vier plus der Drei-Striche-Knopf da
   (navigation.MAX_PRIMAER), der Rest liegt im Blatt. Die Punkte teilen sich
   den Platz gleichmaessig (flex: 1), damit die Leiste ein Raster ist und kein
   Haufen unterschiedlich breiter Kacheln. */
.tabbar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
  display: flex; align-items: stretch;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: var(--sp-1) var(--sp-1) calc(var(--sp-1) + env(safe-area-inset-bottom));
}
.tab {
  position: relative;
  /* Gleiche Breite fuer alle, aber min-width: 0 -- ohne das bestimmt der
     laengste Text ("Meine Übersicht") die Breite ALLER Flaechen. */
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-0);
  /* --tap-hit als Mindesthoehe, nicht als unsichtbare ::after-Flaeche: die
     Punkte stehen dicht nebeneinander, ueberstehende Trefferflaechen wuerden
     sich ueberlappen und der zuletzt gemalte Punkt gewaenne. */
  min-height: var(--tap-hit);
  font-size: var(--fs-xs); font-weight: 500; color: var(--muted); text-decoration: none;
  padding: var(--sp-1) var(--sp-1); border-radius: var(--radius-s);
}
/* Die Beschriftung darf abschneiden statt die Leiste zu sprengen. */
.tab > span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tab--unread { color: var(--brand-ink); }
/* Der Punkt sitzt an der rechten oberen Ecke des SYMBOLS, nicht der Flaeche:
   die Flaechen sind seit dem 01.08. gleich breit und deutlich breiter als ihr
   Inhalt, ein Punkt am Flaechenrand haette frei in der Luft gestanden. */
.tab-badge {
  position: absolute; top: var(--sp-0); left: 50%; margin-left: var(--sp-2);
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--brand); box-shadow: 0 0 0 2px var(--surface);
}
/* Leucht-Punkt am Neuerungen-Tab: pulsiert sanft, solange ungesehene
   Eintraege da sind (has_new_patchnotes), und erlischt mit dem Seitenbesuch. */
.tab-badge--glow { animation: badge-glow 2.2s ease-in-out infinite; }
@keyframes badge-glow {
  0%, 100% { box-shadow: 0 0 0 2px var(--surface), 0 0 3px 0 rgba(var(--brand-rgb),.4); }
  50% { box-shadow: 0 0 0 2px var(--surface), 0 0 11px 3px rgba(var(--brand-rgb),.8); }
}
@media (prefers-reduced-motion: reduce) {
  .tab-badge--glow { animation: none; }
}
.tab svg { width: 22px; height: 22px; flex-shrink: 0; }

/* ── Der Drei-Striche-Knopf und sein Blatt ──────────────────────────────────
   Grundlage ist <details>/<summary>: ohne JavaScript klappt der Inhalt
   trotzdem auf. sheet.js legt Schleier, Fokus-Falle und Ziehgeste darueber. */
.sheet-halter { flex: 1 1 0; min-width: 0; display: flex; }
.sheet-halter > summary { list-style: none; cursor: pointer; width: 100%; }
.sheet-halter > summary::-webkit-details-marker { display: none; }
/* Beim Oeffnen wird der Knopf zum aktiven Element -- sonst sieht man waehrend
   des offenen Blattes nicht mehr, worueber es liegt. */
.sheet-halter[open] > summary { color: var(--brand-ink); font-weight: 600; }

.sheet-schleier {
  position: fixed; inset: 0; z-index: 55;
  background: rgba(var(--text-rgb), .42);
  opacity: 0; transition: opacity .25s var(--ease);
}
.sheet-schleier.is-open { opacity: 1; }
.sheet-schleier[hidden] { display: none; }

.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 56;
  /* 85 und nicht 72: bei zwoelf Bereichen plus Abmelden lag der letzte Punkt
     sonst unter der Kante und man musste scrollen, um ihn ueberhaupt zu
     entdecken (gemessen bei 390x844). Der Streifen darueber bleibt breit
     genug, um zu zeigen, dass die Seite darunter weiterlebt. */
  max-height: 85vh; max-height: 85dvh;
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius-m) var(--radius-m) 0 0;
  box-shadow: var(--shadow-overlay);
  padding-bottom: env(safe-area-inset-bottom);
  /* Nur transform, nie height: eine Hoehen-Animation ruckelt, weil sie das
     Layout in jedem Bild neu rechnet. */
  transform: translateY(100%);
  transition: transform .3s var(--ease);
}
.sheet-halter[open] .sheet, .sheet.is-open { transform: translateY(0); }

/* Der Griff ist die Einladung zum Ziehen -- ohne ihn probiert es niemand. */
.sheet-griff {
  flex-shrink: 0; width: 36px; height: 4px; margin: var(--sp-3) auto var(--sp-1);
  border-radius: var(--radius-pill); background: var(--border);
}
.sheet-inhalt {
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--sp-2) var(--sp-3) var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-1);
}
/* Im Blatt stehen die Punkte als Zeilen: Symbol links, Beschriftung daneben.
   In der Leiste stapeln sie (Symbol ueber Text), weil dort die Breite knapp
   ist -- hier ist sie es nicht, und eine Zeile liest sich schneller. */
.sheet-punkte { display: flex; flex-direction: column; gap: var(--sp-0); }
.sheet .tab {
  flex: 0 0 auto; flex-direction: row; justify-content: flex-start;
  gap: var(--sp-3); font-size: var(--fs-base);
  padding: var(--sp-3); min-height: var(--tap-hit);
  /* Volle Textfarbe statt --muted: in der Leiste ist Grau richtig (dort traegt
     die Farbe den Aktiv-Zustand), hier ist es eine Liste, die man LIEST. */
  color: var(--text);
}
.sheet .tab svg { color: var(--muted); }
/* Zustand schlaegt Grundfarbe: die Regel darueber ist spezifischer als
   .tab--unread und haette den Ungelesen-Zustand allein dem Punkt ueberlassen
   (dieselbe Fehlerklasse wie bei .convo/.home-card am 19.07., wo :hover den
   Zustands-Modifier schlug). */
.sheet .tab--unread { color: var(--brand-ink); font-weight: 600; }
.sheet .tab--unread svg { color: currentColor; }
.sheet .tab > span { max-width: none; }
.sheet .tab[aria-current="page"] { background: var(--nav-active-bg); }
.sheet .tab:hover { background: var(--surface-hover); }
.sheet .tab-badge { position: static; margin: 0 0 0 auto; box-shadow: none; }
.sheet .tab-logout { margin-top: var(--sp-2); border-top: 1px solid var(--border); padding-top: var(--sp-2); }
.sheet .tab-logout .tab { width: 100%; }

@media (prefers-reduced-motion: reduce) {
  /* Kein Fahren, nur Ein- und Ausblenden: die Bewegung ist hier Deko, die
     Information ist "es ist offen". */
  .sheet { transition: opacity .2s var(--ease); transform: none; opacity: 0; pointer-events: none; }
  .sheet-halter[open] .sheet, .sheet.is-open { opacity: 1; pointer-events: auto; }
  .sheet-schleier { transition: none; }
}
/* .is-tapped teilt sich die Optik mit aria-current, weil der angetippte Tab
   in einem Wimpernschlag GENAU der aktive Tab ist. Ein eigener Zwischenton
   waere ein Zustand mehr, den niemand benennen kann -- und beim Ankommen der
   neuen Seite ein zweiter Farbwechsel. Ohne die Rueckmeldung passiert nach dem
   Druck sichtbar nichts, bis der Server antwortet, und man tippt ein zweites
   Mal (Mustafa 24.07.: "ein Tap statt zwei"). Gesetzt und wieder abgeraeumt
   wird die Klasse in app.js. */
.tab[aria-current="page"], .tab.is-tapped { color: var(--brand-ink); font-weight: 600; }
/* Der aktive Punkt der LEISTE bekommt zusaetzlich eine Flaeche. Farbe allein
   traegt die Aussage bei Rot-Gruen-Schwaeche nicht (Graustufen-Gegenprobe aus
   DESIGN.md) -- und seit die Flaechen gleich breit sind, ist auch Platz dafuer. */
.tabbar > .tab[aria-current="page"],
.tabbar > .tab.is-tapped,
.tabbar > .sheet-halter > summary[aria-current="page"] { background: var(--nav-active-bg); }
/* Logout ist ein POST-Formular (Django 6 erlaubt kein GET-Logout); der Button
   sieht aus wie ein Tab. */
.tab-logout { margin: 0; display: flex; }
button.tab { background: none; border: none; cursor: pointer; font-family: inherit; }

/* Inhalt */
/* Der untere Wert ist KEINE Rhythmus-Stufe, sondern der Freiraum ueber der fest
   stehenden Tabbar -- eine Messung an einem konkreten Element, wie --tap-primary.
   Auf --sp-7 gekuerzt wuerde die letzte Zeile jeder Seite unter der Leiste liegen. */
main { max-width: var(--content-max); margin: 0 auto; padding: var(--sp-5) var(--sp-5) var(--tabbar-clearance); }
h1 { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-l); letter-spacing: -.015em; margin: var(--sp-1) 0 var(--sp-4); }
h2 {
  font-size: var(--fs-s); font-weight: 600; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); margin: var(--sp-5) 0 var(--sp-2);
}
.h-date { font-weight: 500; color: var(--muted); opacity: .75; margin-left: var(--sp-1); letter-spacing: .02em; }
/* margin-left trennt die gefloateten Links voneinander UND vom H1-Text --
   ohne ihn lasen sich zwei Nachbarn als ein Wort ("Ueberfaellige PostenAlle
   Zahlungen", Sweep-Fund 23.07.). */
.h-link { float: right; margin-left: var(--sp-3); font-weight: 600; font-size: var(--fs-xs); color: var(--brand-ink); text-decoration: none; text-transform: none; letter-spacing: 0; }
.h-link:hover { text-decoration: underline; }
.page-sub { color: var(--muted); font-size: var(--fs-base); margin: var(--sp-3) 0 var(--sp-4); }
/* Erklaertext in Karten (unter h2) und im Chat-Kopf. */
.page-intro { color: var(--muted); font-size: var(--fs-base); margin: 0 0 var(--sp-4); }

/* KPI. minmax(0,1fr) statt 1fr: ein nacktes 1fr hat min-width:auto und laesst
   vierstellige Betraege die Spalte sprengen statt umzubrechen. */
.kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
.kpi {
  background: var(--surface); border-radius: var(--radius-m); box-shadow: var(--shadow-card);
  padding: var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-0);
  border-top: 3px solid var(--brand);
}
.kpi-label { font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.kpi-value { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-xl); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.kpi-sub { font-size: var(--fs-s); color: var(--muted); }

/* Karten-Listen */
.card {
  background: var(--surface); border-radius: var(--radius-m); box-shadow: var(--shadow-card);
  padding: var(--sp-1) var(--sp-4);
}
.entry { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); padding: var(--sp-4) 0; }
.entry + .entry { border-top: 1px solid var(--border); }
.entry-main { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
/* Schuelernamen koennen beliebig lang sein und haben keine Umbruchstelle;
   ohne das hier schiebt ein langer Name die Betrags-Spalte aus der Karte. */
.entry-name { font-weight: 600; font-size: var(--fs-m); overflow-wrap: anywhere; }
.entry-meta { font-size: var(--fs-s); color: var(--muted); font-variant-numeric: tabular-nums; }
.entry-side { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-2); flex-shrink: 0; max-width: 54%; }

/* Status-Pills (alle vier payment_status-Werte) */
.pill {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-xs); font-weight: 600; line-height: 1.25;
  padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-pill); text-align: right;
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.pill--open { color: var(--status-open-ink); background: var(--status-open-bg); }
.pill--paid { color: var(--status-paid-ink); background: var(--status-paid-bg); }
.pill--covered_by_monthly_prepay { color: var(--status-covered-ink); background: var(--status-covered-bg); }
.pill--credited { color: var(--status-credited-ink); background: var(--status-credited-bg); }

/* Status-Pills Schueler (Student.Status: active/paused/ended) */
.pill--active { color: var(--status-paid-ink); background: var(--status-paid-bg); }
.pill--paused { color: var(--status-covered-ink); background: var(--status-covered-bg); }
/* Eigener, dunklerer Ton statt --muted: --muted auf --border ergibt nur 4.47:1. */
.pill--ended { color: #5F5650; background: var(--border); }

/* Buttons.
   min-height statt Padding-Rechnerei: die Zeilenhoehe aus dem font-Shorthand
   (.8rem/1 = 12.8px) plus 2x.5rem ergab 28.8px -- zu klein zum Treffen. Die
   Hoehe kommt jetzt aus --tap-*, das Padding regelt nur noch die Breite. */
.btn-primary {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-primary);
  white-space: nowrap; text-decoration: none;
  font: 600 .8rem/1 var(--font-body); color: var(--brand-on);
  background: var(--brand); border: none; border-radius: var(--radius-s);
  padding: var(--sp-2) var(--sp-4); cursor: pointer;
  box-shadow: var(--shadow-brand-flat);
}
.btn-primary:hover { filter: brightness(1.05); }
.btn-block { display: flex; width: 100%; padding: var(--sp-3) var(--sp-3); font-size: var(--fs-base); border-radius: var(--radius-m); }

/* Termine: Kopfzeile, Wochen-Navigation, Tagesblöcke */
.page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); }
.page-head h1 { margin-bottom: var(--sp-1); }
.page-head .btn-primary { text-decoration: none; }
.weeknav {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  background: var(--surface); border-radius: var(--radius-m); box-shadow: var(--shadow-card);
  padding: var(--sp-2) var(--sp-2); margin: var(--sp-2) 0 var(--sp-2);
}
.weeknav-arrow {
  display: flex; align-items: center; justify-content: center;
  width: var(--tap-icon); height: var(--tap-icon); border-radius: var(--radius-s);
  font-size: var(--fs-l); line-height: 1; color: var(--brand-ink); text-decoration: none;
}
.weeknav-arrow:hover { background: var(--bg); }
.weeknav-label { display: flex; flex-direction: column; align-items: center; gap: var(--sp-0); text-align: center; }
.weeknav-label span { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-base); }
.weeknav-today { font-size: var(--fs-s); font-weight: 600; color: var(--brand-ink); text-decoration: none; }
.weeknav-today:hover { text-decoration: underline; }
.weekday--today h2 { color: var(--brand-ink); }
.today-chip {
  display: inline-block; font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em;
  color: var(--brand-on); background: var(--brand); border-radius: var(--radius-pill);
  padding: var(--sp-0) var(--sp-2); margin-left: var(--sp-1); vertical-align: middle; text-transform: uppercase;
}
/* Schulfreier Tag in der Wochenansicht -- gleiche Form wie .today-chip, aber
   entsaettigt: "Heute" ist eine Ortsangabe im Kalender, "Osterferien" nur eine
   Randbedingung. Beide in Markenfarbe waeren zwei gleich laute Aussagen.

   text-transform: none gegen die Grossschreibung der h2-Zeile. "OSTERFERIEN" in
   Versalien schreit lauter als der Wochentag daneben -- und der ist hier die
   Ueberschrift. Dieselbe Ausnahme macht .h-link aus demselben Grund. */
.frei-chip {
  display: inline-block; font-size: var(--fs-xs); font-weight: 600; letter-spacing: .02em;
  color: var(--muted); background: rgba(var(--text-rgb), var(--text-a-toenung));
  border-radius: var(--radius-pill); text-transform: none;
  padding: var(--sp-0) var(--sp-2); margin-left: var(--sp-1); vertical-align: middle;
}

/* Die beiden Download-Knoepfe der Auswertung als eine Gruppe rechts. Ohne
   Klammer verteilt das space-between von .report-actions drei Elemente statt
   zwei, und "Geplante Termine" rutscht in die Mitte. */
.report-downloads { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Gleiche Silhouette wie .report-pdf (Hoehe, Rundung, Polsterung), nur
   ungefuellt: die beiden gehoeren zusammen, und genau EINER von beiden ist der
   gefuellte Primaer-Knopf dieses Screens (DESIGN.md 2). btn-ghost waere die
   naheliegende Klasse gewesen, kommt aber eckig und niedriger daher -- neben
   der Pille sah das nach zwei verschiedenen Bausteinen aus statt nach einem
   Paar. Die Klasse aendert deshalb NUR die Geometrie, nicht btn-ghost selbst. */
.report-csv {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--tap-primary); text-decoration: none;
  font: 600 .84rem/1 var(--font-body); color: var(--text);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-pill); padding: var(--sp-2) var(--sp-4);
  transition: border-color .12s var(--ease), transform .12s var(--ease);
}
.report-csv:hover { border-color: var(--text); }
.report-csv:active { transform: scale(.97); }
.report-csv svg { width: 15px; height: 15px; }

/* Derselbe freie Tag im Monatskalender. Bewusst KEIN Chip und keine Farbe aus
   der Termin-Palette: Ferien und Feiertage sind der Hintergrund der Planung,
   nicht ihr Inhalt -- in der Woche vor Ostern stuenden sonst fuenf Kacheln, die
   wie fuenf Termine aussehen. Also entsaettigte Flaeche aus der Textfarbe
   (dieselbe Stufe wie ein abgesagter Termin) und eine kleine Zeile ohne Rahmen.

   .cal-cell.cal-cell--frei und nicht .cal-cell--frei: der Rest der Kalender-CSS
   steht als Inline-Block IM Template und damit spaeter im Dokument. Bei
   gleicher Spezifitaet gewaenne dort .cal-cell { background: var(--bg) }. */
.cal-cell.cal-cell--frei { background: rgba(var(--text-rgb), var(--text-a-toenung)); }
.cal-cell.cal-cell--frei.cal-cell--today { background: var(--surface-akzent); }
.cal-frei {
  font-size: var(--fs-xs); line-height: 1.2; color: var(--muted);
  padding: 0 var(--sp-0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Der Feiertag traegt zusaetzlich Gewicht: an ihm ist geschlossen, in den
   Ferien richtet sich mancher Schueler nach Absprache. */
.cal-frei--feiertag { font-weight: 700; }
.appt-weekday { font-weight: 400; color: var(--muted); font-size: var(--fs-s); margin-left: var(--sp-1); }
.appt-actions { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; margin-left: auto; }
.appt:has(.appt-actions) { flex-wrap: wrap; row-gap: var(--sp-2); }
@media (max-width: 560px) {
  .appt .appt-actions { width: 100%; justify-content: flex-end; }
}
.btn-ghost {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-secondary);
  white-space: nowrap;
  font: 500 .76rem/1 var(--font-body); color: var(--text);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-s);
  padding: var(--sp-2) var(--sp-3); cursor: pointer; text-decoration: none;
}
/* display: inline-flex schlaegt sonst das UA-[hidden]: der Teilen-Knopf, den
   app.js ohne navigator.share versteckt, blieb auf dem Desktop als toter Knopf
   stehen. */
.btn-ghost[hidden] { display: none; }
.btn-primary[hidden] { display: none; }
.btn-ghost:hover { border-color: var(--text); }
.btn-ghost:disabled { color: var(--disabled-ink); background: var(--disabled-bg); border-color: var(--border); cursor: default; }
.btn-ghost:disabled:hover { border-color: var(--border); }
.btn-ghost--danger { color: var(--danger); }
.btn-ghost--danger:hover { border-color: var(--danger); background: #FEF2F2; }

/* Destruktive Aktion raus aus der Reihe der harmlosen. "Absagen" stand direkt
   neben "Verschieben" -- zwei Aktionen, die dasselbe Ziel meinen und
   gegenteilig ausgehen; ein Vertipper kostet den Termin. Bewusst KEINE
   GitHub-artige Danger-Zone: einen Termin abzusagen ist in einem
   Nachhilfebetrieb Alltag, keine Kontoloeschung. Der Abstand ist die
   Sicherung, den Rueckfrage-Dialog liefert data-confirm (app.js) ohnehin.
   :has() statt einer Extra-Klasse -- die Regel gilt fuer JEDES Formular mit
   destruktivem Button, auch fuer kuenftige. */
.detail-actions:has(.btn-ghost--danger),
.appt-actions:has(.btn-ghost--danger) { gap: var(--sp-2); }
.detail-actions > form:has(.btn-ghost--danger),
.appt-actions > form:has(.btn-ghost--danger) { margin-left: auto; padding-left: var(--sp-4); }

/* Formulare (Termin anlegen/verschieben, Serien) */
.form-card { display: flex; flex-direction: column; gap: var(--sp-4); padding: var(--sp-4) var(--sp-4); max-width: 420px; }
.field-hint { margin: var(--sp-1) 0 0; font-size: var(--fs-s); color: var(--muted); }
.form-card.is-dragover { outline: 2px dashed var(--brand); outline-offset: 3px; }
.form-card input, .form-card select, .form-card textarea {
  font: 400 .95rem/1.3 var(--font-body); color: var(--text);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border-field); border-radius: var(--radius-s);
  background: var(--surface); width: 100%;
}
.form-card textarea { resize: vertical; min-height: 4.5rem; line-height: 1.45; }
.form-card input:focus, .form-card select:focus, .form-card textarea:focus {
  border-color: var(--brand); outline: none; box-shadow: 0 0 0 3px rgba(var(--brand-rgb), var(--brand-a-fokus));
}
.form-actions { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-2); margin-top: var(--sp-1); }

/* Monats-Auswertung */
.report-card { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-4) var(--sp-4); margin-bottom: var(--sp-3); }
.report-name { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-m); }
.report-card--total { border-top: 3px solid var(--brand); }
.report-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-2); margin: 0; }
.report-grid dt { font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
/* Erstes Blickziel der Auswertung. Vorher 15.2px -- kleiner als die
   nachgeordnete Ueberfaelligkeits-Aufschluesselung weiter unten (18.4px).
   Und in der Display-Schrift: Sora ist fuer Ueberschriften, Zahlen sind
   Daten und laufen in Inter mit tabular-nums. */
.report-grid dd { margin: 0; font-family: var(--font-body); font-weight: 600; font-size: var(--fs-l); font-variant-numeric: tabular-nums; }
.report-negative { color: var(--danger); }
@media (max-width: 480px) {
  .report-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--sp-3); }
}

/* Dokumente */
.doc { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); padding: var(--sp-3) 0; flex-wrap: wrap; row-gap: var(--sp-2); }
.doc + .doc { border-top: 1px solid var(--border); }
.doc-main { display: flex; flex-direction: column; gap: var(--sp-0); min-width: 0; }
.doc-name { font-weight: 600; font-size: var(--fs-m); color: var(--text); text-decoration: none; overflow-wrap: anywhere; }
.doc-name:hover { color: var(--brand-ink); text-decoration: underline; }
.doc-meta { font-size: var(--fs-s); color: var(--muted); font-variant-numeric: tabular-nums; }
/* Cloud-Austausch: Filterleiste + Kategorie-/Neu-Chips */
.doc-filter { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: 0 0 var(--sp-3); }
.doc-filter input[type="search"] {
  flex: 1; min-width: 10rem;
  font: 400 .88rem/1.3 var(--font-body); color: var(--text);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border-field); border-radius: var(--radius-s); background: var(--surface);
}
.doc-filter select {
  font: 400 .88rem/1.3 var(--font-body); color: var(--text);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border-field); border-radius: var(--radius-s); background: var(--surface);
}
.doc-filter input:focus, .doc-filter select:focus {
  border-color: var(--brand); outline: none; box-shadow: 0 0 0 3px rgba(var(--brand-rgb), var(--brand-a-fokus));
}
/* Neutral statt --status-covered-*: eine Dokument-Kategorie hat nichts mit dem
   Zahlungsstatus "verrechnet" zu tun und darf dessen Blau nicht tragen. */
.doc-cat {
  display: inline-block; font-size: var(--fs-xs); font-weight: 600; letter-spacing: .02em;
  color: #5F5650; background: var(--nav-active-bg);
  border-radius: var(--radius-pill); padding: var(--sp-0) var(--sp-2); margin-right: var(--sp-0); vertical-align: middle;
}
.doc-new {
  display: inline-block; font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--brand-on); background: var(--brand); border-radius: var(--radius-pill);
  padding: var(--sp-0) var(--sp-2); margin-right: var(--sp-0); vertical-align: middle;
}
.form-card input[type="file"] { padding: var(--sp-2); font-size: var(--fs-base); }
/* Ghost, nicht gefuellt: die gefuellte Flaeche gehoert dem Hochladen-Knopf.
   Vorher trugen beide dieselbe Markenfarbe und der Blick fiel zuerst auf die
   Nebenaktion (Datei auswaehlen). */
.form-card input[type="file"]::file-selector-button {
  font: 600 .8rem/1 var(--font-body); color: var(--brand-ink);
  background: transparent; border: 1px solid var(--border); border-radius: var(--radius-s);
  min-height: var(--tap-secondary);
  padding: var(--sp-2) var(--sp-3); margin-right: var(--sp-3); cursor: pointer;
}
@media (max-width: 560px) {
  .doc .appt-actions { width: 100%; justify-content: flex-end; }
}

/* Push-Aktivierung */
.push-card { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-4) var(--sp-4); }
.push-text { margin: 0; font-size: var(--fs-base); color: var(--muted); }
.push-card .btn-primary { align-self: flex-start; }

/* Pruef-Banner der Uebersicht: Text links, Aktion rechts. */
.review-banner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3) var(--sp-4); flex-wrap: wrap; padding: var(--sp-4) var(--sp-4);
  border-left: 3px solid var(--brand);
}
.review-banner .push-text { color: var(--text); }
.review-banner .btn-primary { flex-shrink: 0; }
/* "Schon aktiv" und "gesperrt" sind zwei verschiedene Dinge und sahen bisher
   identisch aus (beide gruen = wie "bezahlt"). Aktiv bestaetigt (--ok-*),
   gesperrt ist grau und verspricht nichts. */
.btn-primary.is-active {
  background: var(--ok-bg); color: var(--ok-ink); cursor: default; filter: none;
  box-shadow: none;
}
.btn-primary:disabled, .btn-primary[aria-disabled="true"] {
  background: var(--disabled-bg); color: var(--disabled-ink); cursor: default; filter: none;
  box-shadow: none;
}
/* Laufende Aktion: der Button bleibt stehen und sagt, dass etwas passiert.
   Vorher hing z. B. "Benachrichtigungen aktivieren" sekundenlang ohne jede
   Rueckmeldung (push.js macht einen echten Netz-Roundtrip). */
.btn-primary[aria-busy="true"], .btn-ghost[aria-busy="true"] { cursor: progress; }
.btn-primary[aria-busy="true"]::before, .btn-ghost[aria-busy="true"]::before {
  content: ""; width: .85em; height: .85em; margin-right: var(--sp-2); flex-shrink: 0;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn-primary[aria-busy="true"]::before, .btn-ghost[aria-busy="true"]::before { animation-duration: 2.4s; }
}

/* Termine */
.appt { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) 0; }
.appt + .appt { border-top: 1px solid var(--border); }
.appt-time { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-base); font-variant-numeric: tabular-nums; color: var(--brand-ink); width: 3.1rem; flex-shrink: 0; }
.appt-name { font-weight: 500; font-size: var(--fs-base); flex: 1; min-width: 0; color: var(--text); text-decoration: none; }
a.appt-name:hover { color: var(--brand-ink); text-decoration: underline; }
.appt-has-note { color: var(--brand); font-size: var(--fs-xs); vertical-align: middle; margin-left: var(--sp-0); }
.appt-duration { font-size: var(--fs-s); font-weight: 600; color: var(--muted); background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-pill); padding: var(--sp-1) var(--sp-2); white-space: nowrap; }

/* Leere Zustaende -- EIN System.
   Vorher gab es sieben: .empty, .note-empty (gestrichelt, r13), .chat-empty
   (mit Icon), .convo-empty (r16), .auftrag-empty (r12), .home-auftrag--empty
   und .home-card--empty. Drei Radien, zwei Paddings, je eigene Typo -- "nichts
   da" sah auf jeder Seite anders aus und las sich wie fuenf verschiedene Apps.

   .empty         Die ruhige Zeile IN einer Karte, wenn deren Liste leer ist.
   .empty-panel   Die eigene Flaeche, wenn ein ganzer Bereich leer ist. Der
                  gestrichelte Rahmen sagt "hier koennte etwas stehen".
   .empty-panel--plain  Ohne Rahmen, fuer Bereiche, die schon einen haben.
*/
.empty { color: var(--muted); font-size: var(--fs-base); padding: var(--sp-3) 0; margin: 0; }
.card > .empty { padding: var(--sp-3) 0; }

.empty-panel {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-1);
  margin: 0; padding: var(--sp-5) var(--sp-4); text-align: center;
  border: 1px dashed var(--border); border-radius: var(--radius-m);
  color: var(--muted); font-size: var(--fs-base);
}
/* Erste Zeile = die Aussage, zweite = der Hinweis. Beide optional. */
.empty-panel > p, .empty-panel > strong { margin: 0; font-size: var(--fs-base); font-weight: 600; color: var(--text); }
.empty-panel span { font-weight: 400; font-size: var(--fs-base); color: var(--muted); }
.empty-panel--plain { border: none; padding: var(--sp-6) var(--sp-4); }
/* Dekorativ (aria-hidden, der Text daneben sagt dasselbe), also ohne
   Kontrast-Pflicht -- aber sichtbar muss es trotzdem sein. --border waere mit
   1.2:1 praktisch unsichtbar gewesen. */
.empty-panel-ic { color: var(--muted); opacity: .45; margin-bottom: var(--sp-1); }
.empty-panel-ic svg { display: block; width: 44px; height: 44px; fill: none; stroke: currentColor; stroke-width: 1.5; }

/* Sackgassen-Karte: abgebrochene Sitzung (403) und unbekannte Adresse (404).
   Beide Seiten sagen dasselbe in derselben Form -- was passiert ist, was zu tun
   ist, ein Weg zurueck -- und muessen deshalb gleich aussehen.

   Sie stand bis zum 25.07. als <style> in csrf_failure.html. Fuer die 404 waere
   sie ein zweites Mal geschrieben worden, und genau so laufen zwei Seiten
   auseinander, die eine Seite sein sollten (DESIGN.md, "Naechste Station" 1).

   Die 500er-Seite gehoert bewusst NICHT dazu: sie darf dieses Stylesheet nicht
   brauchen. Begruendung steht in 500.html. */
.hinweis-card { max-width: 34rem; display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-5); }
.hinweis-card h1 { margin: 0; font-size: var(--fs-l); }
.hinweis-card p { margin: 0; }
.hinweis-lead { color: var(--muted); }
.hinweis-schritte { margin: 0; padding-left: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-1); }
.hinweis-fussnote { font-size: var(--fs-s); color: var(--muted); }
.hinweis-wege { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }

/* Protokoll-Timeline */
/* Lehrer-Filter (Solmaz 23.07.): schlichter GET-Select, per JS auto-submit,
   ohne JS bleibt der Filtern-Knopf (noscript). */
.audit-filter { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin: 0 0 var(--sp-4); }
.audit-filter label { font-size: var(--fs-s); font-weight: 600; color: var(--muted); }
.audit-filter select { min-height: var(--tap-secondary); }
.audit-filter-hinweis { margin: 0 0 var(--sp-4); font-size: var(--fs-s); color: var(--muted); }
.audit-day { margin-top: var(--sp-5); }
.audit-akteur { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.audit-lehrer { font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.audit-item { display: flex; gap: var(--sp-3); padding: var(--sp-3) 0; align-items: flex-start; }
.audit-item + .audit-item { border-top: 1px solid var(--border); }
.audit-time { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-s); font-variant-numeric: tabular-nums; color: var(--muted); width: 2.9rem; flex-shrink: 0; padding-top: var(--sp-0); }
.audit-body { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.audit-detail { font-size: var(--fs-base); }
.badge {
  align-self: flex-start;
  display: inline-flex; align-items: center;
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: .02em;
  padding: var(--sp-0) var(--sp-2); border-radius: var(--radius-pill);
}
.badge--owner { color: var(--brand-ink); background: var(--brand-bg); }
.badge--manual_override { color: var(--status-open-ink); background: var(--status-open-bg); }
.badge--system_automation { color: var(--status-covered-ink); background: var(--status-covered-bg); }
.badge--outlook_sync { color: #1D4ED8; background: #E8EEFC; }

/* Login */
.login-wrap { min-height: 82vh; display: flex; align-items: center; justify-content: center; padding: var(--sp-5) var(--sp-5); }
.login-card {
  background: var(--surface); border-radius: var(--radius-m); box-shadow: var(--shadow-card);
  padding: var(--sp-6) var(--sp-6); width: 100%; max-width: 360px;
  display: flex; flex-direction: column; gap: var(--sp-4);
}
.login-card .wordmark { justify-content: center; margin-bottom: var(--sp-2); }
.login-card .wordmark .logo-img { height: 38px; }
.login-hint { text-align: center; color: var(--muted); font-size: var(--fs-s); line-height: 1.45; margin: var(--sp-3) 0 0; }

/* Datenschutz-Text (Hinweis-Seite + Bestaetigungs-Gate). */
.pp { font-size: var(--fs-base); line-height: 1.6; }
.pp-lead { margin: 0 0 var(--sp-4); color: var(--text); }
.pp h2 { font-size: var(--fs-s); margin: var(--sp-4) 0 var(--sp-1); color: var(--brand-ink); text-transform: none; letter-spacing: 0; }
.pp ul { margin: var(--sp-1) 0 var(--sp-1); padding-left: var(--sp-4); }
.pp li { margin: var(--sp-1) 0; }
/* Platzhalter, die Solmaz vor dem Go-Live fuellen muss -- bewusst auffaellig. */
.pp-todo { background: #FFF3CD; color: #7A5C00; font-weight: 600; padding: 0 var(--sp-1); border-radius: var(--radius-xs); box-shadow: inset 0 0 0 1px rgba(122,92,0,.35); }
.field { display: flex; flex-direction: column; gap: var(--sp-1); }
.field label { font-size: var(--fs-s); font-weight: 600; color: var(--muted); }
.field input {
  font: 400 .95rem/1.3 var(--font-body); color: var(--text);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border-field); border-radius: var(--radius-s); background: var(--surface);
}
.field input:focus { border-color: var(--brand); outline: none; box-shadow: 0 0 0 3px rgba(var(--brand-rgb), var(--brand-a-fokus)); }
.errorlist { list-style: none; margin: 0; padding: 0; color: var(--danger); font-size: var(--fs-s); }

/* Django-Messages als Toast */
.messages { position: fixed; top: .9rem; left: 50%; transform: translateX(-50%); z-index: 40; list-style: none; margin: 0; padding: 0; width: min(92vw, 420px); display: flex; flex-direction: column; gap: var(--sp-2); }
.messages li {
  background: var(--surface); border: 1px solid var(--ok-ink); color: var(--ok-ink);
  border-radius: var(--radius-m); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-base); font-weight: 500;
  box-shadow: var(--shadow-card); text-align: center;
  transition: opacity .4s var(--ease), transform .4s var(--ease);
}
.messages li.error, .messages li.warning { border-color: var(--danger); color: var(--danger); }
.messages li.is-hiding { opacity: 0; transform: translateY(-6px); }

/* "Neue Version verfuegbar"-Hinweis. Schwebt oben mittig wie die Toasts, bleibt
   aber stehen bis zum Klick. app.js nimmt [hidden] weg, sobald der Service-Worker
   eine neue Version uebernommen hat. Die Aktion ist bewusst KEIN gefuellter
   Primaerknopf (DESIGN.md: genau einer je Screen) -- Ghost-Optik wie .btn-ghost. */
.update-banner {
  position: fixed; top: var(--sp-3); left: 50%; transform: translateX(-50%);
  z-index: 45; width: min(92vw, 420px);
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-m); box-shadow: var(--shadow-raised);
}
.update-banner[hidden] { display: none; }
.update-banner-text { flex: 1; font-size: var(--fs-base); color: var(--text); }
.update-banner-btn {
  flex-shrink: 0; font: inherit; font-size: var(--fs-s); font-weight: 600; cursor: pointer;
  min-height: var(--tap-secondary); padding: var(--sp-1) var(--sp-3);
  color: var(--brand-ink); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  transition: border-color .15s var(--ease);
}
.update-banner-btn:hover { border-color: var(--text); }

/* Anmelde-Sperren: dauerhafter Hinweis fuer den Master.
   Anders als .messages KEIN Toast -- er bleibt stehen, bis Solmaz ihn
   wegklickt (Mustafa-Vorgabe 16.07.2026), liegt also im Fluss statt fixed.
   Optik bewusst wie .messages li.error (weisse Karte, roter Rand, rote
   Schrift): --status-open-* waere naheliegend, heisst auf der Uebersicht aber
   schon "hier fehlt Geld" -- ein Sicherheitshinweis in derselben Farbe waere
   auf einen Blick nicht davon zu unterscheiden. */
.login-alerts {
  max-width: var(--content-max); margin: 0 auto; padding: var(--sp-5) var(--sp-5) 0;
  list-style: none; display: flex; flex-direction: column; gap: var(--sp-2);
}
.login-alert {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  background: var(--surface); border: 1px solid var(--danger);
  color: var(--danger); border-radius: var(--radius-m); padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--shadow-card);
}
.login-alert__body { flex: 1; min-width: 0; }
.login-alert__title { margin: 0 0 var(--sp-1); font-weight: 600; font-size: var(--fs-base); }
.login-alert__text { margin: 0; font-size: var(--fs-base); line-height: 1.45; }
.login-alert__meta { margin: var(--sp-1) 0 0; font-size: var(--fs-s); opacity: .75; }
.login-alert form { flex: none; margin: 0; }
.login-alert__close {
  background: none; border: 0; cursor: pointer; color: inherit;
  font-size: var(--fs-l); line-height: 1; padding: var(--sp-0) var(--sp-1); border-radius: var(--radius-s);
}
.login-alert__close:hover { background: rgba(185, 28, 28, .1); }

/* Fokus */
a:focus-visible, button:focus-visible, .tab:focus-visible {
  outline: 2px solid var(--brand-ink); outline-offset: 2px; border-radius: var(--radius-s);
}

@media (prefers-reduced-motion: reduce) {
  .messages li { transition: none; }
}

/* Desktop: Nav nach oben, mehr Luft */
@media (min-width: 768px) {
  body { display: flex; flex-direction: column; min-height: 100vh; }
  .tabbar {
    position: sticky; top: 0; bottom: auto; order: -1;
    justify-content: flex-end; gap: var(--sp-1); padding: var(--sp-2) var(--sp-5);
    border-top: none; border-bottom: 1px solid var(--border);
  }
  /* Oben teilen sich die Punkte den Platz NICHT: die Leiste sitzt rechts und
     soll nur so breit sein wie ihr Inhalt. */
  .tab { flex: 0 0 auto; flex-direction: row; gap: var(--sp-2); font-size: var(--fs-s); padding: var(--sp-1) var(--sp-3); }
  .tab svg { width: 17px; height: 17px; }
  .sheet-halter { flex: 0 0 auto; }
  .tab-badge { position: static; margin: 0 0 0 var(--sp-1); box-shadow: none; }

  /* Ab hier ist das Blatt kein Blatt mehr, sondern ein verankertes Feld unter
     dem Knopf: von unten hochzufahren ergibt neben einer breiten Ansicht
     keinen Sinn. Die Grenze muss zu BLATT_BIS in sheet.js passen.
     --anker-oben/-rechts misst sheet.js am Knopf: das Blatt haengt am
     body-Ende und kennt seinen Knopf nicht als Vorfahren. */
  .sheet {
    top: var(--anker-oben, 60px); right: var(--anker-rechts, var(--sp-5));
    left: auto; bottom: auto;
    width: 15rem; max-height: 80vh; max-height: 80dvh;
    border-radius: var(--radius-m);
    border: 1px solid var(--border);
    transform: translateY(calc(var(--sp-1) * -1)); opacity: 0; pointer-events: none;
    transition: transform .18s var(--ease), opacity .18s var(--ease);
  }
  /* Am Schreibtisch dunkelt ein Menue die Seite nicht ab -- der Schleier bleibt
     aber liegen, damit Klicken daneben weiter schliesst. */
  .sheet-schleier { background: transparent; }
  .sheet-halter[open] .sheet, .sheet.is-open { transform: translateY(0); opacity: 1; pointer-events: auto; }
  .sheet-griff { display: none; }
  .sheet-inhalt { padding: var(--sp-2); }
  main { padding-top: var(--sp-5); padding-bottom: var(--sp-7); }
}

/* Termin-Detail + Notizen */
.detail-card { display: flex; flex-direction: column; gap: 0; padding: var(--sp-1) var(--sp-4); max-width: 480px; }
.detail-row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-4); margin: 0; padding: var(--sp-3) 0; }
.detail-row + .detail-row { border-top: 1px solid var(--border); }
.detail-row span { font-size: var(--fs-s); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); flex-shrink: 0; }
/* Die rechte Spalte traegt Betraege und Uhrzeiten -- ohne tabular-nums tanzen
   die Ziffern zwischen den Zeilen. */
.detail-row strong { font-weight: 500; text-align: right; font-variant-numeric: tabular-nums; }
.detail-actions { display: flex; gap: var(--sp-2); padding: var(--sp-3) 0 var(--sp-2); border-top: 1px solid var(--border); margin-top: var(--sp-1); }
/* Gleiche Breite wie .form-card darueber -- die Notiz-Liste (und ihr
   Leerzustand) brach sonst als einzige Karte in die volle Seitenbreite aus
   (Sweep-Fund 23.07., Termin-Detail). */
.note-card { padding: var(--sp-3) var(--sp-4); margin-top: var(--sp-3); max-width: 420px; }
.note-body { margin: 0 0 var(--sp-1); font-size: var(--fs-base); overflow-wrap: anywhere; }
.note-meta { margin: 0; font-size: var(--fs-s); color: var(--muted); font-variant-numeric: tabular-nums; }

/* Desktop-App-Shell ab 1024px: linke Sidebar + breiter, mehrspaltiger Inhalt.
   Nur fuer eingeloggte Seiten (body.app-shell) — Login bleibt zentriert.
   Additiv ueber dem 768px-Block; gewinnt per Kaskade (spaeter + hoehere Spezifitaet). */
@media (min-width: 1024px) {
  /* 1400px statt 1120px: auf breiten Monitoren wirkte die Mittelspalte
     eingeengt, der Inhalt darf den Platz neben der Sidebar nutzen. */
  :root { --content-max: 1400px; }

  /* Eigene Rasterzeile fuer den Sperr-Hinweis: als Grid-Item ohne Feld wuerde
     er automatisch platziert und das Layout zerreissen (.messages entgeht dem
     nur, weil es fixed ist und gar kein Grid-Item wird). Ohne Hinweis ist die
     Zeile auto == 0 hoch, das Layout bleibt also unveraendert. */
  body.app-shell {
    display: grid;
    grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    grid-template-areas: "nav topbar" "nav alerts" "nav main";
    min-height: 100vh;
  }
  body.app-shell .topbar { grid-area: topbar; }
  body.app-shell .login-alerts {
    grid-area: alerts; max-width: none; padding: var(--sp-5) var(--sp-7) 0;
  }
  /* width: 100% ist noetig: als Grid-Item mit auto-Margins wuerde main sonst
     auf die Inhaltsbreite schrumpfen (fit-content) und die Spalte nie fuellen
     -- die Ursache der "eingeengten" Desktop-Ansicht. */
  body.app-shell main { grid-area: main; padding: var(--sp-6) var(--sp-7) var(--sp-7); width: 100%; }

  /* Tabbar wird zur vertikalen Seitenleiste */
  body.app-shell .tabbar {
    grid-area: nav;
    position: sticky; top: 0; align-self: start; height: 100vh;
    flex-direction: column; justify-content: flex-start; align-items: stretch; gap: var(--sp-0);
    padding: var(--sp-4) var(--sp-2);
    border-top: none; border-bottom: none; border-right: 1px solid var(--border);
    background: var(--surface);
  }
  body.app-shell .tab {
    flex: 0 0 auto;
    flex-direction: row; justify-content: flex-start; gap: var(--sp-3);
    font-size: var(--fs-base); padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-s);
  }
  body.app-shell .tab svg { width: 19px; height: 19px; }
  /* Auch hier .is-tapped mit im Selektor: in der Sidebar ist die Flaeche das
     eigentliche Aktiv-Signal, der Farbwechsel allein geht darin unter. */
  body.app-shell .tab[aria-current="page"], body.app-shell .tab.is-tapped { background: var(--nav-active-bg); }
  body.app-shell .tab-logout { margin-top: auto; }
  body.app-shell .tab-logout .tab { width: 100%; }

  /* In der Sidebar ist Platz fuer ALLE Punkte -- der Drei-Striche-Knopf hat
     hier keine Aufgabe. Das Blatt wird zur schlichten Fortsetzung der Liste:
     kein Knopf, kein Overlay, kein Schleier, nur die restlichen Zeilen. */
  body.app-shell .sheet-halter { flex: 1 1 auto; display: flex; flex-direction: column; }
  /* Der Knopf verschwindet nur, wenn sheet.js das <details> auch wirklich
     aufgeklappt hat (Klasse ist-liste). Ohne Skript bleibt er stehen und
     bedienbar -- besser ein Knopf zu viel als eine halbe Seitenleiste. */
  body.app-shell .sheet-halter.ist-liste > summary { display: none; }
  body.app-shell .sheet-halter.ist-liste > .sheet-schleier { display: none; }
  body.app-shell .sheet {
    position: static; width: auto; transform: none; opacity: 1; pointer-events: auto;
    border: none; border-radius: 0; box-shadow: none; background: none;
    max-height: none; transition: none; flex: 1;
  }
  /* flex: 1 bis hier durch, damit `margin-top: auto` am Abmelden-Punkt wieder
     greift -- er gehoert ans untere Ende der Sidebar und nicht direkt unter
     den letzten Bereich. */
  body.app-shell .sheet-inhalt { overflow: visible; padding: 0; gap: var(--sp-0); flex: 1; }
  body.app-shell .sheet .tab-logout { border-top: none; padding-top: 0; }
}

/* Zurueck-Button (Topbar, immer verfuegbar) -- als PWA gibt es keinen
   Browser-Zurueck, deshalb ein eigener Knopf. Bewusst dezent (Ghost-Optik wie
   .top-bell/.top-reset), nicht mehr der gefuellte Brand-Kreis: der stach als
   einziger gefuellter Knopf zu schwer heraus. Das Tippziel bleibt 40px (#67). */
.topbar-left { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.top-back {
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-icon); height: var(--tap-icon); padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-s); background: transparent;
  color: var(--muted); cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.top-back:hover { color: var(--brand-ink); border-color: rgba(var(--brand-rgb), var(--brand-a-rahmen)); background: rgba(var(--brand-rgb), var(--brand-a-hover)); }
.top-back:active { transform: scale(.92); }
.top-back svg { width: 18px; height: 18px; }

/* Aktivierungs-Glocke (Topbar, alle Seiten) -- dezent wie .ob-replay: Umriss
   statt gefuellter Flaeche, Punkt zeigt "noch nicht abonniert" (data-state
   "off"), verschwindet nach erfolgreichem Abo (push.js setzt "on"). */
.top-bell {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-icon); height: var(--tap-icon); padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-s); background: transparent;
  color: var(--muted); cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.top-bell:hover { color: var(--brand-ink); border-color: rgba(var(--brand-rgb), var(--brand-a-rahmen)); background: rgba(var(--brand-rgb), var(--brand-a-hover)); }
.top-bell[hidden] { display: none; }
.top-bell:active { transform: scale(.92); }
.top-bell svg { width: 18px; height: 18px; }
/* Neu-laden-Knopf: gleiche Form und Groesse wie die Glocke daneben -- zwei
   Icon-Knoepfe in derselben Zeile duerfen nicht unterschiedlich gross sein. */
.top-reset {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-icon); height: var(--tap-icon); padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-s); background: transparent;
  color: var(--muted); cursor: pointer;
  transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.top-reset:hover { color: var(--brand-ink); border-color: rgba(var(--brand-rgb), var(--brand-a-rahmen)); background: rgba(var(--brand-rgb), var(--brand-a-hover)); }
.top-reset:active { transform: scale(.92); }
.top-reset svg { width: 18px; height: 18px; }
/* Waehrend des Aufraeumens: dasselbe Muster wie bei der Glocke -- ohne Anzeige
   tippt man nochmal, und beim zweiten Klick ist der Knopf schon deaktiviert. */
.top-reset.is-busy { color: var(--brand-ink); cursor: progress; }
.top-reset.is-busy svg { animation: spin .7s linear infinite; }

.top-bell-dot {
  position: absolute; top: 6px; right: 6px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--brand); box-shadow: 0 0 0 2px var(--surface);
}
/* Laufende Aktivierung: der Ring ersetzt das Glocken-Icon. push.js setzt
   aria-busy waehrend des Abos -- das kann bis zu 8s dauern (Push-Dienst) plus
   Netz-Roundtrip, und ohne Anzeige tippt man in der Zeit nochmal. */
.top-bell[aria-busy="true"] { color: var(--brand-ink); border-color: rgba(var(--brand-rgb), var(--brand-a-signal)); cursor: progress; }
.top-bell[aria-busy="true"] svg, .top-bell[aria-busy="true"] .top-bell-dot { visibility: hidden; }
.top-bell[aria-busy="true"]::after {
  content: ""; position: absolute; width: 16px; height: 16px;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  animation: spin .7s linear infinite;
}
/* Bei reduzierter Bewegung langsamer statt aus: der Ring IST die Information. */
@media (prefers-reduced-motion: reduce) {
  .top-bell[aria-busy="true"]::after { animation-duration: 2.4s; }
}
.top-bell[data-state="on"] .top-bell-dot { display: none; }
/* Aktiv-Zustand ablesbar: die Glocke wird hervorgehoben (Brand-Ton + getoenter
   Grund), damit "an" auf einen Blick vom "aus" (Umriss + Punkt) unterscheidbar ist. */
.top-bell[data-state="on"] { color: var(--brand-ink); border-color: rgba(var(--brand-rgb), var(--brand-a-signal)); background: var(--nav-active-bg); }
.top-bell[data-state="on"]:hover { background: #EDE6DF; }

/* Push-Rueckmeldung: fixer Toast unten mittig -- layoutunabhaengig garantiert
   sichtbar (das alte, in der Topbar absolut positionierte Kaestchen kam auf dem
   iPhone nicht sichtbar an). Reines HTML/CSS, kein externer Request (DSGVO 10a).
   gruen = an/aus bestaetigt, orange = Hinweis/Problem. */
.push-toast {
  position: fixed; left: 50%; bottom: calc(84px + env(safe-area-inset-bottom));
  transform: translate(-50%, 12px); z-index: 60;
  max-width: min(92vw, 420px); padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-m); font-size: var(--fs-base); line-height: 1.4; color: #fff;
  box-shadow: var(--shadow-overlay);
  opacity: 0; pointer-events: none; transition: opacity .2s var(--ease), transform .2s var(--ease);
}
.push-toast--show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.push-toast--ok { background: var(--ok-ink); }
.push-toast--warn { background: var(--brand-ink); }
@media (prefers-reduced-motion: reduce) { .push-toast { transition: none; } }

/* form-card innerhalb einer bestehenden .card: Feld-Styles ja, eigener
   Rahmen/Schatten nein (Zahlungseingaenge erfassen/anpassen). */
.form-card--nested { padding: 0; border: none; box-shadow: none; background: transparent; max-width: 460px; }

/* Tages-Kopfzeilen der Dashboard-Wochenliste: gliedert die lange Liste je
   Faelligkeitstag, rechts Anzahl + Summe der noch offenen Posten. */
.entry-day {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
}
.entry-day-name {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--brand-ink);
}
.entry-day-date { color: var(--muted); font-weight: 600; margin-left: var(--sp-0); }
.entry-day-sum { font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Pill-Farben, die bisher fehlten: Teilzahlung (Dashboard) und die
   Review-Status der Zahlungseingaenge -- ohne Modifier sah die Pill wie
   nackter Text aus. */
.pill--partial { color: var(--brand-ink); background: var(--brand-bg); }
.pill--pending_review { color: var(--brand-ink); background: var(--brand-bg); }
.pill--modified { color: var(--text); background: rgba(var(--text-rgb), var(--text-a-toenung)); }

/* Mehr Luft zwischen den Bloecken einer Seite (Review-Feedback: "klebt"). */
main > section + section { margin-top: var(--sp-5); }
/* Gleicher Rhythmus fuer Seiten, deren Bloecke direkte .card-Kinder sind
   (Zahlungseingaenge, Pruefen-Detail) -- nicht nur fuer section-Layouts. */
main > * + .card, main > .card + * { margin-top: var(--sp-5); }
.kpis { margin-bottom: var(--sp-5); }
/* Im Widget-Raster regelt das Grid-Gap den Abstand. */
.widget .kpis { margin-bottom: 0; }

/* Lehrkraft in Termin-Zeilen. */
.appt-teacher { font-size: var(--fs-s); font-weight: 500; color: var(--muted); }

/* Wochen-Liste: Tages-Bloecke einklappbar (Standard zu). */
.entry-day-group summary.entry-day { cursor: pointer; list-style: none; margin: 0; padding: var(--sp-3) 0 var(--sp-2); }
.entry-day-group summary.entry-day::-webkit-details-marker { display: none; }
.entry-day-group + .entry-day-group summary.entry-day { border-top: 1px solid var(--border); }
.entry-day-side { display: inline-flex; align-items: center; gap: var(--sp-2); }
.entry-day-chev { width: 15px; height: 15px; color: var(--brand-ink); transition: transform .22s var(--ease); }
.entry-day-group[open] .entry-day-chev { transform: rotate(180deg); }
.entry-day-group .entry:first-of-type { border-top: none; }
.entry-day-group[open] summary.entry-day { border-bottom: 2px solid rgba(var(--brand-rgb),.25); }

/* Widget-Uebersicht: Solmaz stellt sich das Dashboard selbst zusammen. */
.dash-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); }
.dash-custom { position: relative; flex-shrink: 0; }
.dash-custom-toggle {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--tap-icon);
  font: 600 .82rem/1 var(--font-body); color: var(--brand-ink);
  background: var(--brand-bg); border: 1px solid rgba(var(--brand-rgb), var(--brand-a-rahmen)); border-radius: var(--radius-pill);
  padding: var(--sp-2) var(--sp-4);
}
.dash-custom-toggle::-webkit-details-marker { display: none; }
.dash-custom-toggle svg { width: 15px; height: 15px; }
/* Der offene Ton war #FBDFC7 -- brand-ink darauf nur 4.28:1. #FBE7D5 bleibt
   sichtbar dunkler als der geschlossene Zustand und kommt auf 4.54:1. */
.dash-custom[open] .dash-custom-toggle { background: #FBE7D5; }
.dash-custom-panel {
  position: absolute; right: 0; top: calc(100% + .5rem); z-index: 40;
  width: 280px; display: flex; flex-direction: column; gap: var(--sp-2);
  box-shadow: var(--shadow-overlay);
}
.dash-custom-hint { margin: 0 0 var(--sp-1); font-size: var(--fs-s); color: var(--muted); }
.dash-custom-item { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-base); cursor: pointer; }
.dash-custom-item input { accent-color: var(--brand); width: 15px; height: 15px; }
.dash-custom-panel .btn-primary { margin-top: var(--sp-2); align-self: flex-end; }

/* Breite Widgets stapeln in voller Breite; die schmalen Kacheln stehen in
   zwei FESTEN Spalten -- Aufklappen bewegt nur die eigene Spalte, nie die
   Nachbarn (kein Zeilen-Springen wie im Raster). */
.widget-stack { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: var(--sp-4); }
.widget-cols { display: grid; gap: var(--sp-3); grid-template-columns: minmax(0, 1fr); }
.widget-col { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.widget > section > h2 { margin-top: 0; }

/* Widget-Kacheln: alle gleich gross (zugeklappt eine Zeile mit Kurz-Fazit),
   sanft aufklappbar. Der Koerper sitzt in einem Grid-Panel, dessen Zeile von
   0fr auf 1fr faehrt -- gleitet auf/zu statt hart zu springen. Zustand merkt
   sich app.js pro Kachel (localStorage). */
.wtile { padding: 0; overflow: hidden; }
.wtile.is-open { box-shadow: var(--shadow-raised); }
.wtile-head {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  padding: var(--sp-4) var(--sp-4); cursor: pointer; user-select: none;
  background: none; border: none; font: inherit; color: inherit; text-align: left;
}
.wtile-title {
  margin: 0; font-size: var(--fs-s); font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wtile-sum {
  margin-left: auto; font-size: var(--fs-s); color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap; flex-shrink: 0;
}
.wtile-sum--hot { color: var(--brand-ink); font-weight: 600; }
.wtile-chev { width: 16px; height: 16px; color: var(--brand-ink); flex-shrink: 0; transition: transform .22s var(--ease); }
.wtile.is-open .wtile-chev { transform: rotate(180deg); }
/* grid-template-rows ist eine Layout-Eigenschaft und laeuft nicht auf dem
   Compositor -- jeder Frame kostet ein Re-Layout. Bewusst in Kauf genommen:
   die Alternative (max-height auf einen Schaetzwert) springt bei laengeren
   Listen sichtbar. Der Klapp-Bereich ist klein und steht fuer sich. */
.wtile-panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .24s var(--ease); }
.wtile.is-open .wtile-panel { grid-template-rows: 1fr; }
/* Der Clip (Grid-Item) darf KEIN eigenes Padding/Border haben -- sonst faellt
   die 0fr-Zeile nicht unter dessen Hoehe und ein ~11px-Rest bleibt sichtbar.
   Innenabstand daher als Pseudo-Elemente (werden mitgeklappt), Trennlinie als
   inset-Schatten (zaehlt nicht zur Box-Hoehe). */
.wtile-panel > .wtile-body {
  min-height: 0; min-width: 0; overflow: hidden;
  padding: 0 var(--sp-4); box-shadow: inset 0 1px 0 var(--border);
}
.wtile-body::before { content: ""; display: block; height: .4rem; }
.wtile-body::after { content: ""; display: block; height: .55rem; }
.wtile-body > .empty { padding: var(--sp-3) 0; }
@media (prefers-reduced-motion: reduce) {
  .wtile-panel, .wtile-chev, .wtile { transition: none; }
}
.wtile-sub {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); margin: var(--sp-3) 0 var(--sp-0);
}
.wtile-sub:first-child { margin-top: var(--sp-3); }
.wtile-link { display: inline-block; padding: var(--sp-2) 0 var(--sp-2); font-size: var(--fs-s); font-weight: 600; color: var(--brand-ink); text-decoration: none; }
.wtile-link:hover { text-decoration: underline; }

/* Lehrerkennzahlen-Tabelle (wide-Widget). */
.tstat-scroll { overflow-x: auto; max-width: 100%; margin-top: var(--sp-2); }

/* Login: Auge-Knopf im Passwortfeld (Tippfehler sehen statt raten). */
.login-pw-feld { position: relative; }
.login-pw-feld input { width: 100%; padding-right: calc(var(--tap-secondary) + var(--sp-2)); }
.login-pw-auge {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 32px; height: 32px; border: 0; border-radius: 50%; cursor: pointer;
  background: transparent; color: var(--muted); display: grid; place-items: center;
}
/* Trefferflaeche auf --tap-hit, ohne den Knopf zu vergroessern: das Feld ist nur
   rund 38px hoch und reserviert rechts genau --tap-secondary + --sp-2 (44px)
   Platz -- ein wirklich groesserer Knopf wuerde entweder oben und unten aus dem
   Feld ragen oder in den Text laufen. Die 6px Ueberstand je Seite bleiben
   waagerecht innerhalb dieser Reserve und senkrecht im --sp-4-Abstand der
   Login-Karte; der Anmelden-Knopf darunter wird nicht erreicht. */
.login-pw-auge::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: var(--tap-hit); height: var(--tap-hit);
  transform: translate(-50%, -50%);
}
.login-pw-auge:hover { color: var(--brand-ink); }
.login-pw-auge[aria-pressed="true"] { color: var(--brand-ink); }
.login-pw-auge svg { width: 18px; height: 18px; }

/* Zugaenge-Uebersicht: Konten-Tabelle + Re-Auth-Dialog (Passwort-Einsicht). */
.zug-card { padding: var(--sp-4); }
.zug-teacher { display: block; font-size: var(--fs-s); color: var(--muted); }
.zug-pw-zelle { white-space: nowrap; }
.zug-pw { font-family: var(--font-body); letter-spacing: .08em; margin-right: var(--sp-2); }
.zug-pw[data-zustand="offen"] { letter-spacing: 0; font-weight: 600; color: var(--brand-ink); }
/* Als einziges der Auge-/Kreuz-Knoepfchen waechst dieser hier WIRKLICH statt
   nur seine Trefferflaeche: in einer Zelle stehen bis zu vier davon
   nebeneinander (anzeigen / neues Startpasswort / eigenes Passwort /
   deaktivieren), getrennt nur durch den Wortzwischenraum bzw. --sp-2 auf dem
   Handy. Eine unsichtbar vergroesserte Flaeche wuerde die des Nachbarn
   ueberlappen, und der zuletzt gemalte Knopf gewinnt -- "Passwort anzeigen"
   koennte "Neues Startpasswort vergeben" ausloesen. Bei diesen Aktionen ist
   das keine Unschoenheit, sondern ein ausgesperrtes Konto. 34px war unbegruendet;
   --tap-icon ist das Mass, das die App fuer Icon-Knoepfe ohnehin fuehrt. */
.zug-auge {
  width: var(--tap-icon); height: var(--tap-icon); border-radius: 50%; cursor: pointer;
  border: 1px solid var(--border); background: var(--bg); color: var(--muted);
  display: inline-grid; place-items: center; vertical-align: middle;
}
.zug-auge:hover { color: var(--brand-ink); border-color: rgba(var(--brand-rgb), var(--brand-a-rahmen)); }
.zug-auge svg { width: 16px; height: 16px; }
.zug-hinweis { margin: var(--sp-3) 0 0; font-size: var(--fs-s); color: var(--muted); }
.zug-reauth-backdrop { position: fixed; inset: 0; background: var(--text); opacity: .45; z-index: 40; }
.zug-reauth {
  position: fixed; z-index: 41; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(92vw, 340px); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-m); box-shadow: var(--shadow-overlay); padding: var(--sp-4);
}
.zug-reauth-text { margin: var(--sp-2) 0; font-size: var(--fs-s); color: var(--muted); }
.zug-reauth input { width: 100%; }
.zug-reauth-fehler { margin: var(--sp-2) 0 0; font-size: var(--fs-s); color: var(--danger); }
.zug-reauth-aktionen { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-3); }
/* Verwaltung (23.07.): Anlegen/Deaktivieren/Passwort-Setzen. Der Bestaetigen-
   Knopf im Modal ist brand-gefuellt; er ist nie gleichzeitig mit dem
   Seiten-Primaerknopf sichtbar (Backdrop dazwischen). */
.zug-kopf { margin: 0 0 var(--sp-4); }
.zug-reauth strong { display: block; margin-bottom: var(--sp-3); }
.zug-zeile--aus td { color: var(--muted); }
.zug-badge-aus {
  display: inline-block; margin-left: var(--sp-2); padding: var(--sp-0) var(--sp-2);
  font-size: var(--fs-xs); font-weight: 700; color: var(--danger);
  border: 1px solid currentColor; border-radius: var(--radius-pill);
}
.zug-auge--warn:hover { color: var(--danger); border-color: currentColor; }
.zug-feld {
  display: block; margin: 0 0 var(--sp-3); font-size: var(--fs-s);
  font-weight: 600; color: var(--muted);
}
/* Sonst bleibt "Name der Lehrkraft" stehen, wenn die Rolle auf "Nur Chat"
   wechselt -- zugaenge.html schaltet das Feld per hidden. */
.zug-feld[hidden] { display: none; }
.zug-feld input, .zug-feld select { width: 100%; margin-top: var(--sp-1); font-weight: 400; }
.zug-reauth input[name="eigenes_passwort"] { margin-top: var(--sp-1); }
.zug-bestaetigen {
  background: var(--brand); border-color: var(--brand); color: var(--brand-on);
}
.zug-bestaetigen:hover { filter: brightness(1.05); }
.zug-startpw {
  margin: var(--sp-3) 0; padding: var(--sp-2) var(--sp-3); text-align: center;
  font-weight: 700; letter-spacing: .04em; color: var(--brand-ink);
  background: var(--brand-bg); border-radius: var(--radius-s);
  user-select: all; -webkit-user-select: all;
}
/* Handy: die 640px breite Konten-Tabelle lief rechts aus dem Bild -- die
   Passwort-Spalte mit den Aktionen war unerreichbar (Mustafa 23.07.). Auf
   schmalen Schirmen wird jede Zeile zu einer eigenen Karte mit beschrifteten
   Werten, sodass man pro Konto alles auf einmal sieht, ohne seitwaerts zu
   scrollen. Gilt iPhone WIE Android -- reines Breiten-Query, kein iOS-Spezial. */
@media (max-width: 560px) {
  .zug-card { padding: var(--sp-2); }
  .zug-tabelle { min-width: 0; display: block; }
  .zug-tabelle thead { display: none; }
  .zug-tabelle tbody, .zug-tabelle tr, .zug-tabelle td { display: block; }
  .zug-tabelle tr {
    border: 1px solid var(--border); border-radius: var(--radius-s);
    padding: var(--sp-3); margin-bottom: var(--sp-3);
  }
  .zug-tabelle tbody tr:last-child { margin-bottom: 0; }
  .zug-tabelle td { border-bottom: none; padding: var(--sp-1) 0; }
  .zug-tabelle td[data-label]::before {
    content: attr(data-label); display: block; margin-bottom: var(--sp-0);
    font-size: var(--fs-xs); font-weight: 600; color: var(--muted);
  }
  /* Passwort-Zelle: Label auf eigene Zeile (flex-basis 100%), darunter die
     Punkte + Aktions-Knoepfe als umbrechende Reihe mit Abstand. */
  .zug-pw-zelle { white-space: normal; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
  .zug-pw-zelle::before { flex-basis: 100%; }
  .zug-pw { margin-right: 0; }
}
/* Neuerungen-Kalender: Monatsraster, Tage mit Aenderungen orange markiert. */
/* Ansicht-Umschalter: gefuellter Segment-Schalter statt Ghost-Chips (Mustafa
   23.07.: Liste/Kalender klarer hervorheben). Kalender ist die Standard-Ansicht. */
/* Ein Segment-Schalter fuer drei Orte: Neuerungen (Liste/Kalender), die
   Termin-Ansichten (Tag/Woche/Monat) und die Reiter des Dokumentenbereichs
   (Rechnungen/Unterlagen/Ablage, seit 07.08.). Gemeinsame Regeln statt
   zweiter Formensprache -- der Nutzer soll denselben Schalter wiedererkennen.
   Neue Orte binden sich hier ein, statt eigene Klassen zu erfinden. */
.pn-ansicht, .tv-schalter {
  display: inline-flex; gap: var(--sp-1); margin: 0 0 var(--sp-4);
  padding: var(--sp-1); background: var(--surface-akzent);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
}
.pn-seg, .tv-tab {
  display: grid; place-items: center; min-height: var(--tap-secondary);
  padding: 0 var(--sp-4); border-radius: var(--radius-pill);
  font-size: var(--fs-s); font-weight: 600; color: var(--muted); text-decoration: none;
  transition: color .15s var(--ease), background .15s var(--ease);
}
.pn-seg--aktiv, .tv-tab.is-aktiv { background: var(--brand); color: var(--brand-on); font-weight: 700; }
/* Stehen zwei Schalter nebeneinander (Neuerungen: Kalender/Liste + App/Website),
   liest sich der Zwischenraum aus reinem Zeilenumbruch wie EIN Schalter mit vier
   Feldern. Der Abstand macht aus zwei Gruppen wieder zwei. */
.pn-ansicht + .pn-ansicht { margin-left: var(--sp-3); }
.pn-cal { padding: var(--sp-4); }
.pn-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); }
.pn-cal-grid--head { margin-bottom: var(--sp-2); font-size: var(--fs-xs); font-weight: 600; color: var(--muted); text-align: center; text-transform: uppercase; letter-spacing: .04em; }
.pn-cal-tag {
  display: grid; place-items: center; min-height: var(--tap-secondary);
  border-radius: var(--radius-s); font-size: var(--fs-s); color: var(--text);
}
.pn-cal-tag--aussen { color: var(--muted); opacity: .5; }
.pn-cal-tag--markiert {
  background: rgba(var(--brand-rgb), var(--brand-a-schleier));
  color: var(--brand-ink); font-weight: 600; text-decoration: none;
  border: 1px solid rgba(var(--brand-rgb), var(--brand-a-rahmen));
}
.pn-cal-tag--markiert:hover { background: rgba(var(--brand-rgb), var(--brand-a-fokus)); }
.pn-cal-tag--gewaehlt { background: var(--brand); color: var(--brand-on); border-color: var(--brand); }
.pn-cal-tagestitel { margin: var(--sp-5) 0 var(--sp-3); font-size: var(--fs-l); }
.pn-cal-hinweis { margin: var(--sp-3) 0 0; font-size: var(--fs-s); color: var(--muted); }

/* Wischbare Container zeigen ihre Kante (Sweep-Fund 23.07.: Tab-Leiste,
   Kalender und Tabellen wirkten auf Mobile hart abgeschnitten, obwohl sie
   scrollen). Die Maske blendet den Inhalt an der rechten Kante aus --
   ein angeschnittener Eintrag liest sich als "geht weiter" statt als
   Renderfehler. Bewusst zustandslos (kein JS): am Scroll-Ende blendet das
   letzte Stueck minimal aus, der uebliche Preis dieses Musters. */
.tabbar, .cal-card, .tstat-scroll, .cchart {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
}
.tstat { width: 100%; border-collapse: collapse; font-size: var(--fs-s); min-width: 640px; }
.tstat th {
  text-align: left; font-size: var(--fs-xs); font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); padding: var(--sp-1) var(--sp-2) var(--sp-2);
  border-bottom: 1px solid var(--border);
}
.tstat td { padding: var(--sp-2) var(--sp-2); border-bottom: 1px solid var(--border); }
/* Der Kopf einer Zahlenspalte gehoert ueber die Zahlen, nicht daneben. Ohne
   diese Zeile schlaegt `.tstat th` (links) per Spezifitaet `.tstat-num`
   (rechts) -- gemessen 45-73px Versatz zwischen Kopf- und Wert-Kante. */
.tstat th.tstat-num { text-align: right; }
.tstat tbody tr:last-child td { border-bottom: none; }
.tstat-name { font-weight: 600; white-space: nowrap; }
.tstat-link { color: var(--brand-ink); text-decoration: none; }
.tstat-link:hover { text-decoration: underline; }
.tstat-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tstat-neg { color: var(--danger); font-weight: 600; }
.tstat-open { color: var(--brand-ink); font-weight: 600; }
.tstat-hint { margin: var(--sp-2) 0 0; font-size: var(--fs-xs); color: var(--muted); }
.widget-more { margin: var(--sp-2) var(--sp-1) 0; font-size: var(--fs-s); color: var(--muted); }
.widget-chat-unread { margin: 0 0 var(--sp-2); font-size: var(--fs-s); font-weight: 600; color: var(--brand-ink); }
.wchat-row { display: flex; align-items: baseline; gap: var(--sp-2); padding: var(--sp-2) 0; }
.wchat-row + .wchat-row { border-top: 1px solid var(--border); }
.wchat-who { flex-shrink: 0; font-size: var(--fs-s); font-weight: 700; color: var(--brand-ink); }
.wchat-body { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-s); color: var(--text); }
.wchat-time { flex-shrink: 0; margin-left: auto; font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.wnote-row { padding: var(--sp-2) 0; font-size: var(--fs-base); }
.wnote-row + .wnote-row { border-top: 1px solid var(--border); }

@media (min-width: 1024px) {
  .widget-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}

/* Schmale Screens: Datum aus der Topbar nehmen -- Zurueck-Button, Wordmark
   und ?-Button brauchen den Platz. In Widget-Kacheln ist das Datum bei
   heute/morgen redundant und wuerde nur angeschnitten ("13....") enden. */
@media (max-width: 560px) {
  .topbar-date { display: none; }
  .wtile-title .h-date { display: none; }
}

/* ===========================================================================
   Micro-Interactions -- macht die App geschmeidiger. Bewusst klein und nur
   auf INTERAKTIVE Elemente: sanftes Seiten-Einblenden plus tastbares
   Druck-Feedback fuer Buttons und Nav-Tabs, die bisher hart ohne Uebergang
   ansprangen. Statische .card-Container bleiben ruhig; die Topbar-Buttons
   (.top-back/.top-bell) und Chat-/Kachel-Hover haben ihr Feedback schon.
   Alles unter prefers-reduced-motion aus.
   =========================================================================== */

/* Jede eingeloggte Seite blendet beim Laden sanft ein. Nur Opacity, KEIN
   Transform auf main -- ein Transform wuerde position:fixed-Kinder (z. B. den
   Chat-Composer) an main statt am Viewport ausrichten. Die Login-Seite (ohne
   .app-shell) bleibt unberuehrt.

   Bis zum 24.07. blendete hier jede Seite ueber 280 ms ein (page-fade-in).
   Gemessen war das der groesste einzelne Posten der gefuehlten Traegheit:
   ein konstanter Aufschlag von ~290 ms NACH dem Zeichnen, in jeder Bedingung
   gleich (ungedrosselt 285 ms, CPU 4x 289 ms, mit Mobilfunk 293 ms) -- reine
   Wartezeit auf eine Seite, die laengst fertig ist, bei jedem Klick. Bei 30
   Navigationen einer Sitzung rund 8,7 Sekunden. Nutzer mit
   prefers-reduced-motion liefen ohnehin schon ohne diese Einblendung
   (Block unten), die App war fuer sie also nie darauf angewiesen. Weg damit
   fuer alle. tests_keine_seiten_animation haelt das offen. */

/* Native Cross-Document View Transitions (@view-transition) waren hier kurz
   eingebaut, sind aber wieder raus: in echtem Chromium (CI-E2E) blockierte die
   VT-Snapshot-Ebene den Klick auf den Auftrags-Drawer nach einer Navigation
   (test_abhaken_hinterlaesst_keinen_scroll_lock, 30s-Timeout). Ein Uebergang,
   der Klicks direkt nach dem Seitenwechsel schluckt, ist auf dem iPhone genauso
   ein Problem -- kein reiner Testartefakt. Der weisse Blitz ist trotzdem weg:
   den nimmt der html-background oben (verifiziert). Der fluessige Uebergang
   selbst kommt zurueck, sobald er auf echtem iOS-Safari geprueft ist. */

/* Primaer-Button: Hover-Helligkeit lief bisher ohne Uebergang (hart), Tap gab
   nicht nach. */
.btn-primary { transition: filter .15s var(--ease), transform .08s var(--ease); }
.btn-primary:active { transform: translateY(1px); }
.btn-primary:disabled:active, .btn-primary.is-active:active { transform: none; }

/* Ghost-Button: Hover-Rahmen lief ohne Uebergang. */
.btn-ghost { transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease), transform .08s var(--ease); }
.btn-ghost:active { transform: translateY(1px); }
.btn-ghost:disabled:active { transform: none; }

/* Nav-Tabs: Farbwechsel weich, Tap gibt sichtbar nach (spuerbar v. a. am
   Handy). */
.tab { transition: color .15s var(--ease), background .15s var(--ease), transform .08s var(--ease); }
.tab:active { transform: scale(.94); }
/* Die Tap-Rueckmeldung nimmt an diesem Uebergang NICHT teil: color und
   background stehen oben in der Liste, der angetippte Tab haette sich also
   ueber 150ms eingefaerbt -- genau die Verzoegerung, gegen die die Klasse
   ueberhaupt gebaut ist (im Browser nachgemessen, test_flow_nav). Gleiche
   Begruendung wie beim Fokus-Ring: eine Rueckmeldung auf eine Bedienhandlung
   blendet nicht ein, sie ist da. Der Weg zurueck (Klasse faellt weg) laeuft
   wieder ueber die normale Kurve. */
.tab.is-tapped { transition: none; }

/* Zeilen-Hover BEWUSST nur in der Lehrerkennzahlen-Tabelle: .tstat ist 640px
   breit und scrollt horizontal -- der Ton hilft, eine Zeile ueber die Spalten
   hinweg zu verfolgen. Das ist eine Lesehilfe, keine Klick-Zusage.
   .entry/.appt/.doc bekommen bewusst KEINEN Zeilen-Hover: das sind <div>s, nur
   der Name darin ist ein Link. Ein Hover auf der ganzen Zeile wuerde eine
   Klickflaeche versprechen, die es nicht gibt. */
@media (hover: hover) {
  .tstat tbody tr:hover { background: var(--nav-active-bg); }
}
.tstat tbody tr { transition: background .12s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .btn-primary, .btn-ghost, .tab, .tstat tbody tr { transition: none; }
  .btn-primary:active, .btn-ghost:active, .tab:active { transform: none; }
  .top-back, .top-bell, .top-reset { transition: none; }
  .top-back:active, .top-bell:active, .top-reset:active { transform: none; }
  .top-reset.is-busy svg { animation: none; }
}

/* --- Frist-Schnellwahl im Auftrags-Composer (Register + Chat-Schublade) ---
   Solmaz 26.07.: "vorgefertigte Fristen wie 2 Tage 3 Tage oder 7 Tage schnell
   auszuwaehlen ... sonst muss ich selbst alles tun."

   Bewusst native Radios statt versteckter Eingaben mit gebastelten Kaestchen:
   das Radio bleibt sichtbar und bedienbar, die Pille darum ist nur die
   Trefferflaeche. Faellt :has() aus (aeltere Safari-Stände), verliert die
   Auswahl ihre Faerbung -- benutzbar bleibt sie trotzdem, weil das Radio
   selbst anzeigt, was gewaehlt ist. */
.frist-wahl { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); border: 0; padding: 0; margin: 0; min-width: 0; }
.frist-wahl legend { padding: 0; margin: 0 0 var(--sp-1); font-size: var(--fs-xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.frist-wahl label {
  display: inline-flex; align-items: center; gap: var(--sp-1); min-height: var(--tap-secondary);
  padding: var(--sp-1) var(--sp-3); border: 1px solid var(--border-field); border-radius: var(--radius-pill);
  background: var(--surface); font-size: var(--fs-s); color: var(--text); cursor: pointer; white-space: nowrap;
}
.frist-wahl label:has(input:checked) {
  border-color: rgba(var(--brand-rgb), var(--brand-a-rahmen)); background: var(--nav-active-bg);
  color: var(--brand-ink); font-weight: 600;
}
/* width/flex ausdruecklich: beide Composer haben eine Sammelregel
   ("jedes input in dieser Karte ist 100% breit"), die fuer Text- und
   Datumsfelder gedacht ist. Trifft sie ein Radio, fuellt der Kreis die Pille
   und die Beschriftung rutscht hinaus. Die Regel hier ist spezifischer und
   gewinnt unabhaengig von der Ladereihenfolge. */
.frist-wahl input[type="radio"] { flex: 0 0 auto; width: auto; min-width: 0; margin: 0; accent-color: var(--brand); }
.frist-wahl input[type="date"] { min-height: var(--tap-secondary); flex: 0 1 auto; width: auto; }
.frist-wahl-oder, .frist-wahl small { font-size: var(--fs-xs); color: var(--muted); }

/* Restfrist am rechten Rand der Auftragszeile im Register.
   Grau bis zwei Tage vor Ablauf, dann rot UND fett -- Farbe allein traegt die
   Aussage bei Rot-Gruen-Schwaeche nicht (DESIGN.md, Graustufen-Gegenprobe). */
.auf-rest { flex-shrink: 0; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }
.auf-rest--dringend { color: var(--danger); font-weight: 700; }

/* Bearbeiten/Loeschen in der Zahlungsliste (Solmaz #17). Nebeneinander und
   ohne Zeilenumbruch, damit die Spalte nicht die Tabellenhoehe verdoppelt. */
/* Die Knopfreihe darf umbrechen, die einzelnen Knoepfe nicht. Mit
   `white-space: nowrap` auf dem Container blieb die ganze Reihe in einer Zeile
   und schob sich ueber den Text daneben -- sichtbar geworden am 04.08.2026,
   als bei einer bezahlten Rechnung ein dritter Knopf dazukam. */
.zahl-aktionen { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-2); align-items: center; }
.zahl-aktionen > * { white-space: nowrap; }

/* Eine Rechnungszeile traegt drei Knoepfe und ist damit die breiteste Zeile der
   App. Die 54 % aus `.entry-side` reichen dafuer am Handy nicht; statt die
   Grenze fuer ALLE Zeilen zu lockern, bricht hier die Zeile selbst um. */
.entry--rechnung { flex-wrap: wrap; }
.entry--rechnung .entry-side { max-width: none; align-items: flex-end; }
@media (max-width: 34rem) {
  .entry--rechnung .entry-side { width: 100%; align-items: flex-start; }
  .entry--rechnung .zahl-aktionen { justify-content: flex-start; }
}

/* Lehrkraft-Filter ueber Monat/Woche/Tag (Solmaz #18). Sitzt unter dem
   Ansichts-Umschalter und bleibt bewusst zurueckhaltend: er blendet aus, was
   ohnehin sichtbar ist, und ist keine Hauptaktion der Seite. */
.tv-filter { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-4); }
.tv-filter label { font-size: var(--fs-xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.tv-filter select { min-height: var(--tap-secondary); font: inherit; font-size: var(--fs-s); color: var(--text); padding: var(--sp-1) var(--sp-2); border: 1px solid var(--border-field); border-radius: var(--radius-s); background: var(--surface); }

/* Anhang im Auftrags-Register (Solmaz #9/#10). */
.auf-anhang { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--fs-xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.auf-anhang input[type="file"] { font-size: var(--fs-s); font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--text); }
.auf-anhang-link { display: inline-flex; align-items: center; gap: var(--sp-1); margin-top: var(--sp-1); font-size: var(--fs-xs); font-weight: 600; color: var(--brand-ink); text-decoration: none; word-break: break-word; }
.auf-anhang-link:hover { text-decoration: underline; }
/* Sprachmemo im Register -- gleiche Masse wie .bubble-audio (chat.css), damit
   derselbe Ton auf allen drei Flaechen gleich aussieht. */
.auf-audio { display: block; margin-top: var(--sp-1); width: 250px; max-width: 100%; height: 44px; }
.auf-anhang-link svg { flex-shrink: 0; width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

/* Langer Auftragstext klappt zu (27.07.). Solmaz schreibt seine Anforderungen
   in EINEN Auftrag -- der laengste hat 2208 Zeichen. Sieben davon untereinander
   waren 93% der Seitenlaenge reiner Fliesstext, die Liste als Uebersicht damit
   unbrauchbar ("man muss 100 Jahre runterscrollen").
   <details> statt Klapp-JS: der Zustand gehoert dem Browser, funktioniert ohne
   Skript (CSP), ist per Tastatur bedienbar und meldet auf-/zugeklappt von
   selbst an die Vorlesefunktion. Vorschau und Volltext sind zwei echte
   Textknoten (display:none statt ::after-content), damit der Name der
   Schaltflaeche in beiden Zustaenden genau ein sinnvoller Satz ist. */
.auf-mehr { min-width: 0; }
.auf-mehr > summary {
  display: flex; align-items: baseline; gap: var(--sp-2);
  cursor: pointer; list-style: none; min-width: 0;
  font-size: var(--fs-s); color: var(--muted);
}
.auf-mehr > summary::-webkit-details-marker { display: none; }
.auf-mehr > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius-s); }
/* Die Vorschau ist EINE Zeile und wird abgeschnitten -- ohne das waere die
   zugeklappte Zeile so hoch wie der Absatz, den sie ersetzen soll. */
.auf-vorschau { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.auf-mehr-wort { flex-shrink: 0; font-weight: 600; color: var(--brand-ink); }
.auf-mehr-wort--auf { display: none; }
.auf-mehr[open] > summary .auf-vorschau,
.auf-mehr[open] > summary .auf-mehr-wort--zu { display: none; }
.auf-mehr[open] > summary .auf-mehr-wort--auf { display: inline; }
.auf-volltext {
  margin: var(--sp-2) 0 0; font-size: var(--fs-s); color: var(--text);
  white-space: pre-wrap; overflow-wrap: anywhere;
  padding-left: var(--sp-3); border-left: 2px solid var(--border);
}

/* Auftrags-Kachel der Uebersicht (27.07.). Bis dahin fuehrte die Uebersicht
   Notizen, aber nicht die Auftraege -- die gemeinsame To-do-Liste war nur
   ueber das Register erreichbar. */
.wauftrag-row { display: flex; align-items: baseline; gap: var(--sp-2); padding: var(--sp-1) 0; font-size: var(--fs-s); }
.wauftrag-row + .wauftrag-row { border-top: 1px solid var(--border); }
.wauftrag-title { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.wauftrag-frist { flex-shrink: 0; font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.wauftrag-frist--dringend { color: var(--danger); font-weight: 700; }

/* Auftragszeile stapelt am Handy (27.07.). Bis dahin lag die Zeile auch bei
   390px im Reihen-Layout: Restfrist und "Öffnen" nahmen die Breite, dem Titel
   blieben GEMESSENE 152 von 390 Pixeln, und ein Titel wie "Rückfragen zu
   Rollen und Teamleiter (zu deinen Punkten 14 und 19)" brach auf sechs Zeilen
   um. Die Karte war damit hoch, obwohl der Text laengst zugeklappt war.
   Ab 560px bleibt alles wie es war -- am Schreibtisch ist die Breite da. */
@media (max-width: 560px) {
  /* Umbrechen statt stapeln: der Titelblock nimmt die erste Zeile ganz,
     Restfrist und "Öffnen" teilen sich die zweite. Bei drei gestapelten
     Kindern stand der Knopf allein auf einer dritten Zeile und kostete rund
     70px je Auftrag fuer nichts. */
  .auf-zeile { flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
  .auf-zeile .entry-main { flex: 1 1 100%; }
  .auf-zeile .auf-rest { text-align: left; }
  .auf-zeile .btn-ghost { margin-left: auto; }
}

/* Zweite Abteilung im Auftrags-Register: "Von dir gestellt" (27.07.).
   Zugeklappt, weil sie eine Nachfrage ist und keine Aufgabe -- der Grund, die
   Seite zu oeffnen, steht darueber. Dieselbe <details>-Mechanik wie beim
   langen Text, nur eine Ebene hoeher. */
.auf-abteilung { margin-top: var(--sp-4); }
.auf-abteilung > summary {
  display: flex; align-items: center; gap: var(--sp-2);
  cursor: pointer; list-style: none;
  min-height: var(--tap-secondary);
  font-size: var(--fs-s); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.auf-abteilung > summary::-webkit-details-marker { display: none; }
.auf-abteilung > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius-s); }
.auf-abteilung-titel { flex-shrink: 0; }
/* Die Zahl traegt die Aussage im zugeklappten Zustand: ohne sie muesste man
   aufklappen, um zu sehen, ob ueberhaupt etwas offen ist. */
.auf-abteilung-zahl { flex: 1; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--brand-ink); }
.auf-abteilung-pfeil { flex-shrink: 0; display: inline-flex; width: 18px; height: 18px; color: var(--muted); transition: transform .22s var(--ease); }
.auf-abteilung-pfeil svg { width: 100%; height: 100%; }
.auf-abteilung[open] .auf-abteilung-pfeil { transform: rotate(180deg); }
.auf-abteilung > .entry-list { margin-top: var(--sp-2); }
@media (prefers-reduced-motion: reduce) {
  .auf-abteilung-pfeil { transition: none; }
}

/* ── Sprachmemo aufnehmen ────────────────────────────────────────────────
   Stand bis zum 01.08. als .composer-mic/.composer-rec* in chat.css, weil es
   die Aufnahme nur im Chat gab. Seit die Auftraege ebenfalls Sprachmemos
   annehmen, sitzt dieselbe Leiste an DREI Stellen (Chat-Composer,
   Auftrags-Register, Auftrags-Fenster) -- deshalb hier, ortsunabhaengig
   benannt, und in chat.css nur noch das, was im Chat wirklich anders ist.

   Die Leiste ERSETZT sichtbar die Eingabezeile, ist also nie gleichzeitig mit
   deren Senden-Knopf am Schirm: der gefuellte Marken-Knopf (Aufnahme beenden)
   und der Senden-Knopf teilen sich denselben Platz, genau EINER ist je Zustand
   sichtbar (DESIGN.md: ein gefuellter Primaer-Knopf je Screen). */
.mikro {
  flex-shrink: 0; width: var(--tap-secondary); height: var(--tap-secondary);
  border-radius: 50%; border: none; background: transparent;
  display: grid; place-items: center; cursor: pointer; color: var(--muted);
}
.mikro:hover { color: var(--brand-ink); background: rgba(var(--brand-rgb), var(--brand-a-hover)); }
.mikro svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.mikro[hidden] { display: none; }

.rec-leiste {
  display: flex; align-items: center; gap: var(--sp-3);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: var(--sp-1);
}
.rec-leiste[hidden] { display: none; }
.rec-knopf {
  flex-shrink: 0; width: var(--tap-icon); height: var(--tap-icon); border-radius: 50%; border: none;
  display: grid; place-items: center; cursor: pointer;
  background: transparent; color: var(--muted);
}
.rec-knopf[hidden] { display: none; }
.rec-knopf svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.rec-knopf--verwerfen:hover { color: var(--danger); }
.rec-knopf--pause:hover { color: var(--brand-ink); }
.rec-knopf--pause svg { grid-area: 1 / 1; width: 17px; height: 17px; fill: currentColor; stroke: none; }
/* Ein Knopf, zwei Sinnbilder: waehrend der Aufnahme die Pause-Balken, im
   angehaltenen Zustand das Weiter-Dreieck. Ein Pause-Zeichen, das im
   angehaltenen Zustand stehen bleibt, sagt das Gegenteil dessen, was der
   naechste Druck tut. */
.rec-knopf--pause .rec-ico-weiter { display: none; }
.rec-leiste.is-paused .rec-knopf--pause .rec-ico-pause { display: none; }
.rec-leiste.is-paused .rec-knopf--pause .rec-ico-weiter { display: block; }
.rec-knopf--senden { background: var(--brand); color: #fff; box-shadow: var(--shadow-brand); transition: transform .12s var(--ease); }
.rec-knopf--senden:active { transform: scale(.92); }
.rec-knopf--senden svg { width: 18px; height: 18px; stroke-width: 2.2; }
.rec-punkt {
  flex-shrink: 0; width: 10px; height: 10px; border-radius: 50%;
  background: var(--danger); animation: rec-pulse 1.3s ease-in-out infinite;
}
/* Angehalten: der Puls steht still, sonst sieht Pause aus wie Aufnahme. */
.rec-leiste.is-paused .rec-punkt { animation: none; opacity: .35; }
.rec-zeit { font-variant-numeric: tabular-nums; font-weight: 700; font-size: var(--fs-base); color: var(--brand-ink); }
.rec-hinweis { flex: 1; font-size: var(--fs-s); color: var(--muted); }
@keyframes rec-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) {
  .rec-punkt { animation: none; }
  .rec-knopf--senden { transition: none; }
  .rec-knopf--senden:active { transform: none; }
}
/* Am Handy wachsen die Tippziele wie ueberall auf --tap-icon. */
@media (max-width: 560px) {
  .mikro { width: var(--tap-icon); height: var(--tap-icon); }
  .mikro svg { width: 20px; height: 20px; }
  /* "Aufnahme laeuft ..." brach bei 390px auf zwei Zeilen um und machte die
     Leiste doppelt so hoch (gemessen 01.08.). Der Satz ist dort auch
     entbehrlich: der rote Puls und die laufende Uhr sagen dasselbe. Nicht
     entfernt, sondern nur optisch weggenommen -- fuer die Vorlesefunktion ist
     er die einzige Angabe, dass gerade aufgenommen wird. */
  .rec-hinweis {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: var(--sp-0);
    overflow: hidden; clip: rect(0 0 0 0); border: 0;
  }
}
.rec-fehler { margin: var(--sp-1) 0 0; font-size: var(--fs-s); color: var(--danger); font-weight: 600; }
.rec-fehler:empty { display: none; }
/* Fuss des Auftrags-Composers: Mikrofon links, der eine gefuellte Knopf rechts. */
.auftrag-fuss { display: flex; align-items: center; gap: var(--sp-2); }
.auftrag-fuss .auftrag-add { flex: 1; }
/* Die [hidden]-Regeln sind KEIN Beiwerk: eine eigene display-Angabe schlaegt
   das eingebaute `[hidden] { display: none }` des Browsers, und die Zeile
   bliebe waehrend der Aufnahme sichtbar stehen. Gemessen am 01.08. -- das
   Skript setzte hidden korrekt, am Schirm aenderte sich nichts. */
.auftrag-fuss[hidden], .auf-neu-fuss[hidden] { display: none; }

/* ── Leiste anpassen (Auftrag #27) ──────────────────────────────────────────
   Zwei Listen desselben Bauplans: oben die vier Plaetze in gewaehlter
   Reihenfolge, unten der Rest. Die Zeile ist eine Trefferflaeche nach
   --tap-hit, nicht nur Text mit Knoepfen daneben. */
.leiste-karte + .leiste-karte { margin-top: var(--sp-4); }
.leiste-titel { font-size: var(--fs-m); margin: var(--sp-4) 0 var(--sp-2); }
.leiste-liste { list-style: none; margin: 0; padding: 0; }
.leiste-zeile {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0; min-height: var(--tap-hit);
}
.leiste-zeile + .leiste-zeile { border-top: 1px solid var(--border); }
.leiste-symbol { width: 22px; height: 22px; flex: none; color: var(--muted); }
.leiste-name { flex: 1; min-width: 0; font-size: var(--fs-base); }
.leiste-knoepfe { display: flex; gap: var(--sp-1); flex: none; }
.leiste-knopf {
  display: grid; place-items: center;
  width: var(--tap-hit); height: var(--tap-hit);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  background: var(--surface); color: var(--text); cursor: pointer;
}
.leiste-knopf svg { width: 18px; height: 18px; }
.leiste-knopf:hover { border-color: var(--text); }
/* Gesperrt heisst sichtbar gesperrt: ein Pfeil am Listenrand, der gleich
   aussieht wie ein bedienbarer, laesst den Nutzer ins Leere tippen. */
.leiste-knopf:disabled { color: var(--disabled-ink); background: var(--disabled-bg); cursor: default; }
.leiste-knopf:disabled:hover { border-color: var(--border); }
.leiste-knopf--weg { color: var(--danger); }
.leiste-knopf--weg:hover { border-color: var(--danger); }
.leiste-knopf--rein:not(:disabled) { color: var(--brand); }
.leiste-hinweis { color: var(--muted); font-size: var(--fs-s); margin: var(--sp-2) 0 var(--sp-4); }
.leiste-leer { color: var(--muted); font-size: var(--fs-base); padding: var(--sp-4) 0; }
.leiste-leer[hidden] { display: none; }
.leiste-standard, .leiste-sichern { margin-top: var(--sp-4); }
.leiste-sichern[hidden] { display: none; }

/* Teams (Solmaz #30). Eine Karte je Team: Mitgliederliste, darunter die
   Zeilen zum Zuordnen, Leiten, Umbenennen und Aufloesen. Alle Werte aus den
   Tokens -- die Seite bringt keinen eigenen Massstab mit. */
.team-card { display: flex; flex-direction: column; gap: var(--sp-3); }
.team-zeile {
  display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--sp-2);
}
/* Das Feld traegt unten schon den Abstand der Zeile; ohne diese Zeile stuende
   es doppelt. */
.team-zeile .zug-feld { margin: 0; flex: 1 1 14rem; }
.team-liste { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: var(--sp-1); }
.team-liste li {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); padding-block: var(--sp-1);
  border-bottom: 1px solid var(--border);
}
.team-liste li:last-child { border-bottom: 0; }
.team-fussnote { font-size: var(--fs-s); color: var(--muted); }

/* ── Team-Chat (Solmaz #30 Punkt 4) ──────────────────────────────────────
   Die Blasen selbst kommen aus chat.css -- ein Team-Gespraech soll aussehen
   wie ein Gespraech und nicht wie eine zweite, aehnliche Sache. Hier steht
   nur, was der 1:1-Chat nicht kennt: die Themenliste und das schmale
   Sendefeld ohne Sprachmemo und ohne Nachladen. */
.themaform { display: grid; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.themaform-zeile { display: flex; gap: var(--sp-2); }
.themaform-zeile input { flex: 1 1 auto; min-width: 0; }

.themenliste { display: grid; gap: var(--sp-2); list-style: none; padding: 0; margin: 0; }
.thema {
  display: grid;
  gap: var(--sp-1);
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--card);
  text-decoration: none;
  color: inherit;
}
.thema:hover { border-color: var(--accent); }
.thema-kopf { display: flex; align-items: center; gap: var(--sp-2); }
.thema-titel { font-size: var(--fs-m); font-weight: 600; }
/* Der Punkt traegt die Aussage nicht allein -- daneben steht die letzte
   Nachricht, und die Zeile ist ohne Farbe genauso lesbar. */
.thema-punkt {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
.thema-zu { font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.thema-vor { font-size: var(--fs-s); color: var(--muted); }
.thema-zeit { font-size: var(--fs-xs); color: var(--muted); }

.themaverlauf { display: grid; gap: var(--sp-3); margin-bottom: var(--sp-5); }

/* Das Sendefeld traegt dieselben Werte wie die Formularfelder der App
   (.field input) -- ohne diese Zeilen steht hier der nackte Browser-Kasten
   in Schreibmaschinenschrift, was am Bildschirm sofort auffaellt. */
.themasenden { display: grid; gap: var(--sp-2); }
.themasenden textarea {
  width: 100%;
  font: 400 .95rem/1.4 var(--font-body);
  color: var(--text);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-s);
  background: var(--surface);
  resize: vertical;
}
.themasenden textarea:focus {
  border-color: var(--brand);
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--brand-rgb), var(--brand-a-fokus));
}
.themasenden-anhang { font-size: var(--fs-s); color: var(--muted); display: grid; gap: var(--sp-1); }
.themasenden-anhang input { font-size: var(--fs-s); }
/* Nicht ueber die volle Breite: der Knopf ist eine Handlung, kein Balken. */
.themasenden .btn-primary { justify-self: start; }
.themaschliessen { margin-top: var(--sp-5); }
/* Der Weg zurueck traegt die Markenfarbe wie jeder andere Weg in der App.
   Ohne diese Zeile stand hier das Browser-Blau mit Unterstreichung -- an
   einer Stelle, an der sonst nichts blau ist. */
.themazurueck {
  display: inline-block;
  margin-bottom: var(--sp-3);
  font-size: var(--fs-s);
  font-weight: 600;
  color: var(--brand-ink);
  text-decoration: none;
}
.themazurueck:hover { text-decoration: underline; }
