/* ==========================================================
   Design system — modern, minimal, hand-drawn SVG icons only
   (no icon CDN dependency).

   Hausschrift war bis 28.08.2026 "Brown" (Lineto), danach kurz Inter,
   seit 28.08.2026 Geist (SIL Open Font License, selbst gehostet unter
   css/fonts/ - weiterhin kein Web-Font-Dienst).

   Warum zweimal gewechselt wurde: Brown lag nur in 400 und 700 vor, alles
   dazwischen hat der Browser gefaelscht. Inter behob das, war dem Nutzer in
   der Praxis aber "zu schwer, nicht filigran genug" - und das zu Recht:
   Browns "light"-Schnitt lief bei uns als 400, Inters echtes 400 ist
   deutlich fetter. Die ganze App wurde damit auf einen Schlag schwerer,
   ohne dass sich eine einzige Zahl im Stylesheet geaendert haette.

   Geist traegt die Achse 100-900 und wird deshalb hier bewusst UNTER 400
   gefahren: Flieaenstext 350, Ueberschriften 500 (siehe --weight-text und
   h1-h4 weiter unten). Das ist der eigentliche Hebel gegen den klobigen
   Eindruck - nicht die Wahl der Schrift.

   Instrument Sans war der Favorit des Nutzers und ist als Zeichnung auch
   die eleganteste der geprueften Kandidatinnen - sie liefert aber nur
   400-700 und kann damit gar nicht leichter werden. Deshalb Geist.
   ========================================================== */

/* Eine Datei je Zeichensatz-Bereich. latin-ext wird nur geladen, wenn
   tatsaechlich ein Zeichen daraus vorkommt (unicode-range) - deutsche
   Umlaute liegen in latin, die Zusatzdatei kostet im Alltag also nichts. */
@font-face {
  font-family: "Geist";
  src: url("fonts/geist-latin-wght-normal.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
                 U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Geist";
  src: url("fonts/geist-latin-ext-wght-normal.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Geist";
  src: url("fonts/geist-latin-wght-italic.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

:root {
  color-scheme: light;
  /* Kompletter Stil-Umbau nach Vorbild katalyst.5studios.net (Chat-Verlauf:
     "Kannst du eine externe Seite analysieren und den Stil von dort
     weitestgehend kopieren?" -> "Ja, dann bauen wir gleich alles um auf
     diesen neuen Stil.") - Werte unten sind NICHT nur optisch geschätzt,
     sondern 1:1 aus den dortigen CSS-Variablen (shadcn/Tailwind, OKLCH-
     Farbraum) in sRGB-Hex umgerechnet (Björn-Ottosson-Formel), siehe
     /tmp/oklch.py aus der Analyse-Session. Cleaner/flacher/reduzierter als
     vorher: hellere neutrale Töne, kaum noch farbige Schlagschatten, ein
     kräftiges Teal statt des gedeckten dusty-blue als Standard-Akzent. */
  /* Feinschliff 28.08.2026 (Nutzer: "edler, schicker, cleaner, weniger
     altbacken"; Light-Mode ist der Hauptmodus). Zwei Eingriffe, sonst
     nichts:

     1. Die Neutraltöne bekommen einen leichten Blaustich. Rein neutrale
        Grautöne (#e5e5e5, #737373) wirken auf Bildschirmen stumpf; eine
        Spur Blau darin liest sich als "sauber" statt als "grau".
     2. --color-text war reines Schwarz. Schwarz auf Weiß ist der härteste
        Kontrast, den ein Bildschirm kann - es sticht, statt zu wirken.
        #11151c ist ein sehr dunkles Schiefer und liegt immer noch weit
        über jeder Lesbarkeitsanforderung (siehe Kontrastwerte im
        Prüfprotokoll der Umbau-Sitzung).

     Die Helligkeiten selbst bleiben praktisch, wo sie waren - es geht um
     den Farbton, nicht um Hell/Dunkel. */
  --color-bg: #f7f8fa;
  --color-bg-alt: #eef0f4;
  --color-surface: #ffffff;
  --color-surface-2: #fafbfc;
  --color-border: #e4e7ec;
  --color-border-strong: #cfd5de;
  /* Hilfslinien und Donut-Spur in den Grafiken. Eigenes Token, weil sie
     schwaecher sein muessen als ein Rahmen: sie liegen HINTER den Daten. */
  --chart-grid: #edeef5;
  --color-text: #11151c;
  --color-text-muted: #626b7a;
  --color-text-faint: #7e8798;  /* 2.6:1 waren zu wenig selbst fuer Zierrat - jetzt 3.6:1 */

  /* "Ocean" - jetzt Katalysts kräftiges Teal/Cyan (Chat-Verlauf: "Ja, auf
     Katalyst-Teal umstellen") statt des vorherigen dusty-blue. Die anderen
     7 Themes weiter unten bleiben bewusst unverändert. */
  --color-accent: #008eab;
  --color-accent-hover: #007592;
  --color-accent-strong: #00516b;
  --color-accent-soft: #d5f1f7;
  --color-accent-2: #615cdc;

  --color-success: #21c45d;
  --color-success-soft: #d5f5da;
  --color-warning: #f59f0a;
  --color-warning-soft: #ffe7cb;
  --color-danger: #ef4343;
  --color-danger-soft: #ffdfda;
  /* Zwei Einzelfälle, die vorher direkt als Hex-Wert im jeweiligen Selektor
     standen (.stat-icon.tone-violet / .btn-danger:hover) - als Variable
     ausgelagert, damit der Dark-Mode-Block weiter unten sie mit überschreiben
     kann, statt jeden hartkodierten Fund einzeln suchen zu müssen. */
  --color-violet-soft: #dcf7fb;
  --color-danger-soft-strong: #ffcac2;

  /* Zusätzliche gedeckte Badge-Töne NUR für frei erweiterbare, nicht-
     semantische Werte wie die Beleg-Zahlungsart (Chat-Verlauf: "Kannst du
     die Zahlungsarten farblich voneinander abheben? Keine grellen Farben
     bitte.") - bewusst eigene, blasse Töne statt der success/warning/danger-
     Variablen oben, weil die eine Bedeutung ("gut"/"Achtung"/"Fehler")
     transportieren, die für eine Zahlungsart nicht passt. Sechs Töne reichen
     aus, um mehrere Werte optisch zu trennen, ohne bunt/laut zu wirken -
     siehe paymentMethodTone() in pages/receipts.js für die Zuordnung
     (feste Werte wie Bar/Überweisung + Hash-Fallback für freien Text). */
  --color-tone-blue: #3a5a8c;
  --color-tone-blue-soft: #dde6f4;
  --color-tone-purple: #6b4f96;
  --color-tone-purple-soft: #e8dff2;
  --color-tone-rose: #9c4f68;
  --color-tone-rose-soft: #f5dfe5;
  --color-tone-amber: #8a6a2a;
  --color-tone-amber-soft: #f2e8cf;
  --color-tone-teal: #2f7d6e;
  --color-tone-teal-soft: #d9f0e9;
  --color-tone-slate: #55606e;
  --color-tone-slate-soft: #e3e6ea;

  /* Sidebar folgt jetzt Light/Dark-Mode statt fest dunkelblau zu sein (Chat-
     Verlauf: "Auf Katalyst-Stil umstellen" - dort ist die Sidebar hell,
     sehr reduziert, kleine Icons statt farbiger Flächen). Werte hier sind
     die Light-Variante, der Dark-Mode-Block weiter unten überschreibt sie
     komplett. --sidebar-active bleibt der einzige Farbverlauf-Rest (Avatar/
     Logo-Badge) - bezieht seine zwei Farben aus --color-accent/-accent-2
     und passt sich dadurch automatisch an Modus UND Akzent-Theme an. */
  --sidebar-bg: #fafafa;
  --sidebar-text: #555555;
  --sidebar-text-active: #060606;
  --sidebar-border: #e5e5e5;
  --sidebar-hover: #eeeeee;
  /* Eine Stufe ÜBER --sidebar-hover, für Flächen, die schon im Ruhezustand
     auf --sidebar-hover liegen (das Suchfeld). Ohne die griff der Hover dort
     zum Aktiv-Verlauf - siehe .sidebar-search-trigger:hover. */
  --sidebar-hover-strong: #e2e2e2;
  --sidebar-active: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-2) 130%);

  /* Je eine Stufe weicher (28.08.) - der sichtbarste Einzelhebel für
     "moderner", und als Token an genau einer Stelle zurückzunehmen. */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 22px;
  /* Global-Visual-Redesign 2026-08-16 (Phase 3 Foundations): 999px
     (Pill-Form) wurde bisher an 29 Stellen hartcodiert statt über einen
     Token ausgedrückt - jetzt konsolidiert. */
  --radius-full: 999px;

  /* Karten trugen bisher Rahmen UND Schlagschatten. Beides zusammen ist
     der Grund, warum eine Oberfläche "gebastelt" wirkt - ein Rahmen genügt,
     um eine Fläche abzugrenzen. --shadow-sm ist deshalb fast auf null
     gezogen (statt jede der Karten-Regeln einzeln anzufassen); md und lg
     bleiben kräftig, weil sie das Schweben von Menüs und Dialogen tragen,
     wo Höhe tatsächlich eine Bedeutung hat. */
  --shadow-sm: 0 1px 1px rgba(16, 24, 40, 0.025);
  --shadow-md: 0 6px 20px -6px rgba(16, 24, 40, 0.10);
  --shadow-lg: 0 20px 48px -16px rgba(16, 24, 40, 0.16);
  --shadow-glow: 0 0 0 3px var(--color-accent-soft);

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-8: 48px;

  /* Design-System-Cleanup 2026-08-16 (docs/design-system-audit-2026-08-16.md,
     Phase 2/Foundations) - semantische Typografie-Tokens. Rein additiv: an
     dieser Stelle referenziert noch NICHTS diese Variablen, bestehende
     Regeln bleiben unverändert. Werte sind bewusst NICHT neu erfunden,
     sondern aus den im Audit gezählten, tatsächlich am häufigsten
     verwendeten Werten übernommen (Prompt-Abschnitt 3: "Nicht blind neue
     Werte übernehmen") - Phase 3 stellt dann Schritt für Schritt bestehende
     Regeln auf diese 6 Stufen um, statt weiterhin pro Komponente eigene
     Zahlen zu definieren.
     - Page Title (h1): war schon 23px Desktop / 20px Mobile - unverändert,
       nur token-isiert.
     - Section Title (h2, Card-/Listen-Überschriften wie "Produktionsliste"):
       war schon 16px, unverändert.
     - Entity Title (Produktions-/Kunden-/Portfolio-/Dateiname - der
       eigentliche Inhalt): bisher je nach Komponente 13.5-14.5px bei
       600-700 Gewicht (siehe Audit Abschnitt 2) - EINE Zahl für alle.
     - Body (Fließtext, Tabellenzellen, Formularwerte): häufigster
       Einzelwert im gesamten Stylesheet (13px, 42 Fundstellen).
     - Meta (Datum, Größe, Anzahl, sekundäre Zeile unter einem Titel):
       zweithäufigster Wert (11.5px, 47 Fundstellen inkl. 12.5px-Nachbarn).
     - Small (nur echte Mikro-Info wie Tags/Zähler-Pills, siehe Prompt
       Abschnitt 6 - "Sekundär bedeutet nicht automatisch sehr klein",
       daher bewusst NICHT die kleinsten gefundenen Werte wie 9.5px/10px
       als Basis, sondern 10.5px als unterste noch gut lesbare Stufe). */
  --text-page-title: 23px;
  --text-section-title: 16px;
  --text-entity-title: 14px;
  --text-body: 13px;
  --text-meta: 11.5px;
  --text-small: 10.5px;

  /* 3 Gewichtsstufen statt der bisher 5 gleichzeitig genutzten (400/500/
     600/700/800, siehe Audit). 700 wird bewusst NICHT als allgemeiner
     "Bold"-Token aufgenommen - bleibt ein bewusster, hartkodierter
     Sonderfall für die ganz wenigen Stellen, die stärker wirken sollen als
     ein Entity Title (z.B. Page Title, Kunden-Kürzel), statt als
     Standard-Hervorhebung für "irgendwie wichtig" zu dienen (Prompt
     Abschnitt 7/8). */
  /* 350 statt 400 fuer Flieaenstext (28.08.2026, Nutzer: "zu schwer, nicht
     filigran genug"). Nur mit einer Variable-Schrift ueberhaupt moeglich -
     mit Brown waere das eine Faelschung durch den Browser gewesen. */
  --weight-text: 350;
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;

  --sidebar-width: 252px;
  --sidebar-width-collapsed: 68px;
  /* Neue rechte Icon-Leiste (Katalyst-Vorbild, Chat-Verlauf: "Was ich auch
     super finde ist die Leiste auf der rechten Seite, können wir das bei uns
     auch integrieren?") - eigene, schmale dritte Grid-Spalte in .app-shell,
     unabhängig vom Sidebar-Einklapp-Zustand (bleibt immer gleich schmal). */
  --rail-width: 56px;
  /* Gemeinsame Easing-Kurve für die neuen, etwas "lebendigeren" Übergänge
     (Chat-Verlauf: "Viele Sachen sind auch schick animiert, das könnte ich
     mir bei uns auch gut vorstellen") - sanftes Ausschwingen statt reinem
     linear/ease, konsistent an den wichtigsten Hover-/Öffnen-Stellen
     verwendet statt überall eigene Kurven zu erfinden. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* Drei Dauern, gestaffelt nach Groesse der Bewegung (28.08.2026).
  
     Vorher standen in dieser Datei ein Dutzend Werte zwischen 0.1s und 0.6s
     nebeneinander, und 90 der 121 Uebergaenge gaben ueberhaupt keine Kurve
     an - dann nimmt der Browser "ease", also ausgerechnet die eine Kurve,
     die nach Voreinstellung aussieht. --ease gab es zwar schon, benutzt
     wurde es an 31 Stellen.
  
     Jetzt tragen 120 der 121 Uebergaenge dieselbe Kurve; die eine Ausnahme
     ist ein Fortschrittsbalken, der nicht beschleunigen darf. Ebenfalls
     bewusst stehen geblieben: das federnde cubic-bezier(0.34, 1.56, 0.64, 1)
     bei Abhak-Bestaetigungen, das absichtlich ueberschwingt. */
  --dur-fast: 120ms;   /* Farbe, Rahmen, Hover */
  --dur-base: 180ms;   /* Modals, Toasts, Ein-/Ausblenden */
  --dur-slow: 240ms;   /* Aufklappen, Layoutwechsel */

  /* Geist, selbst gehostet (siehe @font-face oben). Der System-Stack
     dahinter greift nur, solange die Schrift lädt - font-display: swap. */
  --font: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* --font-alt zeigte auf "Brown Alt", einen zweiten Fett-Schnitt mit
     anderen Ziffernformen. Den gibt es bei Geist nicht als eigene Familie -
     die Variable-Achse deckt das ab. Bleibt als Alias bestehen, damit
     bestehende Verwendungen im Stylesheet nicht ins Leere zeigen. */
  --font-alt: var(--font);
}

/* ==========================================================
   Dark Mode — überschreibt nur die Farb-Tokens von oben, nichts sonst
   (Radien/Spacing/Font bleiben gleich). Wird per data-theme="dark" auf
   <html> aktiviert (siehe js/theme.js + der Inline-Script in index.html,
   das das schon vor dem ersten Render setzt, um ein Aufblitzen im falschen
   Modus zu vermeiden). Die Sidebar ist jetzt ebenfalls Modus-abhängig statt
   fest dunkel (Chat-Verlauf: "Auf Katalyst-Stil umstellen") - eigene
   --sidebar-* Werte hier, dunkler/neutraler als die Content-Fläche, analog
   zum Vorbild.
   ========================================================== */
html[data-theme="dark"] {
  /* Sagt dem Browser, in welcher Welt er native Bedienelemente zeichnet -
     Auswahlmenues, Datumswaehler, Bildlaufleisten, den Autofill-Hintergrund.
     Ohne diese Zeile bleiben die hell, egal wie gruendlich man die eigenen
     Flaechen einfaerbt (30.08.2026). */
  color-scheme: dark;
  /* Siehe Kommentar im :root-Block. Zwei Unterschiede zu vorher:
     der Grund ist nicht mehr fast schwarz (#080a0e -> #0e1014), damit
     Flächen darauf überhaupt als Flächen lesbar sind statt im Grund zu
     verschwinden; und die Rahmen sind eine Spur heller, weil sie hier
     die Arbeit übernehmen, die im Light-Mode der Schatten macht. */
  --color-bg: #0e1014;
  --color-bg-alt: #14171d;
  --color-surface: #171a21;
  --color-surface-2: #1e222b;
  --color-border: #2a2f3a;
  --color-border-strong: #3a4150;
  --chart-grid: #242932;
  --color-text: #e8eaf0;
  --color-text-muted: #9aa1b1;
  --color-text-faint: #7b8395;  /* siehe Light-Mode: angehoben auf 4.6:1 */

  --color-accent: #22b8d1;
  --color-accent-hover: #68cfe4;
  --color-accent-strong: #cbf0f8;
  --color-accent-soft: #002a32;
  --color-accent-2: #9fa5ff;

  --color-success: #4dcb70;
  --color-success-soft: #102b16;
  --color-warning: #faab3f;
  --color-warning-soft: #382409;
  --color-danger: #ff6b64;
  --color-danger-soft: #421c19;
  --color-violet-soft: #002a32;
  --color-danger-soft-strong: #522420;

  --color-tone-blue: #9db8e0;
  --color-tone-blue-soft: #1c2a3d;
  --color-tone-purple: #cbb3e6;
  --color-tone-purple-soft: #2a2036;
  --color-tone-rose: #e3aebb;
  --color-tone-rose-soft: #34212a;
  --color-tone-amber: #e3c98a;
  --color-tone-amber-soft: #362c17;
  --color-tone-teal: #8fd4c1;
  --color-tone-teal-soft: #16302a;
  --color-tone-slate: #b7bfc9;
  --color-tone-slate-soft: #23262d;

  --sidebar-bg: #0f0f0f;
  --sidebar-text: #a4a4a4;
  --sidebar-text-active: #f5f5f5;
  --sidebar-border: #232323;
  --sidebar-hover: #1f1f1f;
  --sidebar-hover-strong: #2c2c2c;

  /* Nur im Dunkelmodus: helle Schrift auf dunklem Grund wirkt mit
     Subpixel-Glaettung fett und ausgefranst - hier hilft die
     Graustufen-Glaettung tatsaechlich. Siehe Kommentar bei body. */
  -webkit-font-smoothing: antialiased;

  --shadow-sm: 0 1px 1px rgba(0, 0, 0, 0.20);
  --shadow-md: 0 6px 20px -6px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 20px 48px -16px rgba(0, 0, 0, 0.60);
}

/* ==========================================================
   Akzentfarben-Themes — Chat-Verlauf: "Und die globale Farbwelt auch. Mach
   doch 4-5 Themes." -> später auf 8 erweitert + neu abgestimmt: "die Farben
   etwas weniger Signalfarben-mäßig machen, eher stilvolle gedeckte Farben /
   Pastelltöne" (galt für ALLE Themes inkl. Standard "ocean", siehe :root/
   dem Dark-Block oben). Läuft UNABHÄNGIG vom Hell/Dunkel-Toggle (per
   data-accent="..." auf <html>, siehe THEMES/applyAccent() in theme.js,
   Auswahl-UI in layout.js) - überschreibt nur die Akzent-Tokens
   (--color-accent*), nicht die Grundfarben. "ocean" ist der Standard (=
   die Werte aus :root/dem Dark-Block oben), braucht deshalb keinen eigenen
   Block. --sidebar-active und alle Schatten/Verläufe, die var(--color-
   accent) referenzieren, aktualisieren sich automatisch mit - nur die
   Roh-Token unten müssen pro Theme gesetzt werden.
   Dark-Varianten (html[data-theme="dark"][data-accent="..."]) sind bewusst
   eigene, aufgehellte Werte statt einer automatischen Berechnung, gleiches
   Prinzip wie der bestehende Dark-Mode-Block oben. ========================================================== */
html[data-accent="violet"] {
  --color-accent: #96407c;
  --color-accent-hover: #793465;
  --color-accent-strong: #5f254e;
  --color-accent-soft: #deced9;
  --color-accent-2: #9a50a5;
}
html[data-theme="dark"][data-accent="violet"] {
  --color-accent: #c889b5;
  --color-accent-hover: #d9b5ce;
  --color-accent-strong: #eddee8;
  --color-accent-soft: #3a1d31;
  --color-accent-2: #caa4d0;
}

html[data-accent="forest"] {
  --color-accent: #397f52;
  --color-accent-hover: #2c6340;
  --color-accent-strong: #1e482d;
  --color-accent-soft: #bdd1c4;
  --color-accent-2: #548e48;
}
html[data-theme="dark"][data-accent="forest"] {
  --color-accent: #76bc90;
  --color-accent-hover: #a2cdb2;
  --color-accent-strong: #cce1d3;
  --color-accent-soft: #1d3928;
  --color-accent-2: #9ac492;
}

html[data-accent="sunset"] {
  --color-accent: #c15e33;
  --color-accent-hover: #a14e2b;
  --color-accent-strong: #843d1f;
  --color-accent-soft: #efe2dc;
  --color-accent-2: #c89e4c;
}
html[data-theme="dark"][data-accent="sunset"] {
  --color-accent: #dea891;
  --color-accent-hover: #ebcec2;
  --color-accent-strong: #f9f3f0;
  --color-accent-soft: #422215;
  --color-accent-2: #e4d2af;
}

html[data-accent="berry"] {
  --color-accent: #c44565;
  --color-accent-hover: #ab3653;
  --color-accent-strong: #8f2842;
  --color-accent-soft: #f5ebed;
  --color-accent-2: #c67162;
}
html[data-theme="dark"][data-accent="berry"] {
  --color-accent: #dfa4b3;
  --color-accent-hover: #eed3da;
  --color-accent-strong: #ffffff;
  --color-accent-soft: #3f1721;
  --color-accent-2: #e7c6c1;
}

/* Drei neue Themes (Chat-Verlauf: "Kannst du bitte 8 Farbthemes einbauen") -
   gleiches Prinzip wie oben, nur zusätzliche Farbfamilien: kühles Blaugrau,
   gedecktes Oliv/Moos und warmer Sandton. Sättigung/Helligkeit aller 8
   Themes bewusst als Kompromiss gewählt (Chat-Verlauf: "Etwas stärker
   dürfen die Farben sein, mach einen Kompromiss bitte") - kräftiger als die
   erste, sehr blasse Pastell-Fassung, aber weiterhin klar gedeckter als die
   ursprünglichen reinen Signalfarben. */
html[data-accent="slate"] {
  --color-accent: #4d6389;
  --color-accent-hover: #3e506f;
  --color-accent-strong: #2d3d58;
  --color-accent-soft: #d1d5db;
  --color-accent-2: #5e8297;
}
html[data-theme="dark"][data-accent="slate"] {
  --color-accent: #93a3bd;
  --color-accent-hover: #bcc4d2;
  --color-accent-strong: #e1e4ea;
  --color-accent-soft: #222935;
  --color-accent-2: #acbec8;
}

html[data-accent="olive"] {
  --color-accent: #717f39;
  --color-accent-hover: #58632c;
  --color-accent-strong: #40481e;
  --color-accent-soft: #cdd1bd;
  --color-accent-2: #5b8e48;
}
html[data-theme="dark"][data-accent="olive"] {
  --color-accent: #aebc76;
  --color-accent-hover: #c4cda2;
  --color-accent-strong: #dde1cc;
  --color-accent-soft: #34391d;
  --color-accent-2: #9fc492;
}

html[data-accent="sand"] {
  --color-accent: #b08245;
  --color-accent-hover: #936c39;
  --color-accent-strong: #79572a;
  --color-accent-soft: #ece6df;
  --color-accent-2: #b7715c;
}
html[data-theme="dark"][data-accent="sand"] {
  --color-accent: #d4bb9b;
  --color-accent-hover: #e5d8c8;
  --color-accent-strong: #f8f5f2;
  --color-accent-soft: #3c2e1b;
  --color-accent-2: #ddbfb6;
}

* { box-sizing: border-box; }

/* Safety net: every hand-authored icon SVG (icons.js) has no width/height of
   its own, so without this any icon in a spot that doesn't set an explicit
   size (e.g. inside .btn-ghost) falls back to the browser's default replaced-
   element size and renders oversized/clipped. More specific rules further
   down (.btn svg, .nav-link svg, etc.) still win over this. */
svg { width: 1em; height: 1em; flex-shrink: 0; }

/* Icons are always wrapped as h('span', { html: icon(...) }) - a plain inline
   span. Inline elements reserve baseline/line-height whitespace around their
   content, which visually pushes the icon a few pixels above true center
   inside flex-centered circles/buttons (set-status icons, stat icons,
   avatars, etc.). inline-flex removes that whitespace so every icon centers
   exactly, everywhere, without needing a per-component fix. */
span:has(> svg) { display: inline-flex; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Live-Review
     mit Nutzer über Claude-in-Chrome, Fund: Text in .catalog-table
     (Leistungskatalog) erschien auf dem iPhone plötzlich viel größer als der
     Rest der App, obwohl die Tabelle bereits token-basierte Font-Größen
     nutzt) - Mobile Safari vergrößert Text automatisch ("Text-Size-
     Adjustment"/Font-Boosting), wenn eine Spalte optisch schmaler wirkt als
     der Inhalt, den sie enthält - genau der Fall bei breiten, horizontal
     scrollenden Tabellen wie .catalog-table (min-width:640px auf einem
     ~400px-Viewport). Deaktiviert diese Heuristik app-weit, statt nur lokal
     für eine Tabelle - betrifft potenziell jeden zukünftigen breiten Inhalt
     gleichermaßen, ändert an der normalen (bereits token-basierten)
     Schriftgröße überall sonst nichts. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  font-family: var(--font);
  background: var(--color-bg);
  color: var(--color-text);
  font-size: var(--text-entity-title);
  line-height: 1.55;
  /* KEIN -webkit-font-smoothing: antialiased hier (28.08.2026, Frage des
     Nutzers "wie schaffen es manche Seiten, dass die Schrift so extrem
     scharf aussieht?").
     Der Schalter klingt nach "schaerfer", tut aber das Gegenteil: er
     schaltet von der Subpixel- auf die Graustufen-Glaettung und macht
     Schrift damit DUENNER. Auf hellem Grund - und Hell ist jetzt der
     Hauptmodus - liest sich das als blass und weich, nicht als scharf. Im
     Dunkelmodus ist es umgekehrt: helle Schrift auf dunklem Grund "blueht"
     ohne diese Glaettung auf, deshalb steht sie unten NUR dort.
     text-rendering: optimizeLegibility ist ebenfalls raus - es schaltet
     Kerning und Ligaturen zu, nicht die Schaerfe, und kostet bei langen
     Seiten Rechenzeit. Inter bringt sein Kerning ohnehin mit.
     Was wirklich scharf aussehen laesst: eine fuer Bildschirme gezeichnete
     Schrift (jetzt Inter), genug Gewicht und hoher Kontrast. */
  text-rendering: auto;
  /* 350 (28.08.2026). Der klobige Eindruck kam zu grossen Teilen von hier
     und nicht von der Schriftwahl: Browns "light" lief bei uns als 400,
     ein echtes 400 ist deutlich fetter. Geist kann 350 wirklich zeichnen. */
  font-weight: var(--weight-text);
  /* Zurueckgenommen: ich hatte antialiased heute Vormittag entfernt, weil es
     Schrift NICHT schaerfer macht. Das stimmt weiterhin - es macht sie
     duenner, und genau das ist hier erwuenscht. Zwei Aenderungen liefen
     vorher in dieselbe Richtung (schwerere Schrift UND kraeftigere
     Glaettung), deshalb wirkte es doppelt klobig. */
  -webkit-font-smoothing: antialiased;
  /* Eine Winzigkeit Unterschneidung laesst Flieaenstext ruhiger wirken.
     Auf Ueberschriften kommt weiter unten deutlich mehr. */
  letter-spacing: -0.005em;
}

/* Zahlen, die untereinander stehen, brauchen gleich breite Ziffern - sonst
   wackelt eine Spalte aus Betraegen bei jeder Ziffer. BEWUSST nicht global
   auf body: in Flieaenstext reisst tabular-nums die Ziffern unschoen
   auseinander. Deshalb hier gezielt fuer Tabellen und die Klasse .num. */
table,
.num,
[data-num] {
  font-variant-numeric: tabular-nums;
}

::selection { background: var(--color-accent-soft); color: var(--color-accent-strong); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-border-strong); border-radius: var(--radius-full); }
::-webkit-scrollbar-thumb:hover { background: var(--color-text-faint); }

/* 700 -> 600 (28.08., Nutzer: "weniger altbacken"). Fettschrift war der
   auffaelligste Grund, warum die Oberflaeche derb wirkte: 700 auf jeder
   Ueberschrift laesst eine Seite laut werden. 600 traegt dieselbe
   Gliederung, ohne zu schreien - und ist mit Inter erstmals ein ECHTER
   Schnitt statt eines vom Browser gefaelschten (Brown lag nur in 400 und
   700 vor, die Tokens --weight-medium/-semibold waren bis heute Fiktion).
   Die Unterschneidung waechst mit der Groesse, siehe h1 unten. */
h1, h2, h3, h4 { margin: 0; font-weight: var(--weight-medium); letter-spacing: -0.015em; }
/* Design-System-Cleanup 2026-08-16 (Phase 3/Core Components) - h1/h2 auf
   die neuen Tokens umgestellt, Werte bewusst unverändert (23px/16px, siehe
   Kommentar bei den Token-Definitionen oben) - reiner Refactor ohne
   sichtbaren Effekt. h2 bekommt zusätzlich --weight-semibold statt des von
   h1/h3/h4 geerbten 700 (Prompt Abschnitt 7/8: "Bold nur für sehr wenige
   Situationen" - ein Section Title wie "Produktionsliste" muss nicht so
   stark wirken wie der Page Title darüber). h3 bewusst noch NICHT
   angefasst - wird je nach Screen in Phase 4 einzeln konsolidiert (aktuell
   uneinheitlich zwischen 12-15px je Komponente, siehe Audit). */
h1 { font-size: var(--text-page-title); letter-spacing: -0.022em; }
h2 { font-size: var(--text-section-title); font-weight: var(--weight-medium); }
h3 { font-size: 13.5px; }
p { margin: 0; }

/* ----------------------------------------------------------
   Kleinbuchstaben-Label (28.08.2026)
   ----------------------------------------------------------
   Die Beschriftung ueber einer Zahl, einer Liste oder einem Abschnitt.
   Bisher wurde das auf jeder Seite neu erfunden - mal 12.5px in
   --color-text-muted, mal 11px fett, mal als h3. Das ist der Grund,
   warum gleichartige Bereiche unterschiedlich aussahen.

   Klein, gesperrt, halbfett, in der blassesten Textfarbe: das Label soll
   die Zahl darueber NICHT ueberstrahlen, sondern sie nur benennen. Genau
   diese Kombination trennt eine ruhige Oberflaeche von einer lauten -
   und sie ist erst seit dem Wechsel auf Inter sauber moeglich (600 als
   echter Schnitt, siehe Kommentar bei h1-h4). */
.label,
.overline {
  font-size: var(--text-small);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.075em;
  text-transform: uppercase;
  /* -muted, NICHT -faint: klein + gesperrt + Versalien ist die am
     schwersten lesbare Textform, die es gibt - die darf nicht zusaetzlich
     die blasseste Farbe bekommen. Gemessen: -muted kommt im Light-Mode auf
     5.4:1, -faint nur auf 3.6:1 (Richtwert fuer kleinen Text ist 4.5:1). */
  color: var(--color-text-muted);
  line-height: 1.4;
}

/* Dieselbe Form, aber in der Akzentfarbe: fuer die EINE Zeile ueber einer
   Seitenueberschrift ("Donnerstag, 28. August"). Bewusst sparsam - lebt
   davon, dass sie einmal pro Ansicht vorkommt. */
/* Farbpunkt vor der Produktionsnummer (28.08.2026) - die aus dem Film
   gelesene Bildstimmung, siehe production_color in
   migration_preview_sprite_2026-08-28.sql. Bewusst klein und ohne Rahmen:
   ein Erkennungsmerkmal, kein Statusanzeiger. */
.prod-color-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: var(--radius-full);
  margin-right: 6px;
  vertical-align: 1px;
  flex: none;
}

.overline-accent {
  font-size: var(--text-small);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.075em;
  text-transform: uppercase;
  color: var(--color-accent);
  line-height: 1.4;
}

a { color: inherit; text-decoration: none; }

button {
  font-family: inherit;
  font-size: var(--text-body);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: none;
  color: inherit;
  margin: 0;
}

/* ---------- App shell ---------- */

#app { min-height: 100%; }

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-width) 1fr var(--rail-width);
  /* iPhone-Mobile-UX-Audit 2026-08-16 (docs/ux-audit-2026-08-16-iphone-mobile.md,
     2.12): 100vh bezieht sich in Mobile Safari nicht zuverlässig auf die
     tatsächlich sichtbare Höhe (Adressleiste blendet sich dynamisch ein/aus)
     - dvh (dynamic viewport height) folgt dem echten sichtbaren Bereich.
     vh bleibt als Fallback für Browser ohne dvh-Support stehen, die
     dvh-Zeile danach überschreibt sie dort, wo unterstützt. .app-shell ist
     der äußerste Grid-Container der ganzen App, betrifft also jede Seite. */
  min-height: 100vh;
  min-height: 100dvh;
}
/* Echtes Grid-Item der "1fr"-Spalte (siehe layout.js) - ohne min-width: 0
   bläht sich eine CSS-Grid-Spalte auf den Min-Content ihrer Nachfahren auf,
   selbst wenn tiefer verschachtelte Elemente (wie .board-viewport) längst
   eigenes overflow:hidden + min-width:0 haben. Das war die eigentliche,
   bisher übersehene Ursache dafür, dass .board-viewport.getBoundingClientRect()
   auf Board-Seiten die volle 6000px-Canvasbreite statt der echten sichtbaren
   Breite lieferte (siehe .main:has(.board-page) weiter unten - das dortige
   min-width: 0 allein reichte nicht, weil .main gar nicht das Grid-Item
   selbst ist, sondern nur ein Enkelkind davon). */
.app-shell-col { min-width: 0; }

.sidebar {
  background: var(--sidebar-bg);
  /* Etwas knapperes Top/Bottom-Padding als bei anderen Seiten (siehe
     Chat-Verlauf: auf einem 16"-MacBook ragte die Sidebar minimal über die
     Fensterhöhe hinaus und bekam dadurch einen eigenen Scrollbalken) -
     zusammen mit den etwas engeren .nav-group/.sidebar-brand/
     .sidebar-search-trigger-Abständen unten reicht das, ohne dass es gedrängt
     wirkt. */
  padding: var(--space-4) var(--space-3);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  /* Batch 6 (iPhone Mobile-UX-Audit 2026-08-16, Finding 2.12, restliche
     dvh-Stelle): dieselbe .sidebar-Regel bedient zwei Kontexte - am Desktop
     die sticky Grid-Spalte, auf Mobile (siehe @media(max-width:900px)
     weiter unten) das per translateX ein-/ausfahrende Overlay-Drawer-Menü.
     Bei fixed position:fixed mit gesetztem top+bottom würde die Höhe sich
     sonst implizit aus dem Elternelement ergeben - das explizite height
     hier überschreibt das aber weiterhin, dvh-Fallback-Muster siehe
     Kommentar bei .app-shell weiter oben. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  z-index: 50;
}

/* Nur die Navigationsliste selbst scrollt (nicht mehr die ganze .sidebar) -
   dadurch bleibt .sidebar-footer IMMER sichtbar am unteren Rand, statt bei
   viel Inhalt (z.B. im Anpassen-Modus, wo jede Zeile durch Ziehgriff +
   Ausblenden-Button etwas höher wird, siehe .nav-link-editable) unten aus
   dem sichtbaren Bereich zu rutschen. Chat-Verlauf: "Wenn man in der
   Sidebar auf den Bearbeiten-Button klickt, rutscht die Icon-Navigation
   unten leicht außerhalb des Bildschirms." */
.nav-groups-container {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Duenne, zurueckhaltende Scrollleiste statt der globalen 10px-Variante
     (siehe ::-webkit-scrollbar ganz oben in dieser Datei). Chat-Verlauf
     2026-08-27: "Die Scroll-Leiste in der Navigation links entweder
     ausblenden oder sehr duenn und elegant stylen."

     Bewusst nicht komplett ausgeblendet (wie bei .production-tab-bar): die
     Navigation scrollt nur dann, wenn tatsaechlich mehr Eintraege da sind,
     als hineinpassen - genau dann ist ein Hinweis darauf nuetzlich. Der
     Kompromiss: unsichtbar im Ruhezustand, beim Ueberfahren der Navigation
     erscheint ein schmaler Balken.

     scrollbar-width/-color sind der Firefox-Weg, ::-webkit-scrollbar der
     fuer Chrome/Safari - beide noetig, sie ignorieren jeweils die andere
     Variante. */
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color var(--dur-base) var(--ease);
}
.nav-groups-container:hover,
.nav-groups-container:focus-within {
  scrollbar-color: var(--sidebar-border) transparent;
}
.nav-groups-container::-webkit-scrollbar { width: 6px; }
.nav-groups-container::-webkit-scrollbar-track { background: transparent; }
.nav-groups-container::-webkit-scrollbar-thumb {
  background: transparent;
  border-radius: var(--radius-full);
  transition: background var(--dur-base) var(--ease);
}
.nav-groups-container:hover::-webkit-scrollbar-thumb,
.nav-groups-container:focus-within::-webkit-scrollbar-thumb {
  background: var(--sidebar-border);
}
.nav-groups-container::-webkit-scrollbar-thumb:hover { background: var(--color-border-strong); }

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 15px;
  color: var(--sidebar-text-active);
  /* Etwas knapper als vorher (space-5 -> 18px) - seit dem neuen "Chat"-
     Eintrag reichte der bisherige Abstand nicht mehr ganz, siehe Chat-
     Verlauf: "Seitenleiste minimal stauchen, damit der Scrollbalken
     verschwindet". */
  padding: 0 var(--space-2) 18px;
  letter-spacing: -0.01em;
}

/* Echter Schriftzug (shy_Icon.svg, siehe brandMarkSvg in icons.js) auf dem
   bisherigen farbigen Verlaufs-Kachel-Hintergrund (Chat-Verlauf: "Und das
   Teal war eigentlich cool, muss nicht schwarz sein" - der Hintergrund
   bleibt also unverändert wie vor dem Logo-Wechsel, nur das Icon selbst
   wurde ausgetauscht). */
.sidebar-brand-mark {
  width: 32px;
  height: 32px;
  border-radius: 10px;
  background: var(--sidebar-active);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  box-shadow: 0 4px 14px -2px rgba(0, 142, 171, 0.4);
}
.sidebar-brand-mark svg { width: 100%; height: 100%; display: block; }

.sidebar-brand span.dim { color: var(--sidebar-text); font-weight: 500; }

.sidebar-search-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  margin-bottom: 12px;
  background: var(--sidebar-hover);
  border: 1px solid var(--sidebar-border);
  border-radius: var(--radius-sm);
  color: var(--sidebar-text);
  font-size: 12.5px;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* Der Hover griff hier zum Aktiv-Verlauf der Navigation - demselben, mit dem
   die ausgewählte Seite markiert wird. Doppelt falsch: es sah aus wie
   "ausgewählt", obwohl nur der Mauszeiger darauf liegt, und das
   Tastenkürzel-Abzeichen (Hintergrund --sidebar-bg, im Hellmodus fast weiß)
   wurde mit der erzwungenen weißen Schrift unlesbar - das ⌘K verschwand.
   Gemeldet am 31.08.2026 mit Screenshot. Hover ist jetzt eine Stufe, kein
   Sprung: eine Fläche dunkler, Rahmen und Schrift kräftiger. */
.sidebar-search-trigger:hover {
  background: var(--sidebar-hover-strong);
  border-color: var(--color-text-faint);
  color: var(--sidebar-text-active);
}
.sidebar-search-trigger svg { width: 14px; height: 14px; flex-shrink: 0; }
.sidebar-search-label { flex: 1; text-align: left; }
.sidebar-search-kbd {
  font-family: inherit;
  font-size: var(--text-small);
  font-weight: 600;
  padding: 2px 5px;
  border-radius: 4px;
  background: var(--sidebar-bg);
  border: 1px solid var(--sidebar-border);
  color: inherit;
}

/* Etwas knapper als vorher (18px -> 15px) - siehe Kommentar bei .sidebar-brand oben. */
/* 15px -> 24px (28.08.2026): Abstand trennt die Kategorien zuverlaessiger
   als Farbe, siehe Kommentar bei .nav-group-label. */
/* Halbiert (02.09.2026, Nutzer: "Mach bitte die Abstände zwischen den
   Kategorien 50% kleiner"). --space-6 war grosszuegig, als die Leiste
   vier Kategorien hatte; bei sechs schiebt es "Spiele" unter die Kante. */
.nav-group { margin-bottom: var(--space-3); }

/* Katalyst-Vorbild: dezente, kleine Großbuchstaben-Labels statt kräftiger
   Farbe (Chat-Verlauf: "Auf Katalyst-Stil umstellen"). Die frühere
   opacity: 0.7 ist am 28.08.2026 entfallen, siehe unten. */
/* Kategorien in der Seitenleiste (ueberarbeitet 28.08.2026, Nutzer: "man
   erkennt die Kategorien kaum").
   Drei Aenderungen, in dieser Reihenfolge wirksam:
   1. Die Beschriftung war auf 70% Deckkraft heruntergeregelt und damit
      blasser als die Eintraege DARUNTER - eine Ueberschrift, die leiser ist
      als ihr Inhalt, kann nicht gliedern. Jetzt volle Deckkraft.
   2. Mehr Luft ueber jeder Gruppe. Abstand trennt zuverlaessiger als jede
      Farbe; ohne ihn haette auch der Punkt unten wenig geholfen.
   3. Ein kleiner Punkt im Ton der Kategorie davor (--nav-tone, gesetzt in
      layout.js). Bewusst nur der Punkt: die Beschriftung selbst einzufaerben
      wuerde die Seitenleiste in sechs Farben zerlegen und mehr Unruhe
      stiften, als sie Ordnung schafft. */
.nav-group-label {
  display: flex;
  align-items: center;
  gap: 7px;
  /* Nachjustiert 28.08.2026 (Nutzer: "Sidebar optisch beruhigen ... Section-
     Headlines kleiner/leichter"). Heute Vormittag hatte ich sie auf volle
     Deckkraft und 600 gezogen, weil sie blasser waren als ihre eigenen
     Eintraege - richtig war die Diagnose, zu viel die Dosis. Jetzt kleiner
     (9.5px statt 10.5), leichter (500 statt 600) und weiter gesperrt: bei
     Versalien traegt die Sperrung die Lesbarkeit, nicht das Gewicht. Den
     Rang gegenueber den Eintraegen sichert jetzt der Abstand darueber. */
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.11em;
  /* Die Beschriftung traegt jetzt den Ton ihrer Kategorie (02.09.2026,
     Nutzer: "die Headlines farblich - wenn sinnvoll - markieren"). Der
     Kommentar oben bei NAV_GROUPS in layout.js sagte bis heute das
     Gegenteil: "faerbt nur einen kleinen Punkt, nicht die Beschriftung
     selbst - eine Seitenleiste in sechs Farben waere lauter als alles,
     was daneben steht." Das stimmt fuer krachende Farben; die
     --color-tone-* sind aber gedeckt, und bei 9.5px in Versalien traegt
     der Ton eher zur Gliederung bei, als dass er laut wuerde. Der
     Rueckfall auf --color-text-faint bleibt fuer Gruppen ohne Ton. */
  color: var(--nav-tone, var(--color-text-faint));
  padding: 0 var(--space-3);
  margin-bottom: 6px;
  font-weight: var(--weight-medium);
}
.nav-group-label::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: var(--radius-full);
  background: var(--nav-tone, var(--sidebar-text));
  flex: none;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  /* 8px -> 7px -> 6.65px -> 6.32px vertikal (Chat-Verlauf: "alle Unterpunkte
     der Navigation links noch 5% näher zusammenschieben" - 6.65px * 0.95 =
     6.3175px, gerundet 6.32px). Macht sich über ~20 Einträge spürbar
     zusammen. */
  padding: 6.32px var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--sidebar-text);
  /* War kurzzeitig 14.85px (+10%, Chat-Verlauf: "Items in der Seitenleiste
     10% größer machen"), jetzt auf Wunsch wieder zurück auf die
     ursprünglichen 13.5px (Chat-Verlauf: "die Sidebar doch wieder 10%
     stauchen, damit alles reinpasst" - mit inzwischen deutlich mehr
     Nav-Einträgen als beim ersten "größer machen" passte die Liste sonst
     nicht mehr ohne eigenen Scrollbalken). .nav-link-editable (Anpassen-
     Modus) trägt IMMER auch die Klasse .nav-link mit - diese Regel gilt
     also automatisch für beide Modi identisch, es kann also gar keinen
     Größensprung beim Wechsel in den Anpassen-Modus geben. */
  font-size: 13.5px;
  font-weight: 500;
  margin-bottom: 0.9px;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.nav-link:hover { background: var(--sidebar-hover); color: var(--sidebar-text-active); }

/* Label ist das einzige Element in der Zeile, das schrumpfen darf (Grip-Icon,
   Haupt-Icon und Ausblenden-Button behalten im Anpassen-Modus ihre feste
   Breite) - dadurch bleibt die Zeilenhöhe IMMER identisch zum normalen
   Modus: der Text weicht per Ellipsis nach links aus, statt in eine zweite
   Zeile umzubrechen (was vorher bei längeren Labels wie "Rechnungswesen"
   oder "Leistungskatalog" die Zeile spürbar höher machte, sobald Ziehgriff +
   Ausblenden-Button den verfügbaren Platz verengten). Chat-Verlauf: "Können
   die Zeilen nicht gleich groß bleiben und nur leicht nach links rutschen
   für den Ziehgriff...". */
.nav-link-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Katalyst-Vorbild: flache "Pille" statt Farbverlauf+Schlagschatten - passt
   sich über --color-accent-soft/-strong automatisch an alle 8 Akzent-Themes
   und Hell/Dunkel an (Chat-Verlauf: "das cleane, moderne und reduzierte"). */
/* Neu gefasst 28.08.2026 (Nutzer: "der aktive Menuepunkt koennte
   charakteristischer werden - z.B. nur eine subtile getoente Flaeche plus
   staerkeres Icon statt der relativ grossen Cyan-Flaeche").
   --color-accent-soft war im Light-Mode ein kraeftiges Cyan und damit das
   lauteste Element der ganzen Seitenleiste. Jetzt traegt die Flaeche nur
   noch eine Ahnung von Akzent, und die Markierung kommt aus zwei leiseren
   Mitteln: dem farbigen Icon und einem schmalen Balken links. Der Balken
   sitzt als ::before im Innenabstand, damit die Zeilenhoehe unveraendert
   bleibt - sonst huepft die Liste beim Seitenwechsel.
   Die erste background-Zeile ist der Rueckfall fuer Browser ohne
   color-mix(); die zweite ueberschreibt sie, wo es unterstuetzt wird. */
.nav-link.active {
  position: relative;
  background: var(--sidebar-hover);
  background: color-mix(in srgb, var(--color-accent) 9%, transparent);
  color: var(--sidebar-text-active);
  font-weight: var(--weight-medium);
  box-shadow: none;
}
.nav-link.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  translate: 0 -50%;
  width: 2.5px;
  height: 15px;
  border-radius: 0 var(--radius-full) var(--radius-full) 0;
  background: var(--color-accent);
}
.nav-link.active svg { color: var(--color-accent); }

/* Icon-Hüpfer beim Hover wieder entfernt (Chat-Verlauf: erst
   "offensichtlicher machen", dann "Die Animation in der Sidebar ist mir zu
   viel, das bitte rausnehmen") - die Sidebar bleibt bei reinem Farbwechsel
   über .nav-link, keine Icon-Transform mehr. */
/* War kurzzeitig 17.6px (+10%, gleiche Änderung wie font-size oben), jetzt
   wieder zurück auf 16px (siehe Kommentar bei .nav-link font-size oben) -
   gilt über den Selektor .nav-link svg ebenfalls für beide Modi identisch. */
.nav-link svg { width: 16px; height: 16px; flex-shrink: 0; }
/* Weniger Icon-Kontrast (28.08.2026, Nutzer). Die Icons standen in derselben
   Farbe wie ihre Beschriftung und verdoppelten damit jede Zeile optisch -
   bei ~25 Eintraegen untereinander ist das der Hauptgrund fuer den dichten
   Eindruck. Sie treten jetzt zurueck und kommen erst beim Hover bzw. auf der
   aktiven Zeile wieder voll heran, wo sie tatsaechlich etwas bedeuten. */
.nav-link svg { color: var(--color-text-faint); transition: color var(--dur-fast) var(--ease); }
.nav-link:hover svg { color: inherit; }

/* ---------- Sidebar-Anpassen-Modus (Chat-Verlauf: "lass doch die Sidebar
   auch komplett frei gestaltbar machen - pro User. Kategorien und
   Unterpunkte frei positionierbar.") - gleiches Grundmuster wie die
   Dashboard-Kacheln (.dashboard-tile-toolbar/-grip/-hide-btn in
   dashboard.js), hier nur auf die vertikale Nav-Liste übertragen: Einträge
   UND Kategorien-Header bekommen im Anpassen-Modus einen Ziehgriff, Einträge
   zusätzlich einen Ausblenden-/Einblenden-Button. */
.nav-link-editable {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: grab;
}
.nav-link-editable:active { cursor: grabbing; }
.nav-link-grip {
  display: flex;
  color: var(--sidebar-text);
  opacity: 0.6;
  flex-shrink: 0;
}
.nav-link-grip svg { width: 13px; height: 13px; }
.nav-link-hide-btn {
  /* Bewusst NICHT größer als die natürliche Zeilenhöhe (Textzeile bei
     font-size 14.85px/line-height 1.55 ≈ 23px) - ein größerer Kreis-Button
     als flex-Kind zwingt sonst (via align-items:center auf .nav-link) die
     GESAMTE Zeile auf seine eigene Höhe, was sich wie ein zusätzliches
     Padding um Icon+Label herum anfühlte (Chat-Verlauf: "Sieht eher aus wie
     ein allgemeines Padding um alle Elemente herum"). Deshalb hier kleiner
     als vorher (22px), damit die Textzeile weiterhin das höchste Element
     bleibt und die Zeilenhöhe zwischen normalem und Anpassen-Modus exakt
     gleich bleibt. */
  margin-left: auto;
  flex-shrink: 0;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: none;
  border: none;
  color: var(--sidebar-text);
  display: flex;
  align-items: center;
  justify-content: center;
}
.nav-link-hide-btn:hover { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
.nav-link-hide-btn svg { width: 11px; height: 11px; }
/* Ausgeblendete Einträge bleiben im Anpassen-Modus sichtbar (gedimmt, mit
   "+"-Button zum Wieder-einblenden statt sie in eine separate Ablage zu
   verschieben, siehe navGroupBlock() in layout.js), damit man sie an ihrer
   gewohnten Stelle wiederfindet. */
.nav-link-editable.is-hidden-item { opacity: 0.45; }
.nav-link-editable.is-hidden-item .nav-link-hide-btn { color: var(--color-accent-2); }

.nav-group-label-editable {
  /* Gefundene Hauptursache des "alles rutscht leicht auseinander"-Effekts:
     padding hier hatte vorher 4px oben/unten ZUSÄTZLICH zur nicht-editierbaren
     .nav-group-label (die padding: 0 ... hat) - macht bei 6 Kategorien 6×8px
     = 48px mehr Gesamthöhe im Anpassen-Modus. Jetzt exakt dieselbe vertikale
     Ausdehnung wie .nav-group-label (padding nur horizontal), damit jede
     Kategorie-Überschrift in beiden Modi exakt gleich hoch bleibt. */
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: grab;
  padding: 0 var(--space-3);
  border-radius: var(--radius-sm);
  margin: 0 0 var(--space-2);
}
.nav-group-label-editable:hover { background: var(--sidebar-hover); }
.nav-group-label-editable:active { cursor: grabbing; }
.nav-group-grip { display: flex; color: var(--sidebar-text); opacity: 0.6; flex-shrink: 0; }
.nav-group-grip svg { width: 12px; height: 12px; }

/* Drop-Ziel-Hervorhebung beim Ziehen über einen Eintrag/eine Kategorie -
   gleiches Prinzip wie .dashboard-tile.is-dragover in dashboard.js. */
.nav-link-editable.is-dragover,
.nav-group-label-editable.is-dragover {
  outline: 2px dashed var(--color-accent-2);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

.sidebar-footer {
  /* Bezugsrahmen fuer das Spotify-Popover, siehe dort. */
  position: relative;
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px solid var(--sidebar-border);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* Zwei Zeilen statt einer einzigen überfüllten Reihe (Chat-Verlauf: "nicht
   optimal so, kannst du das sinnvoller platzieren?") - oben Avatar+Name,
   darunter eine eigene Zeile für die Icon-Aktionen (Papierkorb, Darstellung,
   Logout), rechtsbündig statt an den Namen gequetscht. */
.sidebar-footer-user { display: flex; align-items: center; gap: 10px; min-width: 0; }
.sidebar-footer-actions { display: flex; align-items: center; justify-content: flex-end; gap: 2px; }

.avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--sidebar-active);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12.5px;
  flex-shrink: 0;
  cursor: pointer;
}
/* Hochgeladenes Profilbild (Chat-Verlauf: "User-Bild hochladbar") - ersetzt
   die Initialen-Kachel 1:1, gleiche Größe/Form, nur mit background-image
   statt Textinhalt (siehe rerenderAvatar() in layout.js). */
.avatar-image { background-size: cover; background-position: center; }
.avatar-wrap { position: relative; flex-shrink: 0; }
/* "x"-Button zum Entfernen des Profilbilds - nur bei Hover über den Avatar
   sichtbar, damit die Footer-Zeile im Normalzustand nicht überladen wirkt. */
.avatar-remove-btn {
  position: absolute;
  bottom: -2px;
  right: -2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-danger);
  color: #fff;
  border: 2px solid var(--sidebar-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.avatar-remove-btn svg { width: 8px; height: 8px; }
.avatar-wrap:hover .avatar-remove-btn { opacity: 1; }

.sidebar-user-block { flex: 1; min-width: 0; }
.sidebar-user {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--sidebar-text-active);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidebar-user-role {
  font-size: 11px;
  color: var(--sidebar-text);
}

.btn-logout {
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: 8px;
  color: var(--sidebar-text);
  flex-shrink: 0;
  display: flex;
}
.btn-logout:hover { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
.btn-logout svg { width: 16px; height: 16px; }
/* Papierkorb-Icon im Footer (Chat-Verlauf: "Papierkorb in der Seitenleiste
   ganz unten als Icon neben den Darkmode") - gleicher Look wie die anderen
   Footer-Icons, nur mit Akzentfarbe, wenn man gerade auf /trash ist. */
.btn-logout.active { background: var(--sidebar-hover); color: var(--color-accent); }

/* Akzentfarben-Theme-Auswahl (Chat-Verlauf: "Und die globale Farbwelt auch.
   Mach doch 4-5 Themes.") - kleiner Farbkreis-Button im Footer, öffnet eine
   schmale Popover-Zeile mit den 5 Presets (siehe THEMES in theme.js). */
.accent-swatch-dot {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1.5px solid var(--sidebar-border);
  display: block;
}
.accent-popover {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  display: none;
  gap: 7px;
  padding: 9px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: 60;
  transform-origin: bottom left;
}
.accent-popover.is-open { display: flex; animation: panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.accent-swatch-btn {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid var(--color-border-strong);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.accent-swatch-btn:hover { transform: scale(1.12); border-color: var(--color-text); }

.sidebar-brand-text { flex: 1; }
.sidebar-collapse-toggle {
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: 6px;
  color: var(--sidebar-text);
  flex-shrink: 0;
  display: flex;
}
.sidebar-collapse-toggle:hover { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
.sidebar-collapse-toggle svg { width: 14px; height: 14px; }

/* ---------- Rechte Icon-Leiste (Katalyst-Vorbild) ----------
   Chat-Verlauf: "Was ich auch super finde ist die Leiste auf der rechten
   Seite, können wir das bei uns auch integrieren?" - dritte, schmale
   Grid-Spalte in .app-shell (siehe --rail-width oben), unabhängig vom
   Sidebar-Einklapp-Zustand. Baugleich mit .sidebar in Grundfarben/-abstand,
   aber eine eigene, deutlich schmalere Komponente statt einer Sidebar-
   Variante, weil sie nur Icons + Flyout-Panels zeigt, keine Textliste. */
.right-rail {
  background: var(--sidebar-bg);
  border-left: 1px solid var(--sidebar-border);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: visible;
  z-index: 45;
  /* flex-column, damit .right-rail-icons-bottom (Feedback-Icon, Chat-
     Verlauf: "Ich meine die Seitenleiste am rechten Bildschirmrand, dort
     ganz unten.") sich per margin-top:auto nach unten schieben kann, statt
     direkt unter der oberen Icon-Gruppe zu kleben. */
  display: flex;
  flex-direction: column;
}
.right-rail-icons {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: var(--space-4) 0;
}
.right-rail-icons-bottom { padding-bottom: var(--space-4); }
/* Das Wetter sitzt MITTIG zwischen den beiden Icon-Gruppen (04.09.2026).
   -----------------------------------------------------------------
   margin-top:auto haelt es von der oberen Gruppe weg, margin-bottom:auto
   schiebt die untere an den Rand - zwei gleiche auto-Margins teilen den
   freien Platz zu gleichen Teilen, und genau das heisst "mittig".

   Das margin-top:auto an .right-rail-icons-bottom ist dabei ENTFALLEN:
   mit zwei auto-Margins in der Mitte und einem weiteren unten wuerde der
   Platz auf drei Luecken verteilt, und das Wetter saesse zu hoch. Wer die
   mittlere Gruppe je entfernt, muss es dort wieder eintragen - sonst
   klebt die untere Gruppe direkt unter der oberen. */
.right-rail-mitte { margin-top: auto; margin-bottom: auto; }

/* ---- Das Wetter-Widget ----
   46 px breit in einer 56 px schmalen Leiste. Jede Entscheidung hier
   folgt daraus: keine Ortsangabe, keine Wochentage, kein Text - das
   steht alles im Panel. */
.wetter-knopf {
  width: 46px;
  padding: 6px 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: inherit;
  transition: background var(--dur-fast) var(--ease);
}
.wetter-knopf:hover { background: var(--color-accent-soft); }
.wetter-leiste { display: flex; flex-direction: column; align-items: center; }
/* Heute gross, die Folgetage klein: die haeufigste Frage ist "wie wird's
   heute", und die soll das Auge ohne Lesen beantworten. */
.wetter-heute { display: flex; flex-direction: column; align-items: center; }
.wetter-heute .wetter-grad { font-size: 12.5px; font-weight: 600; color: var(--color-text); }
.wetter-folge-gruppe {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 5px;
  padding-top: 5px;
  border-top: 1px solid var(--sidebar-border);
  width: 32px;
}
.wetter-folge { display: flex; align-items: center; justify-content: center; gap: 3px; }
.wetter-folge .wetter-grad { font-size: 10px; font-weight: 500; color: var(--color-text-muted); }
.wetter-grad { font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.wetter-bild { display: inline-flex; }
/* Die Farbe steckt am Symbol, nicht am Text: Sonne warm, Regen kuehl,
   Wolken neutral. Ein Wetterbild in Textfarbe sagt nichts, ein farbiges
   sagt die Haelfte schon vor dem Lesen der Zahl. */
.wetter-svg.is-sonne, .wetter-svg.is-wolkig { color: var(--color-warning); }
.wetter-svg.is-regen, .wetter-svg.is-schnee { color: #3d7fb8; }
.wetter-svg.is-gewitter { color: var(--color-accent-2, #615cdc); }
.wetter-svg.is-wolken { color: var(--color-text-muted); }

.wetter-panel { width: 316px; }
.wetter-jetzt { font-size: 11.5px; font-weight: 400; color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.wetter-tage { padding: 4px 6px 6px; }
.wetter-tag {
  display: grid;
  grid-template-columns: 46px 20px 1fr 52px;
  align-items: center;
  gap: 8px;
  padding: 5px 7px;
  border-radius: 7px;
}
.wetter-tag.ist-dreh { background: var(--color-accent-soft); }
/* Der duenne Strich markiert die drei Tage, die draussen in der Leiste
   stehen - ohne ihn muesste man raten, welche der sieben Zeilen man
   gerade gesehen hat. */
.wetter-tag.in-leiste { box-shadow: inset 2px 0 0 var(--color-accent); }
.wetter-tag-name { font-size: 12px; font-weight: 600; color: var(--color-text); }
.wetter-tag.ist-dreh .wetter-tag-name { color: var(--color-accent-strong); }
.wetter-tag-datum { display: block; font-size: 10px; font-weight: 400; color: var(--color-text-faint); font-variant-numeric: tabular-nums; }
.wetter-tag-bild { display: flex; justify-content: center; }
.wetter-band { position: relative; height: 5px; background: var(--color-bg-alt); border-radius: var(--radius-full); }
.wetter-band i {
  position: absolute;
  top: 0;
  height: 5px;
  border-radius: var(--radius-full);
  background: linear-gradient(90deg, #3d7fb8, var(--color-warning));
}
.wetter-grade { font-size: 11.5px; color: var(--color-text-muted); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.wetter-grade b { color: var(--color-text); font-weight: 600; }
.wetter-dreh {
  grid-column: 1 / -1;
  margin-left: 54px;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--color-accent-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Der Standort wird erst auf Klick erfragt, nie beim Laden - deshalb
   dieser Knopf und keine Abfrage beim Seitenaufbau. */
.wetter-standort {
  width: calc(100% - 12px);
  margin: 2px 6px 8px;
  padding: 6px;
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 11.5px;
  cursor: pointer;
}
.wetter-standort:hover { border-color: var(--color-accent); color: var(--color-accent-strong); }
.rail-item { position: relative; }
.rail-btn {
  position: relative;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--sidebar-text);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.rail-btn svg { width: 17px; height: 17px; }
/* Rein Farbwechsel beim Hover, kein Icon-Transform mehr - Angleichung an
   die linke Sidebar (Chat-Verlauf zu .nav-link:hover oben: erst
   "offensichtlicher machen", dann "Die Animation in der Sidebar ist mir zu
   viel, das bitte rausnehmen"). Diese rechte Leiste hatte den damals
   entfernten scale+rotate-Hüpfer noch, dadurch fühlten sich linke und
   rechte Seite unterschiedlich an (Chat-Verlauf: "die rechte Seitenleiste
   [hat] eine andere Hover-Animation an als die linke ... Links ist
   korrekt."). */
.rail-btn:hover:not(:disabled) { background: var(--sidebar-hover); color: var(--sidebar-text-active); }
.rail-btn.has-pin { color: var(--color-accent); }
/* Eingeschaltet - bisher nur der Urlaubsmodus, der am 31.08.2026 aus der
   Fusszeile der linken Seitenleiste hierher gezogen ist ("Bitte verschieb
   den Urlaubs-Modus Button nach rechts unten zum Feedback Button"). Der
   einzige Knopf dieser Leiste mit einem AN-Zustand, deshalb eine Flaeche
   statt nur Farbe: pausierte Digest-Mails soll man im Vorbeigehen sehen,
   nicht erst beim Hinschauen. */
.rail-btn.is-active { color: var(--color-accent); background: var(--color-accent-soft); }
.rail-btn.is-active:hover:not(:disabled) { color: var(--color-accent); background: var(--color-accent-soft); }
.rail-btn.is-disabled,
.rail-btn:disabled { opacity: 0.35; cursor: not-allowed; }

.rail-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: var(--radius-full);
  background: var(--color-danger);
  color: #fff;
  font-size: 9.5px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
/* Deutliches "Aufplustern", sobald ein neuer Zähler-Wert eintrifft (Chat-
   Verlauf: "coole kleine Animationen" -> "offensichtlicher machen") - klar
   größerer Ausschlag (1.35 -> 1.9) + eine kurze Rotation, damit es beim
   Vorbeischauen wirklich auffällt statt nur ein winziges Zucken zu sein.
   Klasse wird per JS gesetzt/wieder entfernt, siehe pulseBadge() in
   rightrail.js, nur wenn sich die Zahl TATSÄCHLICH erhöht hat. */
@keyframes badge-pulse {
  0% { transform: scale(1) rotate(0deg); }
  30% { transform: scale(1.9) rotate(-8deg); }
  55% { transform: scale(0.85) rotate(6deg); }
  80% { transform: scale(1.15) rotate(0deg); }
  100% { transform: scale(1) rotate(0deg); }
}
.rail-badge.is-pulsing { animation: badge-pulse 0.7s var(--ease); }

/* Flyout-Panel öffnet nach LINKS (die Leiste sitzt am rechten Rand) - selbes
   Grundmuster wie .accent-popover, hier aber mit Kopfzeile + Liste statt
   nur einer Reihe Farbkreise. */
.rail-panel {
  position: absolute;
  top: -6px;
  right: calc(100% + 8px);
  display: none;
  width: 300px;
  max-height: 70vh;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 60;
  transform-origin: top right;
  animation: panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.rail-panel.is-open { display: block; }
/* Deutliches "Herauspoppen" statt reinem Fade (Chat-Verlauf: "coole kleine
   Animationen" -> "offensichtlicher machen") - startet klar kleiner/weiter
   oben (0.8 statt 0.96, -16px statt -4px) und schwingt dank Overshoot-
   Easing sichtbar übers Ziel hinaus. Wiederverwendet für alle Flyout-
   artigen Panels/Popover (Benachrichtigungen/Favoriten hier, Akzentfarben-
   Auswahl weiter unten). */
@keyframes panel-in {
  from { opacity: 0; transform: scale(0.8) translateY(-16px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}
.rail-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 14px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--color-border);
}
/* "Alle als gelesen" (Chat-Verlauf: "Kann ich Benachrichtigungen auch
   irgendwie als gelesen markieren?" -> "Beides") - klein und unaufdringlich
   in der Kopfzeile des Panels, keine Großbuchstaben/Sperrung wie der Titel
   daneben, damit es klar als eigener Klick-Bereich erkennbar ist. */
.rail-panel-mark-all {
  background: none;
  border: none;
  color: var(--color-accent);
  font-size: 11px;
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  cursor: pointer;
  padding: 2px 4px;
}
.rail-panel-mark-all:hover { text-decoration: underline; }
.rail-panel-mark-all:disabled { opacity: 0.5; cursor: default; text-decoration: none; }
.rail-panel-body { padding: 6px; }
.rail-panel-empty {
  padding: 20px 14px;
  text-align: center;
  color: var(--color-text-faint);
  font-size: 12.5px;
}
.rail-panel-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.rail-panel-row:hover { background: var(--color-bg-alt); }
.rail-panel-row-title {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-panel-row-sub { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 1px; }
.rail-panel-row.is-overdue .rail-panel-row-sub { color: var(--color-danger); }

.rail-panel-pin-current {
  display: flex;
  align-items: center;
  gap: 8px;
  width: calc(100% - 12px);
  margin: 6px 6px 0;
  padding: 9px 10px;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.rail-panel-pin-current svg { width: 14px; height: 14px; }
.rail-panel-pin-current:hover { background: var(--color-accent-soft); color: var(--color-accent-strong); border-color: transparent; }
.rail-panel-pin-current.is-pinned { color: var(--color-accent-strong); background: var(--color-accent-soft); border-style: solid; border-color: transparent; }

.rail-fav-row { padding-right: 6px; }
.rail-fav-row .rail-panel-row-title { flex: 1; min-width: 0; }
/* Titel+Untertitel der Benachrichtigungen-Zeilen (siehe rightrail.js) - im
   Gegensatz zu den Favoriten (nur ein Titel) brauchen die einen
   gemeinsamen Wrapper, der als EIN Flex-Kind neben dem Ausblenden-Button
   wächst, statt dass Titel/Untertitel als zwei separate Flex-Items
   nebeneinander statt übereinander landen würden. */
.rail-panel-row-text { flex: 1; min-width: 0; }
.rail-fav-remove {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 50%;
  color: var(--color-text-faint);
}
.rail-fav-remove svg { width: 10px; height: 10px; }
.rail-fav-remove:hover { background: var(--color-danger-soft); color: var(--color-danger); }

.topbar {
  display: none;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  /* iPhone-Mobile-UX-Audit 2026-08-16 (docs/ux-audit-2026-08-16-iphone-mobile.md,
     2.1): .topbar ist sticky/top:0 und damit die erste Zeile auf JEDER
     Mobile-Seite, hatte aber nirgends einen Ausgleich für die Dynamic
     Island (safe-area-inset-top, auf iPhone 16/17 Pro ~59px) - env() war in
     der gesamten Datei bisher nur einmal für safe-area-inset-BOTTOM im Chat
     im Einsatz. max() fällt auf Geräten ohne Notch/Dynamic Island (oder
     Browsern ohne env()-Support) einfach auf das normale Padding zurück. */
  padding-top: max(var(--space-3), env(safe-area-inset-top));
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  position: sticky;
  top: 0;
  z-index: 40;
}
.topbar-toggle {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text);
}
.topbar-toggle svg { width: 18px; height: 18px; }
.sidebar-backdrop { display: none; }

/* Produktions-Dashboard-Tab-Leiste (Chat-Verlauf: "per Tabs oben auf dem
   Screen dauerhaft einblenden") - siehe components/productiontabbar.js.
   .is-empty blendet die ganze Leiste weg, solange kein Tab offen ist, statt
   dauerhaft eine leere Zeile Platz wegzunehmen. */
.production-tab-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  overflow-x: auto;
  scrollbar-width: none;
}
.production-tab-bar::-webkit-scrollbar { display: none; }
.production-tab-bar.is-empty { display: none; }
.production-tab {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
  padding: 6px 8px 6px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text-muted);
  font-size: 0.82rem;
  line-height: 1.2;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.production-tab:hover { color: var(--color-text); }
/* Drag&Drop-Umsortierung (Chat-Verlauf: "Die Tabs oben möchte ich auch
   umsortieren können per Drag'n'Drop") - gleiche is-dragging/is-dragover-
   Zustände wie überall sonst per makeSortable() (siehe sortable.js). */
.production-tab.is-dragging { opacity: 0.55; }
.production-tab.is-dragover { background: var(--color-accent-soft); border-color: var(--color-accent); }
.production-tab.is-active {
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-bg));
  border-color: var(--color-accent);
  color: var(--color-text);
}
/* Health-Ampel-Punkt (Chat-Verlauf: "Bau mir doch mal noch ein paar coole
   Features ein [...] überrasch mich" -> siehe computeTabHealth() in
   api/productiondashboardui.php). Grün wird bewusst nicht extra betont
   (kein Punkt-Halo), nur amber/rot sollen ins Auge springen. */
.production-tab-health {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--color-success);
}
.production-tab-health.is-amber { background: var(--color-warning); }
.production-tab-health.is-red { background: var(--color-danger); box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-danger) 25%, transparent); }
/* 260 statt 220: seit dem 03.09. stehen drei Teile im Tab statt zwei. */
.production-tab-label { display: flex; align-items: baseline; gap: 6px; max-width: 260px; overflow: hidden; }
.production-tab-number { font-weight: 600; flex-shrink: 0; }
/* Das Kuerzel schrumpft NICHT mit (03.09.2026): bei einem schmalen Tab soll
   als Erstes der Titel weichen, nicht die Kennung, an der man den Tab
   wiedererkennt. Etwas gedaempfter als die Nummer, etwas kraeftiger als der
   Titel - in dieser Reihenfolge liest man sie auch. */
.production-tab-client { flex-shrink: 0; opacity: 0.9; }
/* Nur rechts Abstand: links sorgt schon der gap:6px des Labels dafuer.
   Beides zusammen sah nach zwei Leerzeichen aus. */
.production-tab-client::before,
.production-tab-title::before { content: "·"; margin-right: 5px; opacity: 0.45; }
.production-tab-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0.8; min-width: 0; }
/* Wunsch: "könnte man [...] den Namen im aktiven Tab vollständig zeigen und
   nur in den anderen abgeschnitten?" - die Begrenzung (max-width, siehe oben
   bzw. die 140px im @media weiter unten) gilt nur für inaktive Tabs; der
   aktive Tab darf sich auf seine volle Textbreite ausdehnen. Die Leiste
   selbst scrollt bei Bedarf horizontal (siehe .production-tab-bar oben), ein
   langer aktiver Titel schiebt also höchstens den sichtbaren Ausschnitt,
   bricht aber nichts um. */
.production-tab.is-active .production-tab-label { max-width: none; }
.production-tab-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  color: inherit;
  opacity: 0.6;
}
.production-tab-close:hover { opacity: 1; background: var(--color-border); }
.production-tab-close svg { width: 12px; height: 12px; }

/* Internes Produktions-Dashboard (Chat-Verlauf: "Auf dem Dashboard möchte
   ich absolut ALLES sehen [...] Kann auch zweispaltig sein [...] frei
   umplatzierbar") - siehe pages/productiondashboard.js. */
.pd-loading { padding: var(--space-6); color: var(--color-text-muted); }
.pd-header { position: relative; }
/* Ring links neben Titel+Übersicht statt zentriert darunter (Bug-Report
   per Screenshot: "Der Kreis sollte links neben Titel und Übersichts-Box
   sitzen", Layout-Option A bestätigt) - .pd-header-main ist die neue
   Flex-Zeile aus Ring + rechter Spalte, siehe render() in
   productiondashboard.js. */
.pd-header-main { display: flex; align-items: center; gap: var(--space-6); }
.pd-header-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.pd-header-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.pd-header-top h1 { margin: 0; }
.pd-header-title { min-width: 0; flex: 1; }

.pd-header-mobile-wrap { display: none; }

.pd-header-mobile-bar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; }
.pd-health-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--color-success);
  flex-shrink: 0;
}
.pd-health-dot.is-amber { background: var(--color-warning); }
.pd-health-dot.is-red { background: var(--color-danger); }
.pd-header-mobile-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pd-header-mobile-progress {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 700;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
}
.pd-header-mobile-toggle {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  cursor: pointer;
}
.pd-header-mobile-toggle svg { transition: transform var(--dur-fast) var(--ease); }
.pd-header-mobile-toggle.is-expanded svg { transform: rotate(180deg); }
/* Eingeklappt per Default (Bug-Report: "Übersicht komplett weg, ggfs. nur
   per Button einblendbar") - max-height statt display:none für die
   Klapp-Animation, Inhalt bleibt dabei immer im DOM (kein Re-Render beim
   Öffnen nötig). summarySection() blendet leere Felder inzwischen ohnehin
   aus (Bug-Report: "Übersicht auch sehr viel kompakter machen, leere
   Felder ausblenden"), macht auch die aufgeklappte Ansicht kompakter. */
.pd-mobile-overview { max-height: 0; overflow: hidden; padding: 0 12px; transition: max-height var(--dur-slow) var(--ease); }
.pd-mobile-overview.is-open { max-height: 60vh; overflow-y: auto; padding: 10px 12px 12px; border-top: 1px solid var(--color-border); }
.pd-mobile-overview .expand-panel-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
.pd-mobile-overview .expand-panel-field-label { font-size: 10px; }
.pd-mobile-overview .expand-panel-field-value { font-size: 12.5px; }

/* Hero-Hintergrund aus dem neuesten Preview-Thumbnail (Chat-Verlauf:
   "Hero-Header [...] wie bei Spotify/Apple Music") - großflächig, stark
   verschwommen + abgedunkelt, damit Titel/Badges immer lesbar bleiben.
   --pd-hero-image wird inline vom JS gesetzt (siehe render() in
   productiondashboard.js), ::before statt direktem background, damit der
   blur() nicht auf die Kind-Elemente durchschlägt. */
/* Bug-Report: "Ist einfach optisch nicht schön. Kreis sitzt vertikal nicht
   zentriert, alles optisch nicht ausgewogen." - Ursache war eine
   asymmetrische Innenabstand (oben var(--space-6), unten 0), wodurch die
   Übersicht-Karte unten bündig an der Kartenkante klebte, während oben
   spürbar Luft war - dadurch wirkte die ganze Zeile (inkl. Ring) nach oben
   verschoben, obwohl der Ring selbst korrekt vertikal zentriert war.
   Symmetrischer Abstand oben/unten schafft die optische Balance. */
.pd-header.has-hero {
  border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-5);
  margin: calc(var(--space-4) * -1) calc(var(--space-4) * -1) 0;
  overflow: hidden;
}
.pd-header.has-hero::before {
  content: '';
  position: absolute;
  inset: -20px;
  background-image: var(--pd-hero-image);
  background-size: cover;
  background-position: center;
  filter: blur(28px) brightness(0.45) saturate(1.2);
  transform: scale(1.15);
  z-index: 0;
}
.pd-header.has-hero > * { position: relative; z-index: 1; }
.pd-header.has-hero .pd-header-top h1 { color: #fff; }
.pd-header.has-hero .pd-health-badge {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.28);
  color: #fff;
  backdrop-filter: blur(6px);
}
.pd-header.has-hero .pd-ring-track { stroke: rgba(255, 255, 255, 0.25); }
.pd-header.has-hero .pd-ring-stage-label { color: rgba(255, 255, 255, 0.82); }

/* Header-Bild als Dropzone (Bug-Report: "Kannst du das Header-Bild im
   Produktions-Dashboard als Dropzone einrichten [...]?", dann "ist jetzt
   durch das Image-Icon [...] größer geworden [...] kein Icon, nur
   Dropzone.") - rein Drag&Drop übers ganze Hero, kein Icon/Button mehr.
   ".pd-header.has-hero .pd-hero-drop-hint" statt nur ".pd-hero-drop-hint",
   damit diese Regel spezifischer ist als ".pd-header.has-hero > *" oben
   (das setzt sonst per Kaskade "position:relative" auf jedes direkte
   Kind - genau das hatte den Ablegen-Hinweis/die frühere Icon-Leiste aus
   der Überlagerung in den normalen Textfluss gekippt und den Header dadurch
   sichtbar höher gemacht). */
.pd-header.has-hero .pd-hero-drop-hint {
  position: absolute;
  inset: var(--space-3);
  z-index: 3;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 2px dashed rgba(255, 255, 255, 0.7);
  border-radius: var(--radius-md);
  background: rgba(0, 0, 0, 0.35);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  pointer-events: none;
}
.pd-hero-drop-hint svg { width: 22px; height: 22px; }
.pd-header.has-hero.is-hero-dragover .pd-hero-drop-hint { display: flex; }
.pd-header.has-hero.is-hero-uploading { opacity: 0.7; pointer-events: none; }

/* Fortschritts-Ring (Chat-Verlauf: "wie die Activity Rings der Apple
   Watch", dann "deutlich (!!) größer [...] wirklich plakativ", dann nach
   Live-Check zu groß -> "deutlich kleiner. Ungefähr die Höhe der
   Übersicht-Box") - siehe progressRing() in productiondashboard.js.
   Skalierungsunabhängiges SVG (viewBox 0 0 100 100, kein festes
   width/height im Markup) - die tatsächliche Größe kommt komplett von hier
   über clamp(). */
/* Ring-Mitte per CSS-Grid-Stack (grid-area: 1 / 1 auf SVG + Center-Block) -
   bitgenau zentriert unabhängig von Border/Padding/Sub-Pixel-Rundung.
   Chat-Verlauf: "Zurück zum Text, kein Icon. Statt den Steps oben, eine
   Prozentzahl [...] Prozentzahl oben, Text unten - beides zentriert in der
   Mitte" - .pd-ring-center liegt wieder INNEN im Ring (wie vor dem
   Icon-Zwischenstand), Prozent statt der alten Schritt-Anzeige ("X/7"). */
.pd-ring-wrap {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: clamp(130px, 12.5vw, 180px);
  height: clamp(130px, 12.5vw, 180px);
  margin: var(--space-3) 0;
  opacity: 0;
  transform: scale(0.72);
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.pd-ring-wrap.is-in { opacity: 1; transform: scale(1); }
.pd-ring-svg { grid-area: 1 / 1; width: 100%; height: 100%; }
.pd-ring-svg svg { width: 100%; height: 100%; display: block; }
.pd-ring-track { stroke: var(--color-border); }
.pd-ring-fill {
  /* stroke bewusst NICHT hier gesetzt - kommt als url(#pdRingGrad) direkt
     vom SVG-Attribut in progressRing(), CSS würde das sonst überschreiben. */
  stroke-linecap: round;
  transition: stroke-dashoffset 0.9s cubic-bezier(0.16, 1, 0.3, 1);
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}
.pd-ring-center {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  max-width: 68%;
}
.pd-ring-percent { font-size: clamp(1.3rem, 2.4vw, 1.7rem); font-weight: 800; line-height: 1; color: var(--color-text); }
.pd-ring-stage { font-size: clamp(0.7rem, 1vw, 0.8rem); font-weight: 600; color: var(--color-text-muted); line-height: 1.25; text-align: center; }
.pd-header.has-hero .pd-ring-percent { color: #fff; }
.pd-header.has-hero .pd-ring-stage { color: rgba(255, 255, 255, 0.82); }
/* Bug-Report: "Im Status-Kreis oben gerne auch die Möglichkeit haben, den
   Status zu verändern - es reicht ein Plus und ein Minus [...] Evtl. mit
   reduzierter Deckkraft oberhalb bzw. unterhalb des Text im Center?" - genau
   so: schmale, deckkraft-gedimmte +/- Zeilen direkt über/unter Prozent+Stage
   (siehe progressRing() in productiondashboard.js), volle Deckkraft erst bei
   Hover/Focus, damit der Ring im Ruhezustand weiter ruhig/plakativ bleibt. */
.pd-ring-step {
  border: none;
  background: none;
  color: var(--color-text-muted);
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  padding: 2px 10px;
  cursor: pointer;
  opacity: 0.35;
  border-radius: var(--radius-sm);
  transition: opacity var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pd-ring-step:hover:not(:disabled),
.pd-ring-step:focus-visible:not(:disabled) {
  opacity: 1;
  background: var(--color-bg-alt);
  color: var(--color-text);
}
.pd-ring-step:disabled { opacity: 0; cursor: default; pointer-events: none; }
.pd-header.has-hero .pd-ring-step { color: rgba(255, 255, 255, 0.75); }
.pd-header.has-hero .pd-ring-step:hover:not(:disabled),
.pd-header.has-hero .pd-ring-step:focus-visible:not(:disabled) {
  background: rgba(255, 255, 255, 0.16);
  color: #fff;
}
/* Bug-Report: "Header ist jetzt extrem groß [...] Ring, Produktionstitel
   und Übersicht insgesamt maximal 50% der Höhe" - Ring wird auf Mobile zum
   kleinen, festen Icon-Ring statt weiterhin mit 40vw mitzuwachsen (auf
   einem 390px-Screen vorher bis zu 156px!), sitzt dafür (siehe
   .pd-header-main oben) neben statt über dem Titel. Stage-Label passt bei
   dieser Größe nicht mehr lesbar hinein und entfällt auf Mobile (die
   Phase steht ohnehin in der Übersicht-Karte als "STATUS"-Feld).
   Die vorherigen Mobile-Verkleinerungswerte hier sind überholt - der
   gesamte Desktop-Header inkl. Ring ist auf Mobile jetzt komplett
   ausgeblendet (siehe .pd-header-mobile-wrap weiter oben), Ring-Mobile-
   Anpassungen greifen dadurch nie mehr. */

/* Live-Präsenz-Cluster (Chat-Verlauf: "wer von euch gerade dieselbe
   Produktion offen hat [...] wie bei Figma/Google Docs"). */
.pd-presence-cluster { display: flex; align-items: center; flex-shrink: 0; }
.pd-presence-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  color: #fff;
  border: 2px solid var(--color-surface-2);
  margin-left: -8px;
}
.pd-presence-cluster .pd-presence-avatar:first-child { margin-left: 0; }
.pd-header.has-hero .pd-presence-avatar { border-color: rgba(255, 255, 255, 0.5); }

/* Erfolgs-Animation bei Abgerechnet/Konsolidiert (Chat-Verlauf: "kurzer
   Konfetti-/Glow-Effekt [...] kleiner Belohnungsmoment") - siehe
   triggerCelebration() in productiondashboard.js, rein clientseitig/lokal. */
.pd-celebrate { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 5; }
.pd-confetti {
  position: absolute;
  top: -10px;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  opacity: 0.9;
  animation: pd-confetti-fall 1.6s ease-in forwards;
}
@keyframes pd-confetti-fall {
  to { transform: translateY(220px) rotate(280deg); opacity: 0; }
}

/* Health-Badge im Dashboard-Kopf (siehe computeHealth() in
   productiondashboard.js, gleiche Regel wie computeTabHealth() im Backend -
   Chat-Verlauf: "Bau mir doch mal noch ein paar coole Features ein"). */
.pd-health-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text-muted);
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  flex-shrink: 0;
}
.pd-health-badge .pd-health-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--color-success); flex-shrink: 0; }
.pd-health-badge.is-amber { color: var(--color-warning); border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border)); }
.pd-health-badge.is-amber .pd-health-dot { background: var(--color-warning); }
.pd-health-badge.is-red { color: var(--color-danger); border-color: color-mix(in srgb, var(--color-danger) 40%, var(--color-border)); }
.pd-health-badge.is-red .pd-health-dot { background: var(--color-danger); }

/* Kunden-Dashboard-Link im Header statt eigener Widget-Karte (Bug-Report:
   "Kunden-Dashboard-Link und Produktions-Dashbaord haben aktuell sehr
   ähnliche Icons [...] Wollen wir das Kunden-Dashboard [...] im Header-
   Bereich [...] oben rechts neben der Ampel?") - kompakter Icon-Button,
   gleiche runde Optik wie .pd-health-badge daneben. Kleiner Punkt oben
   rechts zeigt "Link aktiv", ohne dass man erst klicken muss. */
.pd-client-link-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  cursor: pointer;
  flex-shrink: 0;
  padding: 0;
}
.pd-client-link-btn svg { width: 15px; height: 15px; }
.pd-client-link-btn:hover { color: var(--color-text); border-color: var(--color-border-strong); }
.pd-client-link-btn.is-active { color: var(--color-accent); }
.pd-client-link-dot {
  position: absolute;
  top: 0;
  right: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-success);
  border: 2px solid var(--color-bg-alt);
}
.pd-header.has-hero .pd-client-link-btn {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.28);
  color: #fff;
  backdrop-filter: blur(6px);
}
.pd-header.has-hero .pd-client-link-btn.is-active { color: #fff; }
.pd-header.has-hero .pd-client-link-dot { border-color: rgba(255, 255, 255, 0.9); }
/* Mobile Kopf-Leiste (siehe .pd-header-mobile-toggle daneben) - gleiche
   28px-Größe, damit die Reihe optisch ruhig bleibt. */
.pd-client-link-btn.is-mobile {
  width: 28px;
  height: 28px;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
}
.pd-client-link-btn.is-mobile.is-active { color: var(--color-accent); }
.pd-client-link-btn.is-mobile .pd-client-link-dot { border-color: var(--color-surface-2); }
/* Zeiterfassung Start/Stop im Header (Chat-Verlauf: "Mach den Timer im
   Header bitte neben den Client-Dashboard-Link, im selben Stil (nur als
   Play-Button, Stop-Button)") - exakt dieselbe 30px-Kreis-Optik wie
   .pd-client-link-btn, .pd-timer-btn nur für die zusätzlichen
   Zustände (läuft hier / läuft woanders). */
/* Bug-Report: "Wenn ich die Zeiterfassung oben im Header starte, sollte der
   Button insgesamt grün werden, damit man erkennt, dass die Uhr läuft." -
   vorher nur Icon/Rand eingefärbt (kaum auffällig), jetzt komplett gefüllt
   (Hintergrund + Rand + weißes Icon), gleiche Erfolgsfarbe wie sonst im
   Projekt für "aktiv/läuft" (siehe z.B. Health-Ampel/Erfolgs-Animation). */
.pd-timer-btn.is-running { background: var(--color-success); border-color: var(--color-success); color: #fff; }
.pd-timer-btn.is-running:hover { background: var(--color-success); border-color: var(--color-success); color: #fff; }
.pd-timer-btn.is-disabled { opacity: 0.4; cursor: not-allowed; }
.pd-timer-btn.is-disabled:hover { color: var(--color-text-muted); border-color: var(--color-border); }
.pd-header.has-hero .pd-timer-btn.is-running { background: var(--color-success); border-color: var(--color-success); color: #fff; }
.pd-header.has-hero .pd-timer-btn.is-disabled:hover { color: #fff; }
/* Inline Start/Stop-Leiste in der Zeiterfassungs-Kachel (Chat-Verlauf:
   "einmal unten die Kacheln sinnvoll ausbauen, dort direkt mit
   Eingabefeld") - sitzt oberhalb von timeSection()'s bestehender Liste. */
.pd-timer-inline { margin-bottom: var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--color-border); }
.pd-timer-inline-row { display: flex; align-items: center; gap: 8px; }
.pd-timer-inline-input { flex: 1; min-width: 0; }
.pd-timer-inline-billable { display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--color-text-muted); white-space: nowrap; flex-shrink: 0; }
.pd-timer-inline-note { font-size: 12px; margin-bottom: 6px; }
/* Kategorie-Sektionen (Chat-Verlauf: "Wie könnte man das Dashboard visuell
   noch klarer strukturieren? [...] Mach bitte alles") - jede Sektion trägt
   ihre eigene .pd-grid, siehe render() in productiondashboard.js. */
.pd-category { margin-bottom: var(--space-6); }
.pd-category:last-child { margin-bottom: 0; }
.pd-category-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: var(--space-3);
  font-size: 13px;
  font-weight: 700;
}
.pd-category-header svg { width: 16px; height: 16px; }
.pd-category.cat-creative .pd-category-header { color: var(--color-accent); }
.pd-category.cat-team .pd-category-header { color: var(--color-success); }
.pd-category.cat-admin .pd-category-header { color: var(--color-warning); }

/* Kritische-Punkte-Leiste (Chat-Verlauf: "Kritische Punkte direkt unter
   dem Kopf [...] ohne Scrollen sofort sehen, was dringend ist") - siehe
   criticalPointsBar() in productiondashboard.js, erscheint nur bei
   health !== 'green'. */
.pd-critical-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-weight: 600;
  margin-bottom: var(--space-5);
}
.pd-critical-bar svg { width: 15px; height: 15px; flex-shrink: 0; }
.pd-critical-bar.is-red { background: var(--color-danger-soft); color: var(--color-danger); }
.pd-critical-bar.is-amber { background: var(--color-warning-soft); color: var(--color-warning); }

.pd-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  align-items: start;
}
/* Bug-Report: "Viele Kacheln sind breiter als die Bildschirmbreite [...]
   Kreativ & Freigaben sind zu breit" - .pd-column ist als Grid-Item von
   .pd-grid ohne min-width:0 defaultmäßig NIE schmaler als der breiteste
   Inhalt seines Teilbaums (CSS-Spec: automatische Mindestgröße von Grid-
   Items ist content-based, nicht 0) - lange, nicht umbrechende Texte tiefer
   im Baum (z.B. Datei-/Asset-Namen in .expand-documents-name, nur in den
   Kreativ-Widgets Assets/Previews/Board verwendet) können dadurch die
   ganze Spalte (und damit die Kachel) über die Bildschirmbreite hinaus
   aufblähen, statt zu ellipsieren wie vorgesehen. min-width:0 schaltet
   diese content-based Mindestgröße explizit ab. */
.pd-column { display: flex; flex-direction: column; gap: var(--space-4); min-height: 40px; min-width: 0; }
.pd-column.is-dragover-empty { outline: 2px dashed var(--color-border); outline-offset: -2px; border-radius: var(--radius-md); }

.pd-widget {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.pd-widget.is-dragging { opacity: 0.5; }
.pd-widget.is-dragover { outline: 2px dashed var(--color-accent); outline-offset: -2px; }
/* Farbliche Kategorie-Codierung (Chat-Verlauf: "Wie könnte man das
   Dashboard visuell noch klarer strukturieren? Ggfs. auch farblich
   unterstützt?" -> "Farbliche Gruppierung super so.") - schmaler
   Akzentstreifen links, per inset-box-shadow statt border-left, damit die
   abgerundeten Ecken der Karte (overflow:hidden oben) nicht eckig
   durchbrechen. Kategorie kommt automatisch aus WIDGET_CATEGORY_MAP über
   widgetCard(), siehe productiondashboard.js. */
.pd-widget.cat-creative { box-shadow: inset 3px 0 0 0 var(--color-accent); }
.pd-widget.cat-team { box-shadow: inset 3px 0 0 0 var(--color-success); }
.pd-widget.cat-admin { box-shadow: inset 3px 0 0 0 var(--color-warning); }
/* Wichtiges hervorheben (Chat-Verlauf: "Karten mit akutem Handlungsbedarf
   [...] auffälligerer Rahmen/Badge in Ampelfarbe") - überschreibt den
   Kategorie-Streifen oben bewusst NICHT, sondern ergänzt einen zusätzlichen
   Rahmen/Glow in der jeweiligen Ampelfarbe. */
.pd-widget.flag-danger { border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border)); box-shadow: inset 3px 0 0 0 var(--color-danger), 0 0 0 1px color-mix(in srgb, var(--color-danger) 20%, transparent); }
.pd-widget.flag-warning { border-color: color-mix(in srgb, var(--color-warning) 45%, var(--color-border)); box-shadow: inset 3px 0 0 0 var(--color-warning), 0 0 0 1px color-mix(in srgb, var(--color-warning) 20%, transparent); }
.pd-widget-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  /* siehe .pd-widget-grip weiter unten - selbe Begründung, falls der
     Mousedown minimal neben dem Griff-Icon (aber noch im Header) startet. */
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
}
.pd-widget-grip {
  display: flex;
  color: var(--color-text-faint);
  cursor: grab;
  flex-shrink: 0;
  /* Bug-Report: "Die Handles sind schon da, passieren tut aber nichts" -
     ohne user-select:none gewinnt in JEDEM Browser (Safari, Chrome - siehe
     Chat) die native Textselektion das Wettrennen gegen SortableJS' eigene
     Drag-Erkennung, sobald der Mousedown/Pointerdown auf dem Griff beginnt
     und sich minimal bewegt, BEVOR SortableJS' JS-Handler überhaupt greifen
     - sichtbar am blau markierten Text + Textcursor. user-select:none ist
     eine synchrone CSS-Garantie (kein Race-Condition-Risiko wie bei JS-
     preventDefault) und muss auf dem Griff selbst UND dem umgebenden Header
     sitzen, da der Header ebenfalls Text enthält, der sonst mitselektiert
     wird, wenn der Mousedown minimal daneben startet.
     Der zuvor gesetzte forceFallback:true bleibt zusätzlich bestehen
     (bessere Drag-Emulation ggü. verschachtelten Datei-Dropzones), war für
     sich allein aber NICHT die Ursache dieses Symptoms. */
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  touch-action: none;
}
.pd-widget-grip svg { width: 14px; height: 14px; }
/* Bug-Report: "einzelne Fenster [...] verschieben, ausblenden [...] Nur
   Desktop, Mobile ist fix." - Ausblenden-Button + Chip-Leiste, gleiches
   Look&Feel wie .dashboard-tile-hide-btn/.dashboard-hidden-chip auf der
   Startseite (siehe dort), hier aber ohne separaten Anpassen-Modus, da das
   Ausblenden selbst schon die einzige Anpassungshandlung ist. */
.pd-widget-hide-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-left: auto;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  flex-shrink: 0;
}
.pd-widget-hide-btn:hover { background: var(--color-danger-soft, #fde8e8); color: var(--color-danger); }
.pd-widget-hide-btn svg { width: 13px; height: 13px; }
.pd-hidden-tray {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 var(--space-4);
  font-size: 12.5px;
}
.pd-hidden-tray-label { color: var(--color-text-muted); }
.pd-hidden-chip {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px 4px 10px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius-full);
  font-size: 12px;
  cursor: pointer;
}
.pd-hidden-chip:hover { border-color: var(--color-accent); color: var(--color-accent); }
.pd-hidden-chip svg { width: 12px; height: 12px; }

.pd-widget-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  flex: 1;
  min-width: 0;
}
.pd-widget-title svg { width: 14px; height: 14px; color: var(--color-accent); flex-shrink: 0; }
.pd-widget-header-extra { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.pd-widget-body { padding: var(--space-4); min-width: 0; }
/* pd-widget-body enthält oft direkt eine wiederverwendete .expand-panel-
   section (siehe summarySection()/approvalsSection() in productions.js) -
   deren eigener Kopf/Innenabstand würde sonst doppelt wirken. */
.pd-widget-body > .expand-panel-section { background: none; border: none; padding: 0; }
.pd-widget-body > .expand-panel-section > h3 { display: none; }

.pd-simple-list { display: flex; flex-direction: column; gap: 2px; }
/* Bug-Reports: "Bei Assets auch maximal 5 Einträge, dann Scrollen" +
   "File-Transfers auch. Aktuell sind die Boxen oft noch zu riesig bei
   vielen Einträgen." - beide Widgets nutzen unterschiedliche Listen-
   Bausteine (Assets: das geteilte expand-documents-list aus productions.js,
   File-Transfers: das lokale pd-simple-list), deshalb hier gezielt per
   data-widget gescoped statt die Bausteine app-weit zu begrenzen - die
   gleichen Klassen werden auch außerhalb des Produktions-Dashboards (z.B.
   im aufgeklappten Produktions-Panel) ungekappt verwendet. Höhe auf ca. 5
   Zeilen der jeweiligen Zeilenhöhe kalibriert.
   */
.pd-widget[data-widget="assets"] .expand-documents-list { max-height: 260px; overflow-y: auto; }
.pd-widget[data-widget="filetransfer"] .pd-simple-list { max-height: 260px; overflow-y: auto; }
.pd-simple-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 6px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.pd-simple-row:hover { background: var(--color-bg-alt); }
.pd-simple-row svg { width: 16px; height: 16px; color: var(--color-text-faint); flex-shrink: 0; }
.pd-simple-thumb { width: 36px; height: 24px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
/* Wunsch: "Bei Previews und Masters, die kein Thumbnail haben, dasselbe
   'fehlendes Thumbnail'-Symbol wie in Previews & Masters selbst, auf das
   man klicken kann" - previewsWidget() in productiondashboard.js setzt bei
   Video-Dateien ohne Thumbnail hier ein button.pd-simple-thumb.pm-file-
   thumb-empty (Basis-Optik von .pm-file-thumb-empty aus previewsmasters.js
   weiter unten, siehe dort: gestrichelter Rahmen, zentriertes Icon, Hover-
   Akzentfarbe). .pm-file-thumb-empty allein bringt aber weder Breite/Höhe
   noch Rahmenbreite/-farbe/Hintergrund mit (dort normalerweise mit
   .pm-file-thumb kombiniert, das 40x23 nutzt) - hier stattdessen an die
   36x24-Größe der übrigen .pd-simple-thumb-Zeilen dieser Kachel angeglichen,
   damit nichts in der Liste springt. */
.pd-simple-thumb.pm-file-thumb-empty { width: 36px; height: 24px; border: 1px dashed var(--color-border); background: var(--color-bg-alt); cursor: pointer; }
.pd-simple-row-title { font-size: var(--text-body); color: var(--color-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-simple-row-sub { font-size: 12px; color: var(--color-text-faint); }
/* Bug-Report: "kannst du vorhandene Masters bitte klarer markieren?" -
   gleiches Muster wie .dash-master-badge/.is-master auf dem Kunden-
   Dashboard (clientdashboard.php), hier mit den Theme-Variablen statt
   fest verdrahteten Hex-Werten. */
.pd-simple-row.is-master { background: var(--color-warning-soft); }
.pd-simple-row.is-master:hover { background: color-mix(in srgb, var(--color-warning-soft) 70%, var(--color-bg-alt)); }
/* Bug-Report: "Mails auch löschbar machen, kleines x rechts an die Seite
   bitte" - ursprünglich nur bei Hover der Zeile sichtbar; Chat-Verlauf:
   "Das Delete-Icon erscheint auch nur beim hovern, das ergibt keinen Sinn.
   Das sollte immer zu sehen sein." - jetzt dauerhaft mit gedämpfter
   Deckkraft sichtbar, bei Hover/Fokus der Zeile bzw. des Icons selbst voll
   sichtbar. Gilt für alle .pd-simple-row-delete-Vorkommen (Mails, Assets/
   Dokumente/Previews & Masters im Produktions-Dashboard, Voice-Notizen). */
.pd-simple-row-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border: none;
  background: transparent;
  border-radius: 50%;
  color: var(--color-text-faint);
  cursor: pointer;
  opacity: 0.55;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.pd-simple-row-delete svg { width: 13px; height: 13px; }
.pd-simple-row:hover .pd-simple-row-delete,
.pd-simple-row-delete:hover,
.pd-simple-row-delete:focus-visible { opacity: 1; }
.pd-simple-row-delete:hover { color: var(--color-danger); background: var(--color-danger-soft); }
/* R2-Speicher & Kosten pro Produktion (Chat-Verlauf: "wieviel Speicher (R2)
   die Produktion gerade insgesamt verbraucht und welche Kosten das bisher
   verursacht hat") - dritter Reiter in "Verwaltung: Sonstiges", zwei simple
   Kennzahlen-Blöcke statt einer Liste. */
.pd-storage-pane { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-2) 4px; }
.pd-storage-stat-value { font-size: 22px; font-weight: var(--weight-medium); color: var(--color-text); }
.pd-storage-stat-label { font-size: 12px; color: var(--color-text-faint); margin-top: 2px; }
.pd-master-badge {
  display: inline-flex;
  align-items: center;
  background: var(--color-warning);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  margin-right: 7px;
  vertical-align: middle;
  position: relative;
  top: -1px;
}

/* Produktions-Chat-Widget (Chat-Verlauf: "Können wir außerdem im
   Produktions-Dashboard noch ein kleines Chatfenster integrieren, mit
   einem Produktionsbezogenen Chat mit Team-Mitgliedern?") - siehe
   chatWidget() in productiondashboard.js. Bewusst schlanker als das große
   Chat-Modul (chat.js: .chat-message-bubble etc.) - kein Avatar/Sender-Hue,
   nur Sender-Name als kleines Label über eingehenden Bubbles, gleiche
   Akzentfarbe/Bubble-Logik wie .chat-message.is-mine für Wiedererkennung. */
.pd-chat-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 220px;
  overflow-y: auto;
  /* Gleiche Begruendung wie bei .chat-messages (siehe dort): auch dieses
     Fenster steht beim Oeffnen am Ende, und das Produktions-Dashboard
     darunter ist eine lange, scrollende Seite - ohne contain rutscht die
     ganze Seite weg, sobald man im Verlauf einmal gegen den Anschlag
     wischt. */
  overscroll-behavior: contain;
  padding: 2px 2px 6px;
}
.pd-chat-msg {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  max-width: 85%;
  align-self: flex-start;
}
.pd-chat-msg.is-mine { align-items: flex-end; align-self: flex-end; }
/* Bug-Report: "bei den Nachrichten Urheber und Uhrzeit anzeigen" - Meta-Zeile
   über der Sprechblase, Sender links/Uhrzeit rechts nebeneinander (bei
   eigenen Nachrichten fällt der Sender-Name weg, siehe renderBody() -
   .pd-chat-msg-meta zeigt dann nur die Uhrzeit an). */
.pd-chat-msg-meta {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0 2px 2px;
}
.pd-chat-msg-sender {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--color-text-faint);
}
.pd-chat-msg-time {
  font-size: 10px;
  color: var(--color-text-faint);
  opacity: 0.75;
}
.pd-chat-msg-body {
  background: var(--color-bg-alt);
  color: var(--color-text);
  border-radius: var(--radius-md);
  padding: 6px 10px;
  font-size: 12.5px;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
}
.pd-chat-msg.is-mine .pd-chat-msg-body { background: var(--color-accent); color: #fff; }
/* Bug-Report: "starke Zeitverzögerung [...] das hatten wir im Haupt-Chat
   schon mal gefixt" - dezent gedimmte Sprechblase für eine gerade optimistisch
   angehängte, noch nicht vom Server bestätigte eigene Nachricht (siehe
   submit() in productiondashboard.js), analog zu .chat-message-bubble.is-
   pending im Haupt-Chat. */
.pd-chat-msg.is-pending .pd-chat-msg-body { opacity: 0.6; }
.pd-chat-composer { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
.pd-chat-input {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  padding: 6px 12px;
  font-size: 12.5px;
  background: var(--color-surface);
  color: var(--color-text);
}
.pd-chat-input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--shadow-glow); }

/* Kanal-Tabs (Chat-Verlauf: "Wie lösen wir das, wenn es mehrere
   Produktions-Chats gibt - einen z.B. im gesamten Team und einen nur unter
   Admins?") - erscheinen nur, wenn mehr als ein Kanal verfügbar ist (siehe
   channelTabs() in productiondashboard.js), sonst unsichtbar. */
.pd-chat-channel-tabs {
  display: flex;
  gap: 4px;
  padding: 12px var(--space-4) 8px;
}
/* Zusammengefuehrt am 27.08.: der Selektor stand zweimal untereinander (die
   drei display/align-items/gap-Zeilen kamen spaeter als eigener Block dazu,
   als das Unread-Badge in den Tab wanderte). Zwei Bloecke fuer denselben
   Selektor sind genau die Sorte Fragmentierung, die spaeter niemand mehr
   ueberblickt - hier ohne jede Verhaltensaenderung zusammengelegt. */
.pd-chat-channel-tab {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  font-size: 12px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: var(--radius-full);
  cursor: pointer;
}
.pd-chat-channel-tab:hover { background: var(--color-bg-alt); color: var(--color-text); }
.pd-chat-channel-tab.is-active { background: var(--color-accent-soft); color: var(--color-accent-strong); }
/* Chat-Verlauf: "bitte klar markieren, am besten den Reiter (Team/Admin)
   als rotes Badge heften" - bewusst rot/Danger statt Accent-Farbe, damit
   sich der Hinweis auf ungelesene Nachrichten optisch von der aktiven
   Tab-Einfärbung (.is-active, oben) abhebt. */
.pd-chat-unread-badge {
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--radius-full);
  background: var(--color-danger);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* @-Erwähnung (Chat-Verlauf: "Können wir im Chat Fenster [...] auch per @
   Zuordnung automatisch nur an einzelne Team-Member schreiben?" ->
   "Auto-Auswahl an Team-Members [...] bei Eingabe des @ Zeichens") -
   Dropdown direkt unter dem Composer-Input (siehe .pd-chat-input-wrap),
   Chip-Darstellung im Nachrichtentext über renderMentionText(). */
.pd-chat-input-wrap { position: relative; flex: 1; min-width: 0; }
.pd-chat-input-wrap .pd-chat-input { width: 100%; }
.pd-chat-mention-list {
  position: absolute;
  bottom: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  max-height: 160px;
  overflow-y: auto;
  z-index: 20;
}
.pd-chat-mention-option {
  padding: 6px 10px;
  font-size: 12.5px;
  color: var(--color-text);
  cursor: pointer;
}
.pd-chat-mention-option.is-active, .pd-chat-mention-option:hover { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.pd-chat-mention {
  color: var(--color-accent);
  font-weight: 600;
}
.pd-chat-msg.is-mine .pd-chat-mention { color: #fff; text-decoration: underline; text-underline-offset: 2px; }

/* Mail-Dropzone (Chat-Verlauf: "Lass uns mal mit einer Mail-Dropzone
   starten..."). Liste nutzt die gleichen .pd-simple-row/-title/-sub-Klassen
   wie Assets/File-Transfer (siehe oben) - nur die Detail-Ansicht im Modal
   (openMailDetail() in productiondashboard.js) braucht eigene Klassen. */
.pd-mail-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
  color: var(--color-text-muted);
  margin-bottom: 12px;
}
.pd-mail-text {
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--color-text);
  max-height: 50vh;
  overflow-y: auto;
}

/* Aktivitäts-Feed-Widget (Chat-Verlauf: "Aktivitäts-Feed [...] kombinierte
   chronologische Zeitleiste [...] neueste zuerst, relative Zeitangaben",
   dann "Heatmap ist jetzt komisch von der Größe her [...] links die
   Heatmap und rechts daneben die letzten Steps, weniger Punkte bevor es
   scrollt, kleinere Box insgesamt") - siehe activityWidget() in
   productiondashboard.js. Heatmap links mit fester Höhe (keine variable
   Randfläche mehr durch die Flex-Ausrichtung), Liste rechts mit
   niedrigerer max-height, damit die gesamte Karte insgesamt kompakter
   bleibt statt bis zu 420px hoch zu werden. */
.pd-activity-layout { display: flex; gap: var(--space-4); align-items: stretch; }
.pd-heatmap-col { flex-shrink: 0; display: flex; }
.pd-activity-list-col { flex: 1; min-width: 0; }
/* Bug-Report per Screenshot: "Zeig bitte 5 Einträge in der rechten Spalte
   [...] entzerre die Heatmap dann auf die volle Höhe" - max-height auf
   ca. 5 Zeilen (Icon 24px vs. Titel+Sub-Text ~32px -> Text bestimmt die
   Zeilenhöhe) kalibriert, überzählige Einträge scrollen weiterhin. */
.pd-activity-list { display: flex; flex-direction: column; gap: var(--space-2); max-height: 192px; overflow-y: auto; }
.pd-activity-row { display: flex; align-items: flex-start; gap: 10px; }
.pd-activity-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-bg-alt);
  color: var(--color-accent);
  flex-shrink: 0;
  margin-top: 1px;
}
.pd-activity-icon svg { width: 12px; height: 12px; }
.pd-activity-title { font-size: var(--text-body); color: var(--color-text); line-height: 1.3; }
.pd-activity-sub { font-size: 12px; color: var(--color-text-faint); margin-top: 1px; }

/* Aktivitäts-Heatmap (Chat-Verlauf: "GitHub-artige Kalender-Heatmap [...]
   zeigt auf einen Blick Produktionsrhythmus und Flauten", dann "Komischer
   Overflow hier. Scrollbalken bei der Heatmap, unnötig [...] entzerre die
   Heatmap dann auf die volle Höhe") - siehe activityHeatmap() in
   productiondashboard.js, 7 Zeilen x 10 Wochen. KEIN horizontales Scrollen
   mehr (alle 10 Wochen passen in ihre natürliche Breite, kein fixes
   width nötig) - align-content:space-between verteilt die 7 Zeilen über
   die volle Höhe der Spalte (per align-items:stretch auf
   .pd-activity-layout automatisch an die 5-Zeilen-Höhe der Liste
   angeglichen), statt oben zu kleben und unten Leerraum zu lassen. */
.pd-heatmap {
  display: grid;
  grid-template-rows: repeat(7, 11px);
  grid-auto-flow: column;
  grid-auto-columns: 11px;
  justify-content: start;
  align-content: space-between;
  column-gap: 3px;
}
.pd-heat-cell { width: 11px; height: 11px; border-radius: 3px; background: var(--color-border); flex-shrink: 0; }
.pd-heat-cell.is-level-1 { background: color-mix(in srgb, var(--color-accent) 35%, var(--color-bg)); }
.pd-heat-cell.is-level-2 { background: color-mix(in srgb, var(--color-accent) 68%, var(--color-bg)); }
.pd-heat-cell.is-level-3 { background: var(--color-accent); }

.pd-dropzone {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: var(--space-4) var(--space-3);
  margin-bottom: var(--space-3);
  border: 1.5px dashed var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  color: var(--color-text-faint);
  font-size: 12.5px;
  text-align: center;
  cursor: pointer;
}
.pd-dropzone svg { width: 14px; height: 14px; }
.pd-dropzone.is-dragover { color: var(--color-accent); border-color: var(--color-accent); }
.pd-upload-progress { display: flex; flex-direction: column; gap: 6px; }
.pd-upload-progress:not(:empty) { margin-bottom: var(--space-3); }
.pd-upload-progress-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 9px;
  background: var(--color-accent-soft);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
}
.pd-upload-progress-pct { color: var(--color-accent); font-weight: 600; flex-shrink: 0; }

.pd-billing-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }

.pd-kind-toggle { display: flex; gap: 6px; margin-bottom: var(--space-2); }
.pd-kind-toggle .btn.is-active { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }

.main {
  padding: var(--space-6) var(--space-8);
  width: 100%;
}

/* Einblenden bei jedem Seitenwechsel (Chat-Verlauf: "Kannst du dir an
   mehreren Stellen coole kleine Animationen überlegen?" -> "Ich sehe keine.
   Kannst du sie offensichtlicher machen?") - bewusst deutlich kräftiger als
   der erste Versuch (20px statt 6px Versatz, leichtes Einzoomen, fast
   doppelt so lange), damit der Wechsel klar wahrnehmbar ist statt nur
   unterschwellig zu wirken. Klasse wird bei jeder Navigation neu getriggert,
   siehe render() in router.js. */
@keyframes page-in {
  from { opacity: 0; transform: translateY(20px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
.main.page-enter { animation: page-in 0.5s var(--ease); }

/* Stretches the fixed-viewBox SVG to fill its card (see lineChart() in
   chart.js for why this is a CSS stretch rather than a JS-measured exact
   width: a ResizeObserver-based "build at the real container width"
   approach was tried and ended up silently rendering blank in production,
   for reasons that weren't reproducible enough to track down - this plain
   CSS stretch is what reliably worked before and still does). The
   "chunky on wide screens" look this stretch used to cause is mitigated
   in chart.js instead: a much wider design baseline keeps the stretch
   factor closer to 1x on realistic card widths, and vector-effect=
   "non-scaling-stroke" keeps stroke thickness constant regardless of
   scale. display:block avoids the usual inline-element baseline gap. */
svg.line-chart { width: 100%; height: auto; display: block; }
/* Chat-Verlauf: "Die mittlere Box gibt die Höhe vor, die beiden daneben
   gehen mit - das ist korrekt. Allerdings wird der Inhalt der beiden
   äußeren nicht gestreckt. Die Grafiken sollten die volle Höhe nutzen!" -
   trendCard()/growthTrendCard() in dashboard.js stretchen die KACHEL
   korrekt auf die Zeilenhöhe (.dashboard-tile, per syncDashboardRowHeights()
   dort), aber "height:auto" oben lässt das SVG selbst bei seiner eigenen,
   von der Breite abgeleiteten Höhe - der Freiraum blieb darunter einfach
   leer. Nur wenn der Chart auch wirklich das LETZTE Kind seines Flex-
   Containers ist (bei revenueCard() z.B. folgt darunter noch Top Kunden/Top
   Leistungen - dort bewusst NICHT greifen, siehe :last-child), darf er sich
   über flex:1 den kompletten übrigen Platz nehmen; height:100% plus dem
   preserveAspectRatio="none" in chart.js sorgt dafür, dass der Inhalt diese
   Höhe dann auch wirklich ausfüllt statt nur mittig eingepasst zu werden. */
svg.line-chart:last-child { flex: 1 1 auto; min-height: 0; height: 100%; }

/* Chart-Einlauf-Animationen (Zeichnen-Animation, Flächen-Einblenden,
   Punkte-Pop, Donut-Fülltransition) - am 2026-08-22 kurzzeitig entfernt,
   noch am selben Tag auf Wunsch wieder zurückgeholt ("Charts bitte auch
   wieder schicker machen, nur ohne den Glow") - bewusst OHNE den vorherigen
   Drop-Shadow-Glow unter der Linie, alles andere wie zuvor. pathLength="1000"
   (siehe lineChart() in chart.js) macht stroke-dasharray/-dashoffset
   unabhängig von der tatsächlichen Pfadlänge planbar. */
@keyframes line-chart-draw {
  from { stroke-dashoffset: 1000; }
  to { stroke-dashoffset: 0; }
}
@keyframes line-chart-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes line-chart-pop {
  from { opacity: 0; transform: scale(0.4); }
  to { opacity: 1; transform: scale(1); }
}
.line-chart-path {
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: line-chart-draw 900ms var(--ease) forwards;
}
.line-chart-area {
  opacity: 0;
  animation: line-chart-fade 700ms ease 400ms forwards;
}
.line-chart-dot {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  animation: line-chart-pop 380ms ease forwards;
  stroke: var(--color-surface);
}
/* Hilfslinien, Achsenbeschriftung und Donut-Spur folgen dem Modus. In
   chart.js stehen sie als Praesentationsattribut fest verdrahtet (#edeef5 /
   #a6aabe) - ein Attribut, das jede CSS-Regel schlaegt, also genuegt hier
   die Klasse ohne !important. Vorher waren die Linien im Dunkelmodus
   nahezu weiss und zogen den Blick von der Kurve weg (31.08.2026). */
.line-chart-grid { stroke: var(--chart-grid); }
.line-chart-axis { fill: var(--color-text-faint); }
.donut-chart-track { stroke: var(--chart-grid); }
.donut-chart-segment {
  transition: stroke-dasharray 700ms var(--ease);
}

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.page-header p { color: var(--color-text-muted); margin-top: 4px; }

/* Wraps multi-button action groups in the header (e.g. "Kalender-Feed" +
   "Neue Buchung") - without this, the actions container was a plain block
   div with no flex/gap, so its buttons rendered flush against each other. */
.page-header-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }

/* Dashboard-Begrüßung (Chat-Verlauf: "Das 'Willkommen zurück' ist mir zu
   groß, gerne deutlich kleiner und eleganter. Außerdem etwas mehr Platz
   darunter.") - eigene, kleinere Klassen statt .page-header/h1 (23px/700,
   siehe globale h1-Regel), damit die Anpassung nur die Startseite betrifft
   und keine andere Seite (Kunden, Produktionen, ...) mit beeinflusst.
   margin-bottom sorgt für den gewünschten zusätzlichen Abstand zu den
   Kacheln darunter - .dashboard-page selbst bekommt (anders als .main-Kinder
   untereinander) keinen automatischen Zeilenabstand, siehe
   .main > * + * weiter oben, das greift hier nicht, da Begrüßung+Kachel-Grid
   Geschwister INNERHALB von .dashboard-page sind, nicht direkte
   .main-Kinder. */
/* Kopfbereich der Startseite, neu gefasst 28.08.2026 (Nutzer: "'Willkommen
   zurueck, Rafael.' steht derzeit etwas verloren zwischen Tabs und Grid.
   Ich wuerde daraus einen echten Dashboard-Header machen").
   Drei Aenderungen: align-items von center auf flex-start (die Begruessung
   ist jetzt dreizeilig und soll oben buendig mit dem Knopf stehen, nicht
   mittig daneben schweben), deutlich mehr Abstand nach unten, und eine
   eigene Spalte fuer den Text. Die Anzeige "Breites Layout" ist entfallen. */
.dashboard-welcome-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
}
.dashboard-welcome-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.dashboard-welcome-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
/* Zeile unter der Begruessung: was heute offen ist. Bewusst in der
   gedaempften Farbe - sie soll die Begruessung begleiten, nicht mit ihr
   konkurrieren. */
.dashboard-welcome-status {
  font-size: var(--text-body);
  color: var(--color-text-muted);
}
.dashboard-welcome-title {
  /* 18px -> 21px und 700 -> 500 (28.08.2026): groesser, damit der Kopf als
     Kopf gelesen wird, und gleichzeitig leichter, damit er dabei nicht
     lauter wird. Genau die Kombination, die eine Ueberschrift traegt, ohne
     zu schreien - siehe auch h1-h4 weiter oben. */
  font-size: 21px;
  font-weight: var(--weight-medium);
  letter-spacing: -0.022em;
  color: var(--color-text);
}

/* ---------- Produktions-Zeitstrahl (pages/productiontimeline.js) ----------
   Ein CSS-Grid mit einer Spalte je Tag. Ein Balken ist eine Zelle, die sich
   per grid-column ueber mehrere Spalten zieht - dadurch braucht der
   Zeitstrahl keine Bibliothek und keine gerechneten Pixelbreiten, und er
   passt sich der Fensterbreite von allein an.

   Die Namensspalte hat eine FESTE Breite und ist beim seitlichen Scrollen
   angeheftet: ohne sie weiss man nach zwei Wochen Scrollen nicht mehr,
   welche Zeile man gerade liest. */
.tl-card { padding: var(--space-4) 0 var(--space-4) 0; overflow: hidden; }
.tl-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: 0 var(--space-5) var(--space-4);
  font-size: var(--text-meta);
  color: var(--color-text-muted);
}
.tl-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.tl-legend-bar { width: 22px; height: 8px; border-radius: 3px; background: var(--color-accent); opacity: 0.85; }

.tl-scroll { overflow-x: auto; overflow-y: hidden; }
.tl-grid { min-width: 720px; }
.tl-row { display: flex; align-items: stretch; }
.tl-row-label {
  flex: none;
  /* Fest, aber nicht starr (31.08.2026, gemeldet: "der Produktionsname wird
     auch in der Desktop-Ansicht stark abgeschnitten"). 230px reichten für
     "26058 · DirectIP · Step-by-Step Renderings" bei Weitem nicht; auf
     breiten Schirmen darf die Spalte mitwachsen, auf schmalen bleibt sie
     bei den alten 230px, damit vom Zeitstrahl selbst etwas übrig bleibt. */
  width: clamp(230px, 21vw, 340px);
  /* Links eine Stufe knapper: die 8px gehen an den Namen. */
  padding: 0 var(--space-3) 0 var(--space-4);
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  font-size: var(--text-body);
  color: inherit;
  text-decoration: none;
  /* Angeheftet, damit die Zeile beim seitlichen Scrollen lesbar bleibt.
     Der Hintergrund ist Pflicht - sonst scheinen die Balken durch. */
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--color-surface);
}
a.tl-row-label:hover .tl-row-title { color: var(--color-accent); }
.tl-row-dot { width: 7px; height: 7px; border-radius: var(--radius-full); flex: none; }
/* Zwei Zeilen statt einer. Die Zeile ist 38px hoch und nutzte davon 17 -
   Nummer und Kunde stehen jetzt klein darüber, der Titel bekommt die volle
   Spaltenbreite für sich allein. Das ist ungefähr die doppelte Zeichenzahl,
   ohne dem Zeitstrahl daneben einen Pixel wegzunehmen. */
.tl-row-text { min-width: 0; display: flex; flex-direction: column; justify-content: center; line-height: 1.25; }
.tl-row-code {
  font-size: var(--text-small);
  color: var(--color-text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tl-row-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.tl-track { flex: 1; display: grid; position: relative; min-height: 38px; }
.tl-head .tl-track { min-height: 34px; }
.tl-head .tl-row-label { background: var(--color-surface); }

/* Tageszellen: Wochenenden leicht abgesetzt, Montag mit einer Trennlinie -
   ohne diese zwei Anker sind 28 gleiche Spalten nicht lesbar. */
.tl-day, .tl-cell { grid-row: 1; border-left: 1px solid transparent; }
.tl-day.is-weekend, .tl-cell.is-weekend { background: var(--color-bg-alt); }
.tl-day.is-weekstart, .tl-cell.is-weekstart { border-left-color: var(--color-border); }
.tl-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 4px 0;
  font-size: var(--text-small);
  color: var(--color-text-faint);
}
.tl-day-weekday { font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; }
.tl-day-num { font-weight: var(--weight-medium); font-variant-numeric: tabular-nums; }
/* Heute: die einzige Spalte mit Akzentfarbe. Alles andere auf dieser Seite
   ist neutral, damit dieser eine Bezugspunkt sofort ins Auge faellt. */
.tl-day.is-today { color: var(--color-accent); }
.tl-day.is-today .tl-day-num { font-weight: var(--weight-semibold); }
.tl-cell.is-today, .tl-day.is-today {
  box-shadow: inset 1px 0 0 var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 6%, transparent);
}

.tl-bar {
  grid-row: 1;
  align-self: center;
  height: 20px;
  border-radius: var(--radius-sm);
  background: var(--tl-bar, var(--color-accent));
  opacity: 0.9;
  display: flex;
  align-items: center;
  padding: 0 7px;
  z-index: 1;
  min-width: 0;
}
.tl-bar-label {
  font-size: 10px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #fff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Ein Dreh, der vor dem Fenster begann oder darueber hinausreicht, wird am
   Rand gerade abgeschnitten - das zeigt, dass er weitergeht, statt so zu
   tun, als endete er hier. */
.tl-bar.is-clipped-left { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.tl-bar.is-clipped-right { border-top-right-radius: 0; border-bottom-right-radius: 0; }

.tl-marker {
  grid-row: 1;
  align-self: center;
  display: flex;
  align-items: center;
  gap: 4px;
  z-index: 3;
  pointer-events: none;
  white-space: nowrap;
}
.tl-marker-dot { width: 8px; height: 8px; border-radius: var(--radius-full); flex: none; }
.tl-marker.is-delivery .tl-marker-dot, .tl-marker-dot.is-delivery { background: var(--color-tone-blue); }
.tl-marker.is-deadline .tl-marker-dot, .tl-marker-dot.is-deadline { background: var(--color-tone-amber); }
.tl-marker.is-overdue .tl-marker-dot { background: var(--color-danger); }
.tl-marker-label {
  font-size: 10px;
  color: var(--color-text-muted);
  /* Die Beschriftung darf aus ihrer Tagesspalte herausragen - sonst passt
     "Lieferung" in keine 28stel-Spalte. */
  overflow: visible;
}
.tl-marker.is-overdue .tl-marker-label { color: var(--color-danger); font-weight: var(--weight-medium); }

/* ---------- Spotify-Popover in der Seitenleisten-Fusszeile (siehe
   components/spotifybar.js) ----------
   Sass zuerst im Kopfbereich der Startseite. Falscher Ort: Musik laeuft
   waehrend der Arbeit, nicht waehrend man auf die Startseite schaut. Sitzt
   jetzt neben Akzentfarbe, Papierkorb und Hell/Dunkel und uebernimmt deren
   Popover-Geometrie (siehe .accent-popover weiter oben) - gleiche Kante,
   gleicher Abstand, gleiche Einblendung. */
.spotify-popover {
  position: absolute;
  bottom: calc(100% + 8px);
  /* Ueber die volle Breite der Fusszeile statt am Knopf ausgerichtet - die
     Seitenleiste hat overflow: hidden, ein am Knopf haengendes Popover wurde
     rechts abgeschnitten (Bug-Report 28.08.). So ist die Breite durch die
     Leiste selbst begrenzt und kann gar nicht mehr ueberstehen. */
  left: 0;
  right: 0;
  display: none;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: 60;
  transform-origin: bottom left;
}
.spotify-popover.is-open { display: flex; animation: panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.spotify-popover-body { display: flex; flex-direction: column; gap: 10px; }
.spotify-hint { font-size: var(--text-meta); color: var(--color-text-muted); }
.spotify-now { display: flex; align-items: center; gap: 9px; min-width: 0; }
.spotify-now.is-idle { font-size: var(--text-meta); color: var(--color-text-muted); }
.spotify-cover { width: 38px; height: 38px; border-radius: var(--radius-sm); flex: none; object-fit: cover; }
.spotify-meta { min-width: 0; line-height: 1.3; }
/* Beide Zeilen einzeilig mit Ellipsis - ein langer Titel darf das Popover
   nicht in die Hoehe treiben. */
.spotify-title,
.spotify-artist { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.spotify-title { font-size: var(--text-body); font-weight: var(--weight-medium); }
.spotify-artist { font-size: var(--text-meta); color: var(--color-text-muted); }
.spotify-controls { display: flex; align-items: center; justify-content: center; gap: var(--space-2); }
.spotify-btn {
  background: none;
  border: 0;
  padding: 7px;
  border-radius: var(--radius-full);
  color: var(--color-text-muted);
  cursor: pointer;
  display: flex;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.spotify-btn:hover { background: var(--color-bg-alt); color: var(--color-text); }
.spotify-btn svg { width: 16px; height: 16px; }
.spotify-connect {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: none;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: 7px 11px;
  font: inherit;
  font-size: var(--text-meta);
  color: var(--color-text-muted);
  cursor: pointer;
}
.spotify-connect:hover { border-color: var(--color-text-muted); color: var(--color-text); }
.spotify-connect svg { width: 13px; height: 13px; }
/* Welches Konto haengt - ohne das merkt niemand, wenn versehentlich das
   private statt des gewuenschten verbunden ist. */
.spotify-account {
  font-size: var(--text-small);
  color: var(--color-text-faint);
  text-align: center;
}

/* ---------- Systemstatus-Kachel (siehe systemStatusCard() in
   pages/dashboard.js) ---------- */
/* Flexbox statt Grid: mit repeat(auto-fit, ...) legt der Browser Spuren nach
   der verfuegbaren Breite an, nicht nach der Zahl der Module - bei drei
   Modulen in einer breiten Kachel blieb rechts eine leere Spur stehen.
   flex verteilt auf genau das, was da ist. Wichtig, weil Nicht-Admins nur
   EIN Modul sehen (die beiden anderen sind adminOnly). */
.sysstat-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5) var(--space-4);
}
.sysstat-module {
  flex: 1 1 140px;
  min-width: 0;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius-md);
  padding: 4px 6px;
  margin: -4px -6px;
  transition: background var(--dur-fast) var(--ease);
}
a.sysstat-module:hover { background: var(--color-bg-alt); }
.sysstat-head { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
.sysstat-dot { width: 7px; height: 7px; border-radius: var(--radius-full); flex: none; }
/* Gruen/Gelb/Rot bewusst NUR hier - im Rest der Startseite steht Farbe fuer
   Zugehoerigkeit, hier ausnahmsweise fuer Zustand. */
.sysstat-dot.is-ok { background: var(--color-success); }
.sysstat-dot.is-warn { background: var(--color-warning); }
.sysstat-dot.is-danger { background: var(--color-danger); }
.sysstat-label {
  font-size: var(--text-small);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.sysstat-value {
  font-size: 17px;
  font-weight: var(--weight-medium);
  letter-spacing: -0.015em;
  line-height: 1.2;
}
.sysstat-detail {
  font-size: var(--text-meta);
  color: var(--color-text-faint);
  margin-top: 2px;
}

/* Gruppiert Rentals-Toggle + "Ausführlich anlegen" (Produktionen) als EINE
   Einheit statt zwei lose Kinder von .page-header-actions - so lassen sich
   beide gezielt auf einer Zeile halten (siehe mobile Variante unten), ohne
   das generische .page-header-actions-Wrap-Verhalten für andere Seiten
   anzufassen. */
.production-header-actions { display: flex; align-items: center; gap: var(--space-3); }

/* Uniform vertical rhythm between top-level page sections (page-header,
   stat grids, cards, card grids, tables, ...). Every page mounts a flat
   list of section elements directly into <main class="main">, so a single
   sibling rule keeps spacing consistent everywhere instead of relying on
   ad hoc inline margins scattered across individual pages. */
.main > * + * { margin-top: var(--space-5); }

/* BLEIBT HIER STEHEN (Konsolidierung 28.08.2026): Selektoren aus diesem
   Block kommen weiter unten in der Datei ERNEUT vor. Bei gleicher
   Spezifitaet entscheidet die Reihenfolge - ein Verschieben ans Dateiende
   wuerde die Gewichte umdrehen und das Layout still veraendern. */
@media (max-width: 900px) {
  /* iOS Safari zoomt die GESAMTE Seite automatisch hinein, sobald ein Feld
     mit einer Schriftgröße unter 16px fokussiert wird (eigene Eingabehilfe,
     damit man den getippten Text noch lesen kann) - genau das war die
     Ursache für "manche Seiten werden mobil zu groß eingezoomt": jedes
     Antippen von Suchfeldern/Dropdowns/Textfeldern (.field input, die
     Tabellen-Suche, das Suchfeld in searchSelect-Dropdowns, ...) liegt mit
     13-13.5px deutlich darunter. Der Zoom bleibt dann oft auch nach dem
     Verlassen des Feldes bestehen, wirkt also wie ein allgemeines Layout-
     Problem, obwohl er nur durch das Antippen EINES Feldes ausgelöst wurde.
     !important, weil das hier gezielt und pauschal ALLE Eingabefelder auf
     Mobilgeräten übersteuern soll, unabhängig davon, wie spezifisch deren
     eigene font-size-Regel ist - das visuelle Erscheinungsbild am Desktop
     bleibt davon komplett unberührt (nur diese max-width:900px-Regel ändert
     sich, sonst nichts an den betroffenen Selektoren). */
  input, select, textarea { font-size: 16px !important; }

  /* .main prefix bumps specificity above the unconditional .grid-3/.grid-4
     rule further down the file - without it, that later same-specificity
     rule would win the cascade tie regardless of viewport width and this
     override would silently do nothing. */
  .main .grid-3, .main .grid-4 { grid-template-columns: repeat(2, 1fr); }
  .app-shell { grid-template-columns: 1fr; }
  /* Rechte Icon-Leiste ist eine reine Desktop-Ergänzung (Chat-Verlauf: "die
     Leiste auf der rechten Seite") - auf dem Handy ist ohnehin schon die
     komplette Sidebar als Overlay im Weg, eine zusätzliche schmale Spalte
     würde dort nur wertvollen Platz kosten statt Mehrwert zu bringen. */
  .right-rail { display: none; }
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(var(--sidebar-width), 84vw);
    transform: translateX(-100%);
    transition: transform var(--dur-base) var(--ease);
  }
  body.sidebar-open { overflow: hidden; }
  body.sidebar-open .sidebar { transform: translateX(0); box-shadow: var(--shadow-lg); }
  body.sidebar-open .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(15, 15, 35, 0.45);
    z-index: 45;
  }
  .topbar { display: flex; }
  /* Der Einklapp-Toggle ist eine reine Desktop-Funktion (schmale Icon-Bar) -
     auf dem Handy übernimmt weiterhin der Hamburger in der Topbar das
     Ein-/Ausblenden der kompletten Sidebar als Overlay. */
  .sidebar-collapse-toggle { display: none; }
  .main { padding: var(--space-4) var(--space-4) var(--space-8); }
}

/* Diese Mobile-Overrides sind bewusst NICHT hier definiert, sondern ganz am
   Ende dieser Datei (Abschnitt "Mobile @640px - konsolidierte Overrides") -
   siehe Kommentar dort für den Grund (Bugfix-Historie: mehrere davon wurden
   von später im Dokument stehenden, unbedingten Grund-Regeln lautlos wieder
   überschrieben, weil bei gleicher Selektor-Spezifität die SPÄTERE
   Quellreihenfolge gewinnt, unabhängig davon ob eine Regel in einer
   @media-Bedingung steckt oder nicht). */

/* ---------- Cards / layout ---------- */

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-sm);
  /* Grid-Items (z.B. .card als Kind von .grid-2, siehe toInvoiceCard() in
     productions.js) haben per Spezifikation ein implizites min-width:auto,
     das ein Schrumpfen unter die intrinsische Mindestbreite des Inhalts
     verhindert - dasselbe min-width:auto-Problem wie schon bei .stat-card
     in .grid-4 (siehe Kommentar dort). Ohne diese Regel lief die "Zu
     fakturieren"-Karte auf dem Handy über die volle Grid-Spalte hinaus
     (Bug-Report: "die Box ist breiter als 100% - man muss rauszoomen um sie
     zu sehen"), weil ein unschrumpfbares Kind (Abrechnen-Button + Meta-Zeile)
     die ganze Karte in die Breite zog. Für .card als normale Block-Box
     (außerhalb eines Grid/Flex-Kontexts) ist min-width:0 der ohnehin
     geltende Default, diese Regel ändert dort also nichts sichtbar. */
  min-width: 0;
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
  gap: var(--space-3);
  flex-wrap: wrap;
}

.grid { display: grid; gap: var(--space-4); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }

/* ---------- Karten-Grids: gemeinsames Sicherheits-Fundament ----------
   Chat-Verlauf: "Bei Assets (Mobile) sind die Boxen breiter als 100%" ->
   Root Cause war zweigeteilt (fehlender Mobile-1-Spalten-Fallback UND
   fehlendes min-width:0 auf der einzelnen Box, siehe .documents-box weiter
   unten für die volle Herleitung) -> "wieso sind die Seiten überhaupt
   unterschiedlich angelegt?" -> "lass uns [...] auf der kompletten Seite
   vereinheitlichen": jedes der "Produktion/Board/Spiel/Portfolio
   auswählen"-Karten-Raster (.documents-grid, .board-picker-grid,
   .shotdoc-picker-grid, .games-grid, .portfolio-grid,
   .inspiration-board-grid) war unabhängig voneinander entstanden und hatte
   dieselben zwei Bugs entweder gar nicht, nur teilweise, oder inkonsistent
   gefixt. Rückfrage ergab: NICHT die Kartengröße/Spaltenzahl soll überall
   identisch werden (die ist bewusst pro Seite unterschiedlich, z.B.
   Previews & Masters' feste 3-Spalten-Ansicht oder die extra breiten
   Spiele-Kacheln) - nur der Sicherheitsmechanismus dahinter. Jede der oben
   genannten Grid-Klassen bekommt deshalb zusätzlich diese eine Klasse
   (behält aber ihre eigene grid-template-columns/gap-Regel unverändert):
     .entity-card-grid > * { min-width: 0; } - Kind-Selektor statt einer
     Regel direkt auf der einzelnen Kartenklasse, damit KEINE der bisherigen
     Karten-Klassen (.documents-box, .board-picker-card, ...) einzeln
     angefasst werden muss und auch künftige neue Karten in einem dieser
     Grids automatisch erfasst sind.
     Mobile-1-Spalten-Fallback: siehe @media (max-width: 640px) im
     konsolidierten Mobile-Block am Dateiende (gleiche Konvention wie alle
     anderen Mobile-Overrides dort). */
.entity-card-grid > * { min-width: 0; }

.stat-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-4);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
  display: flex;
  /* Chat-Verlauf: "Kannst du den Content innerhalb der Kacheln
     zentrieren?" - seit die Kennzahlen-Kachel im anpassbaren Dashboard-
     Grid auf die Höhe der Nachbarkachel gestreckt wird (.dashboard-tile,
     siehe .dashboard-grid), ist in der Karte oft mehr Platz als der Inhalt
     braucht - Icon+Zahl+Label sollen die zusätzliche Höhe dann mittig statt
     oben ausgerichtet nutzen. */
  align-items: center;
  gap: var(--space-3);
  /* Screenshot-Fund: bei schmaler Kachel (z.B. Kennzahlen-Kachel neben
     "Produktionen nach Phase", oder DB-Backup/FastBill-Sync) wurde die 4.
     Box abgeschnitten/mitten im Wort abgehackt ("Offene Aufgab...",
     "Belegt... Speich..."), statt sauber umzubrechen - .stat-card ist als
     direktes Kind von .grid-4 selbst ein Grid-Item, und Grid-Items haben per
     Spezifikation ein implizites min-width:auto, das ein Schrumpfen unter
     die intrinsische Mindestbreite des Inhalts verhindert (dasselbe
     min-width:auto-Problem wie schon öfter bei Flex-Kindern in dieser
     Session, hier eben auf Grid-Ebene) - die 4 Spalten von .grid-4 blieben
     dadurch bei ihrer Mindestbreite und liefen insgesamt über die Kachel
     hinaus, statt sich auf die verfügbare Breite zu stauchen. min-width:0
     hier erlaubt genau das Schrumpfen, .stat-label (kein white-space:nowrap)
     bricht den Text danach normal um, statt überzulaufen. */
  min-width: 0;
}

/* Lift beim Hover (Chat-Verlauf: "coole kleine Animationen" -> "offensicht-
   licher machen") - deutlich stärker als der erste, sehr dezente Versuch:
   spürbares Anheben + Vergrößern statt nur 2px Verschiebung. */
.stat-card:hover { transform: translateY(-6px) scale(1.02); box-shadow: var(--shadow-lg); border-color: var(--color-accent); }

.stat-icon {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* Weicher gefüllter Verlauf statt einer reinen Outline (Chat-Verlauf:
     "moderne Apple-like Grafiken") - dezent (soft-Ton faded zu transparent),
     damit das Icon selbst weiterhin den Kontrast trägt statt einer lauten
     Fläche. */
  background: linear-gradient(150deg, var(--color-accent-soft) 0%, transparent 100%);
  color: var(--color-accent);
  border: 1.5px solid var(--color-accent-soft);
}
.stat-icon svg { width: 16px; height: 16px; }
.stat-icon.tone-success { color: var(--color-success); border-color: var(--color-success-soft); background: linear-gradient(150deg, var(--color-success-soft) 0%, transparent 100%); }
.stat-icon.tone-warning { color: var(--color-warning); border-color: var(--color-warning-soft); background: linear-gradient(150deg, var(--color-warning-soft) 0%, transparent 100%); }
.stat-icon.tone-danger { color: var(--color-danger); border-color: var(--color-danger-soft); background: linear-gradient(150deg, var(--color-danger-soft) 0%, transparent 100%); }
.stat-icon.tone-violet { color: var(--color-accent-2); border-color: var(--color-violet-soft); background: linear-gradient(150deg, var(--color-violet-soft) 0%, transparent 100%); }

.stat-body { min-width: 0; }
/* 800 -> 500 (28.08.2026). Das war der klobigste Wert im ganzen Stylesheet
   und faellt bei den grossen Kennzahlen auf der Startseite am meisten auf.
   Eine 24px-Zahl braucht kein Extrafett, um aufzufallen - sie ist schon die
   groesste Zahl auf dem Schirm. */
.stat-card .stat-value { font-size: 24px; font-weight: var(--weight-medium); letter-spacing: -0.02em; line-height: 1.15; }
/* Design-System-Cleanup 2026-08-16 (Phase 4b): 12.5px vereinheitlicht auf
   --text-meta (11.5px), gleicher Token wie die kompaktere Dashboard-Kachel-
   Variante unten - beide waren bisher zwei separate Werte für dieselbe
   Rolle (Sekundärlabel unter einem Stat-Wert). */
.stat-card .stat-label { color: var(--color-text-muted); font-size: var(--text-meta); margin-top: 3px; font-weight: 500; }

/* Deutlich kleinere Zahlen/Labels NUR für die Kennzahlen-Kacheln auf dem
   Dashboard (Chat-Verlauf: "Mach die Schriftgröße bei den kleinen Counter-
   Kacheln doch mal deutlich kleiner, so wie Umsatz oben" -> "Die Counter bei
   DB Backup und Fastbill Sync auch.") - angelehnt an die kompakte
   revenueMiniStat()-Typografie der Umsatz-Kachel (15px/600). statCard()
   selbst ist eine geteilte Komponente (auch in games.js/travelexpenses.js/
   timeentries.js verwendet, siehe dortige statCard()-Aufrufe) - bewusst über
   den .dashboard-tile-Vorfahren gescoped, damit dort NICHTS mitgeändert
   wird. Höhere Spezifität (zwei Klassen im Vorfahren-Pfad) gewinnt
   automatisch gegen die allgemeine Regel oben, keine !important nötig. */
.dashboard-tile .stat-card .stat-value { font-size: var(--text-section-title); font-weight: var(--weight-medium); }
.dashboard-tile .stat-card .stat-label { font-size: var(--text-meta); }
.dashboard-tile .stat-icon { width: 28px; height: 28px; }
.dashboard-tile .stat-icon svg { width: 13px; height: 13px; }
/* Chat-Verlauf: "Ok, die kleinen Boxen sind jetzt von der Breite her
   korrekt, allerdings hängt der Inhalt komisch an der Seite. Stell die
   Einzelelemente (Icon, Counter, Titel) doch in diesem speziellen Fall
   übereinander!" - die Basis-.stat-card oben ist row-Layout (Icon links,
   Text-Block rechts daneben), passend für die anderen, breiteren
   statCard()-Kontexte (Games/Reisekosten/Zeiterfassung). In den schmalen
   Dashboard-Kacheln (DB-Backup/FastBill-Sync/Kennzahlen) wirkte das seit dem
   min-width:0-Fix oben nur noch gequetscht, sobald das Label umbricht - hier
   stattdessen Icon oben, Zahl/Label darunter, alles zentriert. */
.dashboard-tile .stat-card {
  flex-direction: column;
  /* align-items zentriert die Kreuzachse (horizontal, bei flex-direction:
     column), justify-content die Hauptachse (vertikal) - Chat-Verlauf: "und
     jetzt noch innerhalb der kleinen Boxen zentrieren" bezog sich genau auf
     Letzteres: ohne justify-content blieb der Inhalt oben hängen, sobald die
     Box (align-items:stretch auf .dashboard-grid, siehe dort) höher als der
     eigene Inhalt gestreckt wird - darunter blieb einfach Leerraum stehen. */
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 8px;
}
.dashboard-tile .stat-body { text-align: center; }
.dashboard-tile .stat-card .stat-label { margin-top: 1px; }

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-weight: 600;
  font-size: var(--text-body);
  transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.btn svg { width: 15px; height: 15px; }
/* Lift beim Hover, "Andrücken" beim Klicken (Chat-Verlauf: "coole kleine
   Animationen" -> "offensichtlicher machen") - deutlich kräftiger als der
   erste Versuch (1px -> 3px Lift + spürbarer Schatten, Klick drückt
   sichtbar unter die Ausgangslage statt nur 1px). :active gewinnt dank
   späterer Reihenfolge in der Kaskade automatisch gegen :hover. */
.btn:hover:not(:disabled) { transform: translateY(-3px) scale(1.03); box-shadow: var(--shadow-md); }
.btn:active:not(:disabled) { transform: translateY(0) scale(0.97); box-shadow: none; }

/* Katalyst-Vorbild: flache Vollfarbe statt Farbverlauf+Schlagschatten -
   Chat-Verlauf: "das cleane, moderne und reduzierte". */
.btn-primary {
  background: var(--color-accent);
  color: #fff;
  box-shadow: none;
}
.btn-primary:hover { background: var(--color-accent-hover); box-shadow: none; }

.btn-secondary { background: var(--color-surface); border-color: var(--color-border-strong); color: var(--color-text); }
.btn-secondary:hover { background: var(--color-bg); border-color: var(--color-text-faint); }

.btn-danger { background: var(--color-danger-soft); color: var(--color-danger); }
.btn-danger:hover { background: var(--color-danger-soft-strong); }

/* Bug-Report: "Der Starten-Button ist nicht schön, passt nicht zum Rest der
   Seite" (Zeiterfassung-Inline-Leiste im Produktions-Dashboard) - gleiche
   dezente "Soft"-Pill-Optik wie .btn-danger oben, nur in Erfolgsgrün statt
   Rot (Start = "go"), statt der zuvor genutzten voll eingefärbten
   .btn-primary, die neben dem schlichten Textfeld/Checkbox zu wuchtig wirkte. */
.btn-success-soft { background: var(--color-success-soft); color: var(--color-success); }
.btn-success-soft:hover { background: var(--color-success-soft); filter: brightness(0.94); }

.btn-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--color-text-muted);
  padding: 7px;
  border-radius: var(--radius-sm);
}
.btn-ghost:hover:not(:disabled) { background: var(--color-bg-alt); color: var(--color-text); }
/* Chat-Verlauf: "Lass den Reset-Button dauerhaft eingeblendet, nur wenn er
   nicht klickbar ist - dann einfach ausgrauen" (Download-Zähler-Reset in
   filetransfer.js) - .btn:disabled deckt nur Buttons mit der .btn-Klasse ab,
   Icon-Buttons wie der Reset-Knopf laufen aber über .btn-ghost.btn-sm ohne
   .btn, daher bislang ohne jede disabled-Optik. Gleicher Grundsatz wie bei
   .rail-btn:disabled weiter oben.
*/
.btn-ghost:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-ghost svg { width: 15px; height: 15px; }

.btn-sm { padding: 6px 11px; font-size: 12px; }

/* Knoepfe, die in einer Kopfzeile nebeneinander stehen, richten sich an
   derselben Hoehe aus (28.08.2026, Bug-Report "Bitte Buttons einheitlich
   hoch machen"). Der eigentliche Fehler war eine gemischte Groessenklasse
   im Markup - diese Regel ist die Absicherung dagegen, dass es beim
   naechsten Knopf wieder passiert: stretch statt center, damit ein
   kleinerer Knopf auf die Hoehe seines Nachbarn waechst, statt mittig
   daneben zu schweben. */
.page-header-actions,
.production-header-actions { align-items: stretch; }
.page-header-actions > .btn,
.production-header-actions > .btn { align-items: center; }
/* Zeile/Trenner/Spalte im Dokument-Kopf (shotdocs.js) - kurze Textlabels
   statt Icon, da alle drei sich vorher dasselbe Plus-Symbol teilten und
   dadurch nicht mehr unterscheidbar waren (Chat-Verlauf: "jetzt drei '+'
   Icons oben - das ergibt keinen Sinn ... sehr kurze Texte ... in kleiner
   Schriftgröße"). */
.btn-compact-label { font-size: 11px; padding: 6px 10px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ---------- Forms ---------- */

.field { margin-bottom: var(--space-3); }
.field label {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--color-text-muted);
  margin-bottom: 6px;
}

.field input, .field select, .field textarea {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  font-family: inherit;
  background: var(--color-surface-2);
  color: var(--color-text);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

/* Without this, browsers render <select> with native OS chrome that ignores
   the `background` above, so selects visibly differed from text inputs/
   textareas even though the CSS rule was identical. A custom chevron
   replaces the native arrow so it still reads clearly as a dropdown. */
.field select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239598ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 15px 15px;
}

/* ALLE Textfelder gleich hoch und gleich ausgerichtet (03.09.2026).
   -------------------------------------------------------------------------
   Nutzer: "Beim Aufgabe anlegen Feld, sieht das 'fällig am' nicht korrekt
   aus." -> "Gut dann bitte alle Formularfelder mobile prüfen! Keine
   Ausnahme!"

   Gemessen, bevor etwas geaendert wurde (e2e/formularfelder.cjs baut die
   Messung nach). In EINEM Formular standen drei verschiedene Hoehen
   nebeneinander:

       Feld                     Desktop   Telefon
       text/email/password/…      38 px     35 px
       date/time                  40 px     37 px
       select                     35 px     31 px

   Fuenf Pixel zwischen Auswahlfeld und Datum, und zwar an jeder der 310
   Stellen, an denen .field vorkommt - nicht nur im Aufgaben-Dialog, der es
   sichtbar gemacht hat.

   URSACHE: die Hoehe entsteht aus dem Inhalt, und `line-height: normal`
   bedeutet fuer jeden Steuerelementtyp etwas anderes. Ein <select> misst
   seine Optionszeile, ein <input type=date> seine internen Segmente, ein
   <input type=text> die blosse Textzeile. Rahmen, Polsterung und Schriftgrad
   sind identisch gesetzt - und das Ergebnis trotzdem drei verschiedene
   Kaesten. Eine feste Hoehe ist deshalb keine Kosmetik, sondern die einzige
   Groesse, die alle drei teilen koennen. (Gemessen: `line-height` in Pixeln
   richtet das <select> gerade, laesst date/time aber unveraendert - deren
   Innenleben haengt nicht an der Zeilenhoehe.)

   Dazu appearance:none und text-align:left fuer date/time: Safari und iOS
   rendern sie mit eigenem Innenpolster und ZENTRIERTEM Wert, waehrend alles
   daneben linksbuendig steht. Genau das zeigte der Screenshot des Nutzers.

   Dieselbe Ursache und dieselbe Loesung wie bei .expand-approval-form weiter
   unten. Dort wurde es am 27.08.2026 schon einmal gemeldet und behoben -
   aber nur fuer JENES Formular. Der Fehler war nie darauf beschraenkt, er
   war dort nur zuerst aufgefallen. Deshalb jetzt bei .field, wo die
   uebrigen Feldtypen auch stehen.

   NICHT dabei, und das ist Absicht: checkbox, radio, color, file, range.
   Die zeichnet der Browser selbst, sie stehen nie in einer Reihe mit
   Textfeldern, und eine erzwungene Hoehe wuerde ihnen nur ihr natuerliches
   Aussehen nehmen. Der Test nimmt sie ausdruecklich aus, statt sie zu
   uebersehen. Ebenso textarea: die soll mit dem Text wachsen. */
.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="search"],
.field input[type="tel"],
.field input[type="url"],
.field input[type="number"],
.field input[type="date"],
.field input[type="time"],
.field input[type="datetime-local"],
.field select {
  height: 38px;
}
.field input[type="date"],
.field input[type="time"],
.field input[type="datetime-local"] {
  -webkit-appearance: none;
  appearance: none;
  text-align: left;
  /* Und die BREITE (03.09.2026, Nutzer: "Es geht aber auch um die Breite,
     das Feld im Bild ist breiter als die anderen!").

     WebKit gibt Datums- und Uhrzeitfeldern eine Eigenbreite aus ihren
     Segmenten (TT.MM.JJJJ plus Zwischenraum). Die `width: 100%` weiter oben
     wird davon auf iOS ueberstimmt: das Feld waechst ueber seinen Behaelter
     hinaus und steht rechts heraus, waehrend die Auswahlfelder darueber
     brav an der Kante enden - genau das zeigt der Screenshot.

     `appearance: none` daruaeber nimmt dem Feld schon einen Teil dieser
     Eigenheit. Die beiden Zeilen hier machen es unabhaengig davon
     unmoeglich: min-width:0 hebt die Mindestbreite auf, die ein Feld als
     Flex-Element sonst aus seinem Inhalt zieht (.field-row ist eine
     Flexbox), max-width:100% deckelt den Rest.

     EHRLICH DAZU: Chromium hat diese Eigenbreite nicht, der Pruefstand kann
     den Fehler also nicht zeigen - er kann nur festhalten, dass die Regeln
     gesetzt sind und kein Feld breiter ist als sein Behaelter. Der Beweis
     auf dem Bildschirm muss vom iPhone kommen. */
  min-width: 0;
  max-width: 100%;
}

/* Aus demselben Grund am Flex-Element selbst: ein .field in einer
   .field-row kann ohne min-width:0 nicht unter die Eigenbreite seines
   Inhalts schrumpfen - dann reisst die ganze Zeile auf, nicht nur das
   eine Feld. Kostet nichts und gilt fuer jeden Feldtyp, nicht nur fuer
   Datumsfelder. */
.field-row > .field { min-width: 0; }

.field input::placeholder, .field textarea::placeholder { color: var(--color-text-faint); opacity: 1; }

.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--color-accent);
  background-color: var(--color-surface);
  box-shadow: var(--shadow-glow);
}

/* Einsatztage darf nie über den tatsächlichen Abholung-Rückgabe-Zeitraum
   hinausgehen (siehe openBookingForm() in bookings.js) - sowohl fürs obere
   Standardfeld als auch pro Artikel-Zeile. Bewusst nicht automatisch
   gekappt/überschrieben, während man tippt, nur visuell markiert, damit die
   Eingabe nicht mitten im Tippen unter der Hand wegspringt. */
/* ---- Grundfarben für Formularfelder (30.08.2026) --------------------
   Gemeldet: "Es gibt immer wieder einzelne Elemente die nicht korrekt auf
   den Dark-Mode umgestellt werden" — Zeiterfassung und Inspo-Dropzone als
   Beispiele.

   Die Ursache war nicht an diesen zwei Stellen, sondern im Fehlen einer
   Grundregel: Felder bekamen ihre Farben ausschließlich über ihre eigene
   Klasse (.field input, .pd-chat-input, .datatable-search input …). Wer ein
   neues Feld baute und die Regel vergaß, bekam den Browser-Standard — im
   Light-Mode unauffällig weiß auf weiß, im Dark-Mode ein leuchtender Balken.
   Das konnte nicht auffallen, solange man in dem Modus arbeitet, in dem der
   Fehler unsichtbar ist.

   :where() hat SPEZIFITÄT NULL. Diese Regel verliert damit gegen jede
   bestehende Klassenregel und kann kein vorhandenes Feld verändern — sie
   greift ausschließlich dort, wo bisher gar nichts stand. Bewusst nur
   Farben, keine Maße: ein Feld, das heute die Browser-Größe hat, soll nicht
   plötzlich springen.

   Ausgenommen sind die Typen, deren Erscheinung der Browser selbst zeichnet
   (Kästchen, Schalter, Regler, Farbwähler, Dateiwahl) - dort macht ein
   eigener Hintergrund die native Darstellung kaputt. Um die kümmert sich
   color-scheme weiter oben. */
:where(
  input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"], [type="submit"], [type="button"], [type="image"]),
  select,
  textarea
) {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}
:where(input, textarea)::placeholder { color: var(--color-text-faint); opacity: 1; }

/* ---- Fokus-Ring (30.08.2026) ----------------------------------------
   Gemeldet als "Was ist das um das x herum?" - ein dicker hellblauer Rahmen
   um den Schliessen-Knopf einer Kachel.

   Das war der Ring des BROWSERS, und er wurde erst durch color-scheme
   sichtbar: vorher zeichnete Chromium ihn in der hellen Welt, also dunkel
   auf dunkler Karte und damit praktisch unsichtbar. Sichtbar ist er
   richtig - ohne Fokus-Anzeige kann man die App nicht mit der Tastatur
   bedienen. Nur soll er aussehen, als haetten wir ihn gewollt.

   Die App hatte fuer die meisten Knoepfe keinen eigenen Fokus-Stil; einzelne
   Stellen (.dash-tile-open, .field input) hatten einen. Diese Regel ist die
   fehlende Grundlage dafuer - wieder mit :where() und damit Spezifitaet
   NULL beim Elementteil, sodass jede vorhandene Klassenregel weiterhin
   gewinnt.

   :focus-visible statt :focus: ein Mausklick auf einen Knopf zeigt damit
   KEINEN Ring, Tab-Bedienung schon. Genau das ist der Unterschied, den der
   Browser selbst auch macht. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

input.is-invalid {
  border-color: var(--color-danger) !important;
  background: var(--color-danger-soft) !important;
}
.field-warning {
  display: block;
  margin-top: 5px;
  font-size: var(--text-meta);
  line-height: 1.3;
  color: var(--color-danger);
  font-weight: 500;
}
.booking-item-days-warning {
  font-size: var(--text-small);
  line-height: 1.25;
  color: var(--color-danger);
  font-weight: 500;
  margin-top: 3px;
}

/* Browsers force a white/yellow background on autofilled fields via an
   internal box-shadow that plain `background` can't override - this was
   the cause of the "Nummer" field looking white while the rest of the form
   kept the intended off-white tone. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--color-text);
  -webkit-box-shadow: 0 0 0px 1000px var(--color-surface-2) inset;
  box-shadow: 0 0 0px 1000px var(--color-surface-2) inset;
  transition: background-color 9999s ease-in-out 0s;
}
.field input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0px 1000px var(--color-surface) inset;
  box-shadow: 0 0 0px 1000px var(--color-surface) inset;
}

/* ==========================================================
   Kunden-Dashboard-Link (Modal, 28.08.2026)
   ----------------------------------------------------------
   Nutzer: "Hier auch alles schoener anordnen". Vorher stand hier ein
   Erklaerblock aus sieben Zeilen, darunter ein schmales Feld mit den beiden
   Knoepfen UNTEREINANDER, dann Passwort und Protokoll mit
   unterschiedlichem Einzug.

   Die Ordnung folgt jetzt der Wichtigkeit: kurz was der Link tut, die
   Einzelheiten zum Ueberfliegen, dann die Zeile, um die es geht - und
   zuletzt die Verwaltung. */
.share-modal { display: flex; flex-direction: column; gap: var(--space-3); }
.share-modal-lead { margin: 0; font-size: 13.5px; }
/* Die Einzelheiten als Liste statt als Fliesstext: es sind drei voneinander
   unabhaengige Aussagen, und die liest man als Absatz nicht zu Ende. */
.share-modal-facts {
  margin: 0;
  padding-left: 17px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: 12.5px;
  color: var(--color-text-muted);
  line-height: 1.5;
}
/* Feld und beide Knoepfe nebeneinander. Das Feld darf schrumpfen (min-width:0),
   sonst draengt sein Inhalt die Knoepfe in die naechste Zeile - genau das
   war vorher zu sehen. */
.share-modal-linkrow { display: flex; align-items: center; gap: 8px; }
.share-modal-linkrow .btn { flex-shrink: 0; }
.share-modal-link {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px;
}
.share-modal-notice {
  margin: 0;
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: #854F0B; background: #FAEEDA;
  border-radius: var(--radius-sm); padding: 8px 12px;
}
.share-modal-notice svg { width: 13px; height: 13px; flex-shrink: 0; }
/* Passwort-Zustand und Protokoll auf EINER Grundlinie, mit Abstand nach
   oben abgesetzt: beides ist Verwaltung, nicht die Hauptsache. */
.share-modal-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 16px;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.share-modal-meta .portfolio-password-row { margin: 0; }

/* ---- Persoenlich einladen (29.08.2026) -----------------------------
   Sitzt im Kunden-Dashboard-Link-Dialog unter dem allgemeinen Link. Bewusst
   mit eigener Trennlinie oben: das ist ein anderer Vorgang als "Link
   kopieren", auch wenn beides denselben Dialog teilt.

   Alle Farben ueber Tokens - der Dialog laeuft in beiden Themes und mit
   acht Akzentfarben. */
.share-invite {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.share-invite-head { font-size: 13px; font-weight: 600; }
.share-invite-lead { margin: 0; font-size: 12.5px; color: var(--color-text-muted); line-height: 1.5; }
.share-invite-row { display: flex; align-items: center; gap: 8px; }
.share-invite-row .btn { flex-shrink: 0; }
.share-invite-input {
  flex: 1;
  min-width: 0;
  padding: 8px 11px;
  font-size: 13px;
  font-family: inherit;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}
.share-invite-note {
  width: 100%;
  padding: 8px 11px;
  font-size: 13px;
  font-family: inherit;
  line-height: 1.5;
  resize: vertical;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}
.share-invite-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.share-invite-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 4px 10px;
  border-radius: var(--radius-full);
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  font-size: 12px;
  font-weight: 500;
}
/* Die zwei Sprach-Flaggen (03.09.2026, Nutzer: "Einfach zwei Flaggen
   hinter die eingeladenen Personen setzen").

   Ein Paar aus zwei Knoepfen, von denen genau einer aktiv ist - optisch
   naeher an einem Schalter als an zwei Knoepfen, damit auf den ersten
   Blick klar ist, dass es eine Entweder-oder-Wahl ist und keine zwei
   Aktionen. Deshalb der gemeinsame Rahmen und die Trennlinie dazwischen
   statt zweier freistehender Knoepfe.

   Der gesperrte Zustand (der Kunde hat selbst gewaehlt) bleibt LESBAR: nur
   gedaempft, nicht ausgegraut. Man will ja sehen, welche Sprache gilt -
   ein unlesbarer Schalter beantwortet die Frage nicht mehr, wegen der man
   hinsieht. */
.share-lang {
  display: inline-flex;
  flex-shrink: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  overflow: hidden;
  background: var(--color-bg);
}
.share-lang-btn {
  border: 0;
  background: transparent;
  color: var(--color-text-muted);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  cursor: pointer;
  line-height: 1.6;
}
.share-lang-btn + .share-lang-btn { border-left: 1px solid var(--color-border); }
.share-lang-btn:hover:not(:disabled):not(.is-an) { background: var(--color-bg-subtle); color: var(--color-text); }
.share-lang-btn.is-an {
  background: var(--color-accent);
  color: #fff;
}
.share-lang-btn:disabled { cursor: default; }
/* Klasse statt :has() - gemessen am 03.09.2026 an den Spiele-Kacheln:
   Chromium wertet :has() bei zur Laufzeit wechselndem Zustand nicht
   zuverlaessig neu aus. Hier waere es vermutlich gutgegangen (der Knoten
   wird ohnehin neu gebaut), aber "vermutlich" ist bei genau diesem
   Selektor heute schon einmal zu wenig gewesen. */
.share-lang.is-gesperrt { opacity: 0.65; }
.share-invite-item .share-lang { align-self: center; }

.share-invite-chip button {
  border: 0;
  background: none;
  cursor: pointer;
  font-family: inherit;
  font-size: 14px;
  line-height: 1;
  padding: 0 3px;
  color: inherit;
  opacity: 0.65;
}
.share-invite-chip button:hover { opacity: 1; }
.share-invite-suggest { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.share-invite-suggest-label { font-size: 12px; color: var(--color-text-faint); }
.share-invite-suggest-btn {
  padding: 4px 10px;
  border-radius: var(--radius-full);
  border: 1px dashed var(--color-border);
  background: none;
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.share-invite-suggest-btn:hover { border-style: solid; color: var(--color-text); }
.share-invite-list { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; }
.share-invite-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--color-border-soft, var(--color-border));
}
.share-invite-item.is-off { opacity: 0.55; }
.share-invite-item-main { flex: 1; min-width: 0; }
.share-invite-item-name { font-size: 13px; font-weight: 500; }
.share-invite-item-sub {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: 11.5px;
  color: var(--color-text-faint);
  margin-top: 2px;
}
.share-invite-item-actions { display: flex; gap: 2px; flex-shrink: 0; }
/* Zustand als Farbe, nicht nur als Text: "noch nicht geoeffnet" ist die
   Information, wegen der man diese Liste ueberhaupt aufmacht. */
.share-invite-state.is-good { color: var(--color-success, #1f8a52); }
.share-invite-state.is-wait { color: var(--color-text-faint); }
.share-invite-state.is-bad { color: var(--color-danger, #a33a3a); }
.share-invite-state.is-off { color: var(--color-text-faint); }

/* Vorschlag ohne hinterlegte Adresse - gestrichelt und gedaempft, damit man
   auf einen Blick sieht, dass hier noch etwas fehlt. */
.share-invite-suggest-btn.is-nomail {
  border-style: dashed;
  color: var(--color-text-faint);
}
.share-invite-suggest-btn.is-nomail:hover { border-style: dashed; color: var(--color-text-muted); }

/* Die Zeile, solange die Adresse zu einem angeklickten Ansprechpartner noch
   fehlt. */
.share-invite-waiting {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--color-accent-strong);
  background: var(--color-accent-soft);
  border-radius: var(--radius-sm);
  padding: 7px 11px;
}

/* Ein Chip, der schon zu einem bekannten Ansprechpartner gehoert. */
.share-invite-chip.is-known { box-shadow: inset 0 0 0 1px var(--color-accent); }

/* "Es gibt etwas Neues" (30.08.2026, Update-Mails). Steht ueber der
   Empfaengerliste, weil es genau die betrifft - und faellt ganz weg, wenn
   nichts anzukuendigen ist. Deutlicher als der Adressbuch-Vorschlag
   darunter: hier ist tatsaechlich etwas zu tun. */
.share-updates {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 4px;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-accent);
  background: var(--color-accent-soft);
}
.share-updates-main { flex: 1 1 260px; min-width: 0; }
.share-updates-head { font-size: 13px; font-weight: 600; color: var(--color-accent-strong); }
.share-updates-sub { margin-top: 3px; font-size: 12px; line-height: 1.5; color: var(--color-text-muted); }
.share-updates-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  font-size: 12.5px;
  color: var(--color-text);
}
.share-updates-list li { padding: 2px 0; }
.share-updates-list li::before { content: '· '; color: var(--color-text-muted); }

/* Der Adressbuch-Vorschlag unter einer Empfaengerzeile. Bewusst leise: er
   ist ein Angebot, keine Aufgabe. */
.share-invite-promote {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  width: 100%;
  order: 3;
  margin-top: 6px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  font-size: 12px;
  color: var(--color-text-muted);
}
.share-invite-link {
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
  font-size: 12px;
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.share-invite-link:hover { color: var(--color-accent-hover); }
.share-invite-select {
  font-family: inherit;
  font-size: 12px;
  padding: 3px 6px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
}
/* Die Empfaengerzeile muss umbrechen duerfen, damit der Vorschlag als
   eigene Zeile darunter Platz hat. */
.share-invite-item { flex-wrap: wrap; }
/* Der einzige Knopf in dieser Liste, der etwas unwiderruflich entfernt -
   deshalb als einziger in der Warnfarbe. */
.share-invite-item-actions .btn-ghost.is-danger { color: var(--color-danger); }
.share-invite-item-actions .btn-ghost.is-danger:hover { color: var(--color-danger); opacity: 0.75; }

.field-row { display: flex; gap: var(--space-3); }
.field-row > .field { flex: 1; }

.checkbox-field { display: flex; align-items: center; gap: 8px; margin-bottom: var(--space-3); }
.checkbox-field input { width: auto; accent-color: var(--color-accent); }
.checkbox-field label { margin: 0; font-weight: 500; color: var(--color-text); }

/* ---------- Table ---------- */

table { width: 100%; border-collapse: collapse; }
thead th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-faint);
  font-weight: 700;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
tbody td {
  padding: 12px var(--space-3);
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr { transition: background var(--dur-fast) var(--ease); }
tbody tr:hover { background: var(--color-bg-alt); }

.table-actions { display: flex; gap: 2px; justify-content: flex-end; }

/* Horizontaler Scroll-Container für breite Tabellen (siehe dataTable() in
   datatable.js) - ohne das schießt eine Tabelle mit vielen Spalten (z.B. die
   "Status setzen"-Kreisicons in bookings.js, die bewusst nie umbrechen)
   bei schmalen Fenstern über die Card-/Seitenbreite hinaus und verschiebt
   die ganze Seite horizontal, statt nur innerhalb der Tabelle zu scrollen. */
.table-scroll { overflow-x: auto; }

/* .col-actions markiert die "Aktionen"-Spalte (jede Spalte mit key:
   'actions' in einer dataTable()-Definition, quer durch alle Seiten) und
   hält sie beim horizontalen Scrollen sichtbar am rechten Rand fest, statt
   dass man erst zum Ende der Tabelle scrollen muss, um z.B. Löschen/
   Bearbeiten zu erreichen. */
.col-actions { position: sticky; right: 0; background: var(--color-surface); z-index: 1; }
thead th.col-actions { z-index: 2; }
tbody tr:hover td.col-actions { background: var(--color-bg-alt); }
/* Feedback-Liste (siehe tableClassName in pages/feedback.js): schmale
   Tabelle ohne Scroll-Bedarf - das Sticky-Pinning riss den Erledigt-Kreis
   sonst mit eigenem Hintergrund + großer Lücke davor an den rechten Rand
   ("Das Feld ist immer noch weiß"). */
.feedback-table .col-actions { position: static; background: transparent; }
.feedback-table tbody tr:hover td.col-actions { background: transparent; }
/* Chat-Verlauf: "Mach die beiden Buttons nicht so gequetscht an die rechte
   Seite. Die sind ohne Abstand direkt aneinander." - die geteilte
   .table-actions-Regel hat nur 2px gap (für dicht gedrängte Icon-Paare wie
   Bearbeiten/Löschen anderswo gedacht); hier etwas großzügiger plus etwas
   Luft zum rechten Zellenrand. */
.feedback-table .table-actions { gap: 10px; padding-right: 4px; }

/* ---------- Data table toolbar / sortable headers / pagination ---------- */

.datatable-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}

/* Rechnungswesen: eigene, serverseitige Archiv-Suchleiste oberhalb der
   Filter-Pills/Tabelle (siehe renderList() in invoicing.js) - nutzt dieselbe
   .datatable-search-Optik wie die eingebaute dataTable-Suche, die hier
   bewusst ausgeblendet ist (showSearch:false). */
.archive-search-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}
.archive-search-bar .datatable-search { max-width: 360px; }

.datatable-search {
  position: relative;
  flex: 1;
  min-width: 200px;
  max-width: 320px;
}
.datatable-search svg {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  color: var(--color-text-faint);
  pointer-events: none;
}
.datatable-search input {
  width: 100%;
  padding: 8px 11px 8px 34px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-body);
  font-family: inherit;
  background: var(--color-surface-2);
  color: var(--color-text);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.datatable-search input:focus {
  outline: none;
  border-color: var(--color-accent);
  background: var(--color-surface);
  box-shadow: var(--shadow-glow);
}

.datatable-count { font-size: 12px; color: var(--color-text-faint); white-space: nowrap; }

/* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Live-Review
   mit Nutzer über Claude-in-Chrome, Fund: "Suchfeld nicht gestylt") -
   Leistungskatalog (servicecatalog.js) nutzt bewusst keine dataTable(), das
   Suchfeld war dadurch ein reines <input> ohne jede Regel, Browser-Default-
   Optik. Gleiche Werte wie .datatable-search input oben (Border/Radius/
   Padding/Focus-Glow), nur ohne die Icon-Wrapper-Struktur - eigenständige
   Klasse statt .datatable-search zu übernehmen, weil hier kein Such-Icon-
   <svg> im Markup existiert. */
.catalog-search-input {
  padding: 8px 11px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-body);
  font-family: inherit;
  background: var(--color-surface-2);
  color: var(--color-text);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.catalog-search-input::placeholder { color: var(--color-text-faint); opacity: 1; }
.catalog-search-input:focus {
  outline: none;
  border-color: var(--color-accent);
  background: var(--color-surface);
  box-shadow: var(--shadow-glow);
}

th.sortable { cursor: pointer; user-select: none; }
th.sortable .th-inner { display: inline-flex; align-items: center; gap: 4px; }
th.sortable:hover { color: var(--color-text-muted); }
th.sortable .sort-icon {
  display: inline-flex;
  width: 11px;
  height: 11px;
  opacity: 0.35;
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
th.sortable .sort-icon svg { width: 11px; height: 11px; }
th.sorted .sort-icon { opacity: 1; color: var(--color-accent); }
th.sorted.sort-desc .sort-icon { transform: rotate(180deg); }

.datatable-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.datatable-pagination .pagination-info { font-size: 12.5px; color: var(--color-text-muted); }
/* Klickbare Seitenzahlen (Chat-Verlauf: "Alle Paginations sollten so
   dargestellt werden, dass man auch direkt auf einzelne Seiten klicken
   kann") - siehe paginationPageTokens() in components/datatable.js. */
.pagination-pages { display: flex; align-items: center; gap: 2px; }
.pagination-page {
  min-width: 28px;
  height: 28px;
  padding: 0 6px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-family: inherit;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pagination-page:hover:not(:disabled) { background: var(--color-surface-2); color: var(--color-text); }
.pagination-page.is-current { background: var(--color-accent); color: #fff; font-weight: 600; cursor: default; }
.pagination-ellipsis { min-width: 18px; text-align: center; color: var(--color-text-faint); font-size: 12.5px; }

/* ---------- Lead avatar ---------- */

.lead-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 700;
  font-size: 11px;
  flex-shrink: 0;
}
.lead-avatar.is-empty {
  background: var(--color-bg-alt);
  color: var(--color-text-faint);
  border: 1.5px dashed var(--color-border-strong);
}
.lead-avatar.is-empty svg { width: 15px; height: 15px; }
.lead-avatar.is-rental {
  background: var(--color-warning-soft);
  color: var(--color-warning);
  border: 1.5px solid var(--color-warning);
}
.lead-avatar.is-rental svg { width: 15px; height: 15px; }

/* ---------- Set-status stage icons ---------- */

.set-status { display: flex; align-items: center; gap: 4px; flex-wrap: nowrap; }
.set-status-icon {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-text-faint);
  padding: 0;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.set-status-icon svg { width: 12.5px; height: 12.5px; }
/* .done ist bei Produktionen inzwischen klickbar (siehe setStatusCell() in
   productions.js - "Status zurücksetzen"), daher cursor:pointer + eigener
   Hover statt des früheren cursor:default. Bei Vermietungen kommt diese
   Klasse gar nicht vor (rentalSetStatusCell() kennt nur current/pending). */
.set-status-icon.done { background: var(--color-accent-soft); border-color: transparent; color: var(--color-accent-strong); }
.set-status.is-finished .set-status-icon.done { background: var(--color-success-soft); color: var(--color-success); }
/* Immer der volle blaue Kreis beim Hover (auch im grünen "is-finished"-
   Zustand) - soll unmissverständlich als "klickbar, springt hierher zurück"
   lesbar sein, nicht nur ein dezenter Schatten (siehe Chat-Verlauf). */
.set-status-icon.done:hover {
  background: var(--color-accent);
  border-color: transparent;
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 4px 10px -2px rgba(0, 142, 171, 0.4);
}
.set-status-icon.current {
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-2) 130%);
  border-color: transparent;
  color: #fff;
  box-shadow: 0 4px 10px -2px rgba(0, 142, 171, 0.4);
  cursor: default;
}
.set-status-icon.pending:hover { border-color: var(--color-accent); color: var(--color-accent); transform: translateY(-1px); }

/* ---------- Donut chart legend (Job Type breakdown) ---------- */

.donut-chart { flex-shrink: 0; }
/* Trennlinien in Kachel-Listen entfernt (28.08.2026, Nutzer: "mehr Flaeche,
   weniger Linien ... zusammen mit den Card-Outlines entsteht ein leicht
   tabellarischer Eindruck"). Die Zeilen gruppieren jetzt ueber Abstand statt
   ueber Striche - der Rahmen der Karte grenzt die Liste schon ab, jede
   Linie darin wiederholt diese Aussage nur.
   Bewusst NICHT ueberall: Tabellen, Reiterleisten und Kalender-Koepfe
   behalten ihre Linien, dort trennt der Strich tatsaechlich Spalten oder
   Ebenen und nicht nur gleichartige Eintraege. */
.donut-legend-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px 0;
  font-size: 12.5px;
}

/* ---------- Startseite: Vermietungsblock (rentalBookingsCard) ---------- */

.rental-booking-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.rental-booking-title { font-weight: 600; font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rental-booking-sub { font-size: var(--text-meta); color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.rental-booking-dates { font-size: var(--text-meta); color: var(--color-text-muted); white-space: nowrap; flex-shrink: 0; }
.rental-hint { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border); }

/* ---------- Startseite: Changelog-Kachel (changelogCard) ----------
   Chat-Verlauf: "wieso ist nur die Changelog-Kachel in der Höhe immer ein
   kleines Stück zu kurz" - der eigentliche Fehler: .changelog-panel stammte
   noch aus der Zeit VOR dem anpassbaren Kachel-Grid, als dieses Panel ein
   eigenständiges Element ÜBER dem Dashboard war und einen eigenen
   margin-bottom/kleineres padding-bottom brauchte, um Abstand zum nächsten
   Element darunter zu schaffen. Jetzt sitzt die Karte in einer Grid-Kachel,
   deren Höhe per JS exakt auf die Nachbarkachel fixiert wird (siehe
   syncDashboardRowHeights() in dashboard.js) - der margin-bottom hat dabei
   einen Teil dieser fixierten Höhe als unsichtbaren Rand "aufgefressen",
   wodurch die sichtbare Karte (Rand/Schatten) immer um genau diesen Betrag
   zu kurz wirkte, obwohl die Kachel selbst korrekt hoch war. Der
   Zeilenabstand zwischen Kacheln kommt bereits vom Grid-"gap" (siehe
   .dashboard-grid) - kein eigener margin/padding-Sonderfall mehr nötig,
   damit die Karte die komplette Kachelhöhe ausfüllt wie jede andere. */
.changelog-panel {
  position: relative;
}
.changelog-panel-close {
  position: absolute;
  top: 14px; right: 14px;
  width: 26px; height: 26px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: none;
  color: var(--color-text-faint);
  font-size: 12px;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  z-index: 1;
}
.changelog-panel-close:hover { background: var(--color-bg-alt); color: var(--color-text); }
.changelog-panel-list {
  max-height: 260px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  /* Negative Rand + entsprechendes Innen-Padding, damit der Scrollbalken
     nicht direkt auf dem Text klebt, ohne dass die Liste dafür schmaler als
     der Rest der Karte wirkt. */
  margin: 0 calc(var(--space-5) * -1);
  padding: 0 var(--space-5);
}
.changelog-panel-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 0;
}
.changelog-panel-item-date {
  flex-shrink: 0;
  width: 68px;
  font-size: 11px;
  color: var(--color-text-faint);
  white-space: nowrap;
  align-self: flex-start;
  margin-top: 1px;
}
.changelog-panel-item-body { flex: 1; min-width: 0; }
.changelog-panel-item-title { font-weight: 600; font-size: 12.5px; }
.changelog-panel-item-desc {
  font-size: 12px;
  color: var(--color-text-muted);
  line-height: 1.4;
  margin: 2px 0 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
/* Aufgeklappt (28.08.2026, Nutzer: "ich habe keine Möglichkeit den ganzen
   Text zu lesen") - die Klemmung faellt weg, der Rest bleibt wie er ist. */
.changelog-panel-item.is-expanded .changelog-panel-item-desc {
  -webkit-line-clamp: unset;
  display: block;
}
/* Die ganze Zeile ist anklickbar, sofern der Eintrag zu einer Seite fuehrt.
   Eintraege ohne Seite (globale Aenderungen) bleiben bewusst stumm - sie
   bekommen weder Zeiger noch Hervorhebung, damit gar nicht erst der
   Eindruck entsteht, da liesse sich etwas anklicken. */
.changelog-panel-item.is-linked { cursor: pointer; border-radius: var(--radius-sm); }
.changelog-panel-item.is-linked:hover { background: var(--color-bg-alt); }
.changelog-panel-item.is-linked:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
.changelog-panel-item-expand {
  flex-shrink: 0;
  align-self: flex-start;
  margin-top: 1px;
  background: none;
  border: 0;
  padding: 4px;
  border-radius: var(--radius-sm);
  color: var(--color-text-faint);
  cursor: pointer;
  display: flex;
  transition: background var(--dur-fast) var(--ease), transform var(--dur-base) var(--ease), color var(--dur-fast) var(--ease);
}
.changelog-panel-item-expand:hover { background: var(--color-bg-alt); color: var(--color-text); }
.changelog-panel-item-expand svg { width: 13px; height: 13px; }
.changelog-panel-item.is-expanded .changelog-panel-item-expand { transform: rotate(180deg); }
/* "Ausprobieren"-Link (Chat-Verlauf: "damit man direkt zu dem Bereich
   springen kann") - springt per Hash-Link direkt zur passenden Seite (siehe
   link-Feld je Eintrag in changelog.js). */
.changelog-panel-item-try {
  flex-shrink: 0;
  display: flex; align-items: center; gap: 3px;
  font-size: var(--text-meta); font-weight: 600;
  color: var(--color-accent);
  text-decoration: none;
  white-space: nowrap;
  padding: 5px 9px;
  border-radius: var(--radius-sm);
  transition: background var(--dur-fast) var(--ease);
}
/* Seit 28.08. ein <span> statt eines <a>: geklickt wird die ganze Zeile
   (siehe changelogItem() in dashboard.js), der Pfeil ist nur noch das
   Zeichen dafuer, dass es irgendwohin geht. */
.changelog-panel-item.is-linked:hover .changelog-panel-item-try { background: var(--color-accent-soft); }
.changelog-panel-item-try svg { width: 12px; height: 12px; }

/* Kompakte Variante (kein Beschreibungstext mehr je Eintrag, siehe
   changelogCard() in dashboard.js) - Chat-Verlauf: "der Content scrollt zu
   früh, die volle Höhe ist gar nicht ausgeschöpft" / "immer noch die volle
   Höhe": CSS Grid allein kann diese Kachel nicht auf die Höhe der
   Nachbarkachel begrenzen, da die Grid-Zeile bei "auto"-Zeilenhöhe am
   unbegrenzt wachsenden Changelog-Inhalt selbst hängt (siehe
   syncFlexScrollTileHeights() in dashboard.js für den vollen Hintergrund).
   Der 420px-Wert hier ist NUR der Fallback, falls die Kachel mal ohne
   Nachbarn in ihrer Zeile steht (z.B. schmaler Bildschirm, umsortiert) -
   im Normalfall überschreibt das JS diesen Wert mit "none", weil dann
   bereits eine feste, an den Nachbarn angeglichene Kachelhöhe greift.
   min-height:0 hält die flex:1/overflow-y:auto-Kette funktionsfähig. */
.changelog-panel-list.is-compact { flex: 1; max-height: 420px; min-height: 0; }
.changelog-panel-list.is-compact .changelog-panel-item { padding: 6px 0; }

/* ---------- Startseite: Kartenrahmen ----------
   Hintergrundbild war ein Test (Chat-Verlauf: "Kannst du mal testweise das
   große Background-Bild auch auf der Hauptseite in den Hintergrund setzen?"),
   wurde auf Wunsch wieder entfernt ("Entfern bitte das Hintergrund-Bild vom
   Dashboard wieder."). Layout-Eigenschaften (Bleed/Padding/Radius) bleiben
   unverändert bestehen, siehe Kommentar zur Breite weiter unten. */
.dashboard-page {
  /* Etwas breiter als der reine Innenraum von .main (Chat-Verlauf: "Kannst
     du das Feld in dem das Hintergrundbild liegt etwas breiter machen? Der
     Abstand zu den beiden Navigationsbereichen kann etwas kleiner sein.") -
     negatives Margin um die Hälfte von .main's horizontalem Padding,
     Innenabstand (padding) um denselben Betrag erhöht, damit Kopfbereich/
     Kacheln innen optisch gleich weit vom Kartenrand entfernt bleiben wie
     vorher, nur die Karte selbst rückt näher an Sidebar/rechte Icon-Leiste.
     Bewusst KEIN voller Bleed bis zum Rand (kein Gegenstück zum kompletten
     .main-Padding) - .main hat je Breakpoint unterschiedliches Padding
     (siehe dort), das unten je Breakpoint einzeln nachgezogen wird. */
  background-color: var(--color-bg);
  border-radius: var(--radius-md);
  margin: 0 calc(-1 * var(--space-5));
  padding: var(--space-4) calc(var(--space-4) + var(--space-5));
}

/* ---------- Startseite: Kopfbereich (Changelog+Todo links, Rechnungen rechts) ----------
   Chat-Verlauf: "beide Kacheln zusammenführen in eine Kachel mit 50% Breite -
   dafür kannst du die Spalte mit Rechnungen dann entsprechend höher machen,
   von ganz oben bis zur Unterkante To-Do." - linke Spalte stapelt Changelog
   und die Todo-Kachel, rechte Spalte (Rechnungen) ist ein einzelner Flex-Item
   und wird durch align-items:stretch am Reihen-Container automatisch auf die
   volle Höhe der linken Spalte gezogen (siehe dashboardTopSection() in
   dashboard.js). */
.dashboard-top-row {
  display: flex;
  gap: var(--space-4);
  align-items: stretch;
  margin-bottom: var(--space-4);
}
.dashboard-top-row > * { flex: 1; min-width: 0; }
.dashboard-top-left {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* .dashboard-top-row/.dashboard-top-left oben werden von dashboard.js seit
   der Dashboard-Customization (siehe .dashboard-grid weiter unten) nicht
   mehr erzeugt - bewusst nicht gelöscht, falls hier noch irgendwo referenziert
   wird, aber inzwischen totes CSS. */

/* ---------- Startseite: anpassbares Kachel-Raster ----------
   Chat-Verlauf: "Siehst du eine Möglichkeit, dass sich jeder User sein
   Dashboard customizen kann? Die Boxen auch unterschiedlich groß machen,
   ganz ausblenden, usw." - 12-Spalten-Grid, jede Kachel bekommt je nach
   gewählter Größe (S/M/L, siehe DASHBOARD_WIDGETS/dashboardTile() in
   dashboard.js) eine unterschiedliche Spaltenanzahl. Die Kachel selbst
   bleibt unverändert (jedes Widget rendert weiterhin seine eigene .card) -
   .dashboard-tile ist nur der Grid-Platzhalter drumherum plus, im
   Anpassen-Modus, eine kleine Werkzeugleiste oben drauf. */
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-4);
  /* Kurzer Versuch, das hier auf "start" umzustellen (Chat-Verlauf:
     "Insgesamt einfach Weißraum vermeiden"), um Kacheln nicht mehr
     bedingungslos auf die höchste Nachbarkachel zu strecken - Ergebnis laut
     Screenshot war aber ein noch größerer Bruch (Kacheln überlappten sich),
     weil eine Grid-Zeile mit sehr unterschiedlich hohen Kacheln (z.B. eine
     Kachel mit internem Sub-Grid neben "Produktionen nach Phase") ohne
     "stretch" nicht mehr sauber auseinandergehalten wurde. Deshalb wieder
     zurück auf "stretch" (Chat-Verlauf ursprünglich: "die Höhe sollte sich
     einheitlich anpassen, damit es keine Lücken gibt") - Grid-Items
     strecken sich auf die Höhe der höchsten Kachel derselben Zeile. Der
     Weißraum-Wunsch bleibt offen, braucht aber einen anderen Ansatz als
     align-items (siehe Notiz bei syncDashboardRowHeights() in
     dashboard.js). */
  align-items: stretch;
}
/* min-height:0 hier ist der eigentliche Fix für "Changelog zeigt die ganze
   Liste ohne Scroll an" - .dashboard-tile ist selbst ein Grid-Item in
   .dashboard-grid, und Grid-Items haben per Spezifikation ein implizites
   min-height:auto (= ihre Inhaltsgröße), das align-items:stretch komplett
   aushebelt: die Kachel wuchs bisher trotzdem immer auf die volle Höhe
   ihres Inhalts, egal wie hoch die Nachbarkachel war. Erst mit min-height:0
   darf die Kachel wirklich auf die gestreckte Zeilenhöhe schrumpfen, wodurch
   die flex:1/min-height:0-Kette weiter unten (Kachel -> Karte -> Liste)
   erst greifen und intern scrollen kann. */
/* container-type:inline-size (Chat-Verlauf: "schau dass die vier Kästchen
   bei so wenig Platz anders gestaltet werden", zu DB-Backup/FastBill-Sync -
   beide 'md'-Kacheln (span 6), deren 4 statCard()-Boxen im .grid-4 bei nur
   ~halber Dashboard-Breite spürbar enger sind als z.B. bei der 'lg'-breiten
   Kennzahlen-Kachel) - eine normale @media-Breakpoint-Regel kennt nur die
   Fensterbreite, nicht die tatsächliche Kachelbreite (auf einem breiten
   Monitor bleibt das Fenster > 900px, obwohl eine einzelne 'md'-Kachel
   trotzdem schmal ist) - @container weiter unten reagiert stattdessen direkt
   auf die Breite DIESES Elements. */
.dashboard-tile { display: flex; flex-direction: column; min-height: 0; container-type: inline-size; }
/* Letztes Kind einer Kachel (im Normalmodus die Karte selbst, im Anpassen-
   Modus .dashboard-tile-body) wächst auf die volle gestreckte Kachelhöhe -
   die Werkzeugleiste (falls vorhanden) bleibt dagegen bei ihrer eigenen
   Inhaltshöhe. min-height:0 verhindert, dass Chart-/Donut-Inhalte den
   Flex-Container trotzdem sprengen. */
.dashboard-tile > * { flex: 0 0 auto; }
.dashboard-tile > *:last-child { flex: 1 1 auto; min-height: 0; }
.dashboard-tile-body { display: flex; flex-direction: column; }
.dashboard-tile-body > * { flex: 1 1 auto; min-height: 0; }
.dashboard-tile.size-sm { grid-column: span 4; }
.dashboard-tile.size-md { grid-column: span 6; }
.dashboard-tile.size-lg { grid-column: span 12; }

/* Gehört zum container-type oben auf .dashboard-tile: eine mehrspaltige
   statCard()-Reihe wird bei knapper Kachelbreite auf 2 Spalten umgestellt
   statt weiter eng gequetscht zu bleiben. */
@container (max-width: 420px) {
  .dashboard-tile .grid-4 { grid-template-columns: repeat(2, 1fr); }
}

/* Bug-Report: "Diese Stats brauchen mobile aktuell extrem viel unnötigen
   Platz." - betrifft ALLE statCard()-Raster innerhalb einer Dashboard-
   Kachel (Kennzahlen mit 8 Karten, DB-Backup/FastBill-Sync mit je 4,
   File-Transfer/R2-Speicher mit je 2 - alle nutzen dieselbe statCard()-
   Komponente, dieser eine Fix deckt sie also automatisch alle ab). Zwei
   Probleme gleichzeitig: (1) die oben zentrierte "Icon oben, Zahl/Label
   darunter"-Spalten-Optik (.dashboard-tile .stat-card weiter oben) braucht
   pro Karte deutlich mehr Höhe als eine einzeilige Icon-links-Reihe, (2)
   drei Spalten wurden bewusst NICHT umgesetzt (Mockup-Vergleich gezeigt) -
   bei den oft langen deutschen Labels ("In Bearbeitung", "Angebote &
   Rechnungen") wäre der Text bei drei Spalten entweder hart abgeschnitten
   oder mehrzeilig, zwei Spalten mit kompakter Zeile (Icon links, Zahl+Label
   rechts daneben, Label per Ellipsis einzeilig) bleibt lesbar UND deutlich
   kompakter als die bisherige Spalten-Optik.
   Bug-Report 2026-08-27: "Hatten wir nicht gesagt wir machen Mobile auf dem
   Dashboard hier zwei Spalten statt einer?" - die eigentlichen Regeln dafür
   standen ursprünglich HIER, wurden aber lautlos vom konsolidierten
   "Mobile @640px"-Block ganz am Ende der Datei überschrieben:
   ".main .grid-4 { grid-template-columns: 1fr; }" dort hat GENAU dieselbe
   Selektor-Spezifität (zwei Klassen) wie ".dashboard-tile .grid-4" hier -
   bei gleicher Spezifität gewinnt CSS immer über die Quellreihenfolge, und
   der Block am Dateiende steht (siehe dessen eigener Kommentar) absichtlich
   IMMER als letztes. Exakt dasselbe Muster, das dort bereits für andere
   Selektoren dokumentiert ist, hat hier also die eigene spätere Fix-Regel
   erwischt. Die tatsächlichen Regeln stehen jetzt im konsolidierten Block
   am Dateiende (siehe dort, direkt nach ".main .grid-4"), damit sie
   garantiert gewinnen. */

/* Werkzeugleiste über jeder Kachel, nur im Anpassen-Modus gerendert (siehe
   dashboardTile() in dashboard.js) - bewusst als eigene kleine Leiste statt
   direkt mit der Karte verschmolzen, damit jedes Widget (Card, reines Grid
   wie die Kennzahlen-Kacheln, ...) unverändert bleibt. */
.dashboard-tile-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  margin-bottom: 6px;
  background: var(--color-bg-alt);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12px;
}
.dashboard-tile-grip {
  display: flex;
  cursor: grab;
  color: var(--color-text-muted);
  flex-shrink: 0;
}
.dashboard-tile-grip svg { width: 15px; height: 15px; }
.dashboard-tile-grip:active { cursor: grabbing; }
.dashboard-tile-title {
  font-weight: 600;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dashboard-tile-sizes {
  display: flex;
  gap: 2px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 2px;
  flex-shrink: 0;
}
.dashboard-tile-size-btn {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 11px;
  font-weight: 700;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  cursor: pointer;
}
.dashboard-tile-size-btn:hover { background: var(--color-bg-alt); }
.dashboard-tile-size-btn.active { background: var(--color-accent); color: #fff; }
.dashboard-tile-hide-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  flex-shrink: 0;
}
.dashboard-tile-hide-btn:hover { background: var(--color-danger-soft, #fde8e8); color: var(--color-danger); }
.dashboard-tile-hide-btn svg { width: 13px; height: 13px; }
.dashboard-tile.is-dragover { outline: 2px dashed var(--color-accent); outline-offset: 4px; border-radius: var(--radius-md); }

.dashboard-hidden-tray {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: var(--space-4);
  font-size: 12.5px;
}
.dashboard-hidden-tray-label { color: var(--color-text-muted); }
.dashboard-hidden-chip {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px 4px 10px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius-full);
  font-size: 12px;
  cursor: pointer;
}
.dashboard-hidden-chip:hover { border-color: var(--color-accent); color: var(--color-accent); }
.dashboard-hidden-chip svg { width: 12px; height: 12px; }

.dashboard-invoices-card { display: flex; flex-direction: column; position: relative; }
/* Titel bekommt rechts Platz für den fest positionierten Switch (siehe
   .dashboard-invoices-switch weiter unten) reserviert, damit er den Text
   nie überlappt. */
.dashboard-invoices-card .card-header h2 { padding-right: 150px; }
/* Bug-Report-Historie (Screenshots): erst "Diese drei Felder werden mir
   jetzt zu groß [...] Lass uns maximal zwei Reihen Rechnungen rechts
   darstellen, danach: scrollen." (behoben über align-self:start, s.u.,
   damals per dashboard.js noHeightSync gezielt auf diese eine Kachel
   angewendet) - dann aber "achte bitte drauf, dass alle drei Kacheln gleich
   groß sind - keine leere Flächen dazwischen wie im Screenshot": CSS-Grid-
   Zeilen richten sich immer nach der HÖCHSTEN Kachel einer Zeile, auch wenn
   eine einzelne Kachel per align-self:start gar nicht mitgestreckt wird -
   der dadurch entstehende Rest blieb als sichtbare Lücke unterhalb dieser
   einen Kachel stehen. "Offene Rechnungen" nutzt seitdem stattdessen den
   flexScroll-Mechanismus (dashboard.js, gleiches Prinzip wie beim
   Changelog-Panel): ganz normal auf die Zeilenhöhe gestreckt, dafür
   scrollt NUR die innere Liste (.dashboard-invoices-list weiter
   unten) intern, falls mehr Inhalt als Platz vorhanden ist. Die
   align-self:start-Klasse selbst bleibt als generischer, aktuell von
   keinem Widget mehr genutzter Baustein bestehen (siehe
   syncDashboardRowHeights() in dashboard.js). */
.dashboard-tile-no-height-sync { align-self: start; }
/* overflow-x explizit auf hidden - sonst macht der Browser daraus bei nur
   gesetztem overflow-y automatisch "auto" (CSS-Spec-Eigenheit), was hier
   einen unnötigen horizontalen Scrollbalken am unteren Kartenrand erzeugte
   (Chat-Verlauf: "den Scrollbalken unten brauchen wir nicht"), obwohl die
   Zeilen selbst schon per Ellipsis abgeschnitten werden.
   Dashboard-Hierarchie-Brief, Phase 4: diese Klasse war eine ungenutzte
   Vorstufe (die A4-Kachel-Ansicht .invoice-tile darunter hatte sie 2026-08-22
   noch am selben Tag abgelöst) - jetzt reaktiviert als die tatsächliche
   Darstellung von invoicesCard(), da der Brief explizit eine kompakte
   priorisierte Liste statt Card-in-Card-Mini-Kacheln fordert. max-height/
   scrollbar-width als FALLBACK für den Fall, dass die Kachel allein in ihrer
   Zeile steht (Normalfall: flexScroll hebt das per JS wieder auf, siehe
   syncDashboardRowHeights() in dashboard.js), gleicher Wert wie
   .changelog-panel-list.is-compact. */
.dashboard-invoices-list { flex: 1; max-height: 420px; min-height: 0; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; scrollbar-width: none; }
.dashboard-invoices-list::-webkit-scrollbar { display: none; }
/* Einzeilige Rechnungszeile (Chat-Verlauf: "Offene Rechnungen einzeilig,
   klarer strukturiert" / "die Rechnungen dürfen jetzt wieder etwas lockerer
   sein") - Label+Meta links (mit Ellipsis statt Umbruch), Fälligkeits-Tag
   rechtsbündig auf derselben Zeile statt darunter. Etwas mehr vertikales
   Padding als die Standard-.briefing-row, damit die Liste nicht zu gedrängt
   wirkt. */
.briefing-row.is-invoice-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 8px;
}
.dashboard-invoice-line {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
  flex: 1;
  overflow: hidden;
}
.dashboard-invoice-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 1;
}
.dashboard-invoice-meta {
  font-size: 11px;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 3;
  min-width: 40px;
}
.briefing-row.is-invoice-row .briefing-row-due {
  flex-shrink: 0;
  font-size: 11px;
  white-space: nowrap;
}

/* Eingehend/Ausgehend-Umschalter (Chat-Verlauf: "Mach doch oben einen
   Switch rein 'Eingehend / Ausgehend' - bei eingehend zeigst du offene
   Belege an, die nicht bezahlt sind") - kompakte Pillen-Gruppe im
   Card-Header, gleiches Prinzip wie z.B. der Auflösungs-Schieberegler in
   transfer.php, hier aber ohne Schiebe-Thumb (nur 2 Optionen, ein einfacher
   Zustandswechsel per is-active reicht). */
.dashboard-invoices-switch {
  display: inline-flex;
  position: absolute;
  top: var(--space-5);
  right: var(--space-5);
  background: var(--color-bg-alt);
  border-radius: var(--radius-full);
  padding: 3px;
  gap: 2px;
  flex-shrink: 0;
}
.dashboard-invoices-switch-btn {
  border: none;
  background: transparent;
  padding: 5px 12px;
  border-radius: var(--radius-full);
  font-size: var(--text-meta);
  font-weight: 700;
  color: var(--color-text-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.dashboard-invoices-switch-btn.is-active {
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-sm);
}
/* Icon-Variante (siehe .dashboard-invoices-switch-icon weiter unten, nur
   auf Mobile sichtbar) - auf Desktop per default versteckt, das Label
   trägt hier den vollen Text. */
.dashboard-invoices-switch-icon { display: none; }
.dashboard-invoices-switch-icon svg { width: 16px; height: 16px; display: block; }

/* Rechts neben Label+Meta: Betrag + Fälligkeits-Tag einer Zeile
   (dashboard.js invoiceRow()/receiptRow()) - kein eigener Kartenrahmen
   mehr (Dashboard-Hierarchie-Brief, Phase 4: A4-Kachel-Ansicht mit
   .invoice-tile ersetzt, siehe Kommentar an .dashboard-invoices-list oben -
   war Card-in-Card, genau das vom Brief benannte Anti-Pattern). */
.dashboard-invoice-amount { font-size: 12px; font-weight: 700; color: var(--color-text); white-space: nowrap; }

/* ---------- Startseite: Tagesbriefing (todoCard) ----------
   Chat-Verlauf: "mach das rückgängig bei To-Dos" - Zeilen-/Abstands-Werte
   wieder auf den ursprünglichen (nicht kompakten) Stand zurückgesetzt. Heute
   und Morgen liefen früher als zwei eigene .briefing-card-Boxen nebeneinander
   (je 50% Breite) - jetzt stecken beide als .dashboard-split-col in EINER
   Kachel (siehe todoCard()/dayColumn() in dashboard.js), getrennt durch einen
   vertikalen Strich statt durch zwei Kartenrahmen. */
/* Chat-Verlauf: "die senkrechte Linie (der Trenner) nicht mehr in der
   Mitte" - lag am grid-gap-Ansatz: bei "gap: var(--space-5)" zwischen zwei
   gleich breiten 1fr-Spuren sitzt die border-left der zweiten Spalte am
   ENDE der Lücke (direkt an ihrer eigenen Box-Kante), nicht in deren Mitte -
   das schiebt die sichtbare Linie um die halbe Lücke (12px) nach rechts aus
   der wahren Kartenmitte. Jetzt stattdessen gap:0 plus symmetrisches
   padding-right (Spalte 1) / padding-left (Spalte 2) - beide Spuren bleiben
   exakt gleich breit, die Linie sitzt exakt auf der echten Spurgrenze
   (= Kartenmitte), der Abstand zum Text bleibt beidseitig gleich (24px). */
.dashboard-split-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
}
.dashboard-split-col { min-width: 0; padding-right: var(--space-5); }
/* Chat-Verlauf: "Auf dem Dashboard stimmt die To-Do Kacheln noch nicht, da
   geht der Content raus aus der Kachel raus." - .briefing-row (siehe weiter
   unten) hat "margin: 0 -8px;", damit der Hover-Hintergrund in der normalen,
   einspaltigen .briefing-grid-Nutzung bündig bis an den Kartenrand reicht.
   In diesem zweispaltigen Split-Layout sitzt die rechte Spalte aber direkt
   an der Karten-Innenkante - der -8px-Bleed lief dort über die Spalte (und
   sichtbar über die Karte) hinaus in die Nachbar-Kachel hinein. Hier daher
   gezielt deaktiviert (kein negativer Rand mehr). Ein zusätzliches
   overflow-x:hidden auf .dashboard-split-col war ein erster Versuch, hat
   aber den "X offen"-Zähler-Kreis in der Sektionsüberschrift angeschnitten
   (verschachtelte Grids/Flexboxen + Sub-Pixel-Rundung ragten minimal über
   die Spalte hinaus, was vorher nie sichtbar war, weil nichts geclippt
   wurde) - wieder entfernt, das margin:0 unten reicht als Fix bereits aus.
   Bewusst NICHT an der geteilten .briefing-row-Regel selbst geändert, damit
   andere Stellen mit .briefing-grid ihren vollen Bleed behalten. */
.dashboard-split-col .briefing-row { margin: 0; }
/* Chat-Verlauf: "Die beiden gesamten Content-Spalten brauchen einfach ein
   padding von rechts." - die zweite Spalte hatte ihr padding-right vorher
   auf 0 gesetzt (nur padding-left für den Trenner-Abstand) - dadurch
   reichte ihr Inhalt (u.a. der "X"-Zähler-Kreis in der Sektionsüberschrift,
   siehe .briefing-section-count) bis an die Karten-Innenkante. Jetzt
   behalten BEIDE Spalten das padding-right der Basisregel oben (Spalte 1
   Abstand zum Trenner, Spalte 2 Abstand zum Kartenrand). */
.dashboard-split-col + .dashboard-split-col {
  border-left: 1px solid var(--color-border);
  padding-left: var(--space-5);
}
.dashboard-split-col-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: var(--space-4);
}
.dashboard-split-col-header h3 { font-size: var(--text-entity-title); font-weight: 700; margin: 0; }
.dashboard-split-col-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
/* Chat-Verlauf: "Wenn die Kachel zu klein wird - so wie jetzt - dann mach nur
   eine Spalte und oben einen Switch (heute/morgen)" - Segmented-Toggle im
   gleichen Look wie der bestehende .shotdoc-scale-toggle (siehe dort),
   standardmäßig ausgeblendet: erst wenn die Kachel unter die @container-
   Schwelle weiter unten fällt, blendet die dortige Regel ihn ein UND
   versteckt die jeweils inaktive Spalte (siehe todoCard()/dayColumn() in
   dashboard.js - .is-active-day wird per Klick auf die Buttons hier
   umgeschaltet). Bei ausreichender Breite bleibt es beim Nebeneinander mit
   Trenner, ganz ohne Switch. */
.dashboard-todo-switch {
  display: none;
  gap: 2px;
  padding: 2px;
  margin-bottom: var(--space-4);
  width: fit-content;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
}
.dashboard-todo-switch-btn {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 12px;
  font-weight: 600;
  padding: 6px 14px;
  border-radius: calc(var(--radius-md) - 2px);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.dashboard-todo-switch-btn:hover { color: var(--color-text); }
.dashboard-todo-switch-btn.is-active { background: var(--color-surface); color: var(--color-accent-strong); box-shadow: var(--shadow-sm); }

/* Ersetzt die frühere reine @media-Stapelung (beide Spalten untereinander)
   durch die Switch-Variante oben - @container reagiert auf die tatsächliche
   Kachelbreite (container-type:inline-size sitzt auf .dashboard-tile, siehe
   dort), nicht nur auf die Fensterbreite, greift also z.B. auch, wenn die
   Kachel im Anpassen-Modus manuell auf 'md' verkleinert wird, obwohl das
   Browserfenster selbst breit bleibt. */
@container (max-width: 640px) {
  .dashboard-todo-switch { display: inline-flex; }
  .dashboard-split-grid { grid-template-columns: 1fr; }
  .dashboard-split-col { display: none; }
  .dashboard-split-col.is-active-day { display: block; }
  .dashboard-split-col + .dashboard-split-col {
    border-left: none;
    padding-left: 0;
  }
}

.briefing-clear { display: flex; align-items: center; gap: 12px; }
.briefing-clear-icon { color: var(--color-success); flex-shrink: 0; display: flex; }
.briefing-clear-icon svg { width: 24px; height: 24px; }
.briefing-clear-title { font-weight: 700; font-size: var(--text-entity-title); }
.briefing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-5);
}
.briefing-section-label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-warning);
  margin-bottom: 8px;
}
.briefing-section-label svg { width: 13px; height: 13px; flex-shrink: 0; }
.briefing-section-count {
  margin-left: auto;
  background: var(--color-warning-soft);
  color: var(--color-warning);
  border-radius: var(--radius-full);
  padding: 1px 8px;
  font-size: var(--text-small);
}
.briefing-row { padding: 7px 8px; border-radius: var(--radius-sm); cursor: pointer; margin: 0 -8px; }
.briefing-row:hover { background: var(--color-bg-alt); }
.briefing-row-title { font-size: 12.5px; font-weight: 600; color: var(--color-text); }
.briefing-row-sub {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 8px;
  font-size: 11px;
  color: var(--color-text-muted);
  margin-top: 1px;
}
/* Fälligkeits-Tag in der "Offene Aufgaben"-Sektion (siehe dueBadge() in
   dashboard.js) - heute fällig hebt sich in Akzentfarbe ab, überfällig in
   Gefahrenfarbe, alles andere bleibt schlicht wie der Rest der Zeile (siehe
   Chat-Verlauf: "nur heute fällig besonders hervorgehoben"). */
.briefing-row-due.is-today { color: var(--color-accent); font-weight: 700; }
.briefing-row-due.is-overdue { color: var(--color-danger); font-weight: 700; }

.donut-legend-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.donut-legend-label { flex: 1; font-weight: 600; }
.donut-legend-value { color: var(--color-text-muted); white-space: nowrap; }

/* ---------- Konsolidierung / Backup-Empfehlung (Tapes page) ---------- */

.consolidation-card { margin-bottom: var(--space-4); border-color: var(--color-accent); border-width: 1.5px; }

.consolidation-list { display: flex; flex-direction: column; gap: var(--space-2); }

.consolidation-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 10px 12px;
  background: var(--color-bg-alt);
  border-radius: var(--radius-md);
  flex-wrap: wrap;
}
.consolidation-row-title { font-weight: 600; font-size: var(--text-body); color: var(--color-text); }
.consolidation-row-sub { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 2px; }
.consolidation-row-rec {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--color-accent-strong);
  white-space: nowrap;
}
.consolidation-row-rec svg { width: 14px; height: 14px; }

/* "Zugewiesene Backups"-Tabelle: jeder Backup-Eintrag als eigener Chip mit
   eigenem Löschen-Button (Chat-Verlauf: "auch einen Delete Button anbieten",
   siehe render() der 'drives'-Spalte in backups.js) - mehrere Chips pro
   Zeile umbrechen bei vielen Tapes derselben Produktion. */
.backup-entry-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--color-bg-alt);
  border-radius: var(--radius-sm);
  padding: 2px 4px 2px 8px;
  margin: 2px 4px 2px 0;
  font-size: 12px;
}
.backup-entry-delete-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  color: var(--color-text-faint);
  background: none;
  border: none;
  cursor: pointer;
}
.backup-entry-delete-btn svg { width: 11px; height: 11px; }
.backup-entry-delete-btn:hover { color: var(--color-danger, #d64545); background: var(--color-danger-soft, rgba(214, 69, 69, 0.1)); }

/* ---------- LTO drive accordion (Tapes page) ---------- */

/* Sperr-Button (Chat-Verlauf: "LTOs manuell sperren") sitzt NEBEN dem
   (weiterhin volle-Breite) Auf-/Zuklapp-Button statt darin, damit ein Klick
   aufs Schloss nicht gleichzeitig die Inhaltsliste öffnet/schließt. */
.drive-card-top { display: flex; align-items: flex-start; gap: var(--space-2); }
.drive-card-icon-btn { flex-shrink: 0; margin-top: 2px; }
.drive-card.is-locked { opacity: 0.68; }
.drive-card.is-locked:hover { opacity: 0.9; }

.drive-card-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
  text-align: left;
  padding: 0 0 var(--space-3);
}
.drive-card-toggle-head { min-width: 0; }
.drive-card-toggle-head h2 { margin-bottom: 3px; }
.drive-card-toggle-head .text-muted { font-size: 12px; }
.drive-card-chevron {
  display: inline-flex;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  transition: transform var(--dur-base) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.drive-card-chevron svg { width: 13px; height: 13px; }
.drive-card-chevron.is-open { transform: rotate(180deg); background: var(--color-accent-soft); color: var(--color-accent-strong); }
.drive-card-toggle:hover .drive-card-chevron { color: var(--color-text); }

.drive-card-body {
  display: none;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.drive-card-body.is-open { display: block; }

/* ---------- Chip button (e.g. copy production number) ---------- */

.chip-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--radius-full);
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  /* Design-System-Cleanup 2026-08-16 (Phase 3): auf --text-meta/
     --weight-semibold umgestellt (vorher 12px/700) - ob .chip-btn für die
     Produktionsnummer überhaupt die richtige Darstellung ist (Prompt
     Abschnitt 14: "Produktionsnummern müssen nicht automatisch als große
     Pills dargestellt werden"), wird gezielt pro Screen in Phase 4
     entschieden - hier nur die generelle Typografie-Vereinheitlichung für
     alle .chip-btn-Verwendungen (u.a. auch Projektordnername-Kopieren). */
  font-weight: var(--weight-semibold);
  font-size: var(--text-meta);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.chip-btn:hover { background: var(--color-accent); color: #fff; }
.chip-btn svg { width: 12px; height: 12px; opacity: 0.7; }

/* Global-Visual-Redesign 2026-08-16 (Phase 5 Pilot: Produktionen) - Prompt
   Abschnitt 14/37: "Produktionsnummern müssen nicht automatisch als Pill
   dargestellt werden" / Beispiel zeigt die Nummer als schlichten Metadata-
   Text ("Straumann · #26052"), nicht als gefüllte Pille. Eigene, schlankere
   Klasse statt .chip-btn selbst zu ändern - .chip-btn wird noch an anderer
   Stelle (equipment.js) für einen echten interaktiven Chip verwendet, der
   hier nicht angefasst werden soll (kein Rollout-Screen in diesem Batch).
   Copy-Funktion bleibt erhalten, nur das visuelle Gewicht sinkt auf
   Metadata-Niveau (kein Hintergrund/Radius/Pill, nur Text + kleines Icon). */
.number-copy-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  padding: 0;
  color: var(--color-text-muted);
  font-weight: var(--weight-semibold);
  font-size: var(--text-meta);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.number-copy-btn:hover { color: var(--color-accent-strong); }
.number-copy-btn svg { width: 11px; height: 11px; opacity: 0.6; }

/* Global-Visual-Redesign 2026-08-16 (Phase 7 Rollout) - gleiche Logik wie
   .number-copy-btn oben: .chip-btn (gefüllte Pille) war hier für den
   eigentlichen Entity-Namen (Equipment) im Einsatz - das widerspricht
   "Entity First" (der Name IST der Inhalt, keine Pille nötig). Text bleibt
   klickbar (öffnet Verfügbarkeits-Kalender), verliert aber die
   Pillen-Optik. */
.entity-name-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: none;
  padding: 0;
  color: var(--color-text);
  font-weight: var(--weight-semibold);
  font-size: inherit;
  cursor: pointer;
}
.entity-name-btn:hover { color: var(--color-accent-strong); }
.entity-name-btn svg { width: 13px; height: 13px; opacity: 0.55; flex-shrink: 0; }
/* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix) - Name in
   eigenem Label-Span statt direktem Textknoten, damit Ellipsis-Truncate
   (mobileTruncate:true, siehe equipment.js) tatsächlich greifen kann - eine
   Ellipsis auf dem ganzen .entity-name-btn würde sonst auch das
   Kalender-Icon mit abschneiden. max-width:100% + min-width:0 sind hier
   nötig, damit sich Button und Label innerhalb der (durch die
   .mobile-truncate-Klasse auf dem <td>, siehe responsive-table-Regeln)
   verfügbaren Breite tatsächlich verkleinern statt zu überlaufen. */
.entity-name-btn { max-width: 100%; }
.entity-name-btn-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix) - Ersatz für
   die rohe <table> in toInvoiceCard() (productions.js, "Zu fakturieren").
   flex-wrap:nowrap + min-width:0 auf .to-invoice-main ist hier der
   entscheidende Teil: erzwingt, dass Titel IMMER eine Zeile bleibt (Ellipsis
   statt Umbruch) und der Abrechnen-Button (flex-shrink:0) IMMER an derselben
   Position rechts sitzt - unabhängig von der Titellänge (Chat-Verlauf:
   "zu lange Titel im Zweifelsfall IMMER kürzen, anstatt den kompletten Rest
   durcheinander zu würfeln"). Divider statt Einzel-Cards (Prompt Abschnitt
   21 "Rows vor Cards") - die äußere .card-Umrandung reicht bereits. */
.to-invoice-list { display: flex; flex-direction: column; }
.to-invoice-row {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-3);
  padding: 7px 2px;
}
.to-invoice-main { flex: 1 1 auto; min-width: 0; }
.to-invoice-title {
  font-size: var(--text-body);
  font-weight: var(--weight-semibold);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.to-invoice-meta {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-top: 2px;
  font-size: var(--text-meta);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.to-invoice-row .btn { flex-shrink: 0; }

/* ---------- Badges / progress ---------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border-radius: var(--radius-full);
  /* Design-System-Cleanup 2026-08-16 (Phase 3): font-size unverändert
     (11.5px == --text-meta), font-weight von 700 auf --weight-semibold
     (600) zurückgenommen - Prompt Abschnitt 13 "Status nicht überbetonen":
     ein Status ist wichtig, muss aber nicht automatisch das visuell
     stärkste Element auf der Karte sein. Betrifft jeden .badge app-weit
     (Produktionen, Rechnungswesen, Aufgaben, Kalender, ...), einheitlich
     statt Screen für Screen einzeln. */
  font-size: var(--text-meta);
  font-weight: var(--weight-semibold);
}
.badge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.badge-success { background: var(--color-success-soft); color: var(--color-success); }
.badge-warning { background: var(--color-warning-soft); color: var(--color-warning); }
.badge-danger { background: var(--color-danger-soft); color: var(--color-danger); }
.badge-accent { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.badge-neutral { background: var(--color-bg-alt); color: var(--color-text-muted); }
.badge-rental { background: var(--color-warning-soft); color: var(--color-warning); border: 1px solid var(--color-warning); font-weight: 700; }

/* Runder Icon-Button für einzelne Tabellenzeilen-Aktionen (Chat-Verlauf:
   "Das Feld ist komischerweise weiß" - .btn.btn-icon in pages/feedback.js
   verwies auf eine Klasse, die es so gar nicht gab, dadurch nur die nackte
   .btn-Grundform ohne jede Größen-/Formangabe -> "Kannst du den Haken in
   einen Kreis setzen?"). Bewusst NICHT .btn-logout wiederverwendet (das ist
   auf die dunkle Sidebar-Hintergrundfarbe abgestimmt, hier sitzt der Button
   auf hellem Karten-/Tabellenhintergrund). */
.table-icon-btn {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text-muted);
}
.table-icon-btn svg { width: 14px; height: 14px; }
.table-icon-btn:hover { background: var(--color-accent-soft); color: var(--color-accent-strong); border-color: var(--color-accent-strong); }
.table-icon-btn.is-done { background: var(--color-success-soft); color: var(--color-success); border-color: var(--color-success); }
/* Zusätzliche gedeckte Töne für nicht-semantische, frei erweiterbare Werte
   (aktuell nur die Beleg-Zahlungsart) - siehe --color-tone-* in :root/Dark-
   Mode-Block oben und paymentMethodTone() in pages/receipts.js. */
.badge-tone-blue { background: var(--color-tone-blue-soft); color: var(--color-tone-blue); }
.badge-tone-purple { background: var(--color-tone-purple-soft); color: var(--color-tone-purple); }
.badge-tone-rose { background: var(--color-tone-rose-soft); color: var(--color-tone-rose); }
.badge-tone-amber { background: var(--color-tone-amber-soft); color: var(--color-tone-amber); }
.badge-tone-teal { background: var(--color-tone-teal-soft); color: var(--color-tone-teal); }
.badge-tone-slate { background: var(--color-tone-slate-soft); color: var(--color-tone-slate); }

/* iOS-artiger An/Aus-Schalter (siehe clients.js statusSwitch()) - ein
   verstecktes Checkbox-Input treibt den Zustand, .switch-slider ist die
   sichtbare Pille. Grün fürs "an" ist hier bewusst kein Bruch mit der
   sonst grün-freien Kanban-/Kalender-Optik (siehe Nutzer-Feedback dort) -
   Status-"Aktiv" nutzt an anderer Stelle (Badges) schon dieselbe Farbe,
   ein Wechsel der Darstellungsform sollte die Bedeutung nicht ändern. */
.switch-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.switch { position: relative; display: inline-block; width: 36px; height: 21px; flex-shrink: 0; }
.switch input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}
.switch-slider {
  position: absolute;
  inset: 0;
  background: var(--color-border-strong);
  border-radius: var(--radius-full);
  transition: background var(--dur-base) var(--ease);
  pointer-events: none;
}
.switch-slider::before {
  content: '';
  position: absolute;
  width: 17px;
  height: 17px;
  left: 2px;
  top: 2px;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transition: transform var(--dur-base) var(--ease);
}
.switch input:checked + .switch-slider { background: var(--color-success); }
.switch input:checked + .switch-slider::before { transform: translateX(15px); }
.switch input:disabled + .switch-slider { opacity: 0.5; cursor: not-allowed; }
.switch-label { font-size: 12.5px; color: var(--color-text-muted); }

.progress-track {
  width: 100%;
  height: 7px;
  border-radius: var(--radius-full);
  background: var(--color-bg-alt);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--color-accent), var(--color-accent-2));
  border-radius: var(--radius-full);
  transition: width var(--dur-base) var(--ease);
}
.progress-fill.warning { background: var(--color-warning); }
.progress-fill.danger { background: var(--color-danger); }
.progress-fill.success { background: var(--color-success); }
.progress-fill.neutral { background: var(--color-text-faint); }

/* ---------- Status stepper (Productions) ---------- */

.status-cell { min-width: 180px; }
.status-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: 7px;
}
/* Design-System-Cleanup 2026-08-16 (Phase 3): --text-meta/--weight-semibold
   statt 11.5px/700 - die Prozentzahl ist Metadata neben dem eigentlichen
   Status-Badge, nicht selbst der Status. */
.status-percent { font-size: var(--text-meta); font-weight: var(--weight-semibold); color: var(--color-text-faint); }

.status-stepper {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-top: 8px;
}
.status-stepper button {
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  width: 22px;
  height: 22px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.status-stepper button:hover:not(:disabled) { background: var(--color-bg-alt); color: var(--color-text); }
.status-stepper button:disabled { opacity: 0.3; cursor: not-allowed; }
.status-stepper button svg { width: 11px; height: 11px; }
.status-stepper-divider { width: 1px; height: 12px; background: var(--color-border); }

/* ---------- Modal ---------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(14, 15, 30, 0.5);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-8) var(--space-4);
  overflow-y: auto;
  z-index: 100;
  animation: fade-in 0.14s var(--ease);
}

.modal {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 560px;
  padding: var(--space-5);
  animation: modal-in 0.22s var(--ease);
}
/* Wider variant for content that needs side-by-side columns (e.g. the
   Buchungen material catalog + added-items picker) - opt-in via
   openModal({ wide: true }) so every other modal keeps its compact width.
   Sized to fill the main content area (viewport minus the sidebar), not
   just "a bit wider" - the backdrop shifts its centering column over by
   --sidebar-width so the modal lines up with (and fills) the content area
   instead of also spanning under the sidebar. */
.modal-backdrop.is-wide {
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
  padding-right: var(--space-3);
  /* Exactly the sidebar's width, no extra gap - the modal starts right
     where the content area starts and fills every pixel out to the right
     edge of the viewport (minus the small breathing margin above). */
  padding-left: calc(var(--sidebar-width) + var(--space-3));
}

.modal.is-wide { max-width: none; width: 100%; margin: 0; }

/* Etwas schmaleres Format fürs Rechnungswesen-Erstell-Formular (Chat-
   Verlauf: "eine etwas reduzierte Breite verwenden, damit es eher wie ein
   A4 Blatt wirkt", dann "Kompromiss zwischen der jetzigen Breite und der
   davor" - "davor" war .is-wide/volle Breite, "jetzt" war 800px) - breiter
   als der Standard-Modal (560px, würde die 8-spaltige Positionstabelle
   abschneiden), aber bewusst nicht .is-wide (volle Breite). Nochmal 40%
   breiter (Chat-Verlauf: "Kannst du das gesamte Angebots/Rechnungs-Fenster
   insgesamt 40% breiter machen?", 1040px * 1,4 = 1456px) - bleibt dank
   width:100% + der Backdrop-Innenabstände auf schmaleren Bildschirmen
   trotzdem automatisch schmaler, nur der Maximalwert am großen Desktop
   steigt. */
.modal.is-document { max-width: 1456px; }

/* Bug-Report: "Das Dropdown ist auch defekt, es ist einfach eine extrem
   lange Liste." - Root Cause: die .is-fullscreen-Scroll-Regeln unten
   (flex:1 1 auto/overflow-y:auto auf .pm-picker) waren komplett in die
   @media(max-width:720px)-Regel eingesperrt. Am Desktop bleibt das Modal
   aber klein/zentriert (siehe Kommentar unten) - dort landete die komplette,
   ungekürzte Produktionsliste dadurch OHNE jede Höhenbegrenzung im kleinen
   Modal, das dementsprechend zu einer riesigen, nicht scrollbaren Liste
   aufriss. Diese Basis-Regel (gilt auf JEDER Breite) begrenzt .pm-picker
   jetzt immer auf einen scrollbaren Bereich - die Mobile-Regel unten
   überschreibt das anschließend gezielt für die echte Vollbild-Darstellung. */
.pm-picker { max-height: 60vh; overflow-y: auto; }

/* Direkt sichtbare, per Suchfeld filterbare Produktionsliste (Chat-Verlauf
   s.o.: "In der Liste unten dann die übliche Anordnung - Kürzel,
   Kunde/Agentur, Produktion.") - ersetzt den vorherigen searchSelect()
   (Dropdown, den man erst öffnen musste) in openReassignModal(). Gleiche
   Feld-Anordnung wie nameLabel in buildArchivedRows(). */
.pm-picker-search {
  width: 100%;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  color: var(--color-text);
  font-size: var(--text-body);
  margin-bottom: var(--space-3);
}
.pm-picker-search:focus { outline: none; border-color: var(--color-accent); }
.pm-picker-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.pm-picker-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.pm-picker-item:hover { border-color: var(--color-accent); }
.pm-picker-item.is-selected { border-color: var(--color-accent); background: var(--color-accent-soft); }
/* Bug-Report: "Beim zuordnen-Dialog (Popup), bitte doch das Kürzel
   weglassen, so dass deutlich mehr Platz für den Titel der Produktion ist."
   - die vorherige .pm-picker-item-short-Spalte ist entfallen (siehe
   openReassignModal() in previewsmasters.js), Kunde/Agentur darf dadurch
   etwas breiter sein (40% statt 30%) und .pm-picker-item-title (flex: 1 1
   auto) bekommt automatisch den ganzen Rest. */
.pm-picker-item-client {
  flex: 0 0 auto;
  color: var(--color-text-muted);
  font-size: var(--text-small);
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pm-picker-item-title {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(0.99); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes action-sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}

/* Action-Sheet (actionSheet() in modal.js, iPhone-Mobile-UX-Audit
   2026-08-16, docs/ux-audit-2026-08-16-iphone-mobile.md 2.10) - am Desktop
   ein kompaktes, oben zentriertes Modal (der ⋮-Trigger, der es öffnet, ist
   dort per .table-actions-mobile-trigger ohnehin ausgeblendet); die
   eigentliche Bottom-Sheet-Optik lebt im @640px-Override weiter unten. */
.modal.action-sheet { max-width: 320px; padding: var(--space-3); }
.action-sheet-list { display: flex; flex-direction: column; gap: 2px; }
.action-sheet-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 11px 12px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-size: var(--text-entity-title);
  font-family: inherit;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}
.action-sheet-item:hover { background: var(--color-bg-alt); }
.action-sheet-item-icon { display: flex; flex-shrink: 0; width: 18px; height: 18px; color: var(--color-text-muted); }
.action-sheet-item-icon svg { width: 100%; height: 100%; }
/* Global-Visual-Redesign 2026-08-16 (Phase 3 Foundations): #dc2626 war ein
   eigener, dritter Rot-Wert neben --color-danger (#ef4343) - konsolidiert. */
.action-sheet-item.is-danger { color: var(--color-danger); }
.action-sheet-item.is-danger .action-sheet-item-icon { color: var(--color-danger); }

/* Zwei Varianten derselben Aktionsspalte (Chat-Verlauf s.o.) - die
   ausführliche Icon-Reihe bleibt am Desktop wie gewohnt, der einzelne
   ⋮-Trigger ist dort ausgeblendet; auf Mobile (@640px-Override) kehrt sich
   das um. Beide werden immer gerendert, nur CSS schaltet um - kein
   Re-Render bei Breakpoint-Wechsel/Resize nötig. */
.table-actions-mobile-trigger { display: none; }

/* Gleiches Zwei-Varianten-Prinzip wie .table-actions-mobile-trigger oben,
   für die "Neue Produktion"-Karte auf der Produktionen-Seite (Batch 2, siehe
   quickAddCard()/quickAddMobileTrigger() in productions.js): der schmale
   Mobile-Trigger-Button ist am Desktop/Tablet ausgeblendet, die volle Karte
   bleibt dort sichtbar; @640px-Override (siehe unten) kehrt das um. */
.quickadd-trigger-mobile { display: none; }

/* ---------- Toast ---------- */

.toast-container {
  position: fixed;
  bottom: var(--space-5);
  right: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  z-index: 200;
}

.toast {
  background: var(--color-text);
  color: #fff;
  padding: 11px 16px;
  border-radius: var(--radius-sm);
  font-size: var(--text-body);
  font-weight: 500;
  box-shadow: var(--shadow-lg);
  animation: toast-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toast.error { background: var(--color-danger); }
.toast.success { background: var(--color-success); }

/* Schwingt deutlich sichtbar von rechts UND unten herein statt nur zu faden
   (Chat-Verlauf: "coole kleine Animationen" -> "offensichtlicher machen") -
   deutlich größerer Versatz (56px statt 16px) + eine echte "Overshoot"-
   Easing-Kurve (cubic-bezier mit Wert > 1), die spürbar übers Ziel
   hinausschwingt und leicht zurückfedert, statt nur sanft einzublenden. */
@keyframes toast-in {
  from { opacity: 0; transform: translate(56px, 24px) scale(0.85); }
  to { opacity: 1; transform: translate(0, 0) scale(1); }
}

/* ---------- Login ---------- */

.login-screen {
  /* dvh-Fallback-Muster siehe Kommentar bei .app-shell weiter oben. */
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  background: var(--color-surface);
}

.login-hero {
  /* Testweise (Chat-Verlauf: "Kannst du testweise mal das Hintergrundbild
     integrieren, dass wir auch bei den File-Transfer-Seiten verwenden?") -
     dasselbe Foto wie in transfer.php (assets/transfer-background.jpg),
     gleiches Abdunkeln-Overlay-Prinzip (rgba(10,12,30,...) darüber) für
     genug Kontrast zum weißen Text. background-color als Fallback, falls
     das Bild mal nicht lädt - vorher war das reine --sidebar-bg-Verlaufsfarbe
     hier. Pfad relativ zu css/style.css (liegt im deploy-Root neben assets/,
     siehe app/deploy-Sync-Konvention). */
  background-color: #0d1420;
  background-image: linear-gradient(rgba(10, 12, 30, 0.55), rgba(10, 12, 30, 0.55)), url("../assets/transfer-background.jpg");
  background-size: cover;
  background-position: center;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-8);
  color: #fff;
  overflow: hidden;
}
.login-hero::before {
  content: '';
  position: absolute;
  width: 480px;
  height: 480px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(0, 142, 171, 0.35), transparent 70%);
  top: -160px;
  right: -160px;
}
.login-hero::after {
  content: '';
  position: absolute;
  width: 360px;
  height: 360px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(6, 182, 212, 0.25), transparent 70%);
  bottom: -140px;
  left: -100px;
}

.login-hero-brand { display: flex; align-items: center; gap: 12px; position: relative; z-index: 1; }
.login-hero-quote {
  position: relative;
  z-index: 1;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.4;
  max-width: 420px;
}
.login-hero-quote span { color: #67e8f9; }
.login-hero-foot { position: relative; z-index: 1; font-size: 12.5px; color: var(--sidebar-text); }

.login-form-side {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
}

.login-card {
  width: 100%;
  max-width: 360px;
}

.login-card h1 { margin-bottom: 4px; }
.login-card p.subtitle { color: var(--color-text-muted); margin-bottom: var(--space-6); }

.form-error {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--color-danger-soft);
  color: var(--color-danger);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  font-size: 12.5px;
  font-weight: 500;
  margin-bottom: var(--space-3);
}
.form-error svg { width: 15px; height: 15px; flex-shrink: 0; }

/* ---------- Misc ---------- */

/* UX-Audit 2026-08-16 (Prompt 2, Finding #8): war zuvor zweimal definiert
   (hier + weiter unten bei den Vimeo-Import-Styles) - font-size/margin der
   zweiten, spezifischeren Fassung hierher zusammengeführt (identisches
   Rendering, da diese wegen ihrer späteren Position im Stylesheet ohnehin
   schon global gewonnen hat). */
.text-muted { color: var(--color-text-muted); font-size: 12.5px; margin: 0 0 10px; }
.text-faint { color: var(--color-text-faint); }
.text-right { text-align: right; }
.text-center { text-align: center; }
/* Zweite, kleinere Zeile in einer Tabellenzelle - z.B. Dateiname unter dem
   Projektnamen oder Ablaufdatum unter dem Erstelldatum in filetransfer.js
   (Chat-Verlauf: "die Zelle unter Projekt zweizeilig - oben Projekt, darunter
   Dateiname"). Bewusst OHNE Kürzung/max-width (Chat-Verlauf: "Der Dateiname
   muss jetzt auch nicht mehr gekürzt werden.") - bricht bei Bedarf einfach in
   eine weitere Zeile um. */
.table-cell-sub { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 2px; }
.text-nowrap { white-space: nowrap; }

/* ---------- Music / LTO status indicators (Productions table) ---------- */

.music-indicator, .lto-indicator {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
  cursor: help;
}
.music-indicator svg, .lto-indicator svg { width: 15px; height: 15px; }
.lto-indicator.is-on { color: var(--color-success); }

/* ---------- Kunde-Feedback indicators (Buchungen table) ---------- */

.feedback-indicator {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: help;
}
.feedback-indicator svg { width: 15px; height: 15px; }
.feedback-indicator.is-confirmed { color: var(--color-success); }
.feedback-indicator.is-note { color: var(--color-accent); }

/* ---------- Custom hover tooltip (data-tooltip="...") -----------
   Native `title` tooltips are slow to appear and unstyled, so anywhere we
   want a reliable, on-brand tooltip (Music/LTO indicators, etc.) we use
   this instead: a small dark bubble with an arrow, positioned above the
   element and shown on hover or keyboard focus. */
[data-tooltip] { position: relative; }
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-text);
  color: #fff;
  padding: 7px 11px;
  border-radius: var(--radius-sm);
  font-size: var(--text-meta);
  font-weight: 500;
  line-height: 1.4;
  width: max-content;
  max-width: 240px;
  white-space: normal;
  text-align: left;
  box-shadow: var(--shadow-md);
  z-index: 60;
  pointer-events: none;
}
[data-tooltip]:hover::before,
[data-tooltip]:focus-visible::before {
  content: '';
  position: absolute;
  bottom: calc(100% + 3px);
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--color-text);
  z-index: 60;
  pointer-events: none;
}
[data-tooltip]:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.empty-state {
  text-align: center;
  padding: var(--space-8) var(--space-4);
  color: var(--color-text-muted);
}
.empty-state svg { width: 32px; height: 32px; color: var(--color-text-faint); margin-bottom: var(--space-3); }

/* Leerer Zustand mit Inhalt (28.08.2026, siehe components/emptystate.js).
   Kein Kasten, kein Rahmen - leer soll leer aussehen, nur nicht stumm. Das
   Symbol bleibt deutlich zurueckgenommen: es soll die Flaeche ordnen, nicht
   auf sich aufmerksam machen. */
.empty-state-icon {
  display: block;
  margin-bottom: var(--space-3);
  color: var(--color-text-faint);
  opacity: 0.55;
}
.empty-state-icon svg { width: 26px; height: 26px; margin: 0 auto; }
.empty-state-text {
  margin: 0;
  font-size: 13.5px;
  color: var(--color-text-muted);
  max-width: 42ch;
  margin-inline: auto;
  line-height: 1.5;
}
.empty-state-hint {
  margin: 5px 0 0;
  font-size: 12px;
  color: var(--color-text-faint);
  max-width: 42ch;
  margin-inline: auto;
}
/* "Erneut versuchen"-Button im gemeinsamen Lade-Fehler-Zustand (siehe
   components/pageLoadError.js) - .empty-state selbst zentriert bereits per
   text-align, der Button braucht nur etwas Abstand zur Fehlermeldung
   darüber. */
.page-load-error .btn { margin-top: var(--space-3); }

.spinner {
  width: 22px;
  height: 22px;
  border: 2.5px solid var(--color-border);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  margin: var(--space-6) auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spinner-sm { width: 16px; height: 16px; border-width: 2px; margin: var(--space-3) auto; }

/* ---------- Expandable table rows (dataTable expandable option) ---------- */
.expand-col { width: 32px; }
.expand-chevron { display: inline-flex; color: var(--color-text-faint); transition: transform var(--dur-base) var(--ease); }
.expand-chevron svg { width: 14px; height: 14px; }
.expand-chevron.is-open { transform: rotate(180deg); color: var(--color-accent); }

/* Reine Mobile-Antipp-Spalte für dataTable()-Tabellen OHNE eigenes
   renderExpanded-Panel (siehe mobileOnlyToggle in datatable.js) - am
   Desktop komplett unsichtbar (dort gibt es überhaupt keine Aufklapp-
   Funktion), taucht erst innerhalb von @media(max-width:720px) unten
   wieder auf. */
.mobile-only-toggle { display: none; }

.is-expandable-row { cursor: pointer; }
.is-expandable-row:hover { background: var(--color-bg-alt); }
.is-expandable-row.is-expanded { background: var(--color-accent-soft); }

.datatable-expanded-row { background: var(--color-surface-2); }
.datatable-expanded-row:hover { background: var(--color-surface-2); }
/* Sanftes Auf-/Zuklappen statt hartem Sprung (Chat-Verlauf: "wenn
   aufklappbar Sachen mit einer kurzen Animation auf- und zuklappen - nicht
   einfach springen") - der "grid-template-rows: 0fr -> 1fr"-Trick animiert
   eine Höhe von/zu "auto", ohne die Zielhöhe vorher per JS ausmessen zu
   müssen (siehe toggleRow() in datatable.js). Padding/Rand wandern dafür
   von der <td> auf den inneren Wrapper, sonst bliebe beim Zuklappen ein
   sichtbarer Rand-/Abstandsrest stehen. */
.datatable-expanded-cell { padding: 0 !important; }
.datatable-expanded-grid {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow) var(--ease);
}
.datatable-expanded-grid.is-open { grid-template-rows: 1fr; }
/* WICHTIG: .datatable-expanded-inner (das eigentliche Grid-Item) darf
   selbst KEIN Padding/Rand tragen - Padding zählt für den Browser als
   Mindestgröße der Box und verhinderte, dass die Zeile beim Zuklappen
   wirklich bis auf 0 zusammenschrumpft (blieb als sichtbarer "halb
   offener" Rest stehen, siehe Chat-Verlauf: "Detailzeilen klappen nicht
   mehr ganz zu"). Padding/Rand wandern deshalb auf einen weiteren,
   inneren Wrapper (.datatable-expanded-content, siehe datatable.js) -
   .datatable-expanded-inner schneidet den beim Zuklappen einfach komplett
   weg (overflow: hidden), statt dass seine eigene Box je eine
   Mindesthöhe > 0 haben könnte. */
.datatable-expanded-inner {
  overflow: hidden;
  min-height: 0;
}
.datatable-expanded-content {
  padding: var(--space-5) var(--space-5);
  border-top: 1px solid var(--color-border);
}

.expand-panel { display: flex; flex-direction: column; gap: var(--space-4); }
.expand-panel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--space-4);
}
.expand-panel-field { min-width: 0; }
.expand-panel-field-label {
  font-size: var(--text-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  margin-bottom: 3px;
}
.expand-panel-field-value { font-size: var(--text-body); color: var(--color-text); overflow-wrap: break-word; }
.expand-panel-field-value.is-empty { color: var(--color-text-faint); }

/* Rechnungswesen: kleiner Inline-Spinner für die Zeile-für-Zeile-Live-
   Aktualisierung im Hintergrund (Chat-Verlauf: "eine gespeicherte Version
   anzeigen und dann Zeile für Zeile im Hintergrund aktualisieren", siehe
   backgroundRefreshRows() in invoicing.js) - steht vor dem Status-Badge,
   solange row._refreshing true ist. Eigene, kleinere Variante statt
   .spinner-inline (dort weiß/für farbige Buttons gedacht) oder .spinner-sm
   (dort block-zentriert) - hier inline in einer Tabellenzelle auf hellem
   Hintergrund. */
.spinner-row-refresh {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  border: 1.5px solid var(--color-border); border-top-color: var(--color-accent);
  animation: spin 0.6s linear infinite; margin-right: 5px; vertical-align: 0px;
}

/* Rechnungswesen-Detailansicht (siehe detailPanel() in invoicing.js, Chat-
   Verlauf: "Es fehlen sehr viel Infos ... keine Gesamtsumme") - kompakte
   Netto/MwSt/Brutto-Leiste über der Positionstabelle. */
.doc-totals-strip {
  display: flex;
  gap: var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-2);
  border-radius: var(--radius-sm);
}
.doc-totals-item { display: flex; flex-direction: column; gap: 2px; }
.doc-totals-item span { font-size: var(--text-small); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-faint); }
.doc-totals-item strong { font-size: var(--text-entity-title); }
.doc-totals-item.is-grand strong { color: var(--color-accent-strong); }
/* FastBill-Freitext-Abschnittsüberschriften ("// ...") innerhalb einer
   Positionsliste (siehe isSectionHeaderLine() in invoicing.js) - optisch wie
   ein kleiner Kategorie-Header statt einer normalen 0,00-€-Zeile. */
.doc-items-table tr.doc-item-header td {
  font-weight: 700;
  font-size: var(--text-meta);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--color-text-faint);
  background: var(--color-surface-2);
  padding-top: 8px;
  padding-bottom: 8px;
}

/* Jeder inhaltliche Block (Freigaben, Abrechnung, Assets, Aufgaben,
   Arbeitszeit...) bekommt eine eigene Karte mit farblich abgesetztem Header
   - vorher lief alles ohne Kontur/Abstand ineinander (Nutzer-Feedback: "mir
   ist das alles zu unübersichtlich"). Die oberste Stammdaten-Zeile
   (.expand-panel-grid direkt im .expand-panel, nicht in einer .expand-panel-
   section) bleibt bewusst ohne Karte - reine Faktenzeile, keine eigene
   "Sektion". */
.expand-panel-section {
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.expand-panel-section h3 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin: calc(var(--space-4) * -1) calc(var(--space-4) * -1) var(--space-4) calc(var(--space-4) * -1);
  padding: var(--space-3) var(--space-4);
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--color-accent-strong);
  background: var(--color-accent-soft);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  border-bottom: 1px solid var(--color-border);
}
.expand-panel-section h3 svg { width: 14px; height: 14px; color: var(--color-accent); flex-shrink: 0; }

/* Unterteilung INNERHALB einer Karte (siehe billingSection() in
   productions.js: Positionstabelle / FastBill-Status / FastBill-Historie
   sind sonst kaum auseinanderzuhalten) - dezenter als eine eigene Karte,
   nur eine Trennlinie + kleinere Zwischenüberschrift. */
.expand-panel-subsection { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px dashed var(--color-border); }
.expand-panel-subsection h4 {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  margin: 0 0 var(--space-3) 0;
  display: flex;
  align-items: center;
  gap: 6px;
}
.expand-panel-subsection h4 svg { width: 13px; height: 13px; color: var(--color-text-faint); }

.client-contacts-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--space-3); }
.client-contact-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
}

.expand-documents-list { display: flex; flex-direction: column; gap: 6px; }
.expand-documents-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  cursor: pointer;
}
.expand-documents-row:hover { border-color: var(--color-accent); }
.expand-documents-row svg { width: 14px; height: 14px; color: var(--color-text-faint); flex-shrink: 0; }
/* Lösch-x + Freigabe-Icon (Chat-Verlauf: "Im Produktions-Dash möchte ich
   auch Assets löschen können." bzw. "Client-Dashboard-Freigabe Icon [...]")
   - .pd-simple-row-delete/-note werden hier wiederverwendet (siehe
   documentsSection() in productions.js); beide sind jetzt IMMER mit
   gedämpfter Deckkraft sichtbar (siehe Basis-Regeln weiter unten/oben,
   Chat-Verlauf: "Das sollte immer zu sehen sein"), Hover hebt sie zusätzlich
   auf volle Deckkraft an. */
/* Anhänge-Auswahl-Popup beim Mail-Import (Chat-Verlauf: "Beim Mail-Upload
   alle Assets auflisten und abfragen, welche davon gespeichert werden
   sollen - nicht einfach blind speichern.") */
.pd-mail-attach-list { display: flex; flex-direction: column; gap: 2px; max-height: 340px; overflow-y: auto; }
.pd-mail-attach-row { display: flex; align-items: center; gap: 10px; padding: 7px 6px; border-radius: var(--radius-sm); cursor: pointer; }
.pd-mail-attach-row:hover { background: var(--color-bg-alt); }
.pd-mail-attach-row svg { width: 16px; height: 16px; color: var(--color-text-faint); flex-shrink: 0; }
.pd-mail-attach-row.is-select-all { font-weight: 600; border-bottom: 1px solid var(--color-border); border-radius: 0; margin-bottom: 4px; padding-bottom: 10px; }
/* Notiz-/Freigabe-Icon an Mails, Assets, Dokumenten, Previews & Masters
   (Chat-Verlauf: "Mails bitte außerdem mit Notiz-icon versehen" bzw.
   "Client-Dashboard-Freigabe Icon [...]") - ursprünglich nur bei Hover der
   Zeile sichtbar; Chat-Verlauf: "Das sollte immer zu sehen sein" (gilt laut
   Nutzer für die ganze Icon-Gruppe, siehe .pd-simple-row-delete oben) -
   jetzt dauerhaft mit gedämpfter Deckkraft sichtbar, aktive Zustände
   (has-note/is-client-shared) und Hover/Fokus heben sich per voller
   Deckkraft + Akzentfarbe klar davon ab. */
.pd-simple-row-note {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border: none;
  background: transparent;
  border-radius: 50%;
  color: var(--color-text-faint);
  cursor: pointer;
  opacity: 0.55;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.pd-simple-row-note svg { width: 13px; height: 13px; }
.pd-simple-row:hover .pd-simple-row-note,
.pd-simple-row-note:focus-visible { opacity: 1; }
.pd-simple-row-note.has-note,
.pd-simple-row-note.is-client-shared { opacity: 1; color: var(--color-accent); }
.pd-simple-row-note:hover { opacity: 1; color: var(--color-accent); background: var(--color-bg-alt); }
.pd-mail-note { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--color-text-muted); font-style: italic; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-mail-note svg { width: 12px; height: 12px; flex-shrink: 0; }

/* Sprachnotizen (Chat-Verlauf: "Voice-Notiz find ich geil, aber lass den
   Button oben in den Header setzen - rechts zu den anderen Icons. Und das
   Ergebnis unten in eine Kachel 'Notizen' aufnehmen.") - Aufnahme-Popup
   (openVoiceRecorderModal() in productiondashboard.js): großer, ruhiger
   Zustand mit pulsierendem roten Punkt statt einer echten Wellenform (kein
   Audio-Analyser nötig, reine CSS-Animation) + monospace-Timer, damit die
   Ziffern beim Hochzählen nicht "wandern". */
.pd-voice-recorder { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 18px 0 6px; }
.pd-voice-status { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--color-text-muted); }
.pd-voice-rec-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-danger);
  animation: pd-voice-pulse 1.4s ease-in-out infinite;
}
@keyframes pd-voice-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.8); }
}
.pd-voice-timer { font-family: 'SF Mono', ui-monospace, Menlo, monospace; font-size: 34px; font-weight: 600; letter-spacing: 0.02em; }

/* "Notizen"-Kachel (Liste der Sprachnotizen, siehe voicenotesWidget() in
   productiondashboard.js) - eigene Kartenform statt .pd-simple-row/-list
   (jede Notiz braucht mehrzeiligen Platz für Player + Transkript, keine
   einzeilige Klickzeile wie Mails/Assets). Kopfzeile-Buttons wiederverwenden
   trotzdem .pd-simple-row-note/-delete (siehe dortige Docs) - der Hover-
   Trigger ist hier an .pd-voicenote-row statt an .pd-simple-row gebunden,
   gleiches Prinzip wie .expand-documents-row weiter oben. */
.pd-voicenote-list { display: flex; flex-direction: column; gap: 10px; }
.pd-voicenote-row { padding: 10px 12px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); }
.pd-voicenote-row:hover .pd-simple-row-note,
.pd-voicenote-row:hover .pd-simple-row-delete { opacity: 1; }
.pd-voicenote-head { display: flex; align-items: center; gap: 8px; }
.pd-voicenote-head > svg { width: 15px; height: 15px; color: var(--color-text-faint); flex-shrink: 0; }
.pd-voicenote-meta { flex: 1; min-width: 0; font-size: 12px; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-voicenote-audio { width: 100%; height: 32px; margin-top: 8px; }
.pd-voicenote-transcript { margin-top: 8px; font-size: 13px; line-height: 1.5; color: var(--color-text); }
.pd-voicenote-transcript.is-empty { color: var(--color-text-faint); font-style: italic; }
.expand-documents-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.expand-documents-sub { font-size: 11px; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Herkunfts-Marker (z.B. "Per File-Transfer geteilt") - Bug-Report: "nie
   diesen Text verwenden" als Titel, aber auf Wunsch weiterhin klein/dünn
   als Kontext-Zeile sichtbar statt komplett zu verschwinden. */
.expand-documents-origin { font-size: 10.5px; font-weight: 400; color: var(--color-text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.expand-todo-list { display: flex; flex-direction: column; gap: 6px; }
.expand-todo-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
}
.expand-todo-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.expand-todo-due { color: var(--color-text-muted); font-size: var(--text-meta); flex-shrink: 0; }
.expand-todo-due.is-overdue { color: var(--color-danger); font-weight: 600; }
.expand-todo-assignee {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.expand-time-list { display: flex; flex-direction: column; gap: 6px; }
.expand-time-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
}
.expand-time-total { font-weight: 700; color: var(--color-text); }

/* FastBill-History (Versionierung, siehe fastbillHistorySection() in
   bookings.js/productions.js + shy_fastbill_documents). */
.expand-fastbill-list { display: flex; flex-direction: column; gap: 6px; }
.expand-fastbill-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  flex-wrap: wrap;
}
.expand-fastbill-date { color: var(--color-text-muted); }
.expand-fastbill-total { font-weight: 700; margin-left: auto; }
.expand-fastbill-link { color: var(--color-accent); text-decoration: none; }
.expand-fastbill-link:hover { text-decoration: underline; }
.expand-fastbill-link.is-empty { color: var(--color-text-faint); }
.expand-fastbill-by {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Frei editierbare Abrechnungsposten je Produktion (siehe billingSection()
   in productions.js + api/productionbilling.php). */
.billing-items-table { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--space-3); }
/* Positionszeilen im "wie ein echtes Invoicing-Tool"-Format (Chat-Verlauf:
   "Ich möchte die Felder eher so wie hier haben ... Menge, Einheit, Art.Nr.,
   Bezeichnung, Einzelpreis, MwSt., Rabatt", Vorbild FastBills eigene Maske) -
   Grid statt Flex, damit Kopfzeile und Zeilen exakt spaltenweise
   ausgerichtet sind. Gemeinsame Klassen für productions.js (Abrechnung-
   Bereich) UND invoicing.js (Erstell-Formular), beide schreiben in dieselbe
   shy_production_billing_items-Tabelle und müssen daher dieselben Felder
   anbieten, sonst würde das jeweils andere Formular beim Speichern Einheit/
   Art.-Nr./Rabatt der anderen Seite überschreiben. */
.billing-item-header, .billing-item-row {
  display: grid;
  /* Menge-Spalte von 60px auf 74px verbreitert (Chat-Verlauf: "für 0,25 Tage
     ist das Mengenfeld etwas zu klein, die 5 sieht man nicht mehr") - bei
     type="number" nimmt der Browser-Stepper (Auf/Ab-Pfeile) einen Teil der
     Breite weg, bei 60px blieb für zweistellige Nachkommastellen wie "0,25"
     zu wenig Platz für die letzte Ziffer. */
  /* Führende 18px-Spalte für den neuen Drag-Griff (siehe .billing-item-grip
     unten, Chat-Verlauf: "Positionszeilen per Drag&Drop sortierbar machen"). */
  grid-template-columns: 18px 74px 84px 84px minmax(160px, 1fr) 96px 68px 84px 28px;
  gap: 6px;
  align-items: start;
}
.billing-item-header {
  align-items: center;
  font-size: var(--text-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  padding: 0 2px;
}
.billing-item-row input { width: 100%; }
/* Bezeichnung als Textarea statt einzeiligem Input (Chat-Verlauf 28.08.2026:
   "Die Felder für die langen Positionen / Infos sind nur einzeilig, das wäre
   gut, wenn man den ganzen Text lesen könnte.").

   Im RUHEZUSTAND jetzt genauso hoch wie die einzeiligen Nachbarn
   (31.08.2026: "Das Textfeld Bezeichnung ist standardmäßig höher als die
   anderen, das sieht auch nicht gut aus") - vorher standen hier fest zwei
   Zeilen, und die Zeile war dadurch immer 18px höher als nötig, auch wenn
   nur „Regie" drinstand. Die Höhe kommt jetzt von autoHoehe() in
   invoicing.js/productions.js und wächst mit dem Text; die Werte hier sind
   der Ruhezustand und zugleich die Rückfallebene, falls das JS nie läuft.

   38px ist nicht geraten: .field input hat 9px Polsterung oben und unten,
   1px Rahmen und 13.5px Schrift mit Zeilenhöhe 1.3 - macht 18 + 2 + 17.6.
   Deshalb hier auch KEIN font: inherit mehr; das holte die 14px des
   Elternteils zurück und war ein Teil des Höhenunterschieds. */
.billing-item-row textarea {
  width: 100%;
  height: 38px;
  min-height: 38px;
  resize: none;
  overflow-y: auto;
  font-family: inherit;
  font-size: 13.5px;
  line-height: 1.3;
}
.billing-item-row-vat { display: flex; align-items: center; gap: 3px; }
.billing-item-row-vat::after { content: "%"; color: var(--color-text-muted); font-size: var(--text-meta); }
.billing-items-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* Ziehbarer Griff zum Umsortieren der Positionszeilen (Chat-Verlauf: "Was
   ich in Fastbill auch oft brauche, ist das Verschieben von Zeilen ... legt
   man unten an und zieht sie dann per Maus an den richtigen Platz.") -
   gleiches visuelles Muster wie .catalog-grip in servicecatalog.js, rein
   client-seitiges Umsortieren (kein eigener Persistenz-Aufruf nötig, die
   Reihenfolge wird erst beim Absenden/Entwurf-Speichern aus der DOM-
   Reihenfolge gelesen). */
.billing-item-grip {
  cursor: grab;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
  touch-action: none;
  /* Die Zeile selbst steht auf align-items: start, damit die Felder oben
     bündig bleiben, wenn das Bezeichnungsfeld mitwächst. Der Griff gehört
     aber zur ganzen Zeile, nicht zu deren erster Textzeile - oben in der
     Ecke sah er aus wie verrutscht (31.08.2026: "die Handles vorne sitzen
     nicht vertikal mittig in einer Zeile"). Gleiches gilt fürs Kreuz auf
     der anderen Seite: beide sind Handlungen an der ZEILE. */
  align-self: center;
}
.billing-item-row > button { align-self: center; }
.billing-item-grip:active { cursor: grabbing; }
.billing-item-row.is-dragging { opacity: 0.4; }
.billing-item-row.is-dragover { box-shadow: inset 0 2px 0 var(--color-accent); }

/* Gruppen-/Zwischensummen-Zeile (Chat-Verlauf 08.08.2026: "Option
   Zwischensummen/Gruppen einzurichten").

   Die erste Fassung war eine graue Fläche mit etwas fetterer Schrift - zu
   leise: in einer Liste aus 30 Positionen sah eine Gruppe aus wie eine
   Zeile, bei der jemand die Zahlen vergessen hatte (31.08.2026: "Vielleicht
   kannst du die Zwischensummen auch visuell besser hervorheben - evtl.
   Badges bzw. farblich hervorgehoben"). Jetzt drei Signale statt einem:
   Akzentfläche, ein Balken an der linken Kante und ein Abzeichen vor dem
   Titel. Der Abstand oben ist bewusst größer als unten - eine Überschrift
   gehört zu dem, was UNTER ihr steht. */
.billing-item-row.is-group {
  background: var(--color-accent-soft);
  border-left: 3px solid var(--color-accent);
  border-radius: 6px;
  padding: 6px 6px 6px 3px;
  margin: 14px 0 4px;
}
/* Die erste Zeile der Liste braucht den trennenden Abstand nach oben nicht. */
.billing-items-table > .billing-item-row.is-group:first-child { margin-top: 2px; }
/* Abzeichen und Titel teilen sich EINE Gitterzelle (siehe billingGroupRow()
   in invoicing.js/productions.js). */
.billing-group-main { display: flex; align-items: center; gap: 8px; min-width: 0; }
.billing-group-main input { flex: 1; min-width: 0; }
/* Die Zwischensumme der Gruppe, rechts in derselben Zelle - dort, wo in den
   Zeilen darunter die Beträge stehen (01.09.2026, Nutzer: "Kann es sein dass
   die 'Gruppe/Zwischensumme' gar keine Zwischensumme anzeigt, sondern nur
   einen simplen Titel?"). Sie stand bis dahin nur unten im Summenblock, am
   anderen Ende des Formulars. Leer, solange unter der Überschrift noch
   nichts steht - eine 0,00 € an einer frisch angelegten Gruppe sieht aus wie
   ein Fehler. */
.billing-group-sum {
  flex: none;
  font-size: 12.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--color-accent-strong, var(--color-text));
  padding-left: 8px;
  white-space: nowrap;
}
.billing-group-badge {
  flex: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: var(--radius-full);
  /* NICHT weiß auf --color-accent: gemessen sind das 3.85:1 im Hellmodus und
     2.37:1 im Dunkelmodus - beides unter den 4.5:1, die kleine Schrift
     braucht, und im Dunkeln ein regelrecht blendender Fleck. Stattdessen das
     Token-PAAR strong/soft: das ist genau dafür gedacht, in jedem der acht
     Akzent-Themen und in beiden Modi ein dunkles und ein helles Ende zu
     liefern - im Hellmodus hell auf dunkel, im Dunkelmodus dunkel auf hell. */
  background: var(--color-accent-strong);
  color: var(--color-accent-soft);
  white-space: nowrap;
}
.billing-item-row.is-group input {
  border: none;
  background: transparent;
  font-weight: 600;
  font-size: 13.5px;
  color: var(--color-accent-strong, var(--color-text));
  padding-left: 2px;
}
.billing-item-row.is-group input::placeholder { color: var(--color-text-faint); font-weight: 500; }

/* Live-Summenzeile unter der Positionstabelle (Chat-Verlauf: "immer unten
   eine Live-Anzeige der Gesamtsumme, netto, rabatt, brutto usw."). */
.billing-totals {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 4px 0 var(--space-3);
  padding: 8px 10px;
  background: var(--color-bg-subtle);
  border-radius: 8px;
  font-size: 12.5px;
}
.billing-totals-row { display: flex; justify-content: space-between; gap: var(--space-3); }
.billing-totals-row.is-grand {
  font-weight: 700;
  font-size: 13.5px;
  margin-top: 3px;
  padding-top: 5px;
  border-top: 1px dashed var(--color-border);
}
.billing-group-subtotals {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 3px;
  padding-bottom: 5px;
  border-bottom: 1px dashed var(--color-border);
}
.billing-totals-row.billing-group-subtotal { color: var(--color-text-muted); font-size: var(--text-meta); }

/* Fortschritts-Log im "FastBill-Historie importieren"-Dialog (siehe
   openImportModal() in invoicing.js). */
.import-log {
  font-size: 12px;
  font-family: var(--font-mono, ui-monospace, monospace);
  max-height: 240px;
  overflow-y: auto;
  background: var(--color-bg-subtle);
  border-radius: 8px;
  padding: 10px 12px;
  margin-top: 8px;
  white-space: pre-line;
  color: var(--color-text-muted);
}

/* Leistungskatalog (siehe servicecatalog.js) - gruppierte Tabelle, gleicher
   farbig abgesetzter Kategorie-Header wie die Karten im Aufklapp-Panel. */
.catalog-group { margin-bottom: var(--space-5); }
.catalog-group:last-child { margin-bottom: 0; }
.catalog-group-header {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-accent-strong);
  background: var(--color-accent-soft);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-3);
  /* UX-Audit 2026-08-16 (Prompt 2, Finding #8): war zuvor als zweite,
     separate Regel weiter unten definiert - hierher zusammengeführt. */
  cursor: default;
}
/* Mobile-Komplettaudit: min-width sorgt dafür, dass die 7 Spalten unterhalb
   dieser Breite echt horizontal scrollen (siehe .table-scroll-Wrapper in
   servicecatalog.js) statt sich unleserlich in die Bildschirmbreite zu
   quetschen. */
.catalog-table { width: 100%; min-width: 640px; border-collapse: collapse; font-size: var(--text-body); }
.catalog-table th {
  text-align: left;
  font-size: var(--text-small);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  padding: 4px 10px;
  font-weight: 700;
}
.catalog-table td { padding: 7px 10px; border-bottom: 1px solid var(--color-border); vertical-align: top; }
.catalog-table tr:last-child td { border-bottom: none; }
.catalog-table th.text-right, .catalog-table td.text-right { text-align: right; }

/* Drag&Drop-Griff (gleiches Muster wie .shotdoc-grip in shotdocs.js) und
   Inline-Bearbeiten-Inputs (Chat-Verlauf: "Felder direkt bearbeiten ...
   indem man reinklickt") - randlose Inputs, die wie normaler Text wirken,
   bis man reinklickt. */
.catalog-grip-cell { width: 26px; padding-left: 4px !important; padding-right: 0 !important; }
.catalog-grip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  color: var(--color-text-faint);
  cursor: grab;
  border-radius: var(--radius-sm);
}
.catalog-grip:hover { color: var(--color-text-muted); background: var(--color-surface-hover); }
.catalog-grip svg { width: 15px; height: 15px; }
.catalog-row.is-dragging, .catalog-group-header.is-dragging { opacity: 0.4; }
.catalog-row.is-dragover { box-shadow: inset 0 2px 0 var(--color-accent); }
.catalog-group-header.is-dragover { box-shadow: inset 0 0 0 2px var(--color-accent); }
.catalog-group-dropzone { height: 10px; }
.catalog-group-dropzone.is-dragover { height: 26px; border-radius: var(--radius-sm); box-shadow: inset 0 0 0 2px var(--color-accent); background: var(--color-accent-soft); }
/* Auf-/zuklappbar (siehe groupBlock() in servicecatalog.js) - animiert über
   denselben grid-template-rows 0fr->1fr-Trick wie .datatable-expanded-grid/
   .booking-items-group-collapse (Chat-Verlauf: "diese softe Animation beim
   auf- und zuklappen überall auf der Seite integrieren"). Der Grid-Item
   selbst (.catalog-group-collapse-inner) trägt bewusst KEIN eigenes
   Padding/Border (das lebt auf .table-scroll/.catalog-group-dropzone
   darunter) - sonst kann die Zeile nie ganz auf 0 zusammenfallen, siehe
   Kommentar bei .datatable-expanded-grid. */
.catalog-group-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow) var(--ease);
}
.catalog-group-collapse.is-open { grid-template-rows: 1fr; }
.catalog-group-collapse-inner { overflow: hidden; min-height: 0; }
.catalog-chevron { display: inline-flex; flex-shrink: 0; color: var(--color-text-faint); }
.catalog-chevron svg { width: 15px; height: 15px; transition: transform var(--dur-fast) var(--ease); }
.catalog-chevron.is-collapsed svg { transform: rotate(-90deg); }
.catalog-inline-input {
  width: 100%;
  border: 1px solid transparent;
  background: transparent;
  font: inherit;
  color: inherit;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
}
/* Preis/MwSt-Zellen (Chat-Verlauf: "Diese Spalten sehen außerdem komisch
   aus") - Input + Einheit ("€"/"%") nebeneinander in fester, schmaler
   Breite statt volle Zellenbreite, sonst umbricht die Einheit in eine neue
   Zeile sobald die Spalte schmaler wird. */
.catalog-num-cell { display: inline-flex; align-items: center; justify-content: flex-end; gap: 3px; white-space: nowrap; }
.catalog-num-cell .catalog-inline-input { width: 56px; text-align: right; flex: none; }
.catalog-num-cell > span { color: var(--color-text-faint); font-size: 12px; }
/* MwSt-Zelle ist ein <select> (feste Auswahl statt Freitext, siehe
   editableCell() in servicecatalog.js) - ohne eigenes appearance:none
   rendert v.a. macOS Safari dafür sein natives Auf/Ab-Pfeilchen-Icon
   zwischen Zahl und "%"-Zeichen (Chat-Verlauf: "diese zwei Pfeile sind
   zwischen Zahl und Einheit - kannst du die nicht einfach weglassen?") -
   komplett entfernt statt durch ein eigenes Chevron ersetzt, damit es
   optisch wie das benachbarte Preisfeld wirkt (reiner Text, kein sichtbarer
   Dropdown-Indikator). */
select.catalog-inline-input {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
}
/* text-align wird vom eigentlichen Options-Text eines <select> in Chrome/
   Safari meist ignoriert - die feste 56px-Breite oben (früher für die jetzt
   entfernten Browser-Pfeile gedacht) ließ die Zahl dadurch sichtbar zu weit
   vom "%"-Zeichen entfernt stehen (Chat-Verlauf: "Mach das MWST Feld bitte
   rechtsbündig, damit die Zahl näher am Prozentzeichen steht"). Breite
   stattdessen auf den Inhalt schrumpfen lassen statt auf Text-Ausrichtung zu
   setzen - rückt die Zahl so verlässlich nah ans "%" heran, unabhängig vom
   Browser. Selektor braucht dieselbe Spezifität wie die 56px-Regel oben
   (zwei verkettete Klassen), sonst gewinnt die dort weiterhin. */
.catalog-num-cell select.catalog-inline-input {
  width: auto;
  min-width: 0;
  padding-right: 2px;
  text-align: right;
  text-align-last: right;
}
.catalog-inline-input:hover { background: var(--color-surface-hover); }
.catalog-inline-input:focus {
  outline: none;
  border-color: var(--color-accent);
  background: var(--color-bg);
}

td.text-right .catalog-inline-input { text-align: right; }
/* Zahlenfeld ohne Browser-Stepper-Pfeile (Chat-Verlauf: "Wir brauchen da
   diese Stepper nicht, einfach ein Textfeld.") - wirkt optisch wie ein
   normales Textfeld, behält aber die numerische Tastatur/Validierung von
   type="number". Klasse ist bewusst generisch (nicht an .catalog-*
   gebunden), damit sie auch außerhalb des Leistungskatalogs wiederverwendbar
   ist. */
.no-spinner::-webkit-outer-spin-button,
.no-spinner::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.no-spinner[type='number'] { -moz-appearance: textfield; appearance: textfield; }

/* Freigabe-Modul (siehe approvalsSection() in productions.js) - Zeitleiste
   aus Preview/Feedback/Freigabe-Einträgen + kompaktes Inline-Formular.
   Bug-Report: "generell einen Counter einbauen, so dass bei mehrfachem
   Auftreten eines Menüpunkts [...] ein kleines Badge mit der Zahl daneben
   steht [...] erst beim zweiten Auftreten [...] betrifft alle Punkte außer
   eingehende Dinge wie Feedback erhalten oder Freigabe erhalten [...] Den
   Counter oben (Predproduction, Postproduction) können wir dann entfernen."
   - ersetzt den vormaligen separaten Runden-Zähler (.expand-approval-rounds/
   .approval-round*, siehe approvalRoundsBar() in productions.js, inzwischen
   entfernt) durch eine kleine Nummer direkt am jeweiligen Eintrag - runde,
   neutrale Zahl statt Pill, damit sie sich von der ×N-Zusammenfassung
   (.expand-approval-count) klar unterscheidet: ×N heißt "diese Zeile
   fasst N Einträge zusammen", die Zahl hier heißt "der wievielte Eintrag
   dieser Art insgesamt". */
.expand-approval-occurrence {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: 10px;
  font-weight: 700;
}
/* Vertikale Timeline (Chat-Verlauf: "Kannst du den Freigabe/Feedback Block
   visuell genauso aufbauen wie auf dem Client Dashboard?") - an
   .dash-timeline* in clientdashboard.php angeglichen: durchgehende Linie
   links, farbige Punkte je Etappen-Ton statt gerahmter Karten-Zeilen. Hier
   weiterhin editierbar, deshalb zusätzlich .expand-approval-actions in der
   Kopfzeile (die reine Kunden-Ansicht hat das nicht). */
.expand-approval-list { position: relative; padding: 4px 4px 4px 34px; margin-bottom: var(--space-3); }
.expand-approval-line { position: absolute; left: 15px; top: 14px; bottom: 14px; width: 2px; background: var(--color-border); }
.expand-approval-row { position: relative; padding-bottom: 16px; }
.expand-approval-row:last-child { padding-bottom: 0; }
.expand-approval-icon {
  position: absolute;
  left: -34px;
  top: 0;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--color-surface);
  box-shadow: 0 0 0 1px var(--color-border);
}
.expand-approval-icon svg { width: 13px; height: 13px; }
.expand-approval-icon.tone-accent { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.expand-approval-icon.tone-warning { background: var(--color-warning-soft); color: var(--color-warning); }
.expand-approval-icon.tone-success { background: var(--color-success-soft); color: var(--color-success); }
.expand-approval-icon.tone-neutral { background: var(--color-bg-alt); color: var(--color-text-muted); }
.expand-approval-body { flex: 1; min-width: 0; }
/* Bug-Report per Screenshot: "Keine unnötigen Umbrüche für die Icons rechts
   bitte" - .expand-approval-toprow selbst bricht NIE um (flex-wrap:nowrap),
   dadurch bleiben die Action-Icons (flex-shrink:0) immer auf der ersten
   Zeile rechts fixiert. Nur .expand-approval-top (Art/Zeit/Kürzel) darf bei
   wenig Platz intern umbrechen, siehe approvalRow()/approvalGroupRow() in
   productions.js für die zugehörige Markup-Struktur. */
.expand-approval-toprow { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
.expand-approval-top { flex: 1; min-width: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; font-size: 12.5px; }
.expand-approval-kind { font-weight: 600; color: var(--color-text); }
.expand-approval-time { color: var(--color-text-faint); font-size: 11.5px; }
.expand-approval-by {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  font-size: 9.5px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.expand-approval-actions { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; margin-left: auto; }
.expand-approval-note { margin-top: 4px; font-size: 12.5px; color: var(--color-text-muted); overflow-wrap: break-word; }
/* Gruppierte Einträge (Chat-Verlauf: "Freigabe & Feedback auch wie auf dem
   Client Dashboard zusammenfassen bei gleichen Einträgen damit die Box
   kompakter wird") - ×N-Badge im gleichen Look wie .dash-timeline-count in
   clientdashboard.php, Aufklapp-Pfeil rotiert wie bereits an anderer Stelle
   im Projekt üblich (siehe .chat-conv-section-label.is-toggle.is-expanded). */
.expand-approval-count {
  font-weight: 700;
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 999px;
  line-height: 1.5;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
}
.expand-approval-toggle svg { transition: transform var(--dur-fast) var(--ease); }
.expand-approval-toggle.is-expanded svg { transform: rotate(180deg); }
.expand-approval-subgroup { margin-top: 6px; padding-left: 14px; border-left: 2px solid var(--color-border); display: flex; flex-direction: column; gap: 12px; }
.expand-approval-subgroup .expand-approval-row { padding-bottom: 0; }
/* Raster statt umbrechender Reihe (28.08.2026, Nutzer: "alles schoener
   anordnen"). Vorher flex mit flex-wrap: in der schmalen Panel-Spalte kippte
   jedes Feld auf eine eigene Zeile - fuenf gestapelte Kaesten fuer eine
   Eingabe, die aus einer Auswahl und einem Satz besteht.

   Oben die drei kurzen Angaben nebeneinander, darunter Notiz und Knopf. Die
   Zuordnung laeuft ueber Klassen, nicht ueber :nth-child - sonst waere sie
   beim naechsten eingeschobenen Feld still falsch. */
.expand-approval-form {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 0.75fr);
  align-items: center;
  gap: 8px;
  padding-top: var(--space-3);
  border-top: 1px dashed var(--color-border);
}
.expand-approval-form .approval-f-note { grid-column: 1 / 3; }
.expand-approval-form .approval-f-add { grid-column: 3 / 4; justify-content: center; }
/* Unter 520px reicht die Breite fuer drei Spalten nicht mehr - dann Notiz
   und Knopf nebeneinander, die drei Angaben darueber in zwei Zeilen. */
@media (max-width: 520px) {
  .expand-approval-form { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .expand-approval-form .approval-f-kind { grid-column: 1 / -1; }
  .expand-approval-form .approval-f-note { grid-column: 1 / -1; }
  .expand-approval-form .approval-f-add { grid-column: 1 / -1; }
}
/* Bug-Report ("Unter Produktionen->Freigabe ist das Dropdown nicht mehr
   gestylt"): dieses Formular sitzt bewusst NICHT in einem .field-Wrapper
   (kompaktes Inline-Formular statt Formularzeilen mit Labels, siehe
   Kommentar bei approvalAddForm() in productions.js) - dadurch griff die
   allgemeine .field select/input-Gestaltung (Rahmen, Hintergrund, eigener
   Pfeil statt OS-Chrome) hier nie, nur die font-size/padding-Overrides
   unten. Gleiches, bereits an anderer Stelle etabliertes Muster wie bei
   .filter-bar select (siehe dortiger Kommentar am Dateiende) - hier separat
   wiederholt statt über .field, aus demselben Grund. */
.expand-approval-form select,
.expand-approval-form input[type="date"],
.expand-approval-form input[type="time"],
.expand-approval-form input[type="text"],
.expand-approval-form .approval-f-add {
  box-sizing: border-box;
  height: 34px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  font-family: inherit;
  padding: 0 9px;
  background: var(--color-surface-2);
  color: var(--color-text);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.expand-approval-form select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 30px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239598ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 13px 13px;
  cursor: pointer;
}
/* Bug-Report: "In Safari sind die Input-Felder immer noch sehr
   unterschiedlich, teilweise auch zu breit." - Root Cause: Safari rendert
   input[type=date]/[type=time] mit eigenen internen UA-Paddings/-Höhen und
   zentriert den Wert standardmäßig, während select/input[type=text] linksbündig
   sind und die oben gesetzte Höhe/Padding 1:1 übernehmen - dieselbe CSS-
   Regel sieht dadurch in Safari pro Feldtyp unterschiedlich groß aus, obwohl
   Breite/Padding/Höhe identisch gesetzt sind. appearance:none entfernt
   Safaris natives Eigen-Padding der Datum-/Uhrzeit-Segmente, text-align:left
   gleicht die Ausrichtung an die anderen Felder an. */
.expand-approval-form input[type="date"],
.expand-approval-form input[type="time"] {
  -webkit-appearance: none;
  appearance: none;
  text-align: left;
  /* Die Breite nachgereicht (03.09.2026). Am 27.08. wurde hier die
     Ausrichtung repariert, die Eigenbreite von WebKit aber nicht - und
     genau die hat der Nutzer heute im Aufgaben-Dialog gemeldet ("das Feld
     im Bild ist breiter als die anderen"). Dass es diese Zeilen hier auch
     braucht, hat nicht mein Blick gefunden, sondern der Waechter in
     e2e/formularfelder.cjs, Abschnitt 5: er verlangt von JEDEM Kontext,
     der Datumsfelder gestaltet, alle vier Eigenschaften - und hat diese
     Stelle beim ersten Lauf gemeldet.
     Diese Leiste ist eine Flexbox, hier reisst ein nicht schrumpfbares
     Feld die ganze Zeile auf, nicht nur sich selbst. */
  min-width: 0;
  max-width: 100%;
}
.expand-approval-form select:hover,
.expand-approval-form input[type="date"]:hover,
.expand-approval-form input[type="time"]:hover,
.expand-approval-form input[type="text"]:hover {
  border-color: var(--color-accent);
}
.expand-approval-form select:focus,
.expand-approval-form input[type="date"]:focus,
.expand-approval-form input[type="time"]:focus,
.expand-approval-form input[type="text"]:focus {
  outline: none;
  border-color: var(--color-accent);
  background-color: var(--color-surface);
  box-shadow: var(--shadow-glow);
}
/* Die frueheren max-width-Angaben fuer Datum/Uhrzeit sind entfallen - im
   Raster bestimmen die Spalten die Breite, und zwei konkurrierende Quellen
   fuer dieselbe Groesse sind eine zu viel. */

/* Category groups for the read-only Packliste (see itemsSection() in
   bookings.js) - same top-level-category grouping as the editable booking
   form's groupedItemsPanel(), just without any drag/collapse controls here
   since this view is read-only. */
.expand-item-groups { display: flex; flex-direction: column; gap: 14px; }
.expand-item-group-header {
  font-size: var(--text-small);
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  margin-bottom: 6px;
}
.expand-item-list { display: flex; flex-direction: column; gap: 6px; }
.expand-item-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
}
.expand-item-name { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.expand-item-name-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.expand-item-client-note {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px; font-size: var(--text-meta); font-style: italic;
  color: var(--color-text-muted); white-space: normal;
}
.expand-item-client-note svg { width: 12px; height: 12px; flex-shrink: 0; }
/* Erledigt-Kennzeichnung für Kundenanmerkungen (siehe
   migration_booking_client_notes_resolved.sql) - note text stays visible
   (never deleted), just visually de-emphasized once staff have dealt with
   it, on both the global note (.is-resolved-note on the summary field's
   span) and each per-item note. */
.is-resolved-note { color: var(--color-text-faint); text-decoration: line-through; text-decoration-color: var(--color-border-strong); }
.expand-panel-field-value span.is-empty { color: var(--color-text-faint); }
.expand-item-status { flex-shrink: 0; display: flex; gap: 6px; }

/* Read-only "Inhalt" list for a Physische-Kombination (kit) row here too -
   same content as itemRow()'s kitContents in the booking form, just
   visually attached under the item card instead of its own row. */
.expand-item-block:has(.expand-item-kit-contents) .expand-item-row {
  border-bottom: none;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.expand-item-kit-contents {
  padding: 6px 10px 8px;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-top: none;
  border-bottom-left-radius: var(--radius-sm);
  border-bottom-right-radius: var(--radius-sm);
  font-size: 11px;
  color: var(--color-text-faint);
}
.expand-item-kit-contents-label { font-weight: 600; color: var(--color-text-muted); margin-bottom: 2px; }
.expand-item-kit-contents-list { margin: 0; padding-left: 16px; list-style: disc; }
.expand-item-kit-contents-list li { padding: 1px 0; }

.tabs { display: flex; gap: var(--space-2); border-bottom: 1px solid var(--color-border); margin-bottom: var(--space-4); }
.tab {
  padding: var(--space-2) var(--space-3);
  font-weight: 600;
  color: var(--color-text-muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--dur-fast) var(--ease);
}
.tab.active { color: var(--color-accent); border-color: var(--color-accent); }

.year-pills { display: flex; gap: 6px; margin-bottom: var(--space-4); }
.year-pill {
  padding: 7px 15px;
  border-radius: var(--radius-full);
  font-weight: 600;
  font-size: 12.5px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  color: var(--color-text-muted);
  transition: all var(--dur-fast) var(--ease);
}
.year-pill:hover { border-color: var(--color-accent); color: var(--color-accent); }
.year-pill.active {
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-2) 130%);
  border-color: transparent;
  color: #fff;
  box-shadow: 0 6px 14px -4px rgba(0, 142, 171, 0.38);
}

/* ---------- To-Do Kanban board ---------- */

/* Nutzt die verfügbare Bildschirmhöhe statt nur so hoch wie der Inhalt zu
   sein - auf großen Monitoren wirkte das Board davor sehr "abgeschnitten"
   mit viel Leerraum darunter. Der Offset ist eine Annäherung an Header +
   Main-Padding (kein exaktes Messen möglich ohne JS/ResizeObserver, siehe
   chart.js-Kommentar zu warum das hier bewusst vermieden wird); min-height
   statt height, damit der Bereich bei vielen Karten trotzdem mitwächst. */
/* Zwei Spalten (Eigene/Team) statt vormals drei Status-Spalten (siehe
   Chat-Verlauf: "Offen/In Arbeit besser Eigene/Team machen") -
   kanban-board-single (Nicht-Admins, siehe todos.js updateBoard()) fällt auf
   eine einzelne, volle Breite zurück, weil "Team" für sie strukturell immer
   leer wäre (die API liefert ihnen ohnehin nur die eigenen Aufgaben). */
.kanban-board {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
  align-items: stretch;
  min-height: calc(100vh - 260px);
}
.kanban-board.kanban-board-single { grid-template-columns: 1fr; max-width: 640px; }

.kanban-column {
  /* min-width:0 ist hier Pflicht, nicht Feinschliff (04.09.2026).
     -----------------------------------------------------------------
     Ein Grid-Element hat als Mindestbreite seinen min-content - und
     solange die Inhaltszeile einer Aufgabe auf white-space:nowrap stand,
     war ihr min-content die GANZE Zeile. Eine Kundenanmerkung mit 260
     Zeichen hat die Spalte damit auf 1647 px aufgeblasen, das Board aus
     dem Fenster geschoben und die ganze Seite seitlich scrollen lassen.
     Gemeldet als "scheint als wuerde der Content keinen Zeilenumbruch
     haben" - das Symptom war der fehlende Umbruch, die Wirkung eine
     verschobene Seite.
     Die Zeile bricht seit demselben Tag um (siehe .todo-zeile-inhalt),
     das allein wuerde reichen. Diese Regel bleibt trotzdem: sie schuetzt
     die Spalte vor JEDEM kuenftigen Inhalt, der nicht umbrechen kann -
     ein langer Dateiname zum Beispiel. */
  min-width: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-top: 3px solid var(--kanban-accent, var(--color-border-strong));
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-sm);
}
.kanban-column-eigene { --kanban-accent: var(--color-accent); }
.kanban-column-team { --kanban-accent: var(--color-accent-2); }

.kanban-column-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-2) var(--space-3);
  font-weight: 700;
  font-size: 13.5px;
  color: var(--color-text);
}

.kanban-column-count {
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border-strong);
  color: var(--color-text-muted);
  font-size: var(--text-meta);
  font-weight: 700;
  padding: 1px 8px;
  border-radius: var(--radius-full);
}

/* flex: 1 lässt jede Spalte die volle Board-Höhe ausfüllen (statt am
   kürzesten Inhalt auszurichten) und scrollt bei vielen Karten intern,
   statt die ganze Seite in die Länge zu ziehen. */
.kanban-column-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
  min-height: 40px;
  overflow-y: auto;
  padding-right: 2px;
}

.kanban-column-empty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: var(--color-text-faint);
  font-size: 12.5px;
  padding: var(--space-4) 0;
}

/* Team-Spalte: nach Person gruppiert statt einer flachen Liste (siehe
   teamColumn()/todoGroup() in todos.js) - kanban-column-body-grouped
   deaktiviert das eigene Scrollen/flex:1 der äußeren .kanban-column-body
   (das übernehmen die Karten-Listen INNERHALB jeder Gruppe), damit
   Gruppenüberschriften nicht mitten im Scroll-Container "schweben". */
.kanban-column-body.kanban-column-body-grouped { overflow-y: visible; }
.todo-group { margin-bottom: var(--space-3); }
.todo-group:last-child { margin-bottom: 0; }
.todo-group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 6px 4px 8px;
}
/* Name in der Personenfarbe statt einheitlich grau (siehe assigneeColor() in
   todos.js) - deutlich kräftiger als die vorherige kleine, graue
   Versalien-Überschrift, damit man auf einen Blick sieht, wessen Gruppe man
   vor sich hat. */
.todo-group-name {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13.5px;
  font-weight: 700;
}
.todo-group-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.todo-group-empty {
  font-size: 12px;
  color: var(--color-text-faint);
  padding: var(--space-2) 4px var(--space-3);
}

/* "Kürzlich erledigt" - einklappbarer Streifen unterhalb des Boards (siehe
   recentlyDoneStrip() in todos.js), ersetzt die frühere dauerhafte
   "Erledigt"-Spalte. */
.todo-done-strip-wrap:empty { display: none; }
.todo-done-strip {
  margin-top: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.todo-done-strip-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: var(--space-3);
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--color-text-muted);
  text-align: left;
}
.todo-done-strip-toggle:hover { color: var(--color-text); }
.todo-done-strip-toggle svg { width: 14px; height: 14px; }
.todo-done-strip-body {
  display: flex;
  flex-direction: column;
  padding: 0 var(--space-3) var(--space-3);
}
.todo-done-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--color-border);
}
.todo-done-row-title {
  flex: 1;
  min-width: 0;
  font-size: var(--text-body);
  color: var(--color-text-muted);
  text-decoration: line-through;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* .kanban-card-delete ist standardmäßig auf opacity:0 und wird sonst nur
   über :hover auf .kanban-card (siehe dort) eingeblendet - eine
   Erledigt-Zeile ist aber keine .kanban-card, deshalb hier eigens sichtbar
   gemacht (leicht abgeblendet, volle Deckkraft erst bei Hover auf die Zeile). */
.todo-done-row .kanban-card-delete { opacity: 0.5; }
.todo-done-row:hover .kanban-card-delete { opacity: 1; }

/* Aus der Karte wurde eine ZEILE (04.09.2026).
   -----------------------------------------------------------------
   Nutzer: "extrem viel Weissraum, schwer zu erfassen [...] unnoetige
   Zeilenumbrueche".

   Gemessen an seinem Bildschirmfoto: 117 px je Aufgabe, elf Aufgaben
   1287 px. Danach: 34 px je Aufgabe ohne Zusatz, elf Aufgaben knapp
   400 px - ein Drittel Bildschirm statt drei.

   Der Rahmen faellt weg und wird eine Haarlinie: zwanzig gerahmte
   Kaesten untereinander sind zwanzig Objekte, eine Liste mit Trennlinien
   ist EIN Objekt mit zwanzig Zeilen. Genau das ist der Unterschied
   zwischen "durchsehen" und "ueberfliegen". Der Schatten geht mit -
   er hob jede Zeile einzeln hervor und damit keine. */
.kanban-card {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--color-border-soft, var(--color-border));
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  cursor: grab;
  transition: box-shadow var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.kanban-card:hover { background: var(--color-bg-alt); }
.kanban-card:last-child { border-bottom: 0; }

/* ---- Die Zeile selbst ---------------------------------------------
   Ein Raster, keine Flex-Kette: die Spalten sollen ueber ALLE Zeilen
   buendig stehen, auch wenn eine davon kein Datum oder keine Produktion
   hat. Mit flex-gap allein wandert das Datum je nach Titellaenge, und
   genau dieses Wandern macht eine Liste unlesbar.
   Die 1fr-Spalte traegt Titel und Produktion; min-width:0 daran ist
   Pflicht, sonst schiebt ein langer Titel Datum und Papierkorb hinaus,
   statt sich selbst zu kuerzen. */
.todo-zeile-haupt {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 22px;
}
.todo-zeile-mitte {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.todo-zeile-titel {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kanban-card.is-done .todo-zeile-titel { text-decoration: line-through; color: var(--color-text-muted); }
/* Rechtsbuendig, tabular-nums - sonst tanzen die Ziffern.
   Dass die Daten UNTEREINANDER stehen, leistet nicht diese Regel, sondern
   das flex:1 an .todo-zeile-mitte: die Mitte nimmt allen uebrigen Platz,
   und was danach kommt, sitzt in jeder Zeile an derselben Stelle. Hier
   stand zuerst ein min-width:62px "fuer die Buendigkeit" - gemessen am
   04.09.2026 aendert es daran nichts, und die Gegenprobe dazu blieb
   folgerichtig gruen. Eine Regel, die nichts tut, aber erklaert wird,
   ist schlimmer als keine: sie sieht beim naechsten Lesen nach Absicht
   aus. */
.todo-zeile-datum {
  flex: none;
  text-align: right;
  font-size: 11.5px;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.todo-zeile-datum.is-overdue { color: var(--color-danger); font-weight: 600; }
/* Ohne Datum bleibt die Spalte STEHEN, nur unsichtbar - sonst ruecken
   Papierkorb und Abzeichen bei jeder Zeile an eine andere Stelle. */
.todo-zeile-datum.is-leer { color: transparent; }
/* Der Inhalt: bis zu DREI Zeilen, danach "...". Der Einzug bringt ihn
   unter den Titel, nicht unter den Kreis.
   -----------------------------------------------------------------
   Zuerst stand hier eine einzige Zeile mit nowrap - und die war gleich
   doppelt falsch. Sichtbar: eine Kundenanmerkung ist oft ein ganzer
   Absatz, und eine Zeile davon sagt zu wenig. Unsichtbar: nowrap macht
   den min-content der Zeile so breit wie ihren gesamten Text, und weil
   die Spalte ein Grid-Element ist, wuchs sie mit - bis das Board aus dem
   Fenster ragte.
   Nutzer: "Zeilenumbrueche ja, bitte maximal drei Zeilen darstellen,
   danach ..."
   overflow-wrap:anywhere ist der Rest der Absicherung: die Anmerkungen
   beginnen mit Dateinamen wie 260902_BuechiAG_Maxiclave_Preview.mp4, und
   so ein Wort hat keine Trennstelle. Ohne die Zeile schoebe genau das
   die Spalte wieder auf. */
.todo-zeile-inhalt {
  margin: 2px 0 0 74px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--color-text-muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  overflow-wrap: anywhere;
  /* Der Wortlaut kommt aus comment_body - dem, was der Kunde ins
     Feedback-Feld getippt hat. Und der tippt zeilenweise, ein Timecode pro
     Zeile:

         S. 0:01 Buchi streichen nur miniPilot(R)
         s. 0:19 luecke vor Grad C
         S. 2:17: Buchi streichen

     HTML macht daraus von sich aus einen einzigen Fliesstext, weil ein
     Zeilenumbruch im Quelltext nur ein Leerzeichen ist. Genau so sah es
     aus: drei getrennte Anmerkungen, aneinandergehaengt zu einer
     unlesbaren Zeile (Nutzer, 04.09.2026: "Alles inline, keine
     Umbrueche").

     pre-line ist hier das Richtige und nicht pre oder pre-wrap: es behaelt
     die getippten Umbrueche, laesst aber lange Zeilen weiter normal
     umbrechen und schluckt die Einrueckungen, die in kopiertem Text gern
     mitkommen. Zusammen mit der Klammerung oben heisst das: drei
     Anmerkungen sind drei Zeilen, danach kommen die Punkte. */
  white-space: pre-line;
}
/* Der Prio-Punkt (siehe priorityDot() in todos.js) - 8 px statt einer
   Pille mit 80. */
.todo-prio-punkt {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: block;
}
.todo-checklist-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
}
.todo-checklist-badge svg { width: 12px; height: 12px; }
/* Kein Avatar heisst nicht "kein Platz": der Strich haelt die Spalte
   offen, damit die Titel aller Zeilen an derselben Kante beginnen. */
.todo-assignee.is-leer {
  background: transparent;
  color: var(--color-text-faint);
  box-shadow: inset 0 0 0 1px var(--color-border);
}
/* Der Papierkorb erscheint erst beim Zeigen - anders als beim
   Umbenennen-Stift am Rendering-Paket ist das hier richtig: Loeschen ist
   die seltenste und die einzige nicht umkehrbare Handlung der Zeile, und
   zwanzig Papierkoerbe untereinander laden zum Verklicken ein. Bei
   Tastaturfokus ist er sichtbar, sonst waere er nicht erreichbar. */
.kanban-card .kanban-card-delete { opacity: 0; flex: none; }
.kanban-card:hover .kanban-card-delete,
.kanban-card .kanban-card-delete:focus-visible { opacity: 1; }
/* ---- Der Gruppierungs-Umschalter ---- */
.todo-gruppierung {
  display: flex;
  gap: 2px;
  margin-left: 12px;
  padding: 2px;
  background: var(--color-bg-alt);
  border-radius: var(--radius-full);
}
.todo-gruppierung-knopf {
  border: 0;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 11.5px;
  font-weight: 500;
  font-family: inherit;
  padding: 3px 10px;
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.todo-gruppierung-knopf:hover { color: var(--color-text); }
.todo-gruppierung-knopf.is-aktiv {
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.kanban-card.is-dragging { opacity: 0.4; }
/* OS-Datei-Drop direkt auf die Karte (siehe todos.js todoCard()) - bewusst
   ein eigener Zustand statt is-dragging wiederzuverwenden: is-dragging
   markiert die *Quelle* eines internen Reorder-Drags (halbtransparent),
   is-file-dragover das *Ziel* eines externen OS-Datei-Drags (Akzent-Rahmen). */
.kanban-card.is-file-dragover { border-color: var(--color-accent); box-shadow: 0 0 0 2px var(--color-accent-soft); }

.kanban-card-delete {
  background: none;
  border: none;
  color: var(--color-text-faint);
  cursor: pointer;
  padding: 2px;
  display: inline-flex;
  border-radius: var(--radius-sm);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.kanban-card:hover .kanban-card-delete { opacity: 1; }
.kanban-card-delete:hover { color: var(--color-danger); }
.kanban-card-delete svg { width: 14px; height: 14px; }

/* Die Produktion an einer Aufgabenkarte (02.09.2026).
   Nutzer: "eventuell kann man die Sichtbarkeit von aufgaben noch
   verbessern, die einer Produktion zugeordnet sind."

   Vorher: display:flex ueber die volle Kartenbreite, --color-text-muted,
   Filmsymbol davor - das schwaechste Element auf der Karte, obwohl es beim
   Ueberfliegen des Boards meistens die entscheidende Information ist.
   Jetzt ein Abzeichen in der Farbe DIESER Produktion. Der Farbton kommt aus
   --prod-hue (produktionsHue() in todos.js, fest aus der Produktions-ID
   abgeleitet); Saettigung und Helligkeit stehen hier, getrennt fuer hell
   und dunkel - genau wie bei --sender-hue im Chat und aus demselben Grund:
   ein Ton, der auf Weiss lesbar ist, verschwindet auf dem dunklen Grund.

   inline-flex + align-self: flex-start, damit das Abzeichen nur so breit
   wird wie sein Inhalt - die Karte ist ein Flex-Container in Spalten-
   richtung, ein Kind ohne diese Zeile zoege sich ueber die volle Breite und
   saehe wieder aus wie eine Zeile. */
.kanban-card-production {
  display: inline-flex;
  align-self: flex-start;
  max-width: 100%;
  align-items: center;
  padding: 2px 9px;
  border-radius: var(--radius-full);
  font-size: var(--text-meta);
  font-weight: 560;
  letter-spacing: -0.005em;
  background: hsl(var(--prod-hue, 210) 62% 93%);
  color: hsl(var(--prod-hue, 210) 65% 30%);
  margin-bottom: var(--space-2);
  /* Ein langer Produktionstitel darf die Karte nicht auseinanderziehen -
     der volle Text steht ohnehin im title-Attribut. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
html[data-theme="dark"] .kanban-card-production {
  background: hsl(var(--prod-hue, 210) 42% 24%);
  color: hsl(var(--prod-hue, 210) 72% 79%);
}

/* "Heute" im Kopf der Aufgabenseite (02.09.2026, Nutzer: "ein Filter für
   Aufgaben 'Heute' wäre noch nice"). Gleiche Hoehe und Rundung wie die
   Knoepfe daneben; die Zahl dahinter sagt, wie viel ansteht, BEVOR man
   klickt - ein Filter, der sich erst nach dem Klick als leer herausstellt,
   ist ein vertaner Handgriff. */
.todo-heute-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding: 0 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--text-body);
  font-weight: 520;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.todo-heute-btn svg { width: 15px; height: 15px; color: var(--color-text-muted); }
.todo-heute-btn:hover { border-color: var(--color-accent); }
.todo-heute-btn.is-active {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-strong);
}
.todo-heute-btn.is-active svg { color: var(--color-accent-strong); }
.todo-heute-zahl {
  min-width: 20px;
  padding: 0 5px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  font-size: var(--text-small);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.todo-heute-btn.is-active .todo-heute-zahl { background: var(--color-accent); color: #fff; }

.todo-checklist-items { display: flex; flex-direction: column; gap: 3px; margin-bottom: var(--space-2); }
.todo-checklist-item {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: var(--text-meta);
  color: var(--color-text-muted);
  cursor: pointer;
}
.todo-checklist-item input[type="checkbox"] { width: 13px; height: 13px; margin-top: 1px; flex-shrink: 0; accent-color: var(--color-accent); cursor: pointer; }
.todo-checklist-item .is-done { text-decoration: line-through; color: var(--color-text-faint); }

.todo-files-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--color-text-faint);
}
.todo-files-badge svg { width: 12px; height: 12px; }

.todo-assignee {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-accent-soft);
  color: var(--color-accent-strong);
  font-size: var(--text-small);
  font-weight: 700;
}

/* Quick-complete Kreis (Board-Karte + Kalender-Chip) - bewusst dezent
   (leerer Kreis) bis man ihn hovert, damit er die Karte nicht dominiert. */
.kanban-card-check {
  background: none;
  border: 1.5px solid var(--color-border-strong);
  color: transparent;
  cursor: pointer;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  flex-shrink: 0;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.kanban-card-check svg { width: 11px; height: 11px; stroke-width: 3; }
.kanban-card-check:hover { border-color: var(--color-accent); background: var(--color-accent-soft); color: var(--color-accent); }

/* ---- Board/Kalender-Umschalter (Aufgaben) ------------------------- */
.view-toggle {
  display: inline-flex;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 3px;
  gap: 2px;
}
.view-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-weight: 600;
  padding: 7px 12px;
  border-radius: calc(var(--radius-md) - 3px);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.view-toggle-btn svg { width: 14px; height: 14px; }
.view-toggle-btn:hover { color: var(--color-text); }
.view-toggle-btn.is-active { background: var(--color-surface); color: var(--color-accent-strong); box-shadow: var(--shadow-sm); }

/* ---- Kalenderansicht (Aufgaben) ------------------------------------ */
.cal-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.cal-mode-switch {
  display: inline-flex;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 3px;
  gap: 2px;
}
.cal-mode-btn {
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-weight: 600;
  padding: 7px 14px;
  border-radius: calc(var(--radius-md) - 3px);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.cal-mode-btn:hover { color: var(--color-text); }
.cal-mode-btn.is-active { background: var(--color-surface); color: var(--color-accent-strong); box-shadow: var(--shadow-sm); }

.cal-nav { display: flex; align-items: center; gap: var(--space-2); }
.cal-nav svg { width: 15px; height: 15px; }
.cal-range-label { font-weight: 700; font-size: var(--text-entity-title); color: var(--color-text); margin-left: 6px; min-width: 150px; }

/* Füllt die Höhe genau wie das Board (siehe .kanban-board) - Zellen wirkten
   vorher gedrungen, weil die Grid-Reihen sich nur am Inhalt orientierten. */
.cal-body { display: grid; grid-template-columns: 1fr 240px; gap: var(--space-4); align-items: stretch; min-height: calc(100vh - 340px); }
.cal-body-day { grid-template-columns: 1fr 280px; }

.cal-month, .cal-week, .cal-day-view {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.cal-weekday-row { display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid var(--color-border); flex: 0 0 auto; }
/* Design-System-Cleanup 2026-08-16 (Phase 4b): font-size auf --text-small
   (10.5px) statt hardcoded 11px - Wochentags-Kürzel sind Mikro-Info wie das
   Label-Präfix in Tabellenzellen (siehe dort), gleiches Prinzip: bewusst
   klein UND fett, weil bei dieser Größe Fettung für Lesbarkeit sorgt statt
   Überbetonung zu signalisieren. */
.cal-weekday { padding: 9px 0; text-align: center; font-size: var(--text-small); font-weight: 700; color: var(--color-text-faint); text-transform: uppercase; letter-spacing: 0.03em; }

.cal-month-row { display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; }
.cal-month-row + .cal-month-row { border-top: 1px solid var(--color-border); }

.cal-week { display: grid; grid-template-columns: repeat(7, 1fr); grid-template-rows: 1fr; }

.cal-day {
  border-right: 1px solid var(--color-border);
  padding: 8px 7px;
  min-height: 100px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  transition: background var(--dur-fast) var(--ease);
}
.cal-day:last-child { border-right: none; }
.cal-week .cal-day { min-height: 420px; padding: 12px 10px; gap: 6px; }
.cal-day.is-muted { background: var(--color-bg-alt); }
.cal-day.is-muted .cal-day-number { color: var(--color-text-faint); }
.cal-day.is-dragover { background: var(--color-accent-soft); }

.cal-day-head { display: flex; align-items: center; justify-content: space-between; flex: 0 0 auto; }
.cal-day-weekday { font-size: var(--text-small); font-weight: 700; color: var(--color-text-faint); text-transform: uppercase; }
.cal-day-number { font-size: var(--text-body); font-weight: 700; color: var(--color-text); }
.cal-day.is-today .cal-day-number {
  background: var(--color-accent);
  color: #fff;
  width: 21px; height: 21px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
}

.cal-day-chips { display: flex; flex-direction: column; gap: 5px; flex: 1; overflow-y: auto; }
.cal-day-empty { color: var(--color-text-faint); font-size: var(--text-meta); }
.cal-day-more { font-size: var(--text-small); color: var(--color-text-muted); font-weight: 600; padding: 1px 2px; }

/* Gleiche Sprache wie die Kanban-Karte: weiße Fläche, dezenter Rahmen,
   Farbe nur als Akzent (linker Rand) statt vollflächig eingefärbt - genau
   das Feedback ("zu grün, passt nicht zum restlichen Look"). */
.cal-chip {
  --chip-color: var(--color-accent);
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--chip-color);
  border-radius: 8px;
  padding: 10px 9px;
  min-height: 44px;
  box-sizing: border-box;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--color-text);
  line-height: 1.3;
  cursor: grab;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.cal-chip:hover { box-shadow: var(--shadow-md); border-color: var(--color-border-strong); border-left-color: var(--chip-color); }
.cal-chip.is-dragging { opacity: 0.4; }

/* Monatsansicht: bewusst wieder kompakt (siehe Feedback) - hier geht es um
   die Übersicht über viele Tage, nicht ums präzise Greifen einzelner
   Chips wie in Wochen-/Tagesansicht oder der "Ohne Datum"-Ablage. */
.cal-chip.is-sm { gap: 5px; padding: 4px 6px; min-height: 0; border-left-width: 3px; font-size: var(--text-small); }
.cal-chip.is-sm .cal-chip-check { width: 14px; height: 14px; }
.cal-chip.is-sm .cal-chip-check svg { width: 8px; height: 8px; }
.cal-chip-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
.cal-chip-check {
  background: none;
  border: 1.5px solid var(--color-border-strong);
  color: transparent;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  flex-shrink: 0;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.cal-chip-check svg { width: 11px; height: 11px; stroke-width: 3.5; }
.cal-chip-check:hover { border-color: var(--color-accent); background: var(--color-accent-soft); color: var(--color-accent); }

.cal-day-view { padding: var(--space-4); flex: 1; }
.cal-day-list { display: flex; flex-direction: column; gap: var(--space-2); flex: 1; min-height: 120px; border-radius: var(--radius-md); transition: background var(--dur-fast) var(--ease); }
.cal-day-list.is-dragover { background: var(--color-accent-soft); }
.cal-day-empty-state { text-align: center; color: var(--color-text-faint); font-size: var(--text-meta); padding: var(--space-6) 0; }

.cal-unscheduled {
  background: var(--color-bg-alt);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
}
.cal-unscheduled-header { display: flex; align-items: center; justify-content: space-between; font-weight: 700; font-size: var(--text-body); margin-bottom: 6px; flex: 0 0 auto; }
.cal-unscheduled-list { display: flex; flex-direction: column; gap: 6px; flex: 1; min-height: 60px; border-radius: var(--radius-md); transition: background var(--dur-fast) var(--ease); overflow-y: auto; }
.cal-unscheduled-list.is-dragover { background: var(--color-accent-soft); }

.todo-checklist-editor { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-2); }
.todo-checklist-row { display: flex; align-items: center; gap: var(--space-2); }
.todo-checklist-row input[type="text"] { flex: 1; }
.todo-checklist-row input[type="checkbox"] { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--color-accent); }

/* ---------- Arbeitszeit (time tracking) ---------- */

.timer-card.is-running { border-color: var(--color-accent); box-shadow: var(--shadow-glow), var(--shadow-sm); }
.timer-elapsed {
  font-size: 40px;
  font-weight: 800;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--color-accent-strong);
}
.field-checkbox input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--color-accent); }

/* ---------- Vermietung: Verfügbarkeitskalender ---------- */

.avail-months {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
}

.avail-month-title { font-weight: 700; font-size: var(--text-body); margin-bottom: 8px; }

.avail-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 3px;
  margin-bottom: 3px;
}
.avail-weekdays span { text-align: center; font-size: 10px; color: var(--color-text-faint); font-weight: 600; }

.avail-week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 3px; }

.avail-day {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  font-size: var(--text-small);
  font-weight: 600;
  cursor: default;
}
.avail-day.is-empty { visibility: hidden; }

.avail-booking-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-3);
  align-items: center;
  padding: 7px 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 12px;
}
.avail-booking-row:last-child { border-bottom: none; }

/* ---------- Vermietung: Buchungspositionen (Packliste) ---------- */

.booking-items-list {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  max-height: 58vh;
  overflow-y: auto;
  /* Mobile-Audit - .booking-item-row hat eine feste 8-Spalten-Breite (siehe
     dort), auf schmalen Bildschirmen sonst seitlich abgeschnitten statt
     scrollbar. */
  overflow-x: auto;
}
/* Fixed shared column template - header and every item row (saved or not)
   use this exact grid, emitting an empty cell where a column doesn't apply
   (e.g. Raus/Zurück for a not-yet-saved item), so all 8 columns always line
   up regardless of which cells a given row actually fills in. */
.booking-item-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 50px 40px 68px 76px 50px 50px 30px;
  align-items: center;
  gap: var(--space-2);
  padding: 9px 12px;
  /* No border here anymore - see .booking-item-block below, which now owns
     it so the border sits correctly whether or not this row has a
     kit-contents block underneath it. */
}
.booking-item-row input[type="number"] { width: 100%; padding: 5px 6px; font-size: 12px; text-align: center; }
.booking-item-name { display: flex; align-items: center; gap: 5px; min-width: 0; font-weight: 600; font-size: 12.5px; }
.booking-item-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.booking-item-group-price { text-align: right; font-size: 12px; font-weight: 600; }
.booking-item-row > div:last-child { text-align: center; }

/* Drag&Drop-Sortierung (Positionen innerhalb einer Gruppe, siehe
   groupedItemsPanel()/itemRow() in bookings.js). Border moves onto the
   wrapper block itself (owns both the row and any kit-contents beneath it)
   instead of living on the row/kit-contents individually, so it stays
   correct regardless of which of the two happens to be visually last. */
.booking-item-block {
  border-bottom: 1px solid var(--color-border);
  cursor: grab;
}
.booking-item-block:active { cursor: grabbing; }
.booking-item-block:last-child { border-bottom: none; }
.booking-item-block.is-dragging { opacity: 0.4; }
.booking-item-grip {
  flex-shrink: 0;
  display: inline-flex;
  color: var(--color-text-faint);
}
.booking-item-grip svg { width: 13px; height: 13px; }

.booking-item-row.is-header {
  background: var(--color-bg-alt);
  font-size: var(--text-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  padding: 7px 12px;
  text-align: center;
}
.booking-item-row.is-header > div:first-child { text-align: left; }
/* All other header cells (Menge/Tage/Preis/Stk/Gruppe/Raus/Zurück)
   stay centered above their column via the inherited text-align: center
   above - including "Gruppe", even though its data cells below are
   right-aligned (.booking-item-group-price) to line up with the €
   amounts. A dedicated right-align override used to sit here for that
   column, but it made the header visibly hug the right edge instead of
   sitting centered over the column like every other header label. */

/* Read-only "Inhalt" reference list for a Physische-Kombination (kit) line -
   see itemRow() in bookings.js. Sits directly under the kit's own row,
   inside the same .booking-item-block, which owns the border between
   consecutive lines (see above). */
.booking-item-kit-contents {
  padding: 0 12px 9px 12px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--color-text-faint);
}
.booking-item-kit-contents-label { font-weight: 600; color: var(--color-text-muted); margin-bottom: 2px; }
.booking-item-kit-contents-list { margin: 0; padding-left: 16px; list-style: disc; }
.booking-item-kit-contents-list li { padding: 1px 0; }

.booking-items-total {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  border-top: 2px solid var(--color-border-strong);
  background: var(--color-bg-alt);
  font-size: var(--text-body);
  font-weight: 700;
  color: var(--color-text);
}

/* Zwischensumme / Versicherung / Rabatt / Gesamtpreis stack beneath the
   line items - only the final Gesamtpreis row is emphasized like the old
   single-row .booking-items-total was. */
.booking-items-totals {
  display: flex;
  flex-direction: column;
  border-top: 2px solid var(--color-border-strong);
  background: var(--color-bg-alt);
}
.booking-items-total-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 7px 12px;
  font-size: var(--text-body);
  color: var(--color-text-faint);
}
.booking-items-total-row.is-grand {
  font-weight: 700;
  color: var(--color-text);
  font-size: var(--text-entity-title);
  border-top: 1px solid var(--color-border);
  padding-top: 10px;
}
/* "davon Crew & Transport" - eine Erlaeuterungszeile, keine eigene Summe.
   Deshalb kleiner und eingerueckt: sie gehoert zur Zwischensumme darueber
   und soll nicht wie ein weiterer Posten aussehen, der noch dazukommt
   (02.09.2026, Nutzer: "der Gesamtrabatt gilt immer nur für Material, nicht
   für Personal"). */
.booking-items-total-row.is-hint {
  padding-top: 0;
  padding-bottom: 4px;
  padding-left: 24px;
  font-size: var(--text-meta);
  color: var(--color-text-muted);
}
.booking-items-total-row.is-discount input {
  width: 56px;
  text-align: right;
  margin: 0 4px;
  display: inline-block;
}

/* Booking form's material picker: left = full catalog browsable by
   category, right = this booking's added items, same category grouping on
   both sides (see materialCatalogPanel()/groupedItemsPanel() in bookings.js). */
.booking-material-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}

.material-panel { display: flex; flex-direction: column; min-width: 0; }
.material-panel h3 {
  font-size: var(--text-body);
  font-weight: 700;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.material-panel-count { color: var(--color-text-faint); font-weight: 500; }
.material-panel-collapse-links {
  margin-left: auto;
  display: flex;
  gap: 10px;
  font-size: 11px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
}
.material-panel-collapse-link { color: var(--color-text-faint); text-decoration: none; cursor: pointer; }
.material-panel-collapse-link:hover { color: var(--color-text); text-decoration: underline; }

.material-catalog {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: column;
  max-height: 58vh;
  overflow: hidden;
}
.material-catalog-search { position: relative; padding: 8px; border-bottom: 1px solid var(--color-border); flex-shrink: 0; }
.material-catalog-search svg {
  position: absolute; left: 20px; top: 50%; transform: translateY(-50%);
  width: 14px; height: 14px; color: var(--color-text-faint); pointer-events: none;
}
.material-catalog-search input {
  width: 100%;
  padding: 8px 10px 8px 32px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  font-size: var(--text-body);
  font-family: inherit;
  color: var(--color-text);
}
.material-catalog-list { flex: 1; overflow-y: auto; padding: 4px; }

/* --cat-color kommt als Inline-Style von categoryColor() in bookings.js -
   ein deterministischer Hash über den Kategorienamen, damit jede Kategorie
   (Audio, Grip, Power, ...) dauerhaft dieselbe Farbe behält, ohne eine feste
   Zuordnungsliste pflegen zu müssen. */
.material-catalog-group { border-left: 3px solid var(--cat-color, transparent); border-radius: var(--radius-sm); margin-bottom: 2px; }
.material-catalog-group summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 9px;
  font-size: var(--text-meta);
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
}
.material-catalog-group summary::-webkit-details-marker { display: none; }
.material-catalog-group summary:hover { background: var(--color-bg-alt); }
.material-catalog-group-name { display: flex; align-items: center; gap: 7px; color: var(--cat-color); }
.material-catalog-group-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--cat-color); flex-shrink: 0; }
.material-catalog-count { font-weight: 600; color: var(--color-text-faint); text-transform: none; letter-spacing: normal; }

.material-catalog-items { padding-left: 6px; }
.material-catalog-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  font-family: inherit;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}
/* flex:1, damit Badge + Plus-Icon (feste Breite) immer bündig rechts sitzen,
   unabhängig davon wie lang der jeweilige Artikelname ist - ohne dieses
   flex:1 auf dem Label würde justify-content:space-between (frühere Lösung
   mit nur 2 Kindern) das mittlerweile dazwischenliegende Badge irgendwo in
   der Mitte des verfügbaren Platzes platzieren statt direkt neben dem Namen. */
.material-catalog-item-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.material-catalog-item .badge { flex-shrink: 0; }
.material-catalog-item:hover { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.material-catalog-item svg { width: 13px; height: 13px; flex-shrink: 0; color: var(--color-text-faint); }
.material-catalog-item:hover svg { color: var(--color-accent-strong); }

.booking-items-groups { display: flex; flex-direction: column; }
/* Drag&Drop-Sortierung ganzer Gruppen (siehe groupedItemsPanel() in
   bookings.js) - draggable lives on .booking-items-group itself (dragstart
   only actually starts if the gesture began inside the header, see there),
   dragged via the header's grip handle. */
.booking-items-group.is-dragging { opacity: 0.4; }
.booking-items-group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 7px 12px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
  background: var(--color-bg-alt);
  border-bottom: 1px solid var(--color-border);
  cursor: grab;
}
.booking-items-group-header:active { cursor: grabbing; }
.booking-items-group-grip {
  display: inline-flex;
  vertical-align: middle;
  margin-right: 6px;
  color: var(--color-text-faint);
}
.booking-items-group-grip svg { width: 13px; height: 13px; }
.booking-items-group-meta { display: flex; align-items: center; gap: 10px; }
.booking-items-group-count { font-weight: 600; color: var(--color-text-faint); text-transform: none; letter-spacing: normal; }
.booking-items-group-subtotal { font-weight: 700; color: var(--color-text); text-transform: none; letter-spacing: normal; }

/* Auf-/zuklappbar (siehe groupedItemsPanel() in bookings.js) - the chevron
   rotates to point right while collapsed. .booking-items-group-body itself
   stays in the DOM (so drag&drop item-order reads via readDomOrder() keep
   working even for a currently-collapsed group) and is animated open/closed
   via the same grid-template-rows 0fr->1fr trick as .datatable-expanded-grid
   in style.css (Chat-Verlauf: "diese softe Animation beim auf- und
   zuklappen überall auf der Seite integrieren") - .booking-items-group-body
   has no padding/border of its own (that lives on its .booking-item-block
   children), so unlike the dataTable case it can act as the grid item
   directly without needing an extra content wrapper layer. */
.booking-items-group-toggle {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-right: 4px;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text-faint);
  cursor: pointer;
}
.booking-items-group-toggle svg { width: 14px; height: 14px; transition: transform var(--dur-fast) var(--ease); }
.booking-items-group-toggle:hover { color: var(--color-text); }
.booking-items-group.is-collapsed .booking-items-group-toggle svg { transform: rotate(-90deg); }
.booking-items-group-collapse {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--dur-slow) var(--ease);
}
.booking-items-group.is-collapsed .booking-items-group-collapse { grid-template-rows: 0fr; }
.booking-items-group-body { overflow: hidden; min-height: 0; }

/* ---------- searchSelect: searchable dropdown (replaces plain <select> for
   long option lists - Kunden, Agenturen, Produktionen, Ausrüstung) ---------- */

.searchselect { position: relative; display: block; width: 100%; }

.searchselect-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 11px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  color: var(--color-text);
  font-size: 13.5px;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.searchselect-trigger:hover { border-color: var(--color-text-faint); }
.searchselect-trigger:focus-visible,
.searchselect.is-open .searchselect-trigger {
  outline: none;
  border-color: var(--color-accent);
  background-color: var(--color-surface);
  box-shadow: var(--shadow-glow);
}

.searchselect-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.searchselect-label.is-placeholder { color: var(--color-text-faint); }
.searchselect-chevron { flex-shrink: 0; color: var(--color-text-faint); display: inline-flex; }
.searchselect-chevron svg { width: 14px; height: 14px; }

.searchselect-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  /* Labels can now be longer than the trigger itself (e.g. "Nummer · Kunde ·
     Titel" - see productionOptionLabel() in todos.js/bookings.js/etc.) and
     wrap onto multiple lines instead of being cut off with an ellipsis, so
     the panel needs a floor wider than a cramped half-width field-row. */
  min-width: 280px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: 150;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.searchselect-search {
  position: relative;
  padding: 8px;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}
.searchselect-search svg {
  position: absolute;
  left: 20px;
  top: 50%;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  color: var(--color-text-faint);
  pointer-events: none;
}
/* The panel (and this input) can end up nested inside a .field wrapper
   wherever searchSelect() is used in a form - the generic ".field input"
   rules (incl. the :focus variant, which wins on specificity since it adds
   a pseudo-class) would otherwise leak their own border/background/box-shadow
   in here too. !important pins down exactly the look this component wants,
   deliberately and narrowly, not as a general habit. */
.searchselect-search input,
.searchselect-search input:focus {
  width: 100%;
  padding: 8px 10px 8px 32px !important;
  border: 1px solid var(--color-border-strong) !important;
  border-radius: var(--radius-sm) !important;
  background: var(--color-surface-2) !important;
  font-size: var(--text-body);
  font-family: inherit;
  color: var(--color-text);
  outline: none !important;
  box-shadow: none !important;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.searchselect-search input:focus {
  border-color: var(--color-accent) !important;
  background: var(--color-surface) !important;
}

.searchselect-list { max-height: 240px; overflow-y: auto; padding: 4px; }

/* Zwischengruppen (Chat-Verlauf: "die Liste im Dropdown ist komplett
   chaotisch. Kannst du das korrekt sortieren und mit Zwischengruppen
   übersichtlicher machen? Ggfs. auch farblich besser gliedern.") - deutlich
   sichtbarer als vorher: eigener eingefärbter Streifen (Akzentfarbe, dezent)
   statt nur eines kleinen grauen Labels, plus Trennlinie zur vorherigen
   Gruppe. sticky, damit die aktuelle Gruppe beim Scrollen durch eine lange
   Liste (Leistungskatalog hat >10 Kategorien) oben sichtbar bleibt. */
.searchselect-group {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 4px -4px 2px;
  padding: 5px 13px;
  font-size: var(--text-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-accent-strong);
  background: var(--color-accent-soft);
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}
.searchselect-group:first-child { margin-top: -4px; border-top: none; }

.searchselect-option {
  padding: 8px 9px;
  border-radius: var(--radius-sm);
  font-size: var(--text-body);
  line-height: 1.35;
  cursor: pointer;
  white-space: normal;
  overflow-wrap: break-word;
}
.searchselect-option:hover { background: var(--color-bg-alt); }
.searchselect-option.is-selected { background: var(--color-accent-soft); color: var(--color-accent-strong); font-weight: 600; }

.searchselect-empty { padding: var(--space-4); text-align: center; font-size: 12.5px; color: var(--color-text-faint); }

/* ---------- Multi-select (checkbox dropdown) ---------- */
/* Same trigger/panel shell as .searchselect, but sized to sit inline next to
   a button in a page-header row instead of stretching full-width, and with
   checkbox rows that stay open across multiple picks instead of closing
   after each selection like a single-value dropdown would. */
.multiselect { width: auto; min-width: 200px; }
.multiselect-trigger { min-width: 200px; }
.multiselect-panel { left: 0; right: auto; min-width: 230px; }

.multiselect-list { max-height: 260px; padding: 6px; }

.multiselect-option {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 9px;
  border-radius: var(--radius-sm);
  font-size: var(--text-body);
  cursor: pointer;
}
.multiselect-option:hover { background: var(--color-bg-alt); }
.multiselect-option.is-selected { background: var(--color-accent-soft); color: var(--color-accent-strong); font-weight: 600; }
.multiselect-option input[type="checkbox"] {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  accent-color: var(--color-accent);
  cursor: pointer;
}
.multiselect-option span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.multiselect-clear {
  flex-shrink: 0;
  width: 100%;
  padding: 9px;
  border: none;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-family: inherit;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.multiselect-clear:hover { background: var(--color-bg-alt); color: var(--color-text); }
.multiselect-clear.is-hidden { display: none; }

/* ==========================================================
   Inventur (pages/inventory.js) - Kategorie-Auswahl/Verlauf auf der
   Landing-Seite, der Schritt-für-Schritt-Zählbildschirm mit der
   Swipe-Leiste, und der Abweichungs-Bericht am Ende.
   ========================================================== */

.inventory-category-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 6px 18px;
}
.inventory-category-grid .field-checkbox { white-space: nowrap; }
.inventory-category-all {
  grid-column: 1 / -1;
  font-weight: 700;
  padding-bottom: 8px;
  margin-bottom: 4px;
  border-bottom: 1px dashed var(--color-border);
}
.inventory-category-count { color: var(--color-text-faint); font-size: var(--text-meta); }

.inventory-session-list { display: flex; flex-direction: column; gap: 10px; }
.inventory-session-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.inventory-session-info { min-width: 0; flex: 1; }
.inventory-session-cats { font-weight: 700; font-size: 13.5px; }
.inventory-session-meta { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 2px; }
.inventory-session-progress {
  height: 5px;
  background: var(--color-bg-alt);
  border-radius: var(--radius-full);
  margin-top: 8px;
  max-width: 240px;
  overflow: hidden;
}
.inventory-session-progress-bar {
  height: 100%;
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-2) 130%);
  border-radius: var(--radius-full);
}
.inventory-session-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

/* Zählbildschirm - bewusst schmal+zentriert (statt volle Breite), damit es
   sich auf dem Handy (Haupt-Einsatzort, siehe Kommentar in inventory.js)
   wie eine native Vollbild-Karte anfühlt und auf dem Desktop nicht sinnlos
   auseinandergezogen wird. */
.inventory-count-card {
  max-width: 440px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.inventory-count-top { display: flex; align-items: center; justify-content: space-between; }
.inventory-count-progress-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
}
.inventory-count-progress-bar-track {
  height: 4px;
  background: var(--color-bg-alt);
  border-radius: var(--radius-full);
  overflow: hidden;
  margin-top: -8px;
}
.inventory-count-progress-bar-fill {
  height: 100%;
  background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-2) 130%);
  transition: width var(--dur-slow) var(--ease);
}
.inventory-count-item-header { display: flex; align-items: center; gap: 8px; min-height: 22px; }
.inventory-count-mismatch { display: none; }
.inventory-count-item-name { font-size: 20px; line-height: 1.3; margin: 0; }
.inventory-count-soll { font-size: var(--text-meta); color: var(--color-text-muted); font-weight: 500; margin-top: -10px; }
.inventory-count-actions { display: flex; gap: 8px; margin-top: 4px; }
.inventory-count-actions .btn { flex: 1; justify-content: center; }
.inventory-count-next svg { width: 14px; height: 14px; }

/* Swipe-Leiste zum Zählen: großes Zahlendisplay, horizontale Skala mit
   Snap-Punkten (ein Tick pro Stück, jeder 5. beschriftet), fester
   Akzent-Strich in der Mitte als Zeiger. Touch/Trackpad nutzt natives
   Scrollen (kein JS-Handling nötig); auf dem Desktop kann zusätzlich mit
   der Maus gezogen werden (siehe quantitySlider() in inventory.js). */
.inventory-qty-slider { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 6px 0 2px; }
.inventory-qty-display { display: flex; align-items: baseline; gap: 6px; }
.inventory-qty-number {
  font-size: 50px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.inventory-qty-unit { font-size: 15px; color: var(--color-text-faint); font-weight: 600; }
.inventory-qty-track-wrap { position: relative; width: 100%; }
.inventory-qty-center-line {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--color-accent);
  border-radius: 2px;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 1;
}
.inventory-qty-track {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  touch-action: pan-x;
  user-select: none;
  cursor: grab;
}
.inventory-qty-track::-webkit-scrollbar { display: none; }
.inventory-qty-track:active { cursor: grabbing; }
.inventory-qty-tick {
  flex-shrink: 0;
  width: 32px;
  height: 54px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  scroll-snap-align: center;
  cursor: pointer;
}
.inventory-qty-tick-mark { width: 2px; height: 12px; background: var(--color-border-strong); border-radius: 1px; }
.inventory-qty-tick.is-major .inventory-qty-tick-mark { height: 22px; background: var(--color-text-faint); }
.inventory-qty-tick-label {
  font-size: var(--text-small);
  margin-top: 5px;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
}
.inventory-qty-controls { display: flex; align-items: center; gap: 10px; }
.inventory-qty-btn {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  color: var(--color-text);
  line-height: 1;
}
.inventory-qty-btn:active { background: var(--color-bg-alt); }
.inventory-qty-input {
  width: 64px;
  text-align: center;
  padding: 8px 4px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Bericht */
.inventory-report-list { display: flex; flex-direction: column; }
.inventory-report-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--color-border);
}
.inventory-report-row:last-child { border-bottom: none; }
.inventory-report-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; font-weight: 600; font-size: var(--text-body); }
.inventory-report-cat { font-size: var(--text-meta); font-weight: 500; color: var(--color-text-faint); letter-spacing: 0.02em; }
.inventory-report-values {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.inventory-report-arrow { color: var(--color-text-faint); }
.inventory-report-diff { font-weight: 700; padding: 2px 7px; border-radius: var(--radius-full); font-size: var(--text-meta); }
.inventory-report-diff.is-plus { background: var(--color-success-soft); color: var(--color-success); }
.inventory-report-diff.is-minus { background: var(--color-danger-soft); color: var(--color-danger); }
.inventory-report-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* ---------- Globale Suche (Cmd/Strg+K, quicksearch.js) ---------- */

.quicksearch-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 18, 32, 0.5);
  backdrop-filter: blur(2px);
  z-index: 200;
  display: flex;
  justify-content: center;
  padding: 12vh var(--space-4) var(--space-4);
}
.quicksearch-palette {
  width: 100%;
  max-width: 620px;
  max-height: 70vh;
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.quicksearch-input-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text-faint);
}
.quicksearch-input-row svg { width: 17px; height: 17px; flex-shrink: 0; }
.quicksearch-input {
  flex: 1;
  border: none;
  outline: none;
  background: none;
  font-size: 15px;
  color: var(--color-text);
  /* UX-Audit 2026-08-16 (Batch 4+5, Finding 2.8): der lange Desktop-
     Platzhaltertext ("Alles durchsuchen: Kunden, Produktionen, ...") wurde
     auf schmalen Screens ohne Ellipsis hart am rechten Rand abgeschnitten -
     white-space/overflow/text-overflow wirkt bei unterstützenden Browsern
     auch auf den ::placeholder mit. quicksearch.js setzt auf Mobile
     zusätzlich einen kürzeren, VOLLSTÄNDIG sichtbaren Platzhaltertext statt
     sich allein auf die Ellipsis zu verlassen. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.quicksearch-results { flex: 1; overflow-y: auto; padding: var(--space-2); }
.quicksearch-empty { padding: var(--space-5); text-align: center; color: var(--color-text-faint); font-size: var(--text-body); }
.quicksearch-group { margin-bottom: 4px; }
.quicksearch-group-label {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 10px 4px;
  font-size: var(--text-small);
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
}
.quicksearch-group-label svg { width: 12px; height: 12px; }
.quicksearch-row {
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.quicksearch-row.is-active { background: var(--color-accent-soft); }
.quicksearch-row-title { font-size: 13.5px; font-weight: 600; color: var(--color-text); }
.quicksearch-row-sub { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 1px; }
.quicksearch-hint {
  display: flex;
  gap: var(--space-4);
  padding: 9px var(--space-4);
  border-top: 1px solid var(--color-border);
  font-size: 11px;
  color: var(--color-text-faint);
}
.quicksearch-hint kbd {
  font-family: inherit;
  font-size: var(--text-small);
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--color-bg-alt);
  margin-right: 3px;
}
body.quicksearch-open { overflow: hidden; }

/* ---------- Dokumente (documents.js) ---------- */

.documents-grid {
  display: grid;
  /* 50% breiter (Chat-Verlauf: "die Felder insgesamt 50% breiter machen,
     damit die Dateinamen besser lesbar sind") - vorher 300px. */
  grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
  gap: var(--space-4);
}
.documents-box {
  background: var(--color-surface);
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
  /* Bug-Report: "Jetzt ist die Seite noch breiter als vorher" (nach dem
     ersten Mobile-Fix, der .documents-grid von minmax(450px,1fr) auf 1fr
     umstellte) - derselbe min-width:auto-Mechanismus wie bei .card/
     .stat-card (siehe Kommentare dort): als Grid-Item von .documents-grid
     hat diese Box ohne eigenes min-width:0 ein implizites min-width:auto,
     das ein Schrumpfen unter ihre intrinsische Inhaltsbreite verhindert -
     "1fr" ist dabei intern gleichbedeutend mit minmax(auto, 1fr), "auto"
     zieht sich also GENAU diese Inhaltsbreite, nicht die Spaltenbreite. Ohne
     dieses min-width:0 konnte die Box dadurch sogar breiter werden als mit
     dem alten, aber immerhin FESTEN 450px-Minimum zuvor. */
  min-width: 0;
}
.documents-box.is-dragover { border-color: var(--color-accent); background: var(--color-accent-soft); }
.documents-box.is-uploading { border-color: var(--color-accent); }
/* Chat-Verlauf: "Kannst du das bei Assets genau so machen, dass die Boxen
   mit Content einen farblichen Rahmen bekommen?" - 1:1 dieselbe Konvention
   wie .pm-card.has-files bei Previews & Masters, hier eben auf den
   bestehenden gestrichelten Rahmen angewendet statt einen durchgezogenen. */
.documents-box.has-files { border-color: var(--color-accent); }
.documents-box-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: var(--space-3); }
/* Design-System-Cleanup 2026-08-16 (Phase 4d): gleiche Logik wie
   .pm-card-number/.pm-card-title (Previews & Masters, Phase 4a) - Nummer
   ist Sekundärinfo, font-weight von 700 auf --weight-semibold zurückgenommen. */
.documents-box-number { font-size: var(--text-meta); font-weight: var(--weight-semibold); color: var(--color-text-faint); letter-spacing: 0.02em; }
.documents-box-title { font-size: var(--text-entity-title); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.documents-box-client { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 1px; }
.documents-box-count {
  flex-shrink: 0;
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  border-radius: var(--radius-full);
  padding: 2px 9px;
  font-size: 11px;
  font-weight: 700;
}
.documents-dropzone-hint {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: var(--space-4) var(--space-3);
  margin-bottom: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  color: var(--color-text-faint);
  font-size: 12px;
  text-align: center;
}
.documents-dropzone-hint svg { width: 14px; height: 14px; }
.documents-box.is-dragover .documents-dropzone-hint { color: var(--color-accent); }
.documents-uploading { display: flex; flex-direction: column; gap: 6px; }
.documents-uploading:not(:empty) { margin-bottom: 6px; }
.documents-uploading-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  background: var(--color-accent-soft);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
}
.documents-uploading-row svg { width: 14px; height: 14px; color: var(--color-accent); flex-shrink: 0; }
.documents-uploading-row .documents-row-name { margin-bottom: 4px; }
.documents-uploading-row .progress-track { height: 5px; }
.documents-uploading-percent { flex-shrink: 0; font-size: var(--text-meta); font-weight: var(--weight-semibold); color: var(--color-accent-strong); font-variant-numeric: tabular-nums; }
.documents-list { display: flex; flex-direction: column; gap: 14px; }
.documents-empty { font-size: var(--text-meta); color: var(--color-text-faint); text-align: center; margin: 0; }

/* Gruppiert die Dokumente nach Herkunft (direkt hochgeladen / per File-
   Transfer geteilt / vom Kunden hochgeladen) - siehe groupKeyFor() in
   documents.js. Ein farbig abgesetztes Label statt schlichtem Grau, damit
   die Gruppen auf einen Blick auseinanderzuhalten sind, nicht nur durch den
   Text selbst. */
.documents-group { display: flex; flex-direction: column; gap: 6px; }
.documents-group-label {
  align-self: flex-start;
  font-size: var(--text-small);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
  background: var(--color-accent-soft);
  padding: 3px 9px;
  border-radius: var(--radius-full);
}
/* Max. ~10 Zeilen sichtbar, danach Scroll (Chat-Verlauf: "es macht keinen
   Sinn, dass dort alle einzelnen Dateien aufgelistet werden, das ergibt eine
   extrem lange Liste nach unten ... maximal 10 Einträge, dann
   overflow:scroll") - eine .documents-row ist inkl. Padding ~42px hoch, plus
   6px gap zwischen den Zeilen: 10 * 42px + 9 * 6px ≈ 474px, aufgerundet
   480px. Nur die einzelne Gruppe scrollt (z.B. "Per File-Transfer geteilt"),
   nicht die ganze Box - bei mehreren Gruppen bleiben die anderen normal
   sichtbar. */
.documents-group-list { display: flex; flex-direction: column; gap: 6px; max-height: 480px; overflow-y: auto; padding-right: 2px; }
.documents-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  cursor: pointer;
}
.documents-row:hover { border-color: var(--color-accent); }
.documents-row svg { width: 14px; height: 14px; color: var(--color-text-faint); flex-shrink: 0; }
/* Kunden-Dashboard-Freigabe-Icon (Chat-Verlauf: "Assets teilen funktioniert
   nicht ganz, das Icon wird nicht 'aktiv' eingefärbt") - braucht mehr
   Spezifität als die generische ".documents-row svg"-Regel direkt darüber,
   sonst gewinnt deren color:var(--color-text-faint) gegen jede geerbte Farbe
   vom Button (gleiches Muster wie .pm-file-actions .is-shared). */
.documents-row .is-client-shared svg { color: var(--color-accent); }
.documents-row-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.documents-row-sub { font-size: var(--text-meta); color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Zusammengefasste Zeile für mehrere Dateien aus demselben File-Transfer
   (Chat-Verlauf: "wenn ich 10 Dateien per File-Transfer teile, sollten diese
   bei Assets auch gruppiert angezeigt werden - also nur als einzelner
   Upload - nicht jede einzelne Datei") - siehe documentBatchRow() in
   assets.js. Zeigt nur noch den ZIP-Dateinamen, kein Aufklappen mehr (Chat-
   Verlauf: "Kein Dropdown mehr mit den einzelnen Dateien") - .documents-row
   deckt die Optik bereits ab, hier bleibt nur noch der nicht-klickbare
   Cursor als Unterschied zur normalen (anklickbaren) Einzeldatei-Zeile. */
.documents-row-batch { cursor: default; }

/* File-Transfer (app/js/pages/filetransfer.js) - eine große, eigenständige
   Dropzone (nicht pro Produktion wie bei .documents-box), daher deutlich
   mehr Innenabstand und eine zentrierte Icon+Text-Anordnung statt der
   kompakten Kopfzeile dort. */
.filetransfer-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: var(--space-6) var(--space-4);
  margin-bottom: var(--space-3);
  border: 2px dashed var(--color-border-strong);
  border-radius: var(--radius-lg);
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.filetransfer-dropzone.is-dragover, .filetransfer-dropzone.is-uploading {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent);
}
.filetransfer-dropzone-icon { width: 34px; height: 34px; }
.filetransfer-dropzone-icon svg { width: 100%; height: 100%; }
.filetransfer-dropzone-text { font-size: var(--text-entity-title); font-weight: 600; }
.filetransfer-dropzone-hint { font-size: var(--text-meta); color: var(--color-text-faint); }

/* Inspo-Dropzone (Dashboard-Kachel, app/js/pages/dashboard.js
   inspoDropzoneCard()) - Chat-Verlauf: "Wie wärs noch mit einer Inspo-
   Dropzone? Kann man schnell einen Link reindroppen ... aus der
   Zwischenablage Links, Texte, Bilder usw. einfügen. Oder einfach Dateien
   draufdroppen." Gleiches visuelles Grundmuster wie .filetransfer-dropzone
   oben, aber zusätzlich per Tastatur fokussierbar (tabindex, siehe JS), da
   ein 'paste'-Event nur auf einem fokussierten Element feuert - der Fokus-
   Ring dient hier bewusst als "hier klicken, dann Strg+V"-Hinweis statt nur
   als Accessibility-Detail. */
/* is-loading: nur solange die Boards noch laden (siehe inspoDropzoneCard()
   in dashboard.js) - zentriert den Spinner. Wird sofort entfernt, sobald
   echter Inhalt drinsteckt, sonst würde die Flex-Zentrierung Board-Auswahl/
   Dropzone/Eingabezeile weiter zu einer schmalen Spalte zusammenschieben
   statt die volle Kartenbreite zu nutzen. */
.inspo-dropzone-body.is-loading { min-height: 70px; display: flex; align-items: center; justify-content: center; }

.inspo-dropzone-zone {
  /* Chat-Verlauf: "Kann die Inspo-Dropzone nicht einfach immer ihr eigenes
     Feld ausfüllen?" - width:100% + box-sizing explizit statt sich nur auf
     das Block-Element-Default zu verlassen, damit die Dropzone garantiert
     die volle Breite ihrer Spalte (.inspo-dropzone-main) einnimmt. */
  width: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: var(--space-6) var(--space-4);
  border: 2px dashed var(--color-border-strong);
  border-radius: var(--radius-lg);
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
/* is-focused statt (nur) :focus-visible (Chat-Verlauf: "wäre es schön wenn
   das Feld in der Mitte anzeigt, wenn ich es angeklickt habe. Also
   irgendwie ein Focus-Element integrieren.") - eine über echte focus/blur-
   Events gesetzte Klasse (siehe zone.addEventListener('focus', ...) in
   dashboard.js) feuert zuverlässig nach jedem Klick, :focus-visible allein
   ließ sich beim reinen Maus-Klick auf ein Nicht-Formular-Element
   browserabhängig nicht konsistent auslösen. box-shadow-Glow zusätzlich zum
   Farbwechsel, gleiches Prinzip wie .field input:focus (--shadow-glow),
   damit der Fokus wirklich unübersehbar ist. */
.inspo-dropzone-zone.is-focused,
.inspo-dropzone-zone.is-dragover,
.inspo-dropzone-zone.is-busy {
  outline: none;
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent);
  box-shadow: var(--shadow-glow);
}
/* Kurzer grüner Puls nach erfolgreichem Einfügen - einziges Feedback, dass
   etwas angekommen ist, ohne die Kachel gleich zur vollen Galerie
   umzubauen (die bleibt bewusst eine Ebene tiefer, siehe inspiration.js). */
.inspo-dropzone-zone.is-flash {
  border-color: var(--color-success);
  background: var(--color-success-soft);
  color: var(--color-success);
  animation: inspoDropzoneFlash 0.9s ease;
}
@keyframes inspoDropzoneFlash {
  0% { transform: scale(1); }
  30% { transform: scale(1.015); }
  100% { transform: scale(1); }
}
.inspo-dropzone-icon { width: 30px; height: 30px; }
.inspo-dropzone-icon svg { width: 100%; height: 100%; }
.inspo-dropzone-text { font-size: 13.5px; font-weight: 600; }
.inspo-dropzone-hint { font-size: var(--text-meta); color: var(--color-text-faint); }
.inspo-dropzone-zone.is-dragover .inspo-dropzone-hint,
.inspo-dropzone-zone.is-busy .inspo-dropzone-hint,
.inspo-dropzone-zone.is-focused .inspo-dropzone-hint { color: inherit; opacity: 0.75; }

/* Fallback-Zeile unter der Dropzone (Tippen statt Einfügen/Ziehen - z.B. auf
   Touch-Geräten ohne echte Zwischenablage-Paste-Events). */
.inspo-dropzone-fallback { display: flex; gap: 8px; margin-top: 10px; }
.inspo-dropzone-fallback input { flex: 1; min-width: 0; }

/* Zweispaltiges Layout, fest 50/50, IMMER nebeneinander (Chat-Verlauf: erst
   "Kann die Inspo-Dropzone auch den Platz besser nutzen?", dann konkret
   "Mach die Dropzone doch immer auf 50% der Breite der Kachel" - danach noch
   mal explizit "Beides soll nebeneinander stehen und jeweils 50% der Breite
   nutzen", nachdem eine Zwischenversion mit flex-wrap + min-width:220px auf
   einer 'md'-Kachel (nur 6/12 Grid-Spalten, siehe .dashboard-tile.size-md)
   regelmäßig unter die 2×220px+Gap-Schwelle fiel und die Spalten dadurch
   fast immer untereinander gefallen sind statt nur im echten Extremfall).
   flex-basis:0 + gleiches flex-grow (statt 50%) ist der robuste Weg für
   eine garantiert exakte 50/50-Aufteilung unabhängig vom Inhalt - kein
   flex-wrap mehr, die Spalten bleiben bei jeder Kachelbreite nebeneinander.
   min-width:0 bleibt Pflicht (nicht nur netter Zusatz): ohne das ignorieren
   Flex-Kinder ihr eigenes Schrumpfen-Können und richten sich nach der
   intrinsischen Breite ihres Inhalts (z.B. ein <select> mit langem Board-
   Namen) - das war schon einmal die Ursache für ein Überlaufen über den
   Kartenrand hinaus. */
.inspo-dropzone-layout { display: flex; gap: 20px; align-items: flex-start; }
.inspo-dropzone-main,
.inspo-dropzone-recent-panel { flex: 1 1 0; min-width: 0; }
.inspo-dropzone-recent-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  margin-bottom: 10px;
}

/* Zuletzt hinzugefügt - jetzt ein richtiges Grid aus Vorschau + Titel statt
   der vorherigen schmalen Reihe winziger 44px-Quadrate, damit man sofort
   sieht, was zuletzt reinkam, ohne extra zur vollen Galerie zu wechseln.
   auto-fit statt auto-fill: mit wenigen Einträgen (z.B. nur einem einzigen)
   erzeugte auto-fill trotzdem so viele leere, unsichtbare Spalten wie
   theoretisch in die Zeile passen - der eine Eintrag landete dadurch klein
   und linksbündig, während der Rest der Zeile als reiner Leerraum wirkte
   (Chat-Verlauf: "die Dropzone braucht nur 20% der Breite - der Rest ist
   komplett weiß"). auto-fit kollabiert diese leeren Spalten stattdessen,
   ein fester Maximalwert (statt 1fr) verhindert im Gegenzug, dass ein
   einzelner Eintrag auf die volle Zeilenbreite aufgeblasen wird. */
.inspo-dropzone-recent { display: grid; grid-template-columns: repeat(auto-fit, minmax(76px, 96px)); gap: 12px; }
.inspo-dropzone-recent-item { display: flex; flex-direction: column; gap: 6px; cursor: pointer; min-width: 0; }
.inspo-dropzone-recent-thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface-2) center/cover no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.inspo-dropzone-recent-item:hover .inspo-dropzone-recent-thumb { transform: translateY(-2px); border-color: var(--color-accent); }
.inspo-dropzone-recent-thumb svg { width: 18px; height: 18px; }
.inspo-dropzone-recent-label {
  font-size: var(--text-small);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}
.filetransfer-link-box {
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--color-success-soft);
  border: 1px solid var(--color-success);
}
.filetransfer-link-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--color-success);
  margin-bottom: 8px;
}
.filetransfer-link-label svg { width: 14px; height: 14px; }
/* Bearbeiten-Modal "Bisherige Transfers" (Chat-Verlauf: "Titel bzw.
   Dateinamen ändern") - ein Textfeld pro Datei des Transfers, gestapelt statt
   nebeneinander (Dateinamen können lang sein). */
.filetransfer-edit-filenames { display: flex; flex-direction: column; gap: 6px; }

/* Pending-Transfers-Dialog (Chat-Verlauf: "Wenn ich Problem habe über die
   Weboberfläche Daten hochzuladen, lege ich die Daten unter
   storage/file-transfers ab ... gibst du mir beim Aufruf der
   'File-Transfer' Seite einen Dialog"), siehe openPendingTransfersModal() in
   filetransfer.js - EIGENE, spezifische Klasse statt eines verschachtelten
   <label> direkt in .field: ".field label { display:block; ... }" (siehe
   Docblock-Kommentar bei .chat-member-picker weiter unten) würde sonst jede
   Zeile per Nachfahren-Selektor treffen und deren "display:flex" wieder auf
   "block" zurückkippen - exakt derselbe Bug wie dort, hier von vornherein
   vermieden statt nachträglich gefixt.
*/
.filetransfer-pending-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  cursor: pointer;
}
.filetransfer-pending-row input[type="checkbox"] { flex: none; width: auto; }
.filetransfer-pending-row + .filetransfer-pending-row { border-top: 1px solid var(--color-border); }
.filetransfer-pending-row-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Boards (app/js/pages/boards.js) - freies Whiteboard pro Produktion.
   Der Picker-Grid nutzt die üblichen Card-/Grid-Bausteine; der eigentliche
   Canvas-Editor braucht die volle Höhe des Viewports statt der normalen
   Seiten-Innenabstände von .main, daher die :has()-basierte Ausnahme unten -
   nur wenn eine .board-page tatsächlich drinsteckt, wird .main auf volle
   Höhe/ohne Padding/eigenes Overflow umgestellt (Panning statt Scrollen). */
/* Gruppierung nach Produktion (Chat-Verlauf: "die Produktion muss klarer
   erkennbar sein, evtl. auch außerhalb der Kacheln") - die Kacheln selbst
   zeigen seitdem keine Nummer/Titel-Zeile mehr (siehe .board-picker-body
   unten), stattdessen steht die Produktion einmal pro Gruppe als eigene
   Überschrift darüber. */
.board-groups { display: flex; flex-direction: column; gap: var(--space-6); }
.board-group-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: var(--space-3);
  padding-bottom: 8px;
  border-bottom: 1px solid var(--color-border);
}
.board-group-number { font-size: var(--text-body); font-weight: 700; color: var(--color-accent); flex-shrink: 0; }
.board-group-title { font-size: 15px; font-weight: 600; color: var(--color-text); }
.board-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--space-4); }
.board-picker-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  /* padding-top deutlich größer als bei den anderen Seiten - die
     Aktions-Knöpfe (siehe .board-picker-actions) liegen absolut oben rechts
     und würden sonst bei längeren Nummer/Titel-Texten der ersten Zeile
     ("26048 · Graphics Bauchbinden") überlappen (siehe Chat-Verlauf). Der
     zusätzliche Abstand schiebt die ganze Inhalts-Zeile (Vorschaubild+Text)
     komplett unter die Knopfreihe, statt nur die rechte Seite freizuhalten. */
  padding: 46px var(--space-4) var(--space-4);
  min-height: 148px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color var(--dur-base) var(--ease), transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
/* Deutlicherer Lift (Chat-Verlauf: "offensichtlicher machen"), gleiches
   Prinzip wie .stat-card:hover. */
.board-picker-card:hover { border-color: var(--color-accent); transform: translateY(-5px) scale(1.015); box-shadow: var(--shadow-lg); }
/* Vorschaubild (siehe captureThumbnail()/uploadThumbnail() in boards.js) -
   ersetzt das frühere generische Icon, sobald eins existiert; .is-empty
   (noch kein Vorschaubild vorhanden) zeigt weiterhin das Board-Icon als
   Platzhalter, damit die Karte nie komplett leer wirkt. Deutlich größer als
   zuvor (72x54 -> 148x111, gleiches 4:3-Seitenverhältnis wie THUMB_W/
   THUMB_H in captureThumbnail) - vorher wirkte die Karte durch das winzige
   Bild neben viel Text unausgewogen/gedrängt (siehe Chat-Verlauf: "Bild
   auch deutlich größer machen, Texte dann gesammelt rechts daneben"). Die
   Texte daneben sind jetzt als eigener, vertikal zentrierter Block
   zusammengefasst statt am oberen Rand zu kleben.
   */
.board-picker-thumb {
  width: 148px; height: 111px; flex-shrink: 0; border-radius: var(--radius-sm);
  overflow: hidden; background: var(--color-bg-alt);
  display: flex; align-items: center; justify-content: center;
}
.board-picker-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.board-picker-thumb.is-empty { background: var(--color-accent-soft); color: var(--color-accent); }
.board-picker-thumb.is-empty svg { width: 34px; height: 34px; }
.board-picker-body {
  min-width: 0;
  padding-right: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.board-picker-number { font-size: var(--text-meta); color: var(--color-text-faint); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-picker-title {
  font-weight: 600;
  font-size: 16.5px;
  line-height: 1.3;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  word-break: break-word;
}
.board-picker-client { font-size: var(--text-meta); color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-picker-meta { font-size: var(--text-meta); color: var(--color-text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-picker-lock {
  /* Kein eigener margin-top mehr - .board-picker-body ist jetzt selbst ein
     Flex-Container mit gap (siehe oben), der den Abstand zu den anderen
     Textzeilen bereits einheitlich setzt. */
  display: flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600;
  color: #a06a00;
}
.board-picker-lock svg { width: 11px; height: 11px; }
/* Drei Aktions-Knöpfe (Umbenennen/Präsentationslink/Löschen) als Gruppe oben
   rechts - vorher gab es nur einen einzelnen .board-picker-delete-Knopf
   (plus einen ungestylten .board-picker-share ohne eigene CSS-Regel, der
   dadurch als normales Fließ-Element in der Karte statt als Overlay-Button
   auftauchte); jetzt einheitlich alle drei in derselben Optik. */
.board-picker-actions {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.board-picker-card:hover .board-picker-actions,
.board-picker-actions:focus-within { opacity: 1; }
.board-picker-action {
  width: 26px; height: 26px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.board-picker-action svg { width: 13px; height: 13px; }
.board-picker-action:hover { color: var(--color-accent-strong); border-color: var(--color-accent); }
.board-picker-action.is-danger:hover { color: var(--color-danger); border-color: var(--color-danger); }
/* Freigabe-Icon fürs Kunden-Dashboard (Chat-Verlauf: "Moodboard bitte
   ergänzen, oder ist das nicht möglich?") - aktiver Zustand bleibt farbig,
   gleiches Prinzip wie is-danger oben, nur dauerhaft statt nur bei Hover. */
.board-picker-action.is-client-shared { color: var(--color-accent); border-color: var(--color-accent); }

.main:has(.board-page) {
  padding: 0;
  /* dvh-Fallback-Muster siehe Kommentar bei .app-shell weiter oben - hier
     besonders relevant, weil das Board eine feste (statt scrollende) Höhe
     mit overflow:hidden nutzt: bei reinem vh sprang/schnitt der Canvas auf
     Mobile Safari ab, wenn die Adressleiste ein-/ausgeblendet wurde. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Gefundene Ursache des "Bild/Link landet weit rechts"-Bugs: .main ist
     hier ein CSS-GRID-Item (Spalte "1fr" in .app-shell), das trotz
     overflow:hidden UND trotz min-width:0 weiter unten auf .board-page/
     .board-viewport in der Praxis (Chrome/Safari) nicht zuverlässig auf 0
     zurückfiel - .board-viewport.getBoundingClientRect().width maß
     tatsächlich 6000px (== CANVAS_SIZE aus boards.js) statt der echten
     sichtbaren Breite. viewportCenterCanvasPoint() nutzt genau dieses
     rect.width, wodurch neu eingefügte Bilder/Links weit außerhalb des
     sichtbaren Bereichs landeten. min-width: 0 hier direkt auf dem
     Grid-Item behebt es an der Wurzel, unabhängig davon, ob die Vererbung
     über die verschachtelten Flex-Kinder in der jeweiligen Browser-Engine
     zuverlässig durchgereicht wird. */
  min-width: 0;
}
/* min-width: 0 ist hier Pflicht, nicht nur min-height: Ohne das nimmt ein
   Flex-Item standardmäßig min-width: auto an, was heißt "nie kleiner als der
   Inhalt". Der Inhalt hier ist letztlich .board-canvas mit CANVAS_SIZE =
   6000px Breite/Höhe (siehe boards.js) - ohne dieses min-width: 0 blähen
   sich .board-page UND .board-viewport (gleiche Ursache, siehe dort) auf
   volle 6000px Breite auf, weit über den sichtbaren Bildschirm hinaus.
   getBoundingClientRect() liefert dann überall in boards.js (Zentrierung
   beim Öffnen, "Bild/Link in Bildschirmmitte einfügen" usw.) falsche,
   riesige Werte - neue Elemente landen dadurch weit außerhalb des
   sichtbaren Bereichs, und beim erneuten Öffnen eines Boards zentriert
   centerViewOn() ebenfalls falsch, wodurch schon vorhandene Elemente wie
   verschwunden wirken. */
/* position: relative ist hier KEINE Kosmetik (02.09.2026). Die
   Werkzeugleiste darin steht auf position:absolute - ohne einen
   positionierten Vorfahren bezog sie sich auf einen ganz anderen Kasten
   weiter oben im Baum. Ergebnis: "sie sitzt je nach Skalierung des
   Fensters immer woanders und auch ausserhalb des Boards". Genau das. */
.board-page { position: relative; flex: 1; min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }

.board-header {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  flex-shrink: 0;
}
/* Board-Titel (frei umbenennbar, siehe openBoardRenameModal) als primäre
   Überschrift, Produktions-Nummer/-Titel als kleinerer Kontext darunter -
   eine Produktion kann mehrere Boards haben (siehe migration_boards_multi.sql),
   daher reicht der Board-Titel allein nicht mehr, um die Zugehörigkeit zu
   erkennen. */
.board-header-title { display: flex; flex-direction: column; gap: 1px; min-width: 0; overflow: hidden; }
.board-header-name-row { display: flex; align-items: center; gap: 4px; min-width: 0; }
.board-header-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-header-sub { font-size: var(--text-meta); color: var(--color-text-faint); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-title-edit-btn {
  flex-shrink: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: none;
  color: var(--color-text-faint);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.board-title-edit-btn svg { width: 12px; height: 12px; }
.board-title-edit-btn:hover { background: var(--color-bg-alt); color: var(--color-text); }

/* Presence-Avatare "wer ist gerade hier" (Coop, siehe Chat-Verlauf:
   "Coop-Funktion, bei der Leute mit Link mitarbeiten können") - einfache
   Kreis-Initialen statt echter Profilbilder (Gäste haben keinen Account),
   leicht überlappend wie ein typischer Avatar-Stapel. Leer/unsichtbar,
   solange niemand sonst mitarbeitet (siehe renderPresence() in boards.js -
   ein leeres mount() lässt einfach keine Kinder übrig). */
.board-presence-bar { display: flex; align-items: center; flex-shrink: 0; }
.board-presence-avatar {
  width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 11px; font-weight: 700;
  border: 2px solid var(--color-surface);
  margin-left: -8px;
}
.board-presence-avatar:first-child { margin-left: 0; }

/* Global-Visual-Redesign 2026-08-16 (Phase 3 Foundations): eigene, isolierte
   Creme/Gold-Palette durch die bestehenden Warning-Tokens ersetzt (gleiches
   Muster wie .badge-warning/.badge-rental: background --color-warning-soft,
   Text/Border --color-warning). */
.board-lock-banner {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 16px; background: var(--color-warning-soft); border-bottom: 1px solid var(--color-warning); color: var(--color-warning);
  font-size: var(--text-body); flex-shrink: 0;
}
.board-lock-banner svg { width: 15px; height: 15px; flex-shrink: 0; }
.board-lock-banner span:nth-child(2) { flex: 1; }
.board-lock-banner .btn { flex-shrink: 0; }

/* Verschiebbar (02.09.2026, Chat-Verlauf: "Die Arbeitsleiste in einem
   Board koennte man verschiebbar machen"). Solange sie nicht angefasst
   wurde, steht sie oben mittig wie bisher; sobald sie einmal gezogen
   wurde, setzt boards.js left/top in Pixeln und schaltet .ist-verschoben
   ein, was die Mittelzentrierung aufhebt.
   Bewusst NUR fuer die Sitzung: eine gemerkte Position waere entweder
   eine Migration (pro Board) oder ein weiterer Browser-Speicher (pro
   Nutzer) - beides fuer eine Leiste, die man selten verschiebt. */
.board-toolbar-wrap {
  position: absolute; top: var(--space-4); left: 50%; transform: translateX(-50%);
  z-index: 20; display: flex; flex-direction: column; align-items: center; gap: 8px;
  pointer-events: none;
}
.board-toolbar-wrap.ist-verschoben { transform: none; }
/* Der Griff. Er sitzt als schmaler Streifen ueber den Leisten und ist im
   Ruhezustand blass - er soll auffindbar sein, ohne staendig Aufmerksamkeit
   zu ziehen. */
.board-toolbar-griff {
  pointer-events: auto;
  width: 46px; height: 16px;
  display: flex; align-items: center; justify-content: center;
  gap: 3px;
  border-radius: var(--radius-full);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: 0 6px 18px -8px rgba(20, 21, 43, 0.22);
  cursor: grab;
  opacity: 0.55;
  transition: opacity var(--dur-fast) var(--ease);
}
.board-toolbar-griff:hover { opacity: 1; }
.board-toolbar-griff.zieht { cursor: grabbing; opacity: 1; }
.board-toolbar-griff span {
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--color-text-faint);
}
.board-toolbar-wrap > * { pointer-events: auto; }
.board-toolbar {
  display: flex; align-items: center; gap: 2px;
  padding: 5px; background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); box-shadow: 0 8px 24px -8px rgba(20, 21, 43, 0.18);
}
.board-tool-btn {
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); border: none; background: transparent; color: var(--color-text-muted);
  cursor: pointer;
}
.board-tool-btn svg { width: 17px; height: 17px; }
.board-tool-btn:hover { background: var(--color-bg-alt); color: var(--color-text); }
.board-tool-btn.active { background: var(--color-accent-soft); color: var(--color-accent); }
.board-toolbar-sep { width: 1px; height: 22px; background: var(--color-border-strong); margin: 0 4px; flex-shrink: 0; }
.board-zoom-label { font-size: var(--text-meta); color: var(--color-text-faint); min-width: 34px; text-align: center; }

.board-color-row {
  display: flex; gap: 6px; padding: 6px 8px; background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-full); box-shadow: 0 8px 24px -8px rgba(20, 21, 43, 0.18);
}
.board-color-swatch {
  width: 18px; height: 18px; border-radius: 50%; border: 2px solid transparent; padding: 0; cursor: pointer;
}
.board-color-swatch.active { border-color: var(--color-text); }

.board-format-row {
  display: flex; align-items: center; gap: 2px; padding: 5px; background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: 0 8px 24px -8px rgba(20, 21, 43, 0.18);
}
.board-format-btn {
  min-width: 28px; height: 28px; padding: 0 6px; display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); border: none; background: transparent; color: var(--color-text-muted);
  cursor: pointer; font-size: var(--text-body);
}
.board-format-btn:hover { background: var(--color-bg-alt); color: var(--color-text); }
.board-format-toggle.active { background: var(--color-accent-soft); color: var(--color-accent); }
.board-format-size { font-size: var(--text-meta); color: var(--color-text-faint); min-width: 34px; text-align: center; }

.board-viewport {
  position: relative; flex: 1; min-width: 0; min-height: 0; overflow: hidden; cursor: grab;
  background-color: var(--color-bg-alt);
  background-image: radial-gradient(circle, var(--color-border-strong) 1px, transparent 1px);
  background-size: 22px 22px;
}
.board-viewport.panning { cursor: grabbing; }
.board-viewport.tool-crosshair { cursor: crosshair; }
.board-viewport.board-dropzone-active {
  outline: 3px dashed var(--color-accent);
  outline-offset: -3px;
  background-color: var(--color-accent-soft);
}
.board-viewport.board-dropzone-active::after {
  content: 'Bild hier ablegen';
  position: absolute;
  top: var(--space-4);
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-accent);
  color: #fff;
  font-size: var(--text-body);
  font-weight: 600;
  padding: 6px 14px;
  border-radius: var(--radius-md);
  pointer-events: none;
  z-index: 50;
}
.board-canvas { position: relative; transform-origin: 0 0; }
/* Ohne explizite width/height hier gewinnt die globale "svg { width: 1em;
   height: 1em; }"-Regel (Icon-Reset weiter oben) gegen die width="6000"/
   height="6000"-Attribute auf dem <svg> selbst (CSS schlägt SVG-Präsentations-
   attribute) - das Board-SVG wurde dadurch auf 1em x 1em zusammengestaucht.
   viewBox skalierte den kompletten Inhalt (Pfeile, Freihandlinien) auf dieses
   winzige Fenster herunter, wodurch beides praktisch unsichtbar wurde, obwohl
   pointer-events/Positionierung korrekt waren. */
.board-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
/* pointer-events: none hier + pointer-events: auto explizit auf .board-el
   (siehe unten) - ohne das blockiert dieser transparente, aber ansonsten ganz
   normale 100%x100%-Layer JEDEN Klick auf darunterliegende SVG-Linien/-Pfade
   (svg liegt im DOM davor, also optisch "darunter"), auch an Stellen ohne
   ein einziges Element - der Klick trift dann nur den leeren Layer
   (e.target === elementLayer), zählt für den Select-Tool-Handler in
   boards.js als Klick auf den Canvas-Hintergrund und deselektiert/pannt
   statt die Linie auszuwählen. */
.board-element-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }

/* Magnet-Linien beim Ziehen (siehe computeSnap()/renderGuides() in boards.js)
   - eigene Ebene über den Elementen, nie klickbar, wird während des Ziehens
   dynamisch befüllt/geleert. Farbe bewusst pink/magenta statt --color-accent,
   damit die Guides sich klar von der blauen Auswahl-Outline (.board-el.selected)
   absetzen. */
.board-guide-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 60; }
.board-guide-line { position: absolute; background: #ff3d92; }
.board-guide-line.is-vertical { width: 1px; }
.board-guide-line.is-horizontal { height: 1px; }

/* Auswahlrahmen beim Aufziehen einer Mehrfachauswahl (siehe
   startMarqueeSelect() in boards.js) - eigene Ebene, gleiches Prinzip wie
   .board-guide-layer, nur mit dem normalen Accent-Blau statt Magenta, weil
   das hier eine "Auswahl", keine Ausrichtungshilfe ist. */
.board-marquee-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 55; }
.board-marquee {
  position: absolute;
  border: 1px solid var(--color-accent);
  background: var(--color-accent-soft);
  opacity: 0.55;
}

.board-el { position: absolute; cursor: grab; pointer-events: auto; }
.board-el.dragging { cursor: grabbing; opacity: 0.92; }
.board-el.selected { outline: 2px solid var(--color-accent); outline-offset: 3px; border-radius: 2px; }
.board-preview { pointer-events: none; opacity: 0.7; }

/* Gesperrte Elemente (siehe setElementLocked() in boards.js) - normaler
   "grab"-Cursor macht keinen Sinn mehr, wenn Ziehen ohnehin blockiert ist;
   bei gleichzeitiger Auswahl gestrichelt statt durchgezogen, damit "gesperrt"
   optisch sofort vom normalen Auswahl-Zustand unterscheidbar bleibt. */
.board-el.is-locked { cursor: default; }
.board-el.is-locked.selected { outline-style: dashed; }
.board-lock-badge {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-text);
  color: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  pointer-events: none;
  z-index: 5;
}
.board-lock-badge svg { width: 10px; height: 10px; }

/* Vorschau-Modal für den EML-Import (siehe openEmlPreview() in boards.js) -
   editierbares Textfeld etwas größer als der Standard-.field-Textarea-Look,
   plus eine Checkbox-Bildergalerie zum gezielten Aus-/Abwählen einzelner
   Bilder (z.B. um eine Signatur/ein Logo aus der Mail zu überspringen). */
.eml-preview-text { min-height: 160px; font-size: var(--text-body); line-height: 1.5; }
.eml-image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 10px;
  max-height: 320px;
  overflow-y: auto;
  padding: 2px;
}
.eml-image-item {
  display: block;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
  cursor: pointer;
  background: var(--color-surface-2);
}
.eml-image-item img { display: block; width: 100%; height: 90px; object-fit: cover; }
.eml-image-item-foot {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  font-size: var(--text-meta);
  color: var(--color-text-muted);
}
.eml-image-item-foot input[type="checkbox"] { flex-shrink: 0; }
.eml-image-item-foot span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Mit KI aufbauen"-Button + Block-Liste (siehe Chat-Verlauf: "Claude API
   dransetzen, die mir das intelligent aufbaut und direkt logisch
   verknüpft") - ersetzt bei Nutzung das einfache Textfeld durch eine Liste
   editierbarer Blöcke, je einer wird beim Import zu einer eigenen Sticky. */
.eml-ai-toolbar { display: flex; justify-content: flex-end; gap: 8px; margin-bottom: 6px; }
.eml-ai-blocks { display: flex; flex-direction: column; gap: 10px; }
.eml-ai-block { display: flex; flex-direction: column; gap: 4px; }
.eml-ai-block-label { font-size: var(--text-meta); color: var(--color-text-muted); font-weight: 600; }

/* Checkbox-Liste erkannter Links im EML-Vorschau-Modal (siehe Chat-Verlauf:
   "Links in der Mail ... als 'Links-Module' importieren") - gleiches
   Karten-Prinzip wie .eml-image-item, nur als einzeilige Liste statt Grid. */
.eml-link-list { display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow-y: auto; }
.eml-link-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  cursor: pointer;
}
.eml-link-item-host { font-weight: 600; font-size: 12.5px; flex-shrink: 0; }
.eml-link-item-url {
  font-size: var(--text-meta);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Karten-Look für die "Box"-Elementtypen (Sticky/Formen/Bild/Link) - größere
   Radien + weiche, mehrstufige Schatten statt der ursprünglich recht
   harten/kleinen Werte, für einen ruhigeren, hochwertigeren Gesamteindruck
   auf dem Board. */
.board-sticky {
  background: #fef08a; border-radius: 14px;
  box-shadow: 0 1px 2px rgba(20, 21, 43, 0.06), 0 10px 24px -12px rgba(20, 21, 43, 0.22);
  padding: 12px 14px; overflow: hidden;
}
.board-sticky-text {
  width: 100%; height: 100%; font-size: var(--text-body); line-height: 1.4; color: #1a1a1a;
  outline: none; overflow: auto; word-break: break-word; white-space: pre-wrap;
  /* Erst editierbar nach Doppelklick (siehe enterTextEditMode in boards.js) -
     bis dahin soll ein Klick/Ziehen NICHT die native
     "Doppelklick markiert Wort"-Textauswahl auslösen, das würde sich mit dem
     eigenen Zieh-Handling auf dem umgebenden .board-el beißen (blaue
     Text-Markierung statt sauberem Verschieben). Sobald contenteditable auf
     "true" steht (aktiv im Editier-Modus), ganz normale Textauswahl wieder
     erlaubt. */
  cursor: grab;
  user-select: none;
}
.board-sticky-text[contenteditable="true"] { cursor: text; user-select: text; }

.board-shape {
  border-radius: 14px;
  box-shadow: 0 1px 2px rgba(20, 21, 43, 0.05), 0 8px 20px -12px rgba(20, 21, 43, 0.18);
}
.board-shape-ellipse { border-radius: 50%; }

/* "Edle weiße Box": das Bild schwimmt mit etwas Weißraum auf einer weißen,
   sanft abgerundeten Karte statt randlos/technisch direkt im Rahmen zu
   sitzen - eigener, etwas kleinerer Radius für das Bild selbst innerhalb des
   Innenabstands wirkt wie ein Passepartout um ein Foto. */
.board-image {
  border-radius: 16px; overflow: hidden; background: #fff; padding: 8px; box-sizing: border-box;
  box-shadow: 0 1px 3px rgba(20, 21, 43, 0.08), 0 14px 28px -14px rgba(20, 21, 43, 0.28);
}
.board-image img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; border-radius: 10px; }

.board-link {
  display: flex; align-items: center; gap: 8px; padding: 12px 14px;
  background: #fff; border: 1px solid rgba(20, 21, 43, 0.06); border-radius: 14px;
  box-shadow: 0 1px 2px rgba(20, 21, 43, 0.05), 0 10px 24px -12px rgba(20, 21, 43, 0.2);
}
.board-link-icon { width: 26px; height: 26px; flex-shrink: 0; border-radius: 50%; background: var(--color-accent-soft); color: var(--color-accent); display: flex; align-items: center; justify-content: center; }
.board-link-icon svg { width: 13px; height: 13px; }
.board-link-body { min-width: 0; flex: 1; }
.board-link-title { font-size: 12.5px; font-weight: 600; color: var(--color-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-link-url { font-size: var(--text-meta); color: var(--color-text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-link-open { flex-shrink: 0; color: var(--color-text-faint); display: flex; }
.board-link-open svg { width: 14px; height: 14px; }
.board-link-open:hover { color: var(--color-accent); }

.board-resize-handle {
  display: none; position: absolute; right: -5px; bottom: -5px; width: 12px; height: 12px;
  border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-surface);
  cursor: nwse-resize;
}
.board-el.selected .board-resize-handle { display: block; }

.board-path { pointer-events: stroke; cursor: pointer; }
/* .board-line ist seit der rechtwinkligen Routung eine <g> (siehe
   buildLineNode in boards.js) mit drei Kindern:
   - .board-line-path: der sichtbare, dünne Pfad (nicht selbst klickbar -
     2.5px Strichbreite wäre praktisch untreffbar).
   - .board-line-hit: unsichtbarer, breiter Klickbereich mit derselben "d"
     (siehe positionLineNode) - fängt Klicks in einem großzügigen Radius um
     den Pfad ab. Dient bei .selected zusätzlich als weicher Auswahl-Halo,
     weil CSS outline auf einer <g> (kein eigenes Geometrie-Rendering) in
     den meisten Browsern gar nicht oder unzuverlässig sichtbar wird.
   - .board-line-dot: kleiner Punkt am Startende, rein dekorativ. */
.board-line-hit {
  fill: none; stroke: var(--color-accent); stroke-width: 20; stroke-linecap: round; stroke-linejoin: round;
  opacity: 0; pointer-events: stroke; cursor: pointer; transition: opacity var(--dur-fast) var(--ease);
}
.board-line.selected .board-line-hit { opacity: 0.16; }
.board-line-dot { pointer-events: none; }

.board-text { padding: 6px 8px; cursor: grab; }
.board-text-content {
  width: 100%; height: 100%; line-height: 1.3; outline: none; overflow: auto;
  word-break: break-word; white-space: pre-wrap;
  /* Kein erzwungenes font-weight/text-transform mehr (ehem. "Hausschrift"
     mit !important) - hat dazu geführt, dass eingefügter/eingegebener Text
     immer fett+Versalien wirkte und der Fett-Umschalter (applyTextStyle())
     wirkungslos blieb. font-weight/Groß-Klein liegen jetzt normal beim
     Bold-Toggle bzw. beim eingefügten Text selbst. */
  font-family: var(--font);
  font-weight: 400;
  /* Erst editierbar nach Doppelklick (siehe .board-sticky-text-Kommentar
     oben, gleicher Grund) - bis dahin keine native Textauswahl. */
  cursor: grab;
  user-select: none;
}
.board-text-content[contenteditable="true"] { cursor: text; user-select: text; }

/* Konnektor-Griffe (makeConnectable in boards.js) - vier kleine Punkte an
   den Kantenmitten jedes anheftbaren Elements, analog zum Resize-Griff an
   der Ecke. Standardmäßig unsichtbar, nur bei Hover/Auswahl eingeblendet,
   damit sie nicht dauerhaft jedes Element zupflastern (bei kleinen Sticky
   Notes wären vier permanente Punkte an den Kanten sonst recht unruhig). */
.board-connector-port {
  position: absolute; width: 10px; height: 10px; border-radius: 50%;
  background: var(--color-accent); border: 2px solid var(--color-surface);
  opacity: 0; transition: opacity var(--dur-fast) var(--ease); cursor: crosshair; z-index: 6; pointer-events: auto;
}
.board-connector-port-top { top: 0; left: 50%; transform: translate(-50%, -50%); }
.board-connector-port-bottom { top: 100%; left: 50%; transform: translate(-50%, -50%); }
.board-connector-port-left { top: 50%; left: 0; transform: translate(-50%, -50%); }
.board-connector-port-right { top: 50%; left: 100%; transform: translate(-50%, -50%); }
.board-el:hover .board-connector-port,
.board-el.selected .board-connector-port { opacity: 1; }
.board-viewport.board-ports-hidden .board-connector-port { display: none; }

/* Ziel-Hervorhebung während des Ziehens eines Konnektors (siehe
   beginConnectorDrag in boards.js) - bewusst ein andersfarbiger, gestrichelter
   Ring statt derselben Outline wie .selected, damit "das hier wird gerade als
   Konnektor-Ziel erkannt" nicht mit "das ist gerade ausgewählt" verwechselt
   wird. */
.board-el.connector-target-hover {
  outline: 2px dashed var(--color-accent); outline-offset: 3px;
}

/* ==========================================================
   Dokumente (app/js/pages/shotdocs.js) - Breakdowns/Storyboards/
   Shotlists. Produktions-Picker/Slot-Karten nutzen dieselben
   .board-picker-number/-title/-client-Bausteine wie Boards (siehe
   oben); hier nur das, was wirklich neu ist: die Typ-Badges auf dem
   Picker, die 3 Dokument-"Slots" pro Produktion, das Konzept-Formular
   und der eigentliche Zeilen-Editor.
   ========================================================== */

/* align-items:stretch (Grid-Default, hier explizit) sorgt dafür, dass alle
   Karten EINER Zeile automatisch dieselbe Höhe bekommen (Chat-Verlauf: "die
   Badges können schon in zwei Zeilen sein, nur insgesamt zwischen den Boxen
   auf einer einheitlichen Höhe") - ein Versuch, die Badges per nowrap/Scroll
   immer auf eine Linie zu zwingen, sah schlechter aus (abgeschnittener Text,
   Scrollbalken) und wurde wieder verworfen; zwei Zeilen Badges sind in
   Ordnung, solange die Karten in der Zeile gleich hoch bleiben. */
.shotdoc-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--space-4); align-items: stretch; }
.shotdoc-picker-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.shotdoc-picker-card:hover { border-color: var(--color-accent); transform: translateY(-1px); }
.shotdoc-picker-body { min-width: 0; }
.shotdoc-badge-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; }
.shotdoc-badge {
  display: flex; align-items: center; gap: 5px;
  padding: 4px 9px;
  border-radius: var(--radius-full);
  font-size: var(--text-small); font-weight: 600;
  background: var(--color-bg-alt); color: var(--color-text-faint);
}
.shotdoc-badge svg { width: 13px; height: 13px; }
.shotdoc-badge.is-active { background: var(--color-accent-soft); color: var(--color-accent-strong); }
/* Gleiche Farbe je Dokumenttyp wie innerhalb einer Produktion (siehe
   .stat-icon.tone-* oben, DOC_TYPES.tone in shotdocs.js) - Chat-Verlauf:
   "Innerhalb einer Produktion haben die einzelnen Listen Farben, kannst du
   das hier bei den Badges daran anpassen?". Überschreibt die generische
   .is-active-Blaufärbung oben nur für Badges mit einem zusätzlichen
   tone-*-Klassennamen (Boards bleibt bewusst ohne Ton, siehe dort). */
.shotdoc-badge.is-active.tone-success { background: var(--color-success-soft); color: var(--color-success); }
.shotdoc-badge.is-active.tone-warning { background: var(--color-warning-soft); color: var(--color-warning); }
.shotdoc-badge.is-active.tone-danger { background: var(--color-danger-soft); color: var(--color-danger); }
.shotdoc-badge.is-active.tone-violet { background: var(--color-violet-soft); color: var(--color-accent-2); }
.shotdoc-badge-count { opacity: 0.7; }

/* Kein max-width mehr (siehe Feedback) - wie auf allen anderen Seiten die
   volle Bildschirmbreite nutzen. Fünf gleich große Kacheln (Boards,
   Breakdown, Storyboard, Shotlist, Renderliste, siehe renderDetail() -
   Chat-Verlauf: "alle 5 Boxen gleich groß") - align-items:stretch (Grid-
   Standard, vorher explizit auf "start" gesetzt) sorgt dafür, dass jede
   Kachel einer Zeile dieselbe Höhe wie die höchste bekommt, unabhängig davon,
   wie viele Einträge sie gerade listet. Das Konzept-Formular ist jetzt kein
   Grid-Kind mehr, sondern steckt in einem Modal (siehe openBreakdownModal()).
*/
.shotdoc-slots { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }
.shotdoc-concept-form h3 { margin: 0 0 4px; }
.shotdoc-slot-card {
  display: flex; align-items: center; gap: var(--space-3);
  text-decoration: none; color: var(--color-text);
  transition: border-color var(--dur-fast) var(--ease);
}
.shotdoc-slot-card:hover { border-color: var(--color-accent); }
.shotdoc-slot-card.is-empty { cursor: default; }
.shotdoc-slot-icon {
  width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--radius-sm);
  background: var(--color-accent-soft); color: var(--color-accent);
  display: flex; align-items: center; justify-content: center;
}
.shotdoc-slot-icon svg { width: 20px; height: 20px; }
.shotdoc-slot-title { font-weight: 600; font-size: 14.5px; display: flex; align-items: center; gap: 8px; }
.shotdoc-gen-badge { font-size: var(--text-small); font-weight: 600; padding: 2px 7px; border-radius: var(--radius-full); background: var(--color-bg-alt); color: var(--color-text-muted); }
.shotdoc-gen-badge.is-generating { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.shotdoc-gen-badge.is-failed { background: var(--color-danger-soft); color: var(--color-danger); }
.shotdoc-slot-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; }
.shotdoc-manual-link {
  background: none; border: none; padding: 0; margin: 0;
  font-size: 12px; color: var(--color-text-faint); text-decoration: underline;
  cursor: pointer;
}
.shotdoc-manual-link:hover { color: var(--color-accent); }

/* Konzept-PDF-Dropzone im Konzept-Formular - das Konzept wird in der Praxis
   fast immer als PDF hochgeladen (siehe Feedback), die Textarea darunter ist
   nur noch eine optionale Ergänzung, kein Ersatz. */
.shotdoc-concept-drop {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4);
  border: 1.5px dashed var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.shotdoc-concept-drop.is-dragover { border-color: var(--color-accent); background: var(--color-accent-soft); }
.shotdoc-concept-drop.has-file { border-style: solid; border-color: var(--color-accent); }
.shotdoc-concept-drop-icon {
  width: 36px; height: 36px; flex-shrink: 0; border-radius: var(--radius-sm);
  background: var(--color-accent-soft); color: var(--color-accent);
  display: flex; align-items: center; justify-content: center;
}
.shotdoc-concept-drop-icon svg { width: 18px; height: 18px; }
.shotdoc-concept-drop-text { min-width: 0; flex: 1; }
.shotdoc-concept-drop-title { font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shotdoc-concept-drop-hint { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 2px; }
.shotdoc-concept-drop-remove { flex-shrink: 0; }

/* Vorschau-/Korrektur-Liste im Batch-Bildimport-Modal (siehe
   openBatchImportModal() in shotdocs.js, Chat-Verlauf: "Batch-Import direkt
   alle Bilder hochzuladen und diese werden den Feldern zugeordnet") - eine
   Zeile pro ausgewählter Datei mit der automatisch geratenen bzw. manuell
   korrigierbaren Ziel-Zeile. */
.shotdoc-batch-list { max-height: 320px; overflow-y: auto; border: 1px solid var(--color-border); border-radius: var(--radius-md); margin-top: 10px; }
.shotdoc-batch-row { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-bottom: 1px solid var(--color-border); }
.shotdoc-batch-row:last-child { border-bottom: none; }
.shotdoc-batch-filename { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; }
.shotdoc-batch-row select { width: 240px; max-width: 55%; flex-shrink: 0; }

.spinner-inline {
  display: inline-block; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.4); border-top-color: #fff;
  animation: spin 0.7s linear infinite; margin-right: 4px; vertical-align: -1px;
}
@keyframes spin { to { transform: rotate(360deg); } }

.shotdoc-header { align-items: flex-start; flex-wrap: wrap; gap: var(--space-3); }
.shotdoc-title-row { display: flex; align-items: center; gap: 10px; margin-top: 4px; flex-wrap: wrap; }
.shotdoc-title-input {
  font-size: 20px; font-weight: 700; border: none; background: transparent;
  padding: 2px 4px; border-radius: 6px; color: var(--color-text);
  /* Tatsächliche Breite kommt aus autosizeTitleInput() (JS, wächst mit dem
     Inhalt) - width hier nur als Fallback vor dem ersten requestAnimationFrame
     und obere Grenze, damit ein sehr langer Titel nicht die ganze Zeile
     sprengt (die Zeile bricht dank flex-wrap auf .shotdoc-title-row nötigenfalls
     um). Kein flex:1 mehr (siehe Chat-Verlauf: "Badge hängt jetzt ganz weit
     rechts" - flex:1 füllte immer die GANZE Zeile, egal wie kurz der Titel
     war, und drückte den Typ-Badge daneben an den rechten Rand). */
  width: 200px; max-width: 100%;
}
.shotdoc-title-input:hover, .shotdoc-title-input:focus { background: var(--color-bg-alt); outline: none; }
/* Design-System-Cleanup 2026-08-16 (Phase 4c): gleiche Logik wie .badge -
   font-weight von 700 auf --weight-semibold zurückgenommen, font-size auf
   --text-small vereinheitlicht (gleicher Wert wie das benachbarte
   .shotdoc-gen-badge). */
.shotdoc-type-pill { font-size: var(--text-small); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: 0.04em; padding: 3px 9px; border-radius: var(--radius-full); background: var(--color-accent-soft); color: var(--color-accent-strong); }
/* Eigene, volle-Breite Zeile statt Teil der Titelzeile (siehe Chat-Verlauf:
   "Badge sitzt immer noch nicht zentriert über der Tabelle") - so ist das
   Badge immer exakt mittig über der Tabelle, unabhängig von der Titellänge. */
.shotdoc-type-pill-row { text-align: center; margin: 14px 0 6px; }
.shotdoc-header .field-row input { max-width: 220px; font-size: 12.5px; padding: 6px 9px; }

/* Bildgröße der Result-Spalte (Chat-Verlauf: "Bei allen Dokumenten würde ich
   gerne die Skalierung insbesondere der Bilder selbst wählen können ...
   primär um die Größe der Bilder, vor allem Result"), siehe
   RESULT_IMAGE_SCALES/resultScaleToggle in shotdocs.js - dreistufiger
   Segmented-Toggle im Dokument-Header, nur sichtbar wenn eine Result-Spalte
   existiert. Gleiches Segmented-Button-Muster wie der bestehende
   Auswählen-Toggle in branding.js, nur mit drei statt zwei Zuständen. */
.shotdoc-scale-toggle {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
}
.shotdoc-scale-btn {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: var(--text-meta);
  font-weight: 600;
  padding: 5px 10px;
  border-radius: calc(var(--radius-md) - 2px);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.shotdoc-scale-btn:hover { color: var(--color-text); }
.shotdoc-scale-btn.is-active { background: var(--color-surface); color: var(--color-accent-strong); box-shadow: var(--shadow-sm); }

.shotdoc-table { margin-top: var(--space-4); overflow-x: auto; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface); }
.shotdoc-row {
  display: grid;
  align-items: stretch;
  border-bottom: 1px solid var(--color-border);
}
.shotdoc-row:last-child { border-bottom: none; }
.shotdoc-row.is-dragover { background: var(--color-accent-soft); }
.shotdoc-row.is-dragging { opacity: 0.4; }
/* Progress-Spalte (Renderliste, siehe progressCell() in shotdocs.js) - die
   Stufen-Reihe (.set-status) ist von Haus aus nur so breit wie ihr Inhalt
   und klebt deshalb ohne diese Klasse linksbündig in der (breiteren)
   Zelle (Chat-Verlauf: "Progress-Bar horizontal in der Zelle zentrieren"). */
.shotdoc-progress-status { justify-content: center; }
/* Lösch-Button am Zeilenende optisch von den Datenspalten davor abheben
   (Chat-Verlauf: "den Delete Button optisch absetzen") - dünne Trennlinie
   plus dezente Rot-Färbung beim Hover, ähnlich der Spalten-Lösch-Aktion
   oben (.shotdoc-col-delete-btn:hover). */
.shotdoc-row-delete-btn { border-left: 1px solid var(--color-border); border-radius: 0; color: var(--color-text-faint); }
.shotdoc-row-delete-btn:hover { color: var(--color-danger, #d64545); background: var(--color-danger-soft, rgba(214, 69, 69, 0.1)); }
/* Text-Label nur für die mobile Kartenansicht gedacht (siehe
   @media(max-width:900px) unten) - am Desktop reicht das Icon in der
   schmalen 36px-Spalte, da wäre der Text zu eng/würde umbrechen. */
.shotdoc-row-delete-label { display: none; }
.shotdoc-header-row { position: sticky; top: 0; background: var(--color-surface-2); z-index: 1; }
/* Grip (links) und Aktions-Icons (rechts, siehe .shotdoc-col-actions unten)
   sind beide absolut positioniert statt Flex-Kinder - vorher hat das Grip-
   Icon trotz opacity:0 im Ruhezustand noch Flex-Platz reserviert, wodurch
   Label/Icon sichtbar nach rechts verschoben und NICHT exakt mittig im
   Spaltenkopf standen (Chat-Verlauf: "Spalten-Headlines zentriert setzen -
   auch die Icons und Texte. Exakt mittig"). Jetzt ist .shotdoc-th-label das
   einzige Flex-Kind und wird per justify-content:center exakt über die
   gesamte Spaltenbreite zentriert; Grip/Aktionen legen sich (nur beim
   Hover sichtbar) einfach darüber. */
.shotdoc-th { position: relative; padding: 8px 10px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-faint); display: flex; align-items: center; justify-content: center; }
.shotdoc-th-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; text-align: center; }
/* Icon statt Text für die vier Renderliste-Checkbox-Spalten (siehe
   CHECKBOX_COLUMN_ICONS/columnLabelEl() in shotdocs.js) - zentriert statt
   linksbündig, Textlabel-Farbe/-größe geerbt vom Spaltenkopf, nur die Größe
   etwas angehoben (die Icons sind sonst kaum sichtbar). */
.shotdoc-th-icon-label { display: flex; align-items: center; justify-content: center; }
.shotdoc-th-icon-label svg { width: 14px; height: 14px; }
/* Absolut positioniert statt als Flex-Kind (siehe Feedback: "Spalten viel zu
   schmal, daher passt die Überschrift und die beiden Buttons nicht rein und
   es flackert") - so beanspruchen die zwei Aktions-Icons NIE reservierten
   Platz in ohnehin schon engen Spalten (Nr./Done etc.), sondern legen sich
   beim Hover einfach über den (dann meist eh nicht gelesenen) Label-Text,
   mit passendem Hintergrund als weicher Übergang. */
.shotdoc-col-actions {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 1px; padding-left: 10px;
  background: linear-gradient(to right, transparent, var(--color-surface-2) 30%, var(--color-surface-2));
  /* Vorher hatten nur die Buttons opacity:0, der Container selbst (mit
     seinem Verlaufs-Hintergrund) war immer sichtbar - in schmalen 68px
     Checkbox-Spalten (Layout/Low-Res/High-Res/Delivery) hat das den
     zentrierten Spalten-Icon dauerhaft verdeckt, nicht nur beim Hover
     (Chat-Verlauf: "ich vermute die verschwinden hinter den Edit/Delete
     Icons" - genau das war der Fall). Jetzt der ganze Container erst beim
     Hover sichtbar, wie die Buttons selbst. */
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.shotdoc-th:hover .shotdoc-col-actions { opacity: 1; }
.shotdoc-col-menu-btn { flex-shrink: 0; }
.shotdoc-col-delete-btn:hover { color: var(--color-danger, #d64545); }
/* Spalten sind wie Zeilen per Grip-Icon per Drag&Drop sortierbar (siehe
   Feedback) - dieselben is-dragover/is-dragging-Zustände wie .shotdoc-row. */
.shotdoc-col-grip {
  position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center; cursor: grab; color: var(--color-text-faint); opacity: 0; transition: opacity var(--dur-fast) var(--ease);
}
.shotdoc-col-grip svg { width: 12px; height: 12px; }
.shotdoc-th:hover .shotdoc-col-grip { opacity: 1; }
.shotdoc-col-grip:active { cursor: grabbing; }
.shotdoc-th.is-dragover { background: var(--color-accent-soft); }
.shotdoc-th.is-dragging { opacity: 0.4; }
.shotdoc-grip { display: flex; align-items: center; justify-content: center; cursor: grab; color: var(--color-text-faint); }
.shotdoc-grip svg { width: 14px; height: 14px; }
.shotdoc-grip:active { cursor: grabbing; }

/* Auswahl-Checkbox statt Grip im Zeilen-Merge-Modus (Chat-Verlauf: "Zeilen
   'mergen'", siehe toggleMergeMode()/shotRow() in shotdocs.js) - nutzt
   dieselbe 32px-Spalte wie der sonst dort sitzende Grip. */
.shotdoc-select-cell { display: flex; align-items: center; justify-content: center; }
.shotdoc-select-cell input[type="checkbox"] { width: 15px; height: 15px; cursor: pointer; }
.shotdoc-row.is-selected { background: var(--color-accent-soft); }
.shotdoc-merge-toggle-btn.is-active { background: var(--color-accent-soft); color: var(--color-accent-strong); border-color: var(--color-accent); }

.shotdoc-row input, .shotdoc-row textarea {
  width: 100%; border: 1px solid transparent; background: transparent;
  font-size: 12.5px; padding: 8px 9px; border-radius: 0; color: var(--color-text);
  min-height: 36px; font-family: inherit;
}
/* Zeilenhöhe passt sich immer dem Content an (siehe Feedback) - Textareas
   wachsen per JS (autosizeTextarea() in shotdocs.js) mit ihrem Inhalt statt
   auf fester Höhe abzuschneiden, deshalb kein manueller Resize-Handle mehr
   und kein eigener Scrollbalken. Die umgebende .shotdoc-row richtet sich per
   align-items:stretch an der jeweils höchsten Zelle einer Zeile aus. */
.shotdoc-row textarea { resize: none; overflow: hidden; line-height: 1.4; }
.shotdoc-row input { min-height: auto; }
.shotdoc-row input:hover, .shotdoc-row textarea:hover,
.shotdoc-row input:focus, .shotdoc-row textarea:focus { border-color: var(--color-border-strong); background: var(--color-bg-alt); outline: none; }
/* .shotdoc-cell statt .shotdoc-row als direkter Elternteil, seit jede Zelle
   in einen Wrapper mit data-label gesteckt wird (siehe shotRow() in
   shotdocs.js, Mobile-Kartenansicht) - display:contents auf .shotdoc-cell
   (siehe unten) macht den Wrapper für das CSS-Grid selbst unsichtbar, aber
   Selektoren müssen trotzdem über den tatsächlichen DOM-Elternteil gehen. */
.shotdoc-cell > input, .shotdoc-cell > textarea { border-right: 1px solid var(--color-border); }
/* Wrapper um jede Zelle (siehe shotRow() in shotdocs.js) - auf dem Desktop
   komplett transparent für das Grid-Layout: display:contents lässt den
   Wrapper selbst keine eigene Box erzeugen, sein einziges Kind wird direkt
   zum Grid-Element an genau der Stelle, an der vorher (ohne Wrapper) das
   Kind selbst stand - am Desktop-Layout ändert sich dadurch nichts. Erst in
   der Mobile-Media-Query weiter unten wird daraus ein echter Block mit
   data-label-Beschriftung. */
.shotdoc-cell { display: contents; }

.shotdoc-shotnr-cell {
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: var(--color-text-faint);
  border-right: 1px solid var(--color-border); padding: 8px 4px;
}

.shotdoc-section-row { grid-template-columns: 32px 1fr 36px !important; background: var(--color-bg-alt); }
.shotdoc-section-label { display: flex; align-items: center; padding: 4px 0; }
/* !important auf font-size: die App-weite Anti-iOS-Zoom-Regel weiter oben
   in dieser Datei ("input, select, textarea { font-size: 16px !important;
   }") gewinnt sonst auf jedem Bildschirm <=900px IMMER gegen eine normale
   (nicht-!important) Regel, unabhängig von Selektor-Spezifität - genau das
   hat die kleine Schrift dieser Trennzeilen-Beschriftung die ganze Zeit
   verhindert (Chat-Verlauf: "In der Trennzeile ist die Schrift nach wie vor
   riesig ... da ist ganz sicher irgendwo ein Fehler drin"). Zwei !important-
   Regeln werden zuerst nach Spezifität verglichen (erst danach nach
   Quellreihenfolge) - ".shotdoc-section-label input" (Klasse+Typ) ist
   spezifischer als das bloße "input" oben und gewinnt deshalb zuverlässig.
   Bewusster Trade-off: beim Antippen dieses einen Feldes (Trennzeile
   umbenennen) kann iOS Safari kurz reinzoomen, da es seltener bearbeitet
   wird als normale Zeilenfelder wiegt das weniger schwer als die permanent
   sichtbare, viel zu große Schrift. */
.shotdoc-section-label input { font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: 11.5px !important; }

/* Genau EIN Bild pro Zelle, deutlich größer als vorher dargestellt (siehe
   Feedback: "damit man etwas erkennt") - füllt die ganze (jetzt breitere)
   Spalte, Klick auf das Bild ersetzt es, ein Hover-Kreuz löscht es. */
.shotdoc-image-cell { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 6px; border-right: 1px solid var(--color-border); transition: background var(--dur-fast) var(--ease); }
/* Drag&Drop-Feedback für die ganze Zelle, nicht nur für die versionierte
   Variante unten (siehe .is-versioned.is-dragover) - vorher ließ sich hier
   gar kein Bild draufziehen, nur per Klick hochladen (Chat-Verlauf: "Ich
   kann kein Bild auf das 'Bild +' Feld ziehen"). */
.shotdoc-image-cell.is-dragover { background: var(--color-accent-soft); }
/* Fokus-Ring, wenn die Zelle für Strg/Cmd+V-Einfügen aus der Zwischenablage
   fokussiert ist (Chat-Verlauf: "Bilder aus der Zwischenablage direkt
   einfügen") - macht sichtbar, dass die Zelle gerade "aktiv" fürs Einfügen
   ist, gleiches Prinzip wie der dragover-Zustand direkt darüber. */
.shotdoc-image-cell:focus,
.shotdoc-image-cell:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
/* Fünf gleich große Kacheln (Boards/Breakdown/Storyboard/Shotlist/
   Renderliste) - eigener Add-Button oben rechts in der Ecke statt inline
   neben dem Titel (Chat-Verlauf: "Mach doch in dem Button einfach ein
   plus-Symbol und das rechts oben in die Ecke"), position:relative auf der
   Kachel selbst als Bezugsrahmen dafür. */
.shotdoc-tile { position: relative; }
.shotdoc-tile-add-btn {
  position: absolute; top: var(--space-5); right: var(--space-5);
  width: 30px; height: 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-accent);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.shotdoc-tile-add-btn svg { width: 15px; height: 15px; }
.shotdoc-tile-add-btn:hover { background: var(--color-accent); border-color: transparent; color: #fff; transform: translateY(-1px); }
/* Titel + Farb-Icon nebeneinander, als EIN Flex-Kind von .card-header (siehe
   dessen justify-content:space-between - mit dem Add-Button jetzt komplett
   raus aus .card-header, siehe .shotdoc-tile-add-btn, würde ein zweites
   direktes Kind sonst an den rechten Rand gedrückt statt eng am Icon zu
   kleben). Etwas kleineres .stat-icon als in den Stat-Cards, passend zur
   kompakteren Kachel-Überschrift. */
.shotdoc-tile-title { display: flex; align-items: center; gap: 10px; }
.shotdoc-tile-title .stat-icon { width: 30px; height: 30px; }
.shotdoc-tile-title .stat-icon svg { width: 14px; height: 14px; }

.shotdoc-image-thumb {
  position: relative; width: 100%; height: 128px; border-radius: 8px; overflow: hidden;
  border: 1px solid var(--color-border); cursor: pointer;
}
.shotdoc-image-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shotdoc-image-del {
  position: absolute; top: 4px; right: 4px; width: 22px; height: 22px; border-radius: 50%;
  background: rgba(0,0,0,0.6); color: #fff; display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity var(--dur-fast) var(--ease); cursor: pointer;
}
.shotdoc-image-del svg { width: 11px; height: 11px; }
.shotdoc-image-thumb:hover .shotdoc-image-del { opacity: 1; }
/* "✨ Generieren" (siehe Chat-Verlauf, GenAI-Moodbild) - overlay-Button
   analog zu .shotdoc-image-del, nur oben LINKS statt rechts. */
.shotdoc-image-regen {
  position: absolute; top: 4px; left: 4px; width: 22px; height: 22px; border-radius: 50%;
  background: rgba(0,0,0,0.6); color: #fff; display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity var(--dur-fast) var(--ease); cursor: pointer; font-size: 11px; border: none; padding: 0;
}
.shotdoc-image-thumb:hover .shotdoc-image-regen { opacity: 1; }
.shotdoc-image-regen:hover:not(:disabled) { background: rgba(0,0,0,0.85); }
.shotdoc-image-regen:disabled,
.shotdoc-image-thumb:hover .shotdoc-image-regen:disabled { opacity: 0; pointer-events: none; }
/* Kompakter als vorher (Chat-Verlauf: "gestrichelten Rahmen mit Bild +
   kleiner machen, damit man leichter in das Feld klicken kann") - der
   Platzhalter füllte vorher fast die ganze Zeilenhöhe mit viel leerem Raum,
   dadurch wirkte er eher wie eine unbestimmte Fläche als ein klar
   klickbarer Button. */
.shotdoc-image-add {
  flex: 1; height: 60px; border-radius: 8px; border: 1.5px dashed var(--color-border-strong);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  color: var(--color-text-faint); font-size: 11px; font-weight: 600;
  cursor: pointer; background: transparent;
}
.shotdoc-image-add:hover { border-color: var(--color-accent); color: var(--color-accent); }
.shotdoc-image-add svg { width: 15px; height: 15px; }
/* "✨ Generieren" neben dem leeren Platzhalter - schmale Extra-Spalte statt
   volle Breite wie .shotdoc-image-add, damit beide nebeneinander passen. */
.shotdoc-image-gen {
  flex-shrink: 0; width: 32px; height: 60px; border-radius: 8px; border: 1.5px dashed var(--color-border-strong);
  display: flex; align-items: center; justify-content: center; font-size: 15px;
  color: var(--color-text-faint); cursor: pointer; background: transparent;
}
.shotdoc-image-gen:hover:not(:disabled) { border-color: var(--color-accent); color: var(--color-accent); background: var(--color-accent-soft); }
.shotdoc-image-gen:disabled { opacity: 0.3; cursor: not-allowed; }
.shotdoc-done-cell { display: flex; align-items: center; justify-content: center; border-right: 1px solid var(--color-border); }
.shotdoc-done-cell input[type="checkbox"] { width: 15px; height: 15px; cursor: pointer; }

/* Versionierte Bildzelle (Renderliste, siehe versionedImageCell() in
   shotdocs.js) - Spaltenlayout statt der zentrierten Reihe von
   .shotdoc-image-cell oben, damit unter dem Thumbnail Platz für die
   Versions-Auswahl bleibt. Drag&Drop-Ziel ist die GANZE Zelle, nicht nur
   der Thumbnail/Add-Button. */
.shotdoc-image-cell.is-versioned { flex-direction: column; align-items: stretch; gap: 4px; transition: background var(--dur-fast) var(--ease); border-radius: 6px; }
.shotdoc-image-cell.is-versioned.is-dragover { background: var(--color-accent-soft); }
/* Höhe = neue 'Mittel'-Stufe (234px, siehe RESULT_IMAGE_SCALES in
   shotdocs.js - Neukalibrierung: altes 'Groß' ist jetzt 'Klein', 'Groß' neu
   ist doppelt so groß wie das alte 'Groß') - das ist der Fallback/Default
   für Dokumente ohne explizit gespeicherten Wert. */
.shotdoc-image-cell.is-versioned .shotdoc-image-thumb { flex: 0 0 auto; height: 234px; }
/* Höhen-Varianten für die drei Result-Bildgrößen (siehe .shotdoc-scale-btn
   oben + RESULT_IMAGE_SCALES in shotdocs.js). Die Zellenbreite selbst kommt
   separat aus widthForColumn() (Grid-Spaltenbreite), hier geht's nur um die
   Thumbnail-Höhe innerhalb der Zelle. Volle Selektorkette (.shotdoc-image-
   cell.is-versioned.shotdoc-result-scale-*) statt nur der Skalierungs-Klasse
   allein - sonst wäre die Spezifität (0,2,0) NIEDRIGER als die 234px-
   Grundregel direkt darüber (0,3,0) und würde von dieser trotz späterer
   Reihenfolge im Stylesheet überstimmt. */
.shotdoc-image-cell.is-versioned.shotdoc-result-scale-small .shotdoc-image-thumb { height: 156px; }
/* 268px statt rechnerischer 312px (Chat-Verlauf: "mach die Stufe 'Groß'
   doch etwas kleiner") - siehe RESULT_IMAGE_SCALES.large in shotdocs.js. */
.shotdoc-image-cell.is-versioned.shotdoc-result-scale-large .shotdoc-image-thumb { height: 268px; }
/* flex:0 0 auto statt nur height (Chat-Verlauf: "Hier sollte der
   gestrichelte Rahmen kleiner sein, damit ich ins Feld klicken kann") - die
   Grund-Regel .shotdoc-image-add setzt "flex: 1", was in DIESEM Kontext
   (Elternelement mit flex-direction:column, siehe .is-versioned oben) den
   flex-basis auf 0% setzt und die Box per flex-grow auf die volle Zeilenhöhe
   aufbläst - ein reines height:60px wurde dadurch komplett ignoriert. */
/* Chat-Verlauf: "Mach die Höhe und Breite doch einfach auf ca. 60% der Box -
   dann hat man außenrum Platz zum klicken" - width:60% statt der vorherigen
   vollen Breite (align-items:stretch auf .is-versioned zieht Kinder sonst
   automatisch auf 100% Breite), margin:0 auto zentriert die schmalere Box
   (funktioniert bei Flex-Kindern auch für die Cross-Achse, hier horizontal
   wegen flex-direction:column am Elternelement). */
/* Chat-Verlauf: "machs doch etwas größer und einfach ein Plus-Symbol rein -
   ohne das Wort 'Bild'" - Text-Label wurde im JS (versionedImageCell() in
   shotdocs.js) entfernt, hier nur noch der größere Plus-Kreis mittig. */
.shotdoc-image-cell.is-versioned .shotdoc-image-add { flex: 0 0 auto; width: 70%; height: 56px; margin: 0 auto; }
.shotdoc-image-cell.is-versioned .shotdoc-image-add svg { width: 22px; height: 22px; }
/* Chat-Verlauf: "das Varianten-Dropdown unter dem Bild auch stylen? Das
   passt nicht so ganz." - vorher reines Browser-Chrome (native Pfeile,
   kein einheitliches Padding/Feld-Look), jetzt im selben Stil wie die
   normalen Formular-Selects (.field select oben) - eigener Chevron statt
   OS-Pfeil, volle Zellenbreite statt Inhaltsbreite, dezenter Hover/Focus. */
.shotdoc-image-version-select {
  width: 100%;
  font-size: var(--text-meta);
  font-weight: 600;
  padding: 6px 26px 6px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background-color: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239598ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 13px 13px;
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.shotdoc-image-version-select:hover { border-color: var(--color-border-strong); }
.shotdoc-image-version-select:focus {
  outline: none;
  border-color: var(--color-accent);
  background-color: var(--color-surface-2);
}

/* ==========================================================
   Inspiration-Board (Chat-Verlauf: "Ich möchte auch ein Inspiration-Board,
   in das man alles reinhauen kann, was man so im Web an Inspiration
   findet." -> "Neue Pinterest-Style Galerie (Empfohlen)"), siehe
   pages/inspiration.js + api/inspiration.php.
   ========================================================== */

.inspiration-board-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-4);
}

.inspiration-board-card {
  position: relative;
  display: block;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
/* Deutlicherer Lift, gleiches Prinzip wie .stat-card:hover (Chat-Verlauf:
   "offensichtlicher machen"). */
.inspiration-board-card:hover { transform: translateY(-6px) scale(1.02); box-shadow: var(--shadow-lg); border-color: var(--color-accent); }

.inspiration-board-cover {
  aspect-ratio: 4 / 3;
  background: var(--color-bg-alt);
  overflow: hidden;
}
.inspiration-board-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.inspiration-board-cover.is-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
}
.inspiration-board-cover.is-empty svg { width: 30px; height: 30px; }

.inspiration-board-body { padding: 12px 14px; }
.inspiration-board-title {
  font-weight: 700;
  font-size: var(--text-entity-title);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.inspiration-board-meta { font-size: 12px; color: var(--color-text-muted); margin-top: 3px; }

.inspiration-board-menu-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.9);
  border: none;
  border-radius: 50%;
  color: var(--color-text);
  box-shadow: var(--shadow-sm);
}
.inspiration-board-menu-btn svg { width: 14px; height: 14px; }
html[data-theme="dark"] .inspiration-board-menu-btn { background: rgba(21, 23, 28, 0.9); }

.inspiration-board-menu {
  position: absolute;
  top: 38px;
  right: 8px;
  z-index: 3;
  display: none;
  flex-direction: column;
  min-width: 150px;
  padding: 6px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}
.inspiration-board-menu.is-open { display: flex; }
.inspiration-board-menu button {
  text-align: left;
  padding: 8px 9px;
  border: none;
  background: none;
  border-radius: 6px;
  font-size: var(--text-body);
  color: var(--color-text);
  cursor: pointer;
}
.inspiration-board-menu button:hover { background: var(--color-bg-alt); }
.inspiration-board-menu button.is-danger { color: var(--color-danger); }
.inspiration-board-menu button.is-danger:hover { background: var(--color-danger-soft); }

.inspiration-add-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.inspiration-add-row input {
  flex: 1;
  min-width: 220px;
  padding: 9px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  font-family: inherit;
  background: var(--color-surface-2);
  color: var(--color-text);
}
.inspiration-add-row input:focus {
  outline: none;
  border-color: var(--color-accent);
  background: var(--color-surface);
  box-shadow: var(--shadow-glow);
}

/* Pinterest-artiges Masonry per CSS-Spalten - kein JS-Layout-Measuring
   nötig, jede Karte bricht einfach in die nächste Spalte um, sobald ihre
   eigene sitzt (Bilder behalten ihr natürliches Seitenverhältnis). */
.inspiration-masonry {
  columns: 4 260px;
  column-gap: var(--space-4);
  margin-top: var(--space-5);
}

.inspiration-card {
  position: relative;
  break-inside: avoid;
  margin-bottom: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
/* Deutlicherer Lift (Chat-Verlauf: "offensichtlicher machen"), gleiches Prinzip wie .stat-card:hover. */
.inspiration-card:hover { transform: translateY(-6px) scale(1.02); box-shadow: var(--shadow-lg); }
.inspiration-card.is-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 140px;
  cursor: default;
}
.inspiration-card img { width: 100%; display: block; }
.inspiration-card-noimage {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 120px;
  background: var(--color-bg-alt);
  color: var(--color-text-faint);
}
.inspiration-card-noimage svg { width: 26px; height: 26px; }
.inspiration-card-body { padding: 10px 12px; }
.inspiration-card-title {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--color-text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.inspiration-card-domain { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 3px; }

.inspiration-card-remove {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 15, 20, 0.55);
  border: none;
  border-radius: 50%;
  color: #fff;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.inspiration-card-remove svg { width: 11px; height: 11px; }
.inspiration-card:hover .inspiration-card-remove { opacity: 1; }

.inspiration-lightbox img {
  width: 100%;
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
}
.inspiration-lightbox-body h3 { margin: 0 0 6px; }
.inspiration-lightbox-body .btn { margin-top: 10px; }

/* Branding-Bereich (Chat-Verlauf: "einen Branding-Bereich bauen, in dem
   alles zum Thema Branding abgelegt wird") - Kachel-Grid pro fester
   Kategorie, gleiches Grundmuster wie .inspiration-card oben, aber als
   gleichmäßiges Grid statt Masonry (Kacheln haben hier immer dieselbe
   Höhe, da auch Nicht-Bild-Dateien einen Platzhalter-Bereich fester Größe
   bekommen). */
.branding-sections { display: flex; flex-direction: column; gap: var(--space-6); margin-top: var(--space-5); }
/* Jede Sektion ist zugleich eine Drop-Zone (Chat-Verlauf: "alle Felder in
   Branding als Drop-Zones anlegen") - gleiches is-dragover-Prinzip wie
   .filetransfer-dropzone/.documents-box, hier auf die ganze Sektion statt
   nur eine einzelne Box angewendet. */
.branding-section {
  border: 1.5px dashed transparent;
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  margin: calc(var(--space-3) * -1);
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.branding-section.is-dragover {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}
.branding-section-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.branding-section-header h3 { margin: 0 0 2px; font-size: 15px; }
.branding-section-header p { margin: 0; font-size: 12.5px; }
.branding-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-3);
}
.branding-empty {
  grid-column: 1 / -1;
  padding: var(--space-5);
  text-align: center;
  font-size: var(--text-body);
  color: var(--color-text-muted);
  background: var(--color-bg-alt);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-lg);
}

.branding-tile {
  position: relative;
  display: block;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  color: inherit;
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
  /* UX-Audit 2026-08-16 (Prompt 2, Finding #8): war zuvor als zweite,
     separate Regel weiter unten definiert (Kachel ist im Auswahl-Modus ein
     <div> statt <a>, braucht daher einen expliziten Cursor) - hierher
     zusammengeführt; schadet auch beim normalen <a>-Fall nicht (hätte ohnehin
     schon einen Pointer-Cursor). */
  cursor: pointer;
}
.branding-tile:hover { transform: translateY(-4px) scale(1.01); box-shadow: var(--shadow-lg); }
.branding-tile img { width: 100%; height: 110px; object-fit: cover; display: block; background: var(--color-bg-alt); }
.branding-tile-noimage {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 110px;
  background: var(--color-bg-alt);
  color: var(--color-text-faint);
}
.branding-tile-noimage svg { width: 26px; height: 26px; }
.branding-tile-ext { font-size: var(--text-small); font-weight: 700; letter-spacing: 0.03em; color: var(--color-text-muted); }
.branding-tile-body { padding: 8px 10px; }
.branding-tile-title {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.branding-tile-meta { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 2px; }

/* Kachel-Aktionsleiste (Chat-Verlauf: "hätte ich gerne eine Funktion um
   Ordner umzubenennen und einzelne Dateien und auch komplette Ordner
   herunterzuladen") - ersetzt den früheren einzelnen Lösch-Button oben
   rechts durch eine kleine horizontale Button-Reihe (Download [+ Umbenennen
   bei Ordnern] + Löschen), gleiches Hover-Einblenden wie zuvor. */
.branding-tile-actions {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 2;
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.branding-tile:hover .branding-tile-actions { opacity: 1; }
.branding-tile-action {
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 15, 20, 0.55);
  border: none;
  border-radius: 50%;
  color: #fff;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.branding-tile-action:hover { background: rgba(15, 15, 20, 0.8); }
.branding-tile-action svg { width: 10px; height: 10px; }
.branding-tile-action.is-danger:hover { background: var(--color-danger, #d9483f); }

/* Zwei Upload-Buttons (Datei/Ordner) nebeneinander im Sektionskopf. */
.branding-section-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* Mehrfachauswahl (Chat-Verlauf: "Gibts die Möglichkeit Dateien per Auswahl
   zu markieren und in einen neuen Ordner zu verschieben, gemeinsam zu
   löschen usw?") - gleiches is-active-Muster wie .shotdoc-merge-toggle-btn. */
.branding-select-toggle-btn.is-active { background: var(--color-accent-soft); color: var(--color-accent-strong); border-color: var(--color-accent); }

.branding-bulk-bar {
  display: none;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  margin: 10px 0;
  background: var(--color-accent-soft);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  font-size: 12.5px;
}
.branding-bulk-count { color: var(--color-accent-strong); font-weight: 600; margin-right: auto; }

/* Kachel im Auswahl-Modus (siehe assetTile() in branding.js) - <div> statt
   <a>, daher Cursor hier explizit setzen; is-selected hebt die Kachel farblich
   ab, gleiches Muster wie andere Auswahl-Zustände in der App
   (z.B. .shotdoc-row.is-selected). */
.branding-tile.is-selected { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.branding-tile-select {
  position: absolute;
  top: 6px;
  left: 6px;
  z-index: 2;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 15, 20, 0.55);
  border-radius: 50%;
}
.branding-tile-select input[type="checkbox"] { width: 15px; height: 15px; cursor: pointer; }

/* Info-Zeile im Sektionskopf - zeigt je nach Ebene entweder den
   Kategorie-Hinweistext ODER die Breadcrumb (siehe headerInfoNodes() in
   branding.js), nie beides gleichzeitig. */
.branding-breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.branding-breadcrumb-item {
  background: none; border: none; padding: 0; margin: 0;
  color: var(--color-text-muted); font-size: 12.5px; cursor: pointer;
}
.branding-breadcrumb-item:hover { color: var(--color-accent); text-decoration: underline; }
.branding-breadcrumb-sep { color: var(--color-text-faint); font-size: 12.5px; }
.branding-breadcrumb-current { color: var(--color-text); font-size: 12.5px; font-weight: 600; }

/* Ordner-Kacheln (Chat-Verlauf: "Ordner hochladen ... als Struktur
   anlegen") - Akzentfarbe statt des neutralen Grau-Tons bei Datei-Kacheln,
   damit Ordner auf den ersten Blick von Dateien zu unterscheiden sind. */
.branding-tile-folder .branding-tile-noimage { color: var(--color-accent); background: var(--color-accent-soft); }
.branding-tile-folder .branding-tile-noimage svg { width: 30px; height: 30px; }
/* Drop-Ziel-Hervorhebung beim Ziehen einer Dateikachel auf eine Ordner-
   Kachel (Chat-Verlauf: "Kann ich sie nicht einfach per Drag n Drop auf
   einen Ordner ziehen?") - gleiches Prinzip wie .shotdoc-image-cell.is-
   dragover an anderer Stelle im Projekt. */
.branding-tile-folder.is-dragover { outline: 2px solid var(--color-accent); outline-offset: -2px; background: var(--color-accent-soft); }

/* PDF-Vorschaubild (Chat-Verlauf: "Vorschaubilder anzeigen von PDFs") -
   object-fit:contain statt cover wie bei normalen Bild-Kacheln, damit eine
   meist hochkant gerenderte Seite nicht beschnitten wird; Innenabstand +
   Hintergrund sorgen für ein sauberes "Passepartout" um die Seite herum. */
.branding-tile-pdf-preview { object-fit: contain; background: var(--color-bg-alt); padding: 6px; box-sizing: border-box; }

/* ---------- Chat (Chat-Verlauf: "Lass uns mit dem Chat beginnen, so wie
   von dir vorgeschlagen") - Zwei-Spalten-Layout, siehe pages/chat.js. ---------- */
.chat-page {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: var(--space-4);
  height: calc(100vh - 220px);
  min-height: 420px;
}
.chat-sidebar {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
/* Chat-Verlauf: "Ich würde im Chat gerne eigene Kanäle mit unterschiedlichen
   Team-Members anlegen können" - "+ Kanal" sitzt in einer eigenen, fixen
   Kopfzeile über der Liste (siehe .chat-conv-list unten, jetzt mit eigenem
   Scroll statt die ganze .chat-sidebar zu scrollen), damit der Button beim
   Scrollen durch viele Gespräche nicht mitwandert. */
.chat-sidebar-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 10px 6px;
  flex-shrink: 0;
}
.chat-sidebar-toolbar-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.chat-new-channel-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg-alt);
  color: var(--color-text);
  font-size: var(--text-meta);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.chat-new-channel-btn:hover { background: var(--color-accent-soft); color: var(--color-accent-strong); border-color: var(--color-accent-strong); }
.chat-new-channel-btn svg { width: 13px; height: 13px; }
.chat-conv-list { flex: 1; min-height: 0; overflow-y: auto; padding: 0 6px 6px; }
.chat-conv-section-label {
  padding: 10px 10px 4px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
/* "Archiviert (N)" - einklappbarer Abschnitt ganz unten (Chat-Verlauf:
   "Chats dann später ausblenden, archivieren ... können"), gleiche Optik
   wie die normale Sektions-Überschrift oben, nur klickbar + mit Chevron. */
.chat-conv-section-label.is-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.chat-conv-section-label.is-toggle:hover { background: var(--color-bg-alt); }
.chat-conv-section-label.is-toggle svg { width: 12px; height: 12px; transition: transform var(--dur-fast) var(--ease); }
.chat-conv-section-label.is-toggle.is-expanded svg { transform: rotate(180deg); }
.chat-conv-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease);
}
.chat-conv-item:hover { background: var(--color-bg-alt); }
.chat-conv-item.is-active { background: var(--color-bg-alt); }
.chat-conv-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--sidebar-active, var(--color-accent));
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12.5px;
  flex-shrink: 0;
  background-size: cover;
  background-position: center;
}
.chat-conv-avatar-team { background: var(--color-accent); }
.chat-conv-avatar-team svg { width: 17px; height: 17px; }
.chat-conv-meta { min-width: 0; flex: 1; }
.chat-conv-name {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-conv-preview {
  font-size: 12px;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 1px;
}
.chat-conv-side { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.chat-conv-time { font-size: var(--text-small); color: var(--color-text-muted); }
.chat-conv-unread-badge {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-full);
  background: var(--color-accent);
  color: #fff;
  font-size: var(--text-small);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.chat-thread {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
}
.chat-thread-header {
  padding: 14px var(--space-4);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
  /* Vorher nur auf Mobile (@media unten) flex - jetzt immer, damit Avatar +
     Titel/Untertitel (Chat-Verlauf: Gruppen-Kanäle mit Bild/Mitgliederzahl)
     UND das neue Options-Menü nebeneinander statt untereinander stehen. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.chat-thread-header-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
.chat-thread-title-col { min-width: 0; }
.chat-thread-title {
  font-weight: 700;
  font-size: 14.5px;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-thread-subtitle { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 1px; }
/* Zurück-Pfeil zur Konversationsliste - nur auf dem Handy sichtbar (siehe
   @media(max-width:900px) unten), wo Liste/Thread sich per
   mobileThreadOpen/updateMobileView() in chat.js gegenseitig abwechseln
   statt beide gleichzeitig gestapelt zu sein. Gleiche Kreisform wie
   .chat-attach-btn. */
.chat-back-btn {
  display: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  cursor: pointer;
}
.chat-back-btn svg { width: 15px; height: 15px; }

/* Options-Menü im Thread-Header (Chat-Verlauf: "Chats dann später
   ausblenden, archivieren bzw. löschen können" + Kanal bearbeiten/verlassen)
   - gleiches Popover-Prinzip wie .rail-panel in rightrail.js (absolut
   positioniert, per is-open-Klasse ein-/ausgeblendet, gleiche
   Herauspopp-Animation), hier aber unter statt neben dem auslösenden Button. */
.chat-thread-options-wrap { position: relative; flex-shrink: 0; }
.chat-thread-options-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.chat-thread-options-btn:hover { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.chat-thread-options-btn svg { width: 16px; height: 16px; }
.chat-thread-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  display: none;
  width: 220px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 60;
  padding: 6px;
  transform-origin: top right;
  animation: panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.chat-thread-menu.is-open { display: block; }
.chat-thread-menu-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  background: none;
  border: none;
  text-align: left;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--color-text);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.chat-thread-menu-item:hover { background: var(--color-bg-alt); }
.chat-thread-menu-item svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--color-text-muted); }
.chat-thread-menu-item.is-danger { color: var(--color-danger); }
.chat-thread-menu-item.is-danger svg { color: var(--color-danger); }
.chat-thread-menu-divider { height: 1px; background: var(--color-border); margin: 6px 4px; }

/* Mitglieder-Auswahl ("Neuer Kanal"- + "Kanal bearbeiten"-Modal, siehe
   memberPickerRow() in chat.js) - Kachel-Raster statt einer langen Liste
   untereinander (Feedback: "kannst du etwas schicker machen, nicht alles
   untereinander"): Avatar + Name als kompakte Chips.
   Bewusst NEU mit CSS GRID statt Flexbox aufgebaut (vorherige Flex-Wrap-
   Variante hat den Namen wiederholt sichtbar falsch/asymmetrisch
   ausgerichtet - "hängt so seltsam rechts in der Luft rum" - vermutlich ein
   Zusammenspiel aus flex-basis/min-width/align-self, das sich nicht robust
   greifen ließ). Mit grid-template-columns: repeat(auto-fill, 76px) hat
   jeder Chip eine vom Grid selbst hart vorgegebene 76px-Spalte - keine
   Content-getriebene Breitenberechnung mehr, dadurch kann der Name
   innerhalb seines Chips gar nicht mehr "abdriften".
   Die native Checkbox bleibt für Klick/Tastatur/A11y erhalten, ist aber
   visuell unsichtbar - Auswahlstatus zeigt sich stattdessen über :has() als
   hervorgehobener Chip-Hintergrund + kleines Häkchen-Badge auf dem Avatar. */
.chat-member-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, 76px);
  justify-content: start;
  gap: 10px 6px;
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 10px;
}
/* .chat-member-picker-row ist ein <label> - die generische Formular-Regel
   ".field label { display: block; ... }" (siehe weiter oben, ~Zeile 1449)
   hat wegen HÖHERER Spezifität (.field label = zwei Selektor-Teile) das
   einfache ".chat-member-picker-row { display: flex }" (nur ein Selektor-
   Teil) trotz späterer Position im Stylesheet überschrieben - das war die
   eigentliche Ursache für die ganze Zeit falsch ausgerichteten Namen (Avatar
   blieb block/linksbündig, nur der Name wurde per text-align:center noch
   korrekt zentriert, daher der konstante Versatz). Fix: Selektor über
   ".chat-member-picker" scopen, das gleicht die Spezifität aus (zwei
   Selektor-Teile) und gewinnt zusätzlich durch die spätere Position im
   Stylesheet. */
.chat-member-picker .chat-member-picker-row {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 8px 4px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  text-align: center;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.chat-member-picker-row:hover { background: var(--color-bg-alt); }
.chat-member-picker-row:has(input:checked) {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
}
.chat-member-picker-row input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.chat-member-picker-avatar {
  position: relative;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-entity-title);
  font-weight: 700;
  flex-shrink: 0;
  background-size: cover;
  background-position: center;
}
.chat-member-picker-row:has(input:checked) .chat-member-picker-avatar::after {
  content: '✓';
  position: absolute;
  bottom: -2px;
  right: -2px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--color-surface);
}
.chat-member-picker-name {
  width: 100%;
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Titel-Zeile im "Kanal bearbeiten"-Modal - Eingabefeld + eigener
   Speichern-Button (bewusst kein automatisches Speichern pro Tastendruck,
   siehe Docblock von openEditChannelModal() in chat.js). */
.chat-group-rename-row { display: flex; gap: 8px; }
.chat-group-rename-row input { flex: 1; }

.chat-messages {
  flex: 1;
  overflow-y: auto;
  /* Bug-Report 02.09.2026 (mobil): "er scrollt immer nur die gesamte Seite,
     nicht den Chat-Verlauf". renderMessages() scrollt diese Liste ans Ende -
     wischt man von dort weiter nach oben, hat sie in dieser Richtung nichts
     mehr zu geben, und der Browser reicht die Geste standardmässig an den
     naechsten scrollbaren Vorfahren weiter (Scroll-Chaining). contain
     unterbindet genau das: die Geste bleibt hier, auch wenn sie ins Leere
     laeuft. Die zweite, tieferliegende Ursache war eine zu hohe Seite -
     siehe .app-shell-col:has(.chat-page) ganz unten in dieser Datei. */
  overscroll-behavior: contain;
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.chat-messages-empty {
  margin: auto;
  color: var(--color-text-muted);
  font-size: var(--text-body);
}
/* Eingehende Nachrichten: Avatar + Spalte (Name/Bubble) nebeneinander,
   Avatar unten an der Bubble ausgerichtet (Chat-Verlauf: "Kannst du die
   Lesbarkeit im Chat durch Profilbilder und verschiedene Farben erhöhen?").
   Eigene Nachrichten bleiben unverändert ohne Avatar (rechts ausgerichtet,
   über die Akzentfarbe schon eindeutig als "ich" erkennbar), daher der
   eigene .is-mine-Sonderfall mit align-items:flex-end statt flex-end auf der
   Kreuzachse einer Reihe. */
/* Chat-Verlauf: "Lässt sich das ganz noch flüssiger machen? Apple-mäßiger?
   Aktuell wirkt es sehr altbacken." - vorher hatte JEDE .chat-message-bubble
   unconditional die "panel-in"-Herauspopp-Animation (siehe .rail-panel),
   das lief bei JEDEM Rerender der Nachrichtenliste (z.B. auch nur eine
   einzelne alte Nachricht gelöscht) für ALLE Bubbles gleichzeitig erneut ab
   - fühlte sich mehr nach "Seite neu aufgebaut" als nach "eine Nachricht
   kam an" an. Jetzt stattdessen eine deutlich dezentere, kürzere Ein-
   blendung NUR für die jeweils neueste Nachricht (.is-new, wird pro Render
   gezielt an genau einer Position gesetzt, siehe renderMessages() in
   chat.js) - kein Bounce/Overshoot mehr, reines sanftes Auftauchen. */
@keyframes chat-message-in {
  from { opacity: 0; transform: translateY(8px) scale(0.97); }
  to { opacity: 1; transform: none; }
}
.chat-message.is-new { animation: chat-message-in 0.32s cubic-bezier(0.22, 1, 0.36, 1); }
.chat-message { display: flex; align-items: flex-end; gap: 8px; max-width: 70%; align-self: flex-start; }
/* Eigene Nachricht: unverändert gegenüber vorher (Flex-Spalte statt -Reihe,
   kein Avatar-Kind) - bewusst NICHT die neue Reihen-Regel oben einfach
   überschrieben, sondern komplett eigenständig, damit sich am
   Breiten-/Ausrichtungsverhalten (shrink-to-fit auf die Bubble-Breite)
   nichts ändert. */
.chat-message.is-mine { display: flex; flex-direction: column; align-items: flex-end; align-self: flex-end; gap: 0; }
.chat-message-col { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
/* Runder Avatar (echtes Profilbild oder farbcodierte Initialen, siehe
   messageAvatarNode() in chat.js) - Farbton kommt aus --sender-hue (per
   Inline-Style pro Nachricht gesetzt), Sättigung/Helligkeit fest hier im
   CSS, damit auf beiden Themes ausreichend Kontrast für die weißen Initialen
   bleibt (Hintergrundfarbe selbst muss dafür nicht zwischen Hell/Dunkel
   wechseln - nur der Namens-Text daneben braucht das, siehe unten). */
.chat-message-avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  flex-shrink: 0;
  background: hsl(var(--sender-hue, 210) 55% 45%);
  background-size: cover;
  background-position: center;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.chat-message-sender {
  font-size: 11px;
  font-weight: 600;
  color: hsl(var(--sender-hue, 210) 58% 40%);
  margin: 0 4px 3px;
}
/* Dunkles Theme: gleicher Farbton, aber deutlich heller/weniger gesättigt,
   sonst zu wenig Kontrast gegen den dunklen Hintergrund (siehe Doc-Kommentar
   oben). */
html[data-theme="dark"] .chat-message-sender { color: hsl(var(--sender-hue, 210) 65% 72%); }
.chat-message-bubble {
  position: relative;
  background: var(--color-bg-alt);
  border-radius: var(--radius-md);
  padding: 8px 12px;
  /* Die frühere "panel-in"-Animation ist jetzt am .chat-message-Wrapper und
     nur für .is-new (siehe oben) - hier nur noch ein sehr dezenter Schatten
     für etwas mehr Tiefe (Apple Messages-artig), kein Bounce mehr auf jeder
     Bubble bei jedem Rerender. */
  box-shadow: var(--shadow-sm);
  transition: opacity var(--dur-fast) var(--ease);
}
.chat-message.is-mine .chat-message-bubble {
  background: var(--color-accent);
  color: #fff;
  /* Platz für den Löschen-Button oben rechts (siehe .chat-message-delete-btn
     unten) - ohne das würde er den Bild-/Text-Inhalt überlappen. */
  padding-right: 30px;
}
/* Optimistisch sofort angezeigte, noch nicht vom Server bestätigte Nachricht
   (Chat-Verlauf: "dauert es kurz bis sie im Chat auftaucht ... manchmal
   erscheint sie doppelt, dann muss man manuell refreshen" - siehe
   sendMessage()/sendAttachmentFile() in chat.js) - dezent abgedunkelt +
   sanftes Pulsieren als "wird gesendet"-Hinweis, kein Spinner nötig. */
@keyframes chat-message-pending-pulse {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 0.85; }
}
.chat-message-bubble.is-pending {
  animation: chat-message-pending-pulse 1.4s ease-in-out infinite;
  box-shadow: none;
}
.chat-message-bubble.is-pending .chat-message-delete-btn { display: none; }
/* Eigene Nachricht löschen (Chat-Verlauf: "Lass mich erstmal Bilder wieder
   aus einem Chat löschen ... wichtig wäre") - dezent, aber bewusst NICHT
   nur bei :hover eingeblendet (Touch-Geräte kennen kein Hover, das Feature
   wäre auf dem Handy sonst unauffindbar), stattdessen dauerhaft leicht
   transparent und beim Berühren/Hover voll sichtbar. */
.chat-message-delete-btn {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.18);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0.55;
  transition: opacity var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.chat-message-delete-btn:hover, .chat-message-delete-btn:focus-visible {
  opacity: 1;
  background: rgba(255, 255, 255, 0.32);
}
.chat-message-delete-btn svg { width: 11px; height: 11px; }
.chat-message-text { font-size: 13.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
.chat-message-time {
  font-size: 10px;
  opacity: 0.65;
  margin-top: 3px;
  text-align: right;
}

.chat-composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px var(--space-4);
  border-top: 1px solid var(--color-border);
  flex-shrink: 0;
}
.chat-input {
  flex: 1;
  resize: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 9px 12px;
  font: inherit;
  font-size: 13.5px;
  color: var(--color-text);
  background: var(--color-bg-alt);
  max-height: 140px;
  /* Bug-Report: "Texteingabe-Input hat immer einen Scrollbalken, auch wenn
     es nur einzeilig ist." - <textarea> setzt per UA-Stylesheet default auf
     overflow:auto, wodurch schon minimale Rundungs-Differenzen zwischen
     scrollHeight und der von autoGrow() (chat.js) gesetzten height
     (Sub-Pixel-Werte werden beim Messen/Setzen unterschiedlich gerundet)
     einen permanenten, hauchdünnen Scrollbalken zeigen - auch bei einer
     einzelnen Zeile. hidden als Default blendet ihn aus; autoGrow() schaltet
     gezielt auf auto zurück, sobald der Text wirklich die max-height von
     140px erreicht (sonst wäre längerer, gedeckelter Text nicht mehr
     scrollbar erreichbar). */
  overflow-y: hidden;
}
.chat-input:focus { outline: none; border-color: var(--color-accent); }
.chat-send-btn {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #fff;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: transform var(--dur-fast) var(--ease);
}
.chat-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
.chat-send-btn:disabled { opacity: 0.5; }
.chat-send-btn svg { width: 16px; height: 16px; }

/* Anhang-Button (Chat-Verlauf: "sollte man auch Bilder und Dokumente
   schicken können") - gleiche Kreisform wie .chat-send-btn, aber neutral
   statt Akzentfarbe, damit der Senden-Button optisch die Hauptaktion
   bleibt. */
/* ---------- Anhang-Entwurf (02.09.2026) ----------
   Chat-Verlauf: "Auch erstmal ins Senden Fenster laden, Text dazu
   schreiben, dann absenden." Die Leiste sitzt ueber dem Eingabefeld und
   ist nur da, wenn wirklich etwas im Entwurf liegt (hidden am Element,
   gesetzt in components/chatdraft.js).

   Waagerecht scrollbar statt umbrechend: ein Schwung Bildschirmfotos
   wuerde die Leiste sonst auf halbe Fensterhoehe wachsen lassen und das
   Gespraech nach oben schieben. */
.chat-draft-bar {
  display: flex;
  gap: 8px;
  padding: 10px var(--space-4) 0;
  overflow-x: auto;
  overflow-y: hidden;
  flex-shrink: 0;
}
.chat-draft-item {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  max-width: 260px;
  padding: 6px 8px 6px 6px;
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
}
.chat-draft-thumb {
  width: 34px; height: 34px; flex: none;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
}
.chat-draft-icon {
  width: 34px; height: 34px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--color-surface-2);
  color: var(--color-text-faint);
}
.chat-draft-icon svg { width: 17px; height: 17px; }
.chat-draft-name {
  font-size: 12.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0;
}
.chat-draft-size { font-size: 11.5px; color: var(--color-text-faint); flex: none; font-variant-numeric: tabular-nums; }
.chat-draft-remove {
  flex: none;
  width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center;
  border: 0; padding: 0;
  border-radius: 50%;
  cursor: pointer;
  font-size: 11px;
  color: var(--color-text-faint);
  background: transparent;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.chat-draft-remove:hover { background: var(--color-danger-soft); color: var(--color-danger); }

/* Die Ablegeflaeche. Ein gestrichelter Rahmen INNEN statt einer eigenen
   Flaeche - so verschiebt sich beim Ueberfahren nichts, und der Inhalt
   bleibt lesbar. */
.chat-thread.is-dropping,
.pd-chat-messages.is-dropping,
.pd-widget-body .is-dropping {
  outline: 2px dashed var(--color-accent);
  outline-offset: -6px;
  background: var(--color-accent-soft);
}

.chat-attach-btn {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.chat-attach-btn:hover:not(:disabled) { background: var(--color-accent-soft); color: var(--color-accent); }
.chat-attach-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.chat-attach-btn svg { width: 16px; height: 16px; }

/* Emoji-Picker (Chat-Verlauf: "kannst du eine Apple-mäßige Emoji-Library
   integrieren?") - Trigger-Button sieht aus wie .chat-attach-btn (gleiche
   Kreisform, reines Unicode-Zeichen "🙂" statt SVG als Inhalt, siehe
   emojiBtn in chat.js). Panel öffnet sich NACH OBEN (bottom: 100%), weil
   der Composer ganz unten in der Spalte sitzt - gleiches Popover-Muster wie
   .chat-thread-menu (position:absolute + .is-open + panel-in-Animation),
   nur eben mit anderem Anker. */
.chat-emoji-wrap { position: relative; flex-shrink: 0; }
.chat-emoji-panel {
  display: none;
  position: absolute;
  left: 0;
  bottom: calc(100% + 8px);
  width: 280px;
  max-height: 320px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  flex-direction: column;
  z-index: 40;
  animation: panel-in 0.16s ease;
}
.chat-emoji-panel.is-open { display: flex; }
.chat-emoji-tabs {
  display: flex;
  gap: 2px;
  padding: 6px;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
  overflow-x: auto;
}
.chat-emoji-tab {
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  font-size: 15px;
  line-height: 1;
  padding: 6px;
  cursor: pointer;
  flex-shrink: 0;
  opacity: 0.55;
  transition: background var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.chat-emoji-tab:hover { opacity: 0.85; background: var(--color-bg-alt); }
.chat-emoji-tab.is-active { opacity: 1; background: var(--color-accent-soft); }
.chat-emoji-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  padding: 8px;
  overflow-y: auto;
}
.chat-emoji-btn {
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  font-size: 19px;
  line-height: 1;
  padding: 5px 0;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.chat-emoji-btn:hover { background: var(--color-bg-alt); transform: scale(1.15); }

/* Links im Nachrichtentext (Chat-Verlauf: "Können wir im Chat Links korrekt
   darstellen") - unterstrichen statt eigener Akzentfarbe auf der eigenen
   (bereits farbigen) Bubble, damit der Kontrast auf var(--color-accent) in
   beiden Themes ausreicht; bei fremden Nachrichten (neutrale Bubble) darf
   die normale Akzentfarbe verwendet werden. */
.chat-message-link { color: inherit; text-decoration: underline; text-underline-offset: 2px; word-break: break-all; }
.chat-message:not(.is-mine) .chat-message-link { color: var(--color-accent-strong); }
/* Bug-Report: "Auch im Dashboard-Chat sollten Links klickbar sein" - gleiche
   .chat-message-link-Klasse (siehe linkifyMessageText() in chat.js, jetzt
   auch im Produktions-Chat-Widget genutzt), nur eigene Zeilen-Klasse
   .pd-chat-msg statt .chat-message dort. */
.pd-chat-msg:not(.is-mine) .chat-message-link { color: var(--color-accent-strong); }

/* Bild-Anhang - Thumbnail mit begrenzter Größe oberhalb einer eventuellen
   Bildunterschrift, siehe attachmentNode() in chat.js. Ist bewusst ein
   <button> statt eines <a target="_blank">, damit der Klick das Bild in
   einem eigenen Pop-up öffnet (openImageLightbox()) statt die Datei direkt
   als Browser-Dokument - nur so lässt sich die EXIF-Auto-Drehung
   unterdrücken ("Bilder im Chat nicht automatisch drehen"). Reset der
   Button-Defaults (Rand/Hintergrund/Abstand), damit es optisch wie zuvor
   der Link aussieht. */
.chat-message-image { display: block; margin-bottom: 6px; border: 0; padding: 0; background: none; border-radius: var(--radius-md); overflow: hidden; max-width: 260px; cursor: pointer; }
.chat-message-image img { display: block; width: 100%; max-height: 280px; object-fit: cover; }

/* Bild-Pop-up (openImageLightbox() in chat.js) - mittig auf dem Bildschirm
   (Chat-Verlauf: "Zentrier das Popup bitte auf dem Bildschirm") statt wie
   normale Modals oben angeheftet, dazu eine eigene, aufs Bild zugeschnittene
   max-width statt der vollen Content-Breite (kein wide:true mehr) - dadurch
   sitzt der Download-Button (jetzt echter Modal-Footer, siehe chat.js) auch
   nicht mehr weit rechts im Leerraum, sondern direkt unter dem Bild. */
.modal-backdrop.chat-image-modal { align-items: center; }
.modal.chat-image-modal { max-width: min(760px, 92vw); }
/* object-fit:contain (Chat-Verlauf: "Bild öffnet sich im Chat-Popup, aber
   leider komplett verzerrt") - ohne das dehnt ein <img> sich auf die volle
   berechnete Box OHNE das Seitenverhältnis zu wahren. */
.chat-image-lightbox-img { display: block; width: 100%; max-height: 70vh; object-fit: contain; border-radius: var(--radius-md); }

/* Dokument-Anhang - kompakte Karte (Endung-Icon + Name/Größe + eigener
   Download-Button), analog zur Datei-Kachel in branding.js, aber schlanker
   (keine Umbenennen/Verschieben-Aktionen nötig). */
.chat-message-file {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 200px;
  border-radius: var(--radius-md);
  padding: 8px 10px;
  margin-bottom: 6px;
  text-decoration: none;
  color: inherit;
  background: rgba(255, 255, 255, 0.16);
}
.chat-message:not(.is-mine) .chat-message-file { background: var(--color-surface); border: 1px solid var(--color-border); }
.chat-message-file-icon { flex-shrink: 0; display: flex; }
.chat-message-file-icon svg { width: 20px; height: 20px; }
.chat-message-file-meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px; }
.chat-message-file-name { font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-message-file-size { font-size: var(--text-small); opacity: 0.75; }
.chat-message-file-download { flex-shrink: 0; display: flex; cursor: pointer; opacity: 0.75; }
.chat-message-file-download svg { width: 14px; height: 14px; }
.chat-message-file-download:hover { opacity: 1; }
/* Abgelaufener Anhang (nach 90 Tagen, siehe includes/ChatAttachments.php):
   sichtbar vorhanden, aber erkennbar nichts mehr zum Anklicken - gestrichelt
   statt gefüllt, damit auf einen Blick klar ist, dass hier mal etwas war und
   nicht etwa gerade etwas lädt. */
.chat-message-file.is-expired {
  background: transparent;
  border: 1px dashed var(--color-border);
  opacity: 0.7;
  cursor: default;
}
.chat-message:not(.is-mine) .chat-message-file.is-expired { background: transparent; }
.chat-message-file.is-expired .chat-message-file-name { font-weight: 500; text-decoration: line-through; }

.chat-push-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  margin-bottom: var(--space-3);
  font-size: var(--text-body);
  color: var(--color-text);
}
.chat-push-banner svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--color-accent); }
.chat-push-banner .btn { margin-left: auto; flex-shrink: 0; }

/* ---------- Spiele (Chat-Verlauf: "5 coole klassische zeitlose Games die
   man zwischendurch spielen kann, mit Highscores innerhalb der User") -
   siehe app/js/pages/games.js. Kachel-Übersicht im .shotdoc-slots-Stil,
   Modal mit Canvas/DOM-Spielfläche links + Bestenliste rechts. ---------- */
/* Deutlich größer als die übrigen 5-Tile-Header-Kacheln (Chat-Verlauf:
   "Mach doch die Spielauswahl-Kacheln deutlich größer") - eigene, größere
   Mindestbreite statt der generischen minmax(240px, ...), damit auch die
   Vorschaubilder (.game-tile-preview) groß genug zum Erkennen sind. */
/* ==========================================================
   Spiele - optische Überarbeitung 01.09.2026
   ----------------------------------------------------------
   Ausgangslage: die Kacheln waren ordentlich, das SPIELFENSTER war der
   schwache Teil - beim Öffnen ein 900px hohes leeres Rechteck mit einem
   Standard-Knopf in der Mitte und einer leeren Spalte rechts. Die Kachel
   versprach mehr, als das Spiel hielt.

   Der Kniff, mit dem alle zwölf Spiele auf einmal davon profitieren: jedes
   bringt seine eigene Farbe mit (GAMES.accent in games.js), gesetzt als
   --spiel auf Kachel und Spielfenster. Alle zwölf mount*()-Funktionen bauen
   ihr Markup nach demselben Muster (.game-canvas-wrap > .game-score +
   .game-canvas), deshalb greifen die Regeln hier überall, ohne dass eine
   einzige davon angefasst werden musste.
   ========================================================== */
/* Vier Spalten, drei Reihen (02.09.2026, Nutzer: "da wir 12 Spiele haben,
   mach bitte drei Reihen und vier Spiele pro Reihe. Dann sieht es sauber
   aus." + "Die Kacheln können ca. 30% größer sein. Bzw. den Platz sinnvoll
   füllen.").

   FESTE vier Spalten statt auto-fit: auto-fit verteilt nach verfuegbarer
   Breite und ergab je nach Fensterbreite drei, vier oder fuenf pro Reihe -
   bei zwoelf Kacheln also gern mal eine letzte Reihe mit zwei Waisen. Vier
   feste Spalten teilen zwoelf sauber auf, und die Kacheln werden dabei von
   selbst breiter, weil sie sich den vollen Platz teilen statt bei 300px
   stehenzubleiben (das war die "30% groesser"-Frage - sie loest sich hier
   mit, ohne eine zweite Zahl irgendwo).

   Die Umbrueche darunter halten das Prinzip durch: 2 Spalten sind 6 volle
   Reihen, 1 Spalte ist eine Liste. Drei Spalten kaemen bei zwoelf zwar auch
   auf, sind aber auf schmalen Fenstern zu eng fuer die Bilder. */
.games-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }
@media (max-width: 1100px) { .games-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px)  { .games-grid { grid-template-columns: 1fr; } }

/* Die Kachel ist jetzt der Bildschirm: die Vorschau läuft randlos bis an die
   Kanten, Name und Zahlen liegen darauf. Deshalb padding: 0 und overflow:
   hidden - der Kartenradius schneidet die Vorschau sauber ab. */
.game-tile {
  cursor: pointer;
  padding: 0;
  overflow: hidden;
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease);
}
.game-tile:hover {
  transform: translateY(-4px);
  /* Der Schein in der Farbe DIESES Spiels statt der App-Akzentfarbe - das
     ist der ganze Unterschied zwischen "zwölf Kacheln" und "zwölf Spielen". */
  box-shadow: 0 18px 40px -14px color-mix(in srgb, var(--spiel) 55%, transparent);
  border-color: var(--spiel);
}
.game-tile-screen { position: relative; aspect-ratio: 3 / 2; background: #12141c; }
.game-tile-preview { position: absolute; inset: 0; }
.game-tile-preview svg { display: block; width: 100%; height: 100%; }
/* Das generierte Bild fuellt den Bildschirm wie die SVG-Vorschau davor.
   object-fit: cover, weil die Generierung quadratisch liefert und die
   Kachel 3:2 ist - ein verzerrtes Arcade-Motiv waere schlimmer als ein
   beschnittenes, und die Motive sind mittig komponiert (siehe
   GAMETILE_STYLE_PROMPT in api/gametiles.php). */
.game-tile-preview img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* VORÜBERGEHEND: der Knopf zum Neuerzeugen einer Kachel (02.09.2026, siehe
   .game-tile-neu in games.js). Liegt oben links, gegenueber dem
   Abspiel-Zeichen, und zeigt sich erst beim Darueberfahren - er ist ein
   Werkzeug fuer den Aufbau, kein Teil des Spiels. */
.game-tile-neu {
  position: absolute;
  top: 8px;
  left: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.game-tile:hover .game-tile-neu { opacity: 1; }
.game-tile-neu:hover { border-color: var(--spiel); }
.game-tile-neu:focus-visible { opacity: 1; outline: 2px solid var(--spiel); outline-offset: 2px; }
.game-tile-neu svg { width: 14px; height: 14px; }
/* Waehrend eine Kachel gebaut wird: das Bild gedimmt, der Knopf dreht sich.
   Eine Generierung dauert bis zu anderthalb Minuten - ohne Rueckmeldung
   klickt jeder ein zweites Mal, und jeder Klick kostet. */
.game-tile.ist-am-bauen .game-tile-preview { opacity: 0.35; }
.game-tile.ist-am-bauen .game-tile-neu { opacity: 1; pointer-events: none; }
.game-tile.ist-am-bauen .game-tile-neu svg { animation: game-tile-dreht 1.1s linear infinite; }
@keyframes game-tile-dreht { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .game-tile.ist-am-bauen .game-tile-neu svg { animation: none; }
}
/* Ein Punkt statt eines Symbols (01.09.2026). Das Symbol wiederholte nur,
   was das Vorschaubild darueber schon sagt. Der Punkt sagt etwas anderes:
   dieses Spiel hat DIESE Farbe - und er ist das einzige Farbige an der
   ruhenden Kachel. */
.game-tile-punkt { width: 7px; height: 7px; border-radius: 50%; background: var(--spiel); flex: none; }
.game-tile-name { font-size: 14px; font-weight: 590; letter-spacing: -0.012em; margin-right: auto; }
/* Der Abspiel-Kranz erscheint erst beim Hover - im Ruhezustand soll die
   Vorschau für sich stehen. */
.game-tile-play {
  position: absolute; top: 10px; right: 10px;
  width: 30px; height: 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--spiel) 88%, #000);
  color: #fff;
  opacity: 0; transform: scale(0.7);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-base) var(--ease);
}
.game-tile-play svg { width: 16px; height: 16px; }
.game-tile:hover .game-tile-play { opacity: 1; transform: scale(1); }

/* Eine Zeile unter dem Bild: Symbol, Name, Spitzenreiter, eigener Bestwert.
   Vorher waren es zwei Zeilen (Name auf dem Bild, Zahlen darunter) - das kostete
   Hoehe und deckte das Bild zu. */
.game-tile-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 13px 15px;
  font-size: 12.5px;
  /* Haarstrich statt Rahmenfarbe: die Trennung soll spuerbar sein, nicht
     sichtbar. */
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.game-tile-stat {
  flex: none;
  color: var(--color-text-faint);
  max-width: 46%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.game-tile-stat.is-leer { font-style: italic; opacity: 0.7; }
.game-tile-own {
  flex: none;
  min-width: 26px;
  padding: 1px 7px;
  border-radius: var(--radius-full);
  text-align: center;
  font-weight: 700;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 10%, transparent);
}

/* ---- Das Spielfenster ---------------------------------------------- */
/* Breiter als vorher (1040 -> 1120): die Bestenliste hat jetzt eine eigene
   Fläche statt nur einer Trennlinie und braucht die Breite. */
.modal.is-game { max-width: 1120px; }
/* position: relative ist der Bezugspunkt fuer .game-drehen (siehe dort) -
   die Dreh-Aufforderung legt sich ueber das ganze Spielfenster, aber unter
   die Kopfzeile mit dem Schliesskreuz. */
.game-modal-body { display: flex; gap: var(--space-5); align-items: flex-start; position: relative; }
.game-play-area { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; align-items: stretch; }
.game-host { width: 100%; }

/* Die Zahlen, gegen die man spielt, standen vorher nur in der Seitenspalte -
   also außerhalb des Blickfelds, sobald das Spiel läuft. */
.game-stats-bar { display: flex; gap: var(--space-5); padding: 0 2px; }
.game-stat { display: flex; flex-direction: column; gap: 1px; }
.game-stat-label { font-size: 10.5px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--color-text-faint); }
.game-stat-value { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- Die Spielfläche -------------------------------------------------
   "Apple-Look, clean, high-end, futuristisch, edel" (01.09.2026). Eine
   erste Fassung ging in die Arcade-Richtung — Neon-Gitter, Scanline-Glanz,
   Bloom über allem. Falsch: das ist genau der 90er-Look, der weg sollte,
   nur greller. Zurückgenommen auf das Gegenteil davon.

   Die Regel hier ist Zurückhaltung. Tiefe entsteht durch einen sehr flachen
   Verlauf und einen weichen, weiten Schatten — nicht durch Leuchten. Die
   Kante ist ein Haarstrich in Weiß bei 8 %, kein farbiger Rahmen. Die Farbe
   des Spiels kommt nur noch an zwei Stellen vor, wo sie etwas bedeutet: am
   Punktestand und am Start-Knopf. Großer Radius, viel Luft, sonst nichts. */
.game-canvas-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  background: linear-gradient(180deg, #16181e 0%, #0d0f14 100%);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 22px;
  padding: 18px;
  isolation: isolate;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 32px 64px -32px rgba(0, 0, 0, 0.85);
}
.game-canvas-wrap > * { position: relative; z-index: 1; }

/* ==========================================================
   Der Moment, in dem ein Spiel angeht (02.09.2026)
   ----------------------------------------------------------
   Nutzer: "Kannst du den Moment in dem man ein Game startet noch etwas
   geiler inszenieren? Das wirkt aktuell nicht so wirklich visuell
   spannend." Gewaehlt wurden drei von vier Vorschlaegen (kein Countdown):
   Roehre geht an, Titel blitzt auf, Rahmen faehrt hoch.

   Angestossen wird das aus inszeniereStart() in games.js - an EINER Stelle,
   weil alle zwoelf Spiele ueber dasselbe Start-Overlay laufen.
   ========================================================== */

/* 1. Die Roehre. Eine schwarze Flaeche ueber dem Feld, die sich von der
      Mitte aus nach oben und unten wegzieht - clip-path von "alles verdeckt"
      auf "nur noch ein Strich in der Mitte". Genau andersherum als man es
      zuerst schreibt: nicht das Bild waechst, das Schwarz weicht.
      pointer-events:none ist Pflicht - die Schicht liegt ueber dem
      Spielfeld, und ein Spiel, dessen erste Klicks im Nichts landen, waere
      ein schlechter Tausch fuer eine Animation. */
.game-crt {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  overflow: hidden;
  border-radius: 22px;
  background: #05060a;
  animation: crt-an 0.62s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes crt-an {
  0%   { clip-path: inset(0 0 0 0); }
  55%  { clip-path: inset(0 0 0 0); }
  100% { clip-path: inset(50% 0 50% 0); }
}

/* Der helle Querstrich, der beim Einschalten kurz steht: erst ein Punkt in
   der Mitte, der in die Breite schiesst, dann verglueht er mit dem
   aufziehenden Bild. */
.game-crt-strahl {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  transform: translateY(-50%) scaleX(0);
  background: linear-gradient(90deg, transparent, #fff 12%, #fff 88%, transparent);
  box-shadow: 0 0 18px 4px rgba(255, 255, 255, 0.55);
  animation: crt-strahl 0.62s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes crt-strahl {
  0%   { transform: translateY(-50%) scaleX(0);    opacity: 0; }
  18%  { transform: translateY(-50%) scaleX(1);    opacity: 1; }
  55%  { transform: translateY(-50%) scaleX(1);    opacity: 1; }
  100% { transform: translateY(-50%) scaleX(1);    opacity: 0; }
}

/* 2. Der Titel als Automaten-Vorspann. Die beiden versetzten Farbschatten
      (::before/::after) sind der Glitch - dieselbe Schrift zweimal, ein paar
      Pixel daneben, in Cyan und Magenta, wie ein schlecht justierter
      Farbstrahl. Sie ziehen ihren Text aus data-text, siehe games.js. */
.game-crt-titel {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(26px, 7vw, 52px);
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--spiel, #fff);
  text-shadow: 0 0 24px color-mix(in srgb, var(--spiel, #fff) 60%, transparent);
  animation: crt-titel 0.88s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.game-crt-titel::before,
.game-crt-titel::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  mix-blend-mode: screen;
}
.game-crt-titel::before { color: #00e5ff; animation: crt-glitch-a 0.34s steps(2, end) 2; }
.game-crt-titel::after  { color: #ff2f6d; animation: crt-glitch-b 0.34s steps(2, end) 2; }
@keyframes crt-titel {
  0%   { opacity: 0; transform: scale(1.14); }
  22%  { opacity: 1; transform: scale(1); }
  62%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.98); }
}
@keyframes crt-glitch-a {
  0%, 100% { transform: translate(0, 0); opacity: 0; }
  50%      { transform: translate(-3px, 1px); opacity: 0.75; }
}
@keyframes crt-glitch-b {
  0%, 100% { transform: translate(0, 0); opacity: 0; }
  50%      { transform: translate(3px, -1px); opacity: 0.75; }
}

/* 3. Der Rahmen faehrt hoch und der Rand glimmt einmal in der Spielfarbe
      nach. Kein transform auf dem Rahmen selbst waehrend des Spiels - die
      Animation laeuft einmal und laesst nichts stehen, weil Pong und Block
      Breaker die Mausposition ueber getBoundingClientRect() umrechnen und
      ein bleibendes scale() die Umrechnung verzoegen wuerde. */
.game-canvas-wrap.ist-angegangen {
  animation: game-faehrt-hoch 0.62s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes game-faehrt-hoch {
  0% {
    transform: scale(0.96);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.06),
      0 32px 64px -32px rgba(0, 0, 0, 0.85);
  }
  45% {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.06),
      0 0 0 1px var(--spiel, transparent),
      0 0 44px -6px var(--spiel, transparent),
      0 32px 64px -32px rgba(0, 0, 0, 0.85);
  }
  100% {
    transform: scale(1);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.06),
      0 32px 64px -32px rgba(0, 0, 0, 0.85);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* games.js baut die Schicht dann gar nicht erst - das hier ist nur das
     Sicherheitsnetz, falls sie doch einmal entsteht. */
  .game-crt, .game-crt-strahl, .game-crt-titel,
  .game-crt-titel::before, .game-crt-titel::after,
  .game-canvas-wrap.ist-angegangen { animation: none; }
  .game-crt { display: none; }
}

/* ==========================================================
   Dreh dein Telefon (02.09.2026)
   ----------------------------------------------------------
   Nutzer: "Ich möchte Games ausschließlich im Landscape-Format spielbar
   machen. Also brauchen wir eine kurze Animation, dass das iPhone gedreht
   werden muss, erst dann gehts los."

   Liegt ueber dem Spielfeld, NICHT ueber dem ganzen Fenster: die Kopfzeile
   mit dem Schliesskreuz bleibt erreichbar. Wer die Aufforderung sieht und
   sich doch anders entscheidet, soll nicht erst drehen muessen, um wieder
   herauszukommen.

   Sichtbar wird sie ausschliesslich ueber .is-an - gesetzt von
   drehAufforderung() in games.js. Ohne diese Klasse ist sie display:none
   und faengt damit auch keine Beruehrungen ab; eine unsichtbare, aber
   liegengebliebene Schicht ueber dem Spielfeld waere der schlimmste
   denkbare Fehler an dieser Stelle (siehe denselben Gedanken bei .game-crt
   weiter oben).
   ========================================================== */
.game-drehen { display: none; }
.game-drehen.is-an {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  text-align: center;
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: rgba(10, 12, 16, 0.82);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
}
/* Das Telefon selbst: ein schlichtes Rechteck mit Rand, das sich einmal
   aufs Querformat dreht, kurz liegen bleibt und zurueckspringt. Absichtlich
   kein Bild und kein Icon-Font - eine Form, die sich dreht, erklaert sich
   ohne Sprache. */
.game-drehen-geraet {
  width: 54px;
  height: 92px;
  border: 2.5px solid rgba(255, 255, 255, 0.9);
  border-radius: 11px;
  padding: 7px 5px;
  animation: dreh-geraet 2.4s var(--ease) infinite;
}
.game-drehen-schirm {
  width: 100%;
  height: 100%;
  border-radius: 4px;
  background: var(--spiel, #22b8d1);
  opacity: 0.55;
}
@keyframes dreh-geraet {
  0%, 22%   { transform: rotate(0deg); }
  45%, 78%  { transform: rotate(-90deg); }
  100%      { transform: rotate(0deg); }
}
.game-drehen-titel {
  font-size: 19px;
  font-weight: 650;
  color: #fff;
  letter-spacing: -0.01em;
}
.game-drehen-text {
  font-size: 13.5px;
  color: rgba(255, 255, 255, 0.62);
  max-width: 260px;
}
/* Der Ausweg fuer alle mit aktiver Ausrichtungssperre - eine Website kann
   ein iPhone nicht drehen (screen.orientation.lock() gibt es in Safari auf
   iOS nicht), und ohne diesen Link waeren die Spiele fuer sie zu. Bewusst
   als schlichter Textlink: der Weg ist eindeutig das Querformat, das hier
   ist die Notausfahrt. */
.game-drehen-trotzdem {
  margin-top: 4px;
  background: none;
  border: none;
  padding: 8px 12px;
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.5);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.game-drehen-trotzdem:hover { color: rgba(255, 255, 255, 0.85); }
@media (prefers-reduced-motion: reduce) {
  .game-drehen-geraet { animation: none; transform: rotate(-90deg); }
}

/* ==========================================================
   Daumentasten (02.09.2026)
   ----------------------------------------------------------
   Nutzer: "sehr viele Spiele lassen sich mobil gar nicht spielen, die
   steuerung geht einfach gar nicht." Kampfarena, Platformer, Invaders und
   Asteroids hoerten ausschliesslich auf die Tastatur.

   Sie liegen IN den Raendern des Spielfelds, nicht darunter: quer ist die
   Hoehe das knappe Gut, und eine Tastenleiste unter dem Feld haette genau
   den Platz gekostet, den das Querformat gerade erst gebracht hat. Die
   Daumen liegen dort ohnehin.

   touch-action: none ist hier keine Feinheit, sondern Voraussetzung: ohne
   das deutet iOS ein Halten-und-Ziehen auf der Taste als Wischgeste und
   scrollt oder zoomt, statt die Taste zu halten.
   ========================================================== */
/* Grundstellung = Hochformat (der Notausgang fuer alle mit
   Ausrichtungssperre): dort ist links und rechts kein Platz, das Feld nimmt
   87% der Breite. Die Tasten stehen deshalb UNTER dem Feld, als normale
   Zeile im Rahmen - nicht darueber. Quer wandern sie in die Seitenraender,
   siehe den Block weiter unten. */
.game-pad {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 10px;
  pointer-events: none;
}
.game-pad-seite { display: flex; flex-direction: column; gap: 8px; align-items: center; }
.game-pad-reihe { display: flex; gap: 8px; }
.game-pad-taste {
  pointer-events: auto;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  /* 52px: Apple empfiehlt 44pt als Mindestmass fuer Tippziele. Bei einer
     Taste, die man im Spiel HAELT statt nur antippt, ist etwas mehr richtig. */
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.28);
  background: rgba(12, 14, 20, 0.42);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: rgba(255, 255, 255, 0.88);
  font-size: 20px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
/* Rueckmeldung beim Halten - ohne die weiss man bei einer durchsichtigen
   Taste nicht, ob der Finger sie ueberhaupt getroffen hat. */
.game-pad-taste.is-gedrueckt {
  background: color-mix(in srgb, var(--spiel, #22b8d1) 55%, rgba(12, 14, 20, 0.5));
  border-color: var(--spiel, #22b8d1);
  transform: scale(0.94);
}
/* Am Rechner haben die Tasten nichts zu suchen - dort gibt es die Tastatur.
   daumenPad() baut sie ohnehin nur bei pointer:coarse, das hier ist das
   Sicherheitsnetz fuer den Fall, dass ein Geraet beides meldet (Laptop mit
   Touchscreen) und der Nutzer trotzdem tippt. */
@media (hover: hover) and (pointer: fine) {
  .game-pad { display: none; }
}

/* Der Punktestand ueber dem Feld. Weiss statt in der Spielfarbe: die Farbe
   ist an dieser Stelle keine Information, sie war nur Dekoration - und zwoelf
   verschieden eingefaerbte Zahlenzeilen sind genau das Bunte, das weg
   sollte. Die Farbe steckt jetzt im Punkt davor. Ziffern in Tabellenbreite,
   damit ein hochzaehlender Punktestand nicht bei jedem Schritt springt. */
.game-score {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  font-weight: 590;
  letter-spacing: -0.005em;
  color: #fff;
  font-variant-numeric: tabular-nums;
}
.game-score::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--spiel);
  flex: none;
}

/* ---- Brettspiele: 2048, Minesweeper, Schach ------------------------
   DRITTE Fassung (01.09.2026), und die ersten beiden waren beide falsch:

   1. Das Raster selbst mit "margin-left/right: auto" deckeln - der Rahmen
      ist ein Flex-Behaelter, und eine automatische Aussenkante auf der
      Querachse schaltet dort das Strecken ab. Die Raster rechnen in
      1fr-Spalten und fielen auf 20px zusammen.
   2. Statt des Rasters den RAHMEN deckeln, und zwar aus einer zur
      Laufzeit gemessenen Variable (--spiel-breite, gesetzt in einem
      requestAnimationFrame). In Chromium sass das sauber - in Safari hing
      das Brett weit rechts aus dem Rahmen heraus. Kein Wunder: das Ergebnis
      hing davon ab, WANN gemessen wird und wie der Browser aspect-ratio
      gegen max-width verrechnet. Zwei Freiheitsgrade zu viel.

   Jetzt ohne jede Messung. Das Brett ist quadratisch (aspect-ratio: 1),
   also IST seine Kantenlaenge seine Hoehe - die laesst sich direkt
   hinschreiben. Eine ausdrueckliche Breite hebt das Strecken sauber auf
   (anders als eine automatische Aussenkante, die es nur abschaltet und die
   Breite auf den Mindestinhalt fallen laesst - siehe Fassung 1), und
   margin-inline zentriert es dann im Rahmen. Kein JavaScript, keine
   Variable, nichts, was zwischen zwei Browsern auseinanderlaufen kann. */
.game-canvas-wrap > .g2048-grid,
.game-canvas-wrap > .mine-grid,
.game-canvas-wrap > .chess-board {
  width: min(100%, 62vh, 560px);
  margin-inline: auto;
}
.chess-status-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.chess-resign-btn { flex-shrink: 0; }
/* Die Hoehenbremse (01.09.2026). Vorher: width:100% + height:auto - ein
   quadratisches Spiel wie Snake (560x560) wurde damit im 1120px breiten
   Fenster ueber 1000px HOCH und passte auf keinen Laptop mehr. Genau daher
   kam der Eindruck "riesiges leeres Rechteck".

   Gebremst wird ueber die BREITE, nicht ueber die Hoehe: --spiel-breite wird
   in games.js aus dem Seitenverhaeltnis des Canvas gerechnet. Der Umweg ist
   noetig, weil das Element und die Zeichenflaeche exakt deckungsgleich
   bleiben MUESSEN - Pong und Block Breaker rechnen die Maus ueber
   getBoundingClientRect() in Spielkoordinaten um (scale = W / rect.width).
   Ein max-height mit object-fit haette das Element breiter gemacht als das
   Bild darin, und das Paddel waere der Maus nicht mehr gefolgt. Genau das
   war die erste Fassung dieser Regel. */
/* NUR das Canvas. Die erste Fassung fasste hier auch die DOM-Raster an
   (2048, Minesweeper, Schach) - mit "margin-left/right: auto", und das war
   der Fehler: der Rahmen ist ein Flex-Container, und eine automatische
   Aussenkante auf der Querachse schaltet dort das Strecken ab. Die Raster
   rechnen intern in 1fr-Spalten und fielen dadurch auf 20px zusammen. Vom
   Pruefstand gefunden, nicht beim Hinsehen.
   Fuer die drei Brettspiele steht die Groesse stattdessen als reine
   CSS-Regel oben an .game-canvas-wrap > .g2048-grid (u.a.).

   HINWEIS (02.09.2026): hier stand "siehe .game-host.is-brett weiter unten"
   - diese Regel gibt es nicht (mehr). games.js setzt die Klasse zwar noch,
   gestylt wird sie nirgends. Der Verweis hat beim Suchen nach der Ursache
   der winzigen Bretter auf dem Handy Zeit gekostet; deshalb steht hier
   jetzt, was tatsaechlich der Fall ist. */
.game-canvas {
  max-width: min(100%, var(--spiel-breite, 100%));
  margin-left: auto;
  margin-right: auto;
}
.game-canvas {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  touch-action: none;
  cursor: crosshair;
}

/* Der Start-Overlay zeigt jetzt die Vorschau des Spiels als Hintergrund
   (--spiel-bild, in games.js gesetzt) statt schwarzer Leere - man sieht
   also schon vor dem ersten Klick, worum es geht. Stark abgedunkelt und
   unscharf, damit die Schrift darauf trotzdem ruhig steht. */
.game-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  border-radius: var(--radius-md);
  text-align: center;
  padding: var(--space-5);
  overflow: hidden;
  /* Milchglas ueber dem ECHTEN Spielfeld (Chat-Verlauf: "Apple-Look, clean,
     high-end, futuristisch, edel", gewaehlte Variante "Milchglas ueber
     allem"). Die Vorgaengerfassung legte eine verwaschene Kopie des
     Vorschaubildes darueber - eine Tapete vor der Sache selbst. Jetzt
     schimmert das Brett durch, auf dem gleich gespielt wird.
     Der Radius muss dem des Rahmens folgen (22px), sonst ragt das Glas an
     den Ecken darueber hinaus. */
  border-radius: 22px;
  background: rgba(10, 12, 16, 0.52);
  backdrop-filter: blur(30px) saturate(150%);
  -webkit-backdrop-filter: blur(30px) saturate(150%);
  animation: panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Die Vorschau als Hintergrund. z-index: -1 waere falsch - damit rutscht sie
   HINTER die eigene Flaeche des Overlays und ist unsichtbar. Sie liegt
   stattdessen darueber, und der Inhalt darueber (z-index: 1 unten). */
.game-overlay > * { position: relative; z-index: 1; }
/* Groesser, leichter, enger - und ohne Schein. Ein Leuchten hinter der
   Schrift ist der schnellste Weg zurueck in die Spielhalle. */
.game-overlay-title {
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.026em;
  color: #fff;
}
.game-overlay-subtitle {
  font-size: 13px;
  line-height: 2;
  color: rgba(255, 255, 255, 0.74);
  max-width: 400px;
}
/* Tastenkappen statt Fließtext - siehe steuerungAlsTasten() in games.js. */
.game-key {
  display: inline-block;
  font-family: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 3px 8px;
  margin: 0 2px;
  border-radius: 6px;
  color: #fff;
  /* Flach. Die abgesetzte Unterkante liess die Kappen wie Tastatur-Plastik
     aussehen - gemeint ist ein Plaettchen, keine Taste zum Druecken. */
  background: rgba(255, 255, 255, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.16);
  white-space: nowrap;
}
/* Weiss, nicht in der Spielfarbe. Auf Milchglas ist Weiss das Auffaelligste,
   was es gibt - es braucht dafuer keine Farbe. Und die Spielfarbe bleibt so
   dem Punktestand vorbehalten, wo sie etwas bedeutet. */
.game-start-btn {
  font: inherit;
  font-weight: 590;
  font-size: 14px;
  letter-spacing: -0.01em;
  padding: 11px 30px;
  border: 0;
  border-radius: var(--radius-full);
  cursor: pointer;
  color: #0c0e14;
  background: #fff;
  box-shadow: 0 10px 28px -12px rgba(0, 0, 0, 0.9);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.game-start-btn:hover { transform: translateY(-1px); box-shadow: 0 14px 32px -12px rgba(0, 0, 0, 0.95); }
.game-start-btn:active { transform: translateY(0); }

/* ---- Bestenliste ---------------------------------------------------- */
/* Vorher nur eine Trennlinie links und darunter 700px Leere. Jetzt eine
   eigene Fläche, die auch dann noch wie etwas aussieht, wenn sie leer ist. */
.game-leaderboard-panel {
  width: 260px;
  flex-shrink: 0;
  align-self: stretch;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.game-leaderboard-panel h3 {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  margin-bottom: var(--space-3);
}
.game-leaderboard-list { display: flex; flex-direction: column; gap: 3px; max-height: 460px; overflow-y: auto; }
.game-leaderboard-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  font-size: 13px;
}
.game-leaderboard-row.is-own { background: var(--color-accent-soft); font-weight: 600; }
.game-leaderboard-rank {
  flex: none;
  width: 21px; height: 21px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-size: 11px;
  font-weight: 700;
  color: var(--color-text-faint);
}
/* Nur die ersten drei tragen einen Kranz - drei sind ein Podest, zehn wären
   Konfetti. Die Farben sind bewusst fest und nicht aus den Tokens: Gold,
   Silber und Bronze sind keine Themenfarben, sie bedeuten etwas. */
.game-leaderboard-row.is-gold .game-leaderboard-rank { background: linear-gradient(150deg, #f5d47a, #d9a520); color: #3d2c00; }
.game-leaderboard-row.is-silber .game-leaderboard-rank { background: linear-gradient(150deg, #e2e6ec, #a8b0bd); color: #2b3038; }
.game-leaderboard-row.is-bronze .game-leaderboard-rank { background: linear-gradient(150deg, #e0a97a, #b57140); color: #3a1f0a; }
.game-leaderboard-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.game-leaderboard-score { font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }

/* ==========================================================
   2048 - das SPIEL, nicht der Rahmen drumherum (01.09.2026)
   ----------------------------------------------------------
   Zwei Dinge waren hier objektiv falsch, unabhaengig vom Geschmack:

   1. Die Brettflaeche nahm `var(--color-border)`. Das ist ein Themen-Token
      und im Hellmodus HELL - ein blassgraues Brett in einem schwarzen
      Rahmen. Ein Spielfeld ist kein Bedienelement der App; es hat seine
      eigene Welt und darf nicht mit dem Thema kippen.
   2. Die Wertfarben waren 8/16/32/64 in vier fast gleichen Braun-Orange-
      toenen und 128/256/512 in drei fast gleichen Gelbtoenen. In 2048 IST
      die Farbe die Information - man liest den Wert am Ton, nicht an der
      Zahl. Sieben nicht unterscheidbare Stufen sind sieben verlorene.

   Die neue Reihe laeuft einmal durch den Farbkreis und wird dabei
   durchgehend heller und gesaettigter: kuehles Grau (2, 4) -> Blau (8, 16)
   -> Violett (32, 64) -> Magenta/Rot (128, 256) -> Bernstein (512, 1024)
   -> Gruen (2048) -> Weiss (darueber). Jede Stufe ist von ihrer Nachbarin
   auf einen Blick zu trennen, und "weiter oben" ist immer "heller".

   Die Schriftgroesse haengt an der Brettbreite (cqi), nicht an einer
   festen Pixelzahl: dasselbe Brett steht auf einem 13-Zoll-Laptop und auf
   einem grossen Monitor, und eine 20px-Zahl in einer 140px-Kachel sieht
   verloren aus.
   ========================================================== */
.g2048-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2.4%;
  aspect-ratio: 1;
  /* Eigene Flaeche statt eines Themen-Tokens - siehe oben. */
  background: #0a0c11;
  border-radius: 16px;
  padding: 2.4%;
  container-type: inline-size;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.g2048-cell {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 11%;
  /* Das leere Feld ist eine Vertiefung, keine Flaeche: ein Hauch dunkler
     als das Brett plus ein Lichtsaum an der Oberkante. Vorher war es
     rgba(255,255,255,0.06) - also praktisch unsichtbar, und ein 2048-Brett
     ohne sichtbares Raster ist schwer zu lesen. */
  background: rgba(255, 255, 255, 0.035);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  font-size: 7.4cqi;
  font-weight: 600;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: #fff;
  transition: background 90ms var(--ease), color 90ms var(--ease), box-shadow 90ms var(--ease);
}

/* Die Reihe. Bis 64 traegt die Kachel helle Schrift, ab 512 dunkle - der
   Umschlagpunkt liegt dort, wo der Untergrund heller wird als die Schrift.
   Der Lichtsaum an der Oberkante ist bei allen gleich stark: er macht aus
   der Flaeche einen Stein, ohne dass es nach Knopf aussieht. */
.g2048-cell[class*="v"] { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16); }
.g2048-cell.v2    { background: #2f3440; }
.g2048-cell.v4    { background: #3d4454; }
.g2048-cell.v8    { background: #3a5ba8; }
.g2048-cell.v16   { background: #4a76d4; }
.g2048-cell.v32   { background: #7059d8; }
.g2048-cell.v64   { background: #9358e0; }
.g2048-cell.v128  { background: #c94fb4; font-size: 6.4cqi; }
.g2048-cell.v256  { background: #e0475f; font-size: 6.4cqi; }
.g2048-cell.v512  { background: #f0913a; font-size: 6.4cqi; color: #1a1206; }
.g2048-cell.v1024 { background: #f5c542; font-size: 5.4cqi; color: #1a1400; }
.g2048-cell.v2048 { background: #37d99b; font-size: 5.4cqi; color: #05231a; }
/* Alles ueber 2048 ist weiss. Nicht noch eine Farbe - an dieser Stelle ist
   die Reihe zu Ende, und Weiss sagt genau das. */
.g2048-cell.vmax  { background: #ffffff; font-size: 5.4cqi; color: #0a0c11; }

/* Neue Kachel und Verschmelzung. Beides sehr kurz (140/180ms) - eine
   Animation, die man bemerkt, steht dem Spiel im Weg: 2048 wird schnell
   gespielt, und ein Zug muss fertig sein, bevor der naechste kommt. */
@keyframes g2048-neu {
  from { transform: scale(0.55); opacity: 0; }
  to   { transform: scale(1); opacity: 1; }
}
@keyframes g2048-verschmolzen {
  0%   { transform: scale(1); }
  55%  { transform: scale(1.13); }
  100% { transform: scale(1); }
}
.g2048-cell.is-neu { animation: g2048-neu 140ms cubic-bezier(0.22, 1.2, 0.36, 1); }
.g2048-cell.is-verschmolzen { animation: g2048-verschmolzen 180ms cubic-bezier(0.34, 1.5, 0.64, 1); }
@media (prefers-reduced-motion: reduce) {
  .g2048-cell.is-neu, .g2048-cell.is-verschmolzen { animation: none; }
}

/* ==========================================================
   Minesweeper (01.09.2026)
   ----------------------------------------------------------
   Auch hier stand `var(--color-border)` als Brettflaeche - im Hellmodus
   hell, mit dunklen Feldern darauf. Dazu waren aufgedecktes und
   verdecktes Feld fast gleich dunkel: der halbe Reiz des Spiels ist, mit
   einem Blick zu sehen, was schon offen ist.

   Jetzt tragen die beiden Zustaende entgegengesetzte Tiefe: verdeckt ist
   ERHABEN (heller, Lichtsaum oben), aufgedeckt ist VERTIEFT (dunkler,
   Schattensaum oben). Das ist genau die Sprache, die Minesweeper seit
   jeher spricht - nur ohne die 3D-Kanten von 1990.
   ========================================================== */
.mine-grid {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  gap: 1.1%;
  aspect-ratio: 1;
  background: #0a0c11;
  border-radius: 16px;
  padding: 1.6%;
  container-type: inline-size;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.mine-cell {
  aspect-ratio: 1;
  border: none;
  border-radius: 14%;
  /* Verdeckt = erhaben. */
  background: linear-gradient(180deg, #333a4a 0%, #2a303e 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
  color: #fff;
  font-weight: 650;
  font-size: 5.6cqi;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: background 90ms var(--ease), box-shadow 90ms var(--ease);
}
.mine-cell:hover { background: linear-gradient(180deg, #3d4557 0%, #333a4a 100%); }
/* Aufgedeckt = vertieft. Der Schattensaum sitzt OBEN, der Lichtsaum unten -
   genau umgekehrt zum verdeckten Feld. Mehr braucht es nicht, damit sich
   die beiden Zustaende auf einen Blick trennen. */
.mine-cell.is-revealed {
  background: rgba(255, 255, 255, 0.035);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5), inset 0 -1px 0 rgba(255, 255, 255, 0.04);
  cursor: default;
}
.mine-cell.is-revealed:hover { background: rgba(255, 255, 255, 0.035); }
.mine-cell.is-mine {
  background: #e0455f;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
  color: #fff;
}
.mine-cell.is-flagged { background: linear-gradient(180deg, #3f4657 0%, #343b4c 100%); }

/* Die Ziffernfarben. Vorher waren 7 und 8 weiss und hellgrau - also
   ausgerechnet die seltensten und gefaehrlichsten Felder die
   unauffaelligsten. Jetzt laeuft auch diese Reihe von ruhig nach laut. */
.mine-cell.n1 { color: #6fa8ff; }
.mine-cell.n2 { color: #4ed88a; }
.mine-cell.n3 { color: #ff7a6b; }
.mine-cell.n4 { color: #b78cff; }
.mine-cell.n5 { color: #ffb454; }
.mine-cell.n6 { color: #45d6e0; }
.mine-cell.n7 { color: #ff5fa8; }
.mine-cell.n8 { color: #ffffff; }

/* Schach - 8x8 DOM-Grid statt Canvas (Unicode-Figuren als Text, siehe
   CHESS_GLYPH in games.js), Zielfeld/Auswahl/Schach als reiner
   Rahmen/Hintergrund statt Overlay-Punkt, damit es unabhängig davon
   funktioniert, ob auf dem Feld eine Figur steht. */
.chess-board {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--color-border);
  /* Leichte Tiefenwirkung fürs ganze Brett (Chat-Verlauf: "maximal
     realistischer Look"), passend zu den neuen Holz-Figuren. */
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
.chess-cell {
  aspect-ratio: 1;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(20px, 4vw, 34px);
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
/* Dezente Textur statt reiner Flachfarbe (Chat-Verlauf: "maximal
   realistischer Look") - feine diagonale Streifen simulieren eine Maserung,
   dazu ein sanftes radiales Licht oben links pro Feld + ein Inset-Schatten
   für eine leicht eingelassene Optik. Bleibt bewusst im bisherigen dunklen
   Blau-Violett-Ton der App statt auf klassisches Holzbraun zu wechseln. */
/* Frischere Felder (01.09.2026). Vorher lagen die beiden Feldfarben bei
   #454864 und #262838 - zwei Blauviolett-Toene, die sich um gerade genug
   unterschieden, dass man das Muster ahnte. Auf einem Schachbrett ist der
   Kontrast der Felder aber keine Geschmacksfrage: man liest Diagonalen und
   Linien daran. Jetzt ein warmes Paar mit deutlichem Abstand, das trotzdem
   nicht nach Plastikbrett aussieht - die Maserung und das Licht von oben
   links bleiben. */
.chess-cell.is-light {
  background:
    repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.045) 0px, rgba(255, 255, 255, 0.045) 1px, transparent 1px, transparent 6px),
    radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.16), transparent 66%),
    #d9c3a0;
  box-shadow: inset 0 0 12px rgba(90, 60, 30, 0.14);
}
.chess-cell.is-dark {
  background:
    repeating-linear-gradient(115deg, rgba(0, 0, 0, 0.14) 0px, rgba(0, 0, 0, 0.14) 1px, transparent 1px, transparent 6px),
    radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.07), transparent 66%),
    #7d5a3c;
  box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.3);
}
.chess-cell.is-selected {
  background: #b9d98a;
  box-shadow: inset 0 0 0 3px rgba(90, 130, 40, 0.7);
}
/* Zielfeld: ein Punkt in der Mitte, kein Rahmen ums ganze Feld. Der
   Rahmen war auf einem besetzten Feld nicht von der Auswahl zu
   unterscheiden - der Punkt sagt "hierhin ziehen", und auf einem
   besetzten Feld wird daraus ein Ring um die Figur (siehe unten). */
.chess-cell.is-target { position: relative; box-shadow: none; }
.chess-cell.is-target::after {
  content: "";
  position: absolute;
  width: 26%; height: 26%;
  border-radius: 50%;
  background: rgba(30, 45, 20, 0.42);
  pointer-events: none;
}
.chess-cell.is-target:not(:empty)::after {
  width: 84%; height: 84%;
  background: transparent;
  border: 4px solid rgba(30, 45, 20, 0.4);
}
.chess-cell.is-check {
  background: radial-gradient(circle, #ff8a8a 15%, #e0455f 78%);
}

/* Echte Figuren-Grafiken (Freepik, siehe chessPieceImg() in games.js) statt
   generierter SVG-Silhouetten (Chat-Verlauf: "hochwertige Grafiken ... für
   das Schachspiel ... maximal realistischer Look"). object-fit:contain
   statt einfachem width/height, weil die freigestellten PNGs je nach
   Figurtyp ein leicht unterschiedliches Seitenverhältnis haben (Turm ist
   z.B. schmaler als der Springer-Kopf). */
.chess-piece {
  width: 78%;
  height: 78%;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  /* Auf dem helleren Brett (01.09.2026) verschwand der alte Schatten
     bei 0.35 auf den Cremefeldern fast - deshalb etwas kraeftiger. */
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.45));
}
.chess-piece img { width: 100%; height: 100%; object-fit: contain; }

/* Modus-/Schwierigkeits-Auswahl vor Spielstart (Chat-Verlauf: "Schach-Spiel,
   bei dem man gegen den Computer spielt" -> "beide Modi" + "3 wählbare
   Stufen") - eigene kleine Panels in mountChess(), wiederverwenden die
   .game-overlay-Optik. */
.chess-setup-row { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.chess-setup-option {
  padding: 7px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.8);
  font-size: var(--text-body);
  font-weight: 600;
  cursor: pointer;
}
.chess-setup-option:hover { background: rgba(255, 255, 255, 0.12); }
.chess-setup-option.is-active { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }

/* Während der Computer rechnet (oder man online auf den Gegner-Zug wartet)
   ist das Brett kurz nicht klickbar (Chat-Verlauf: siehe maybeComputerMove()
   / updateStatus() in games.js). */
.chess-board.is-computer-turn { opacity: 0.6; pointer-events: none; }

/* Team-Mitglied-Übersicht (Chat-Verlauf: "können wir das auch so bauen,
   dass man gegen ein Team-Mitglied spielen kann?") - Liste laufender
   Online-Partien im Modus-Auswahl-Panel. */
.chess-game-list { display: flex; flex-direction: column; gap: 6px; max-height: 180px; overflow-y: auto; width: 100%; max-width: 320px; }
.chess-game-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: rgba(255, 255, 255, 0.05);
  color: #fff;
  font-size: var(--text-body);
  cursor: pointer;
  text-align: left;
}
.chess-game-row:hover { background: rgba(255, 255, 255, 0.1); }
.chess-game-row-name { font-weight: 600; }
.chess-game-row-badge { font-size: var(--text-meta); color: rgba(255, 255, 255, 0.6); flex-shrink: 0; }
.chess-game-row-badge.is-your-turn { color: #2fbf8f; font-weight: 600; }
.chess-game-row-badge.is-done { color: rgba(255, 255, 255, 0.4); }
.chess-game-list-empty { font-size: var(--text-meta); color: rgba(255, 255, 255, 0.55); }

/* Belege-Archiv (Chat-Verlauf: "Belege integrieren, dort kann man Belege
   per Drag N Drop reinwerfen ... auch per Foto ... automatisch entzerren,
   als PDF speichern") - siehe pages/receipts.js + pages/receiptscanner.js. */
/* Eigene, moderate Breite statt wide:true (Chat-Verlauf: "Das Beleg
   bearbeiten Feld kann kompakter sein, das geht über die gesamte Breite
   aktuell") - analog .modal.is-document (Rechnungswesen-Erstell-Formular),
   das den gleichen Kompromiss zwischen Standard-560px und voller Breite
   löst. 640px reicht für die 200px-Vorschau + das Formular nebeneinander,
   ohne wie bei .is-wide über die ganze Content-Fläche zu gehen. */
/* Lieferant-Spalte in der Archiv-Tabelle: feste max-width statt frei
   umbrechendem Text (Chat-Verlauf: "auf dem normalen Desktop die Zeilen
   einzeilig bleiben, also den Lieferanten entsprechend kürzen") - ohne
   table-layout:fixed wächst eine <table>-Spalte sonst mit ihrem Inhalt und
   ein langer Name macht die ganze Zeile mehrzeilig. Voller Name bleibt per
   title-Tooltip abrufbar (siehe receipts.js). */
.receipt-supplier-cell {
  display: inline-block;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
/* Bezahlt-Status-Button (Chat-Verlauf: "reicht aber ein Euro-Zeichen ...
   Beim Klick drauf (wenn unbezahlt), kurzer Dialog mit Datum" + "Funktion,
   mit der der Status bezahlt ... wieder entfernt werden kann - mit
   Rückfrage") - unbezahlt bleibt neutral (Standard-.btn-ghost), bezahlt
   bekommt dieselbe Erfolgs-Farbe wie .badge-success zur Wiedererkennung.
   Bewusst nur farbige Kontur statt Vollfläche (Chat-Verlauf: "Ich würde das
   Feld auch nicht vollflächig blau machen, nur eine farbige Kontur") -
   background bleibt transparent, nur border+Text in Erfolgsfarbe. */
.receipt-paid-btn.is-paid {
  color: var(--color-success);
  background: transparent;
  border: 1px solid var(--color-success);
}
/* ---------- Zahl-QR-Code (GiroCode) ----------
   Chat-Verlauf: "Pack den QR Code ganz links in die Spalte, dort rechts
   neben das PDF Icon" + "beim Klick --> JPG-Download mit QR-Code".

   Der Knopf ist im Ruhezustand blass und faellt nicht auf: er steht in
   JEDER Zeile, auch in den Hunderten laengst bezahlten. Nur zwei Zustaende
   heben sich ab - bereit (Bankverbindung geprueft) und Abweichung. Waere
   er durchgehend farbig, waere die Warnfarbe nichts mehr wert. */
.receipt-preview-cell { display: flex; align-items: center; justify-content: center; gap: 2px; }
.receipt-qr-btn { opacity: 0.34; transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
.receipt-qr-btn:hover { opacity: 1; }
.receipt-qr-btn.is-bereit { opacity: 0.75; color: var(--color-success); }
.receipt-qr-btn.is-bereit:hover { opacity: 1; }
/* Abweichung zwischen Beleg und Stammdaten: die einzige Stelle, an der
   dieser Knopf laut wird. Siehe zahlcodeLage() in app/js/girocode.js. */
.receipt-qr-btn.is-warnung { opacity: 1; color: var(--color-danger); }

/* Der Bankverbindungs-Dialog. Kontonummern werden hier ZIFFERNWEISE
   verglichen - deshalb Festbreitenschrift und Ziffern in Tabellenbreite:
   in einer Proportionalschrift ist eine 1 schmaler als eine 8, und genau
   das macht das Vergleichen zweier IBANs untereinander unzuverlaessig. */
.receipt-bank-intro { margin-bottom: var(--space-3); }
.receipt-iban-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.receipt-iban-input.is-fehler { border-color: var(--color-danger); }
.receipt-iban-pruef { font-size: 12px; margin-top: 4px; color: var(--color-text-faint); }
.receipt-iban-pruef.is-ok { color: var(--color-success); }
.receipt-iban-pruef.is-fehler { color: var(--color-danger); }
.receipt-iban-hinweis {
  font-size: 12.5px;
  line-height: 1.6;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  margin-bottom: var(--space-3);
}
.receipt-iban-hinweis.is-warnung {
  background: var(--color-danger-soft);
  border-color: var(--color-danger);
  color: var(--color-text);
}
.receipt-iban-paar { margin: 7px 0; display: flex; flex-direction: column; gap: 3px; }
.receipt-iban-paar code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 12.5px;
}

.modal.is-receipt { max-width: 640px; }
.receipt-modal-grid { display: grid; grid-template-columns: minmax(0, 200px) 1fr; gap: var(--space-4); align-items: start; }
.receipt-modal-preview { display: flex; flex-direction: column; gap: 8px; align-items: center; text-align: center; }
.receipt-preview-img { width: 100%; max-height: 260px; object-fit: contain; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-bg-alt); }
.receipt-modal-form { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

/* Filterleiste im Archiv-Tab (Chat-Verlauf: "Filterung nach Monaten,
   Jahren, Beleg-Arten, Bezahlarten usw." - am Vorbild der Städtli-
   Referenzseite ausgerichtet). Jahr/Zahlungsart nutzen bewusst die
   bestehenden .year-pills/.year-pill-Klassen (siehe weiter oben,
   identisch zur Jahresauswahl in finance.js) statt eigener Pill-Klassen. */
.receipt-filter-bar { margin-top: var(--space-2); }
/* .receipt-filter-selects gibt es seit dem 27.08. nicht mehr: Layout UND
   Feld-Optik (Hoehe, Rahmen, eigener Chevron, Fokus) kommen jetzt aus der
   gemeinsamen .filter-bar am Dateiende, das Markup benutzt direkt diese
   Klasse (siehe receipts.js). Hier stand vorher eine eigene Kopie derselben
   Regeln - sie war der Grund, warum dieselbe Art Leiste auf zwei Seiten
   unterschiedlich aussah. Nebenbei entschaerft: .receipt-filter-selects
   select:focus und .filter-bar input:focus sind gleich spezifisch, die
   spaeter stehende .filter-bar-Regel haette den Fokus-Schein hier also
   ohnehin still ausgehebelt (siehe CLAUDE.md, Stolperfalle 2). */

/* Verrechnung-Tab (Städtli-Vorbild, siehe receipts.js's renderSettlementTab/
   buildSettlementView) - Zeilenliste privater Belege/manueller Einträge +
   abschließender Zwei-Personen-Saldo. */
.receipt-settlement-rows { display: flex; flex-direction: column; }
.receipt-settlement-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}
.receipt-settlement-row:last-child { border-bottom: none; }
.receipt-settlement-row-main { flex: 1; min-width: 0; text-align: left; background: none; border: none; padding: 0; cursor: pointer; }
.receipt-settlement-row-name { font-size: 13.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.receipt-settlement-row-sub { font-size: var(--text-meta); margin-top: 2px; }
.receipt-settlement-row-amount { text-align: right; white-space: nowrap; font-size: 13.5px; }
.receipt-settlement-summary {
  background: var(--color-bg-alt);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  margin-top: var(--space-4);
}
.receipt-settlement-summary-row { display: flex; justify-content: space-between; font-size: 13.5px; margin-bottom: 6px; }
.receipt-settlement-summary-result { padding-top: var(--space-3); margin-top: 6px; border-top: 1px solid var(--color-border-strong); }

/* Kamera-Aufnahme + Eck-Anpassen (pages/receiptscanner.js) - eigene, kleine
   Klassen statt Wiederverwendung von .filetransfer-*, weil hier ein
   <video>/<canvas>-Vorschaubereich statt einer Datei-Liste gebraucht wird. */
.receipt-camera-frame {
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #000;
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
}
.receipt-camera-video { width: 100%; height: 100%; object-fit: cover; }

/* Vollbild-Scanner (pages/receiptscanner.js, Ausbau 2026-08-21) - eigener
   fixed-Layer direkt auf document.body statt modal.js/openModal(), siehe
   Docblock dort. z-index 150: über .modal-backdrop (100), unter
   .toast-container (200) - Fehler-Toasts (z.B. Kamera-Zugriff verweigert)
   bleiben so sichtbar. */
.receipt-scanner-overlay {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: #000;
}
body.receipt-scanner-open { overflow: hidden; }
.receipt-scanner-screen {
  position: absolute;
  inset: 0;
  flex-direction: column;
}
.receipt-scanner-frame { position: relative; flex: 1; overflow: hidden; background: #000; }
.receipt-scanner-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.receipt-scanner-hint {
  position: absolute;
  left: 50%;
  bottom: 132px;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 13px;
  font-weight: 500;
  padding: 8px 16px;
  border-radius: 999px;
  white-space: nowrap;
  pointer-events: none;
}
.receipt-scanner-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  padding: 0 0 calc(env(safe-area-inset-bottom, 0px) + 28px);
}
.receipt-scanner-shutter {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  background: #fff;
  border: 4px solid rgba(255, 255, 255, 0.4);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25);
  cursor: pointer;
}
.receipt-scanner-shutter:active { transform: scale(0.92); }
.receipt-scanner-close {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 14px);
  right: 16px;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  cursor: pointer;
}
.receipt-scanner-close svg { width: 18px; height: 18px; }
/* Kontakte zusammenführen (pages/clients.js, Chat-Verlauf: "Wir haben bei
   Kunden viele Duplikate... Können wir hier eine Art 'Kontakt
   zusammenführen' Funktion integrieren?") */
.client-dupes-card { margin-bottom: var(--space-4); border-color: var(--color-warning, #d9a441); }
.client-dupe-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--color-border);
}
.client-dupe-row:last-child { border-bottom: none; }
.client-merge-field-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 10px;
  align-items: center;
  padding: 7px 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 13px;
}
.client-merge-field-row.is-header { font-weight: 600; font-size: 12px; color: var(--color-text-faint); }
.client-merge-field-row:last-child { border-bottom: none; }
.client-merge-field-label { color: var(--color-text-faint); }
.client-merge-field-option { display: flex; align-items: center; gap: 6px; cursor: pointer; }

.receipt-crop-wrap { position: relative; touch-action: none; }
.receipt-crop-img { display: block; border-radius: var(--radius-md); user-select: none; -webkit-user-drag: none; pointer-events: none; }
.receipt-crop-overlay { position: absolute; left: 0; top: 0; pointer-events: none; }
.receipt-crop-handle {
  position: absolute;
  width: 22px;
  height: 22px;
  margin-left: -11px;
  margin-top: -11px;
  border-radius: 50%;
  background: var(--color-accent);
  border: 2px solid #fff;
  box-shadow: var(--shadow-sm);
  cursor: grab;
  touch-action: none;
}
.receipt-crop-handle:active { cursor: grabbing; transform: scale(1.15); }

/* ==========================================================
   Mobile @640px - konsolidierte Overrides (bewusst am Ende der Datei)
   ==========================================================
   Bugfix-Historie (Chat-Verlauf: "Formular bei Produktionen, das ist immer
   noch unverändert" / "Da stimmt etwas nicht"): dieser Block stand vorher
   weiter oben (direkt nach den App-Shell/Sidebar-Regeln, um Zeile 1200) -
   an sich harmlos, ABER mehrere seiner Selektoren (u.a. .card, .modal,
   .modal-backdrop, .login-form-side, .field, .field label, .field input)
   haben an anderer Stelle in dieser Datei eine eigene, unbedingte
   Grund-Regel MIT DERSELBEN Selektor-Spezifität, die weiter UNTEN im
   Dokument steht. CSS entscheidet bei gleicher Spezifität immer über die
   Quellreihenfolge - eine @media-Bedingung verschafft keinen Vorrang. Die
   späteren Grund-Regeln haben die frühere Mobile-Kompaktierung deshalb die
   ganze Zeit lautlos wieder überschrieben, ohne Fehler oder Warnung; sichtbar
   wurde das erst, weil einzelne Werte (z.B. .card padding) sich dadurch nie
   änderten, komplett unabhängig von Deploy-Stand oder Browser-Cache. Fix:
   der komplette Block steht jetzt ganz am Ende der Datei, hinter jeder
   möglichen gleich-spezifischen Grund-Regel, und gewinnt dadurch garantiert
   - sowohl die bereits bekannten Fälle als auch jede künftige Regel, die
   irgendwo weiter oben ergänzt wird. */

/* BLEIBT HIER STEHEN (Konsolidierung 28.08.2026): Selektoren aus diesem
   Block kommen weiter unten in der Datei ERNEUT vor. Bei gleicher
   Spezifitaet entscheidet die Reihenfolge - ein Verschieben ans Dateiende
   wuerde die Gewichte umdrehen und das Layout still veraendern. */
@media (max-width: 640px) {
  .main .grid-2, .main .grid-3, .main .grid-4 { grid-template-columns: 1fr; }
  /* Muss NACH der .main .grid-4-Regel direkt oben stehen (siehe Kommentar
     ganz oben in diesem Block) - gleiche Spezifität (zwei Klassen), gewinnt
     hier per Quellreihenfolge. Hierher verschoben von weiter oben in der
     Datei (ursprünglich bei .dashboard-tile .grid-4), wo genau diese Regel
     lautlos von der .main .grid-4-Regel überschrieben wurde. Siehe
     ausführlichen Bug-Report-Kommentar dort. */
  .dashboard-tile .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dashboard-tile .stat-card {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    padding: 10px 12px;
    gap: 10px;
  }
  .dashboard-tile .stat-body { text-align: left; min-width: 0; }
  .dashboard-tile .stat-icon { width: 26px; height: 26px; }
  .dashboard-tile .stat-icon svg { width: 12px; height: 12px; }
  .dashboard-tile .stat-card .stat-value { font-size: 15px; line-height: 1.2; }
  .dashboard-tile .stat-card .stat-label {
    font-size: 11px;
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .main { padding: var(--space-3) var(--space-3) var(--space-8); }
  .main > * + * { margin-top: var(--space-4); }
  /* Design-System-Cleanup 2026-08-16 (Phase 3): Token statt hartkodiertem
     Wert umdefiniert (gleicher 20px-Wert wie zuvor, kein sichtbarer
     Unterschied) - jede Komponente, die künftig var(--text-page-title)
     nutzt, profitiert automatisch von derselben Mobile-Verkleinerung, ohne
     an mehreren Stellen einzeln nachgezogen werden zu müssen. */
  :root { --text-page-title: 20px; }
  /* Untertitel unter der Headline kosten auf dem Handy nur Platz, ohne
     Mehrwert zu bringen (Chat-Verlauf: "Mobil können wir aber überall diese
     kleinen Infoboxen unter der Headline entfernen" - bewusst AUSNAHMSLOS,
     unabhängig ob reiner Zähler oder beschreibender Text, siehe frühere,
     engere hideSubtitleOnMobile-Regelung in pageHeader(), layout.js, die
     hiermit durch dieses generelle Ausblenden ersetzt wird). Selektor greift
     sowohl bei pageHeader() als auch bei den wenigen Seiten mit eigenem
     .page-header-Markup (boards.js, shotdocs.js, todos.js), da er strukturell
     (erstes Kind-div, <p> darin) statt über eine Klasse matcht. */
  .page-header > div:first-child > p { display: none; }
  /* Gleiches Prinzip für die separat (nicht über pageHeader()) gerahmten
     Info-Kästen unter der Headline bei Previews & Masters und Assets.
     Bug-Report: "Ich sehe Mobile immer noch diese Beschreibungstexte [...]
     Insbesondere bei Previews & Masters und Assets" - beide Seiten setzen
     auf diesem Element zusätzlich ein Inline-style="display:flex" (für die
     Desktop-Anordnung Text+Zähler nebeneinander, siehe previewsmasters.js/
     assets.js), Inline-Styles gewinnen aber IMMER gegen eine externe
     Stylesheet-Regel ohne !important, unabhängig von deren Spezifität -
     die reine display:none-Regel griff dadurch faktisch nie. !important
     hier bewusst gezielt auf diesen einen, bekannten Konfliktfall
     beschränkt statt generell verwendet. */
  .page-intro-card { display: none !important; }
  /* Chat-Verlauf: "Dann kannst du den Button (z.B. + Board hinzufügen) auf
     dieselbe Höhe wie die Headline - nur rechtsbündig setzen." - Button(s)
     behalten ihre natürliche Breite statt auf 100% gezogen zu werden;
     margin-left:auto auf .page-header-actions schiebt sie an den rechten
     Rand der Zeile, egal ob sie noch neben dem Titel Platz finden (jetzt
     durch den Wegfall der Untertitel meist der Fall) oder bei sehr langen
     Titeln in eine eigene Zeile umbrechen. :not(.btn-icon-header) bleibt wie
     zuvor für icon-only Header-Buttons (z.B. "Ausführlich anlegen") außen
     vor, die schon vorher kompakt waren. */
  .page-header .btn:not(.btn-icon-header) { width: auto; justify-content: center; }
  .page-header-actions { margin-left: auto; }
  /* Chat-Verlauf: "Dieser gesamte Screen braucht auch extrem viel Platz ...
     Setz das 'Ausführlich anlegen' doch ohne Text als Button rechts oben in
     die Ecke neben Rentals ausblenden" - Toggle und Button bleiben auf einer
     Zeile (statt wie vorher übereinander gestapelt), Toggle nimmt den
     verfügbaren Platz, Button bleibt kompakt am rechten Rand. */
  .production-header-actions { width: 100%; justify-content: space-between; }
  .production-header-actions .switch-row { flex: 1 1 auto; min-width: 0; }
  .btn-icon-header { flex: 0 0 auto; padding: 8px; }
  .btn-icon-header .btn-label-text { display: none; }
  .card { padding: var(--space-4); border-radius: var(--radius-md); }
  .field-row { flex-direction: column; gap: 0; }
  .field-row > .field { width: 100%; }
  /* Schnell-anlegen-Formulare (z.B. "Neue Produktion") wirkten trotz
     mehrerer vorheriger Versuche unverändert luftig - siehe Erklärung ganz
     oben in diesem Abschnitt (Quellreihenfolge-Bug). Jetzt an garantiert
     gewinnender Stelle, und spürbar knapper als alle vorherigen Versuche. */
  .field { margin-bottom: 5px; }
  .field label { margin-bottom: 2px; font-size: var(--text-meta); }
  .field input, .field select, .field textarea { padding: 6px 8px; }
  /* Die feste Hoehe von oben mit der kleineren Polsterung hier
     nachgezogen - sonst waeren die Felder auf dem Telefon relativ zur
     Schrift zu hoch. 35px ist die gemessene Hoehe, die ein Textfeld hier
     ohnehin hat; Auswahlfeld und Datum ziehen jetzt mit. */
  .field input[type="text"],
  .field input[type="email"],
  .field input[type="password"],
  .field input[type="search"],
  .field input[type="tel"],
  .field input[type="url"],
  .field input[type="number"],
  .field input[type="date"],
  .field input[type="time"],
  .field input[type="datetime-local"],
  .field select {
    height: 35px;
  }
  /* Agentur/Kunde-Felder im Dokumenten-Kopf (shotdocs.js) sind rohe <input>-
     Elemente ohne .field-Wrapper, bekommen die width:100%-Regel oben also
     nicht. .shotdoc-meta-row bleibt bewusst eine Zeile statt spaltenweise zu
     stapeln (Chat-Verlauf: "Die beiden Input-Felder deutlich kleiner und in
     eine Zeile unter den Titel setzen") - flex-basis:0 verteilt die
     Restbreite gleichmäßig auf beide Felder, knapperes Padding macht sie
     insgesamt kompakter. */
  .shotdoc-header .shotdoc-meta-row { flex-direction: row; gap: 8px; }
  .shotdoc-header .shotdoc-meta-row input {
    max-width: none;
    width: auto;
    flex: 1 1 0%;
    min-width: 0;
    padding: 6px 8px;
  }
  .modal-backdrop { padding: var(--space-3); align-items: center; }
  .modal { padding: var(--space-4); max-height: 88vh; overflow-y: auto; }
  .year-pills { flex-wrap: wrap; }
  .login-form-side { padding: var(--space-4); }

  /* iPhone-Mobile-UX-Audit 2026-08-16 (docs/ux-audit-2026-08-16-iphone-mobile.md,
     2.10): auf Mobile ersetzt der einzelne ⋮-Trigger die ausführliche
     Icon-Reihe (siehe Kommentar bei .table-actions-mobile-trigger weiter
     oben) - und das dadurch geöffnete actionSheet() wird hier zum echten
     Bottom-Sheet: unten angedockt, volle Breite, obere Ecken abgerundet,
     Home-Indicator-Safe-Area als zusätzliches unteres Padding. */
  .table-actions-desktop { display: none; }
  .table-actions-mobile-trigger { display: inline-flex; }

  /* iPhone-Mobile-UX-Audit 2026-08-16 (docs/ux-audit-2026-08-16-iphone-mobile.md,
     2.2): auf schmalen Bildschirmen steht die Changelog-Kachel allein in
     ihrer Grid-Zeile (siehe .main .grid-2/.grid-3/.grid-4 oben, einspaltig)
     - syncDashboardRowHeights() in dashboard.js gleicht die Höhe dann NICHT
     an (early return bei "allein in der Zeile", siehe Kommentar dort) und
     lässt den 420px-CSS-Fallback aus style.css wirken, wodurch die Karte
     praktisch den ganzen ersten Bildschirm füllte, bevor überhaupt die
     eigentlichen Dashboard-Kacheln sichtbar wurden. Eigener, deutlich
     knapperer Mobile-Wert (~2 Einträge sichtbar, Rest weiter per
     eingebautem Scroll erreichbar statt komplett zu verschwinden). */
  .changelog-panel-list.is-compact { max-height: 190px; }
  .modal-backdrop.action-sheet { align-items: flex-end; padding: 0; }
  .modal.action-sheet {
    max-width: none;
    width: 100%;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: var(--space-4) var(--space-3) max(var(--space-4), env(safe-area-inset-bottom));
    animation: action-sheet-up 0.18s var(--ease);
  }

  /* iPhone-Mobile-UX-Audit 2026-08-16 (docs/ux-audit-2026-08-16-iphone-mobile.md,
     2.5, P0-Fund): jede leere Preview-/Master-Dropzone (Header + "Dateien
     hierher ziehen oder klicken"-Hinweis + "Noch keine ...-Datei."-Text)
     brauchte einzeln ca. 130-150px - bei zwei Sektionen pro Produktion füllte
     eine einzelne Produktion OHNE Dateien fast den kompletten Bildschirm,
     nur eine Produktion passte pro Scrollstrecke. Reine Flex-Reorder der
     BESTEHENDEN Elemente aus fileSection() (previewsmasters.js) zu einer
     einzeiligen Zeile "▶ Preview   Keine Datei   + Upload" - kein DOM-Umbau
     nötig, Klick/Drag&Drop auf die ganze Sektion funktioniert unverändert
     (siehe click-Handler in fileSection()). :has(.pm-empty) trifft
     zuverlässig nur auf leere Sektionen (.pm-empty wird nur gerendert, wenn
     filesOfKind() leer ist, siehe renderAll()); :not(.is-uploading) nimmt
     eine Sektion während eines laufenden Uploads bewusst aus, damit die
     Fortschritts-Zeile (.pm-uploading-row) ihre normale, nicht
     zusammengequetschte Breite behält. */
  .pm-section:has(.pm-empty):not(.is-uploading) {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--space-2);
    padding: 8px 10px;
  }
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-section-header {
    order: 1;
    margin-bottom: 0;
    flex: 0 0 auto;
  }
  /* "0"-Zähler ist bei einer leeren Sektion redundant zum "Keine Datei"-Text
     direkt daneben - spart auf der engen Zeile Platz. */
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-section-count { display: none; }
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-file-list {
    order: 2;
    flex: 1 1 auto;
    min-width: 0;
  }
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-empty {
    margin: 0;
    text-align: left;
    font-size: 12px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-dropzone-hint {
    order: 3;
    flex: 0 0 auto;
    margin-bottom: 0;
    padding: 5px 9px;
    white-space: nowrap;
  }
  /* Der ausführliche Hinweistext passt nicht mehr auf eine kompakte Zeile -
     Icon bleibt, Text weicht einem kurzen "+ Upload" (::after statt einer
     zweiten Textänderung in previewsmasters.js, damit Desktop/Tablet
     unverändert den ausführlichen Text zeigen). */
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-dropzone-hint span:last-child { display: none; }
  .pm-section:has(.pm-empty):not(.is-uploading) .pm-dropzone-hint::after { content: '+ Upload'; }

  /* Batch 3 (Previews & Masters "Laufend"-Tab, iPhone Mobile-UX 2026-08-16,
     Finding 2.5): der P0-Fix oben (Batch 1) hat nur die LEEREN Sektionen
     kompaktiert - Sektionen MIT Dateien zeigten weiterhin zusätzlich zu den
     Datei-Zeilen den vollen "Dateien hierher ziehen oder klicken"-Hinweis
     samt Padding obendrüber, obwohl Tippen/Ziehen auf die ganze Sektion
     (siehe click-Handler in fileSection(), previewsmasters.js) davon völlig
     unabhängig weiter funktioniert - der Hinweistext war dort reine
     Redundanz. :has(.pm-file-row) trifft zuverlässig nur auf Sektionen mit
     mindestens einer aktiven Datei (siehe renderAll() - ein pm-file-row pro
     aktiver Datei). Card/Section/Zeilen-Abstände zusätzlich leicht
     verschlankt, damit auf 402px Breite mehr als eine Produktion sichtbar
     ist; Klickflächen (Zeilenhöhe, Icon-Buttons) bleiben unverändert groß
     genug für Touch. */
  .pm-section:has(.pm-file-row) .pm-dropzone-hint { display: none; }
  .pm-card { padding: var(--space-3); }
  .pm-sections { gap: var(--space-2); }
  .pm-section { padding: 8px 10px; }
  .pm-file-list { gap: 4px; }
  .pm-file-row { padding: 6px 8px; }
  /* Vimeo-Metazeile (Auflösung/Codec o.ä., siehe vimeoMetaSummary() in
     previewsmasters.js) ist eine dritte, rein optionale Zeile pro Datei -
     bei aktivierten Vimeo-Metadaten wuchs jede betroffene Zeile dadurch
     spürbar. Bleibt über den Tooltip (title=vimeoTooltip) weiter erreichbar,
     Desktop/Tablet zeigen sie unverändert direkt an. */
  .pm-file-row .pm-file-sub.text-muted { display: none; }

  /* Batch 2 (Produktionsliste, iPhone Mobile-UX 2026-08-16, Finding 2.3.1):
     "Neue Produktion" stand als volle Formular-Karte fest über der Liste -
     unterhalb von 640px (wo .grid-2 ohnehin einspaltig wird, siehe oben)
     musste man erst am kompletten Formular vorbeiscrollen, um überhaupt eine
     bestehende Produktion zu sehen. quickAddCard() bleibt unverändert die
     Karte für Desktop/Tablet; quickAddMobileTrigger() (siehe productions.js)
     ist ein schmaler Button, der dasselbe Formular per actionSheet() in
     einem Bottom-Sheet öffnet - gleiche Sheet-Optik wie das ⋮-Menü.
     !important nötig: die Karte trägt in quickAddCard() ein inline
     style="display:flex" (fürs Flex-Layout des Formulars/Submit-Buttons),
     inline styles gewinnen sonst gegen jede Klassenregel, unabhängig von
     @media/Quellreihenfolge. */
  .quickadd-card { display: none !important; }
  .quickadd-trigger-mobile { display: flex; width: 100%; justify-content: center; }

  /* Batch 4+5 (Suche + Portfolio, iPhone Mobile-UX 2026-08-16, Finding 2.8):
     der Footer-Hinweis ("↑↓ navigieren ↵ öffnen Esc schließen") ist eine
     reine Tastatur-Bedienhilfe - auf einem Touchscreen ohne Pfeiltasten/
     Escape/Enter komplett bedeutungslos, ein Tap auf ein Ergebnis wählt es
     ohnehin direkt aus. Die VORHERIGE Regel (siehe
     ".quicksearch-hint span:not(:first-child)") blendete versehentlich
     genau das Falsche aus: sie ließ nur den ERSTEN Hinweis ("↑↓
     navigieren") übrig und versteckte die anderen beiden - jetzt komplett
     weg. */
  .quicksearch-hint { display: none; }

  /* Finding 2.7.1: Portfolio-Formular (Titel/Status/Beschreibung) stand vor
     der Video-Liste - genau wie beim "Neue Produktion"-Formular bei
     Produktionen (Batch 2) musste man erst daran vorbeiscrollen. Gleicher
     CSS-only-Ansatz wie dort: .portfolio-detail wird auf Mobile zum
     Flex-Container, die drei Karten bekommen über eigene Klassen
     (portfolio-videos-card/-meta-card/-share-card, siehe portfolios.js)
     eine explizite Reihenfolge - Video-Liste zuerst, Formular/Teilen-Block
     danach. Auf Desktop/Tablet bleibt die ursprüngliche DOM-Reihenfolge
     unangetastet (kein Flex-Container dort, order hätte keine Wirkung). */
  .portfolio-detail { display: flex; flex-direction: column; }
  .portfolio-videos-card { order: 1; }
  .portfolio-meta-card { order: 2; }
  .portfolio-share-card { order: 3; }
}

/* Batch 6 (Video-Player, iPhone Mobile-UX 2026-08-16, Finding 2.9): der
   Plyr-Player öffnete bisher als klassisches zentriertes Desktop-Modal
   (siehe .pm-video-modal-player video { max-height:72vh } weiter unten,
   zusammen mit der 92vw-Breitenbegrenzung des Modals selbst) - auf
   402×874 nahm das Video dadurch nur ~26% der Viewport-Höhe ein, umgeben
   von viel abgedunkeltem Rand. Auf Mobile übernimmt das Modal stattdessen
   den kompletten Bildschirm.
   Live-Review-Fund 2026-08-16 ("wäre es mobil schön, wenn sich nur der
   Film in einem Fenster öffnet ... ohne große weiße Box drumherum" /
   "Es bleibt immer der weiße Kopfbereich oben um die Dynamic Island
   herum"): der bisherige Ansatz behielt trotzdem die normale, HELLE
   .modal-Hintergrundfarbe samt Titelzeile + Safe-Area-Padding - das ergab
   genau die beklagte weiße Fläche oben (und unten, sobald das
   Seitenverhältnis nicht exakt zum Screen passte, "hängt der Content auch
   nicht mittig sondern leicht nach unten versetzt" - asymmetrisches
   Padding oben/unten durch unterschiedlich große Safe-Area-Insets zog die
   zentrierte Video-Box nach unten). Jetzt: Modal-Hintergrund schwarz
   (statt var(--color-surface)), kein Padding mehr (Video geht bis an die
   Kante, dadurch auch wirklich mittig) - die Titelzeile bleibt FUNKTIONAL
   (Schließen-Button, ESC/Backdrop-Klick greifen weiterhin über
   openModal()), wird aber visuell zu einem transparenten, semi-dunklen
   Kreis-Button, der über der Videoecke schwebt statt eine eigene
   Zeile/Balken zu beanspruchen - Safe-Area-Inset schiebt NUR diesen Button
   von der Dynamic Island weg, nicht mehr die ganze Fläche.
   EIGENE Media-Query statt Teil des großen @max-width:640px-Blocks oben
   (Bug-Report: "Im Landscape Modus hängt er oben und lässt sich auch nicht
   komplett fullscreen darstellen, maximal ca. 50% des Bildschirms.") - ein
   quergedrehtes Handy ist in Landscape oft BREITER als 640px (z.B. iPhone
   15 Pro: 852px), fiel dadurch bisher aus der reinen max-width-Bedingung
   heraus und landete beim normalen, kleinen Desktop-Modal. Die zusätzliche
   (max-height:640px) and (orientation:landscape)-Bedingung fängt genau
   diesen Fall zusätzlich ab, unabhängig von der Breite - Tablets im
   Querformat (Höhe deutlich über 640px) bleiben davon unberührt. */
/* Live-Review-Fund 2026-08-17 ("Wenn ich mobile einen Film öffne, öffnet
   sich immer so ein riesiges schwarzes Fenster, obwohl der Film ja nur
   einen sehr kleinen Teil davon ausmacht... genau so wie in der
   Desktop-Version"): der Fullscreen-Ansatz oben (Modal = kompletter
   Bildschirm, Video mit height:100% gestreckt) hatte einen Denkfehler -
   das <video>-Element wurde per Inline-Style UND CSS auf height:100% der
   kompletten Bildschirmhöhe gezwungen; object-fit:contain hat den
   eigentlichen Film darin zwar korrekt im Seitenverhältnis eingepasst
   (Letterboxing), aber da beides (Letterboxing-Balken UND
   Modal-Hintergrund) schwarz war, sah man optisch nur noch eine riesige
   schwarze Fläche, in der der eigentliche Film wie ein kleiner Ausschnitt
   wirkte - exakt der gemeldete Bug. Jetzt wie am Desktop: das Modal selbst
   ist fit-content (schrumpft auf die tatsächliche Video-Größe statt sich
   zu strecken), das Video bekommt width/height:auto (behält sein
   natives Seitenverhältnis, keine erzwungene 100%-Streckung mehr) und wird
   nur nach oben durch max-width/max-height begrenzt, damit große Filme
   nicht über den Bildschirm hinausragen. Titelzeile bleibt der
   schwebende, transparente Schließen-Button (siehe Kommentar oben), jetzt
   aber über der Ecke der tatsächlichen (kleineren) Video-Box statt der
   ganzen Bildschirmecke. */

/* BLEIBT HIER STEHEN (Konsolidierung 28.08.2026): Selektoren aus diesem
   Block kommen weiter unten in der Datei ERNEUT vor. Bei gleicher
   Spezifitaet entscheidet die Reihenfolge - ein Verschieben ans Dateiende
   wuerde die Gewichte umdrehen und das Layout still veraendern. */
@media (max-width: 640px), (max-height: 640px) and (orientation: landscape) {
  .modal-backdrop.pm-video-modal {
    align-items: center;
    padding: max(var(--space-3), env(safe-area-inset-top)) max(var(--space-3), env(safe-area-inset-right)) max(var(--space-3), env(safe-area-inset-bottom)) max(var(--space-3), env(safe-area-inset-left));
  }
  .modal.pm-video-modal {
    position: relative;
    width: fit-content;
    max-width: 100%;
    height: fit-content;
    max-height: 100%;
    border-radius: var(--radius-md);
    overflow: hidden;
    padding: 0;
    background: #000;
  }
  .modal.pm-video-modal .modal-header {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    z-index: 2;
    margin-bottom: 0;
    width: auto;
  }
  .modal.pm-video-modal .modal-header h2 { display: none; }
  .modal.pm-video-modal .modal-header .btn-ghost {
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    border-radius: var(--radius-full);
    width: 32px;
    height: 32px;
    justify-content: center;
  }
  .modal.pm-video-modal .pm-video-modal-player {
    display: flex;
    border-radius: 0;
  }
  .modal.pm-video-modal .pm-video-modal-player video {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: calc(100dvh - max(var(--space-3), env(safe-area-inset-top)) - max(var(--space-3), env(safe-area-inset-bottom)));
    object-fit: contain;
  }
}

/* ---------- Previews & Masters (Chat-Verlauf: "einen neuen Punkt
   'Previews & Masters' bauen, unter 'Assets' ... für alle laufenden und
   vergangenen Produktionen Previews bzw. Masters hochladen und
   archivieren können") - siehe app/js/pages/previewsmasters.js. ---------- */

.pm-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}
/* .booking-view-tabs/.booking-view-tab (Buchungen: Liste/Kalender-
   Umschalter, Chat-Verlauf: "FullCalendar: Buchungen/Verleih") teilen sich
   dieselben Regeln wie .pm-tabs/.pm-tab - gleiches visuelles Muster für
   "zwei bis vier gleichrangige Ansichten umschalten", kein Grund, es ein
   zweites Mal zu definieren. */
.pm-tabs, .booking-view-tabs {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
}
.pm-tab, .booking-view-tab {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-weight: 600;
  padding: 7px 14px;
  border-radius: calc(var(--radius-md) - 2px);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  /* Bug-Report: "'Nicht zugeordnet' bricht immer um, das ist nicht schön."
     .pm-tab ist ein Flex-Item von .pm-tabs (display:inline-flex) - ohne
     diese beiden Regeln durfte der Button per Default (flex-shrink:1,
     min-width:auto) unter seine Text-Breite schrumpfen, sobald alle vier
     Tabs zusammen nicht mehr auf eine mobile Bildschirmbreite passten -
     der längste Label-Text ("Nicht zugeordnet") brach dadurch als
     einziger sichtbar um, statt dass die ganze Leiste reagiert. */
  white-space: nowrap;
  flex-shrink: 0;
}
.pm-tab:hover, .booking-view-tab:hover { color: var(--color-text); }
.pm-tab.is-active, .booking-view-tab.is-active { background: var(--color-surface); color: var(--color-accent-strong); box-shadow: var(--shadow-sm); }

/* Kalender-Ansicht in Buchungen (FullCalendar, lazy geladen - siehe
   loadFullCalendar() in bookings.js) - FullCalendar bringt seine eigenen
   Styles per JS mit, hier nur der Wrapper-Abstand + eine dezente
   Ladeanzeige, solange das Bundle noch nachlädt. */
.booking-calendar { min-height: 120px; }
.booking-calendar:empty::before {
  content: 'Kalender wird geladen…';
  display: block;
  padding: 40px 0;
  text-align: center;
  color: var(--color-text-faint);
  font-size: 13px;
}
.booking-calendar .fc { font-family: inherit; }
.booking-calendar .fc-event { cursor: pointer; border: none; font-size: 11.5px; font-weight: 600; padding: 1px 4px; }
.booking-calendar .fc-toolbar-title { font-size: 16px; font-weight: 700; }
.booking-calendar .fc-button-primary {
  background: var(--color-surface);
  border-color: var(--color-border-strong);
  color: var(--color-text);
  box-shadow: none;
  text-transform: none;
}
.booking-calendar .fc-button-primary:hover { background: var(--color-surface-hover); }
.booking-calendar .fc-button-primary:not(:disabled).fc-button-active {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #fff;
}
.booking-calendar .fc-daygrid-day.fc-day-today { background: var(--color-accent-soft); }

.pm-search {
  position: relative;
  display: flex;
  align-items: center;
  /* flex:1 statt nur min-width (Chat-Verlauf: "Feld zu schmal") - wächst mit,
     sobald .pm-toolbar umbricht (siehe flex-wrap oben) und die Suche allein
     in ihrer eigenen Zeile steht, statt bei 240px stehen zu bleiben, obwohl
     deutlich mehr Platz frei wäre. */
  flex: 1 1 260px;
  min-width: 200px;
}
.pm-search svg {
  position: absolute;
  left: 11px;
  /* top:50%+transform statt nur "left" (Chat-Verlauf: "Lupe hängt unten") -
     als absolut positioniertes Element zählt .pm-search trotz
     align-items:center nicht mehr zur Flex-Ausrichtung, die Lupe blieb ohne
     eigene vertikale Zentrierung an ihrer statischen Position (unten) hängen. */
  top: 50%;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  color: var(--color-text-faint);
  pointer-events: none;
}
.pm-search-input {
  width: 100%;
  padding: 8px 11px 8px 32px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  background: var(--color-surface);
  color: var(--color-text);
}
.pm-search-input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--shadow-glow); }

.pm-grid {
  display: grid;
  /* Chat-Verlauf: "mach mal testweise nur drei Kacheln pro Zeile" - fixe
     3-Spalten statt des bisherigen auto-fill/minmax(360px)-Verhaltens, als
     Test. Bei Bedarf einfach wieder auf die auto-fill-Zeile zurücksetzen. */
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-4);
}

.pm-card { padding: var(--space-4); }
/* Chat-Verlauf: "Einträge bei 'Laufend' die bereits hinterlegte Dateien
   haben mit einem leichten farbigen Rand markieren, in der Custom-Color des
   Users" - einfach der bestehende 1px-Rahmen von .card (Grundfarbe
   var(--color-border)) umgefärbt, keine neue Border-Breite/-Box. */
.pm-card.has-files { border-color: var(--color-accent); }
/* Seit dem 31.08.2026 steht rechts in der Kopfzeile ggf. der Knopf zum
   Nachziehen fehlender Vorschaubilder - deshalb flex statt Block. Er darf
   NICHT schrumpfen: "12 ohne Vorschaubild" auf zwei Zeilen umgebrochen
   sieht aus wie ein Fehler. */
.pm-card-header {
  margin-bottom: var(--space-3);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}
.pm-thumb-backfill { flex: none; white-space: nowrap; }
/* Design-System-Cleanup 2026-08-16 (Phase 4a): .pm-card-number ist eine
   Sekundärinfo (wie ein Badge/Status), .pm-card-title IST der Inhalt -
   Titel muss stärker wirken als Nummer. font-weight:700 auf beiden hob sie
   künstlich auf dieselbe Stufe; Nummer auf --weight-semibold (600)
   zurückgenommen, Titel bleibt bei 700 (rare, bewusste Ausnahme, siehe
   :root-Kommentar zu --weight-*) - jetzt klar unterscheidbar. */
.pm-card-number { font-size: var(--text-meta); font-weight: var(--weight-semibold); color: var(--color-text-faint); letter-spacing: 0.02em; }
.pm-card-title { font-size: var(--text-entity-title); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pm-card-client { font-size: var(--text-meta); color: var(--color-text-muted); margin-top: 1px; }

/* Preview/Master untereinander statt nebeneinander (Feedback: "damit man
   die Dateinamen besser lesen kann") - bei zwei schmalen Spalten wurden
   Dateinamen wie "260724..." abgeschnitten; untereinander hat jede Sektion
   die volle Kartenbreite zur Verfügung. */
.pm-sections {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}

.pm-section {
  min-width: 0;
  background: var(--color-bg-alt);
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.pm-section.is-dragover { border-color: var(--color-accent); background: var(--color-accent-soft); }
.pm-section.is-uploading { border-color: var(--color-accent); }
.pm-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-2); }
.pm-section-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-meta);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.pm-section-label svg { width: 13px; height: 13px; }
.pm-section-count {
  flex-shrink: 0;
  background: var(--color-surface);
  color: var(--color-text-muted);
  border-radius: var(--radius-full);
  padding: 1px 8px;
  font-size: var(--text-small);
  font-weight: 700;
}
.pm-dropzone-hint {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px;
  margin-bottom: var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-faint);
  font-size: 11px;
  text-align: center;
}
.pm-dropzone-hint svg { width: 13px; height: 13px; }
.pm-section.is-dragover .pm-dropzone-hint { color: var(--color-accent); }

.pm-uploading { display: flex; flex-direction: column; gap: 6px; }
.pm-uploading:not(:empty) { margin-bottom: 6px; }
.pm-uploading-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  background: var(--color-accent-soft);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  font-size: 12px;
}
.pm-uploading-row svg { width: 13px; height: 13px; color: var(--color-accent); flex-shrink: 0; }
.pm-uploading-row .pm-file-name { margin-bottom: 4px; }
.pm-uploading-row .progress-track { height: 5px; }
/* Design-System-Cleanup 2026-08-16 (Phase 4a): gleiche Logik wie
   .status-percent (Phase 3) - Prozentzahl ist Statusinfo, nicht der
   dominante Wert der Zeile, font-weight daher von 700 auf --weight-semibold
   zurückgenommen. */
.pm-uploading-percent { flex-shrink: 0; font-size: var(--text-meta); font-weight: var(--weight-semibold); color: var(--color-accent-strong); font-variant-numeric: tabular-nums; }

.pm-file-list { display: flex; flex-direction: column; gap: 6px; }
.pm-empty { font-size: var(--text-meta); color: var(--color-text-faint); text-align: center; margin: 4px 0; }

.pm-file-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 12px;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.pm-file-row:hover { border-color: var(--color-accent); }

/* Aeltere Fassungen unter ihrer eigenen Zeile (02.09.2026, Nutzer: "V1
   ausblenden und V2 einblenden"). Eingerueckt und gestrichelt, damit auf
   einen Blick klar ist: das gehoert zur Zeile darueber und ist nicht mehr
   die aktuelle Fassung. */
/* ---------- Lieferpaket in der internen Dateiliste (03.09.2026) --------
   Nutzer: "Wenn ich einen Stapel Renderings hochlade, dann legt er jetzt
   Bild einzeln als Preview an, obwohl sie zu einem Paket gehören. Ich muss
   dann auch bei jedem Bild einzeln auf die Freigabe und Feedback klicken,
   es schnell 50 oder 100 Klicks sind."

   Das Paket gab es in den Daten schon (preview_deliveries, seit dem
   29.08.) - im Kunden-Dashboard ist es der Kontaktbogen. Intern war die
   Liste flach. Die Kopfzeile macht es sichtbar UND bedienbar: die zwei
   Schalter darin gelten für alle Dateien der Lieferung.

   Standardmäßig zugeklappt, deshalb ist die Kopfzeile selbst die
   Klickfläche - eine Lieferung mit 50 Bildern ist als Paket gemeint, und
   wer sie aufklappt, will die einzelne Datei. */
.pm-delivery {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  overflow: hidden;
}
.pm-delivery-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  cursor: pointer;
  user-select: none;
}
.pm-delivery-head:hover { background: var(--color-surface); }
.pm-delivery-caret {
  display: inline-flex;
  color: var(--color-text-muted);
  transition: transform var(--dur-fast) var(--ease);
}
.pm-delivery-caret svg { width: 15px; height: 15px; }
.pm-delivery-head.is-open .pm-delivery-caret { transform: rotate(90deg); }
/* Titel oben, Anzahl darunter - derselbe Aufbau wie an den Dateizeilen
   (.pm-file-name + .pm-file-sub), die diese Gruppe zusammenfasst.
   Umgebaut am 04.09.2026: vorher standen beide NEBENeinander, und die
   Anzahl stand ausserdem noch im Titel ("Renderings vom 29.08.2026 · 3
   Dateien" neben "3 Dateien"). Der Titel schreibt sie seit heute nicht
   mehr mit, siehe createDelivery in api/previewsmasters.php.

   flex:1 statt margin-right:auto schiebt die Schalter ans rechte Ende -
   das leistete vorher die Anzahl, die jetzt keine eigene Spalte mehr
   ist. min-width:0 daneben ist Pflicht, nicht Geschmack: ohne es
   schrumpft ein Flex-Kind nie unter seine Inhaltsbreite, ein langer
   Titel schoebe die Schalter aus der Zeile. */
.pm-delivery-text { flex: 1; min-width: 0; }
/* 12px wie .pm-file-row darunter - vorher stand hier keine font-size, die
   Ueberschrift erbte also die Body-Groesse und war sichtbar groesser als
   die Dateizeilen, die sie zusammenfasst ("Fontgroesse in der
   Rendering-Gruppen-Headline an den Rest anpassen"). Die Zahl steht
   bewusst als Zahl da und nicht als Variable: .pm-file-row daneben macht
   es genauso, und zwei verschiedene Schreibweisen fuer dieselbe Groesse
   waeren der Anfang des naechsten Auseinanderlaufens. */
/* Titel und Stift in einer Zeile. Der Stift steht DANEBEN, nicht am
   rechten Rand: dort sitzen die beiden Sammelschalter, und ein dritter
   runder Knopf daneben waere von ihnen nicht zu unterscheiden - obwohl er
   etwas ganz anderes tut. Am Titel ist er eindeutig dem Titel zugeordnet,
   genauso wie der Stift an der Board-Kopfzeile (.board-title-edit-btn). */
.pm-delivery-titel-zeile { display: flex; align-items: center; gap: 4px; min-width: 0; }
/* IMMER sichtbar (04.09.2026, zweiter Anlauf).
   -----------------------------------------------------------------
   Die erste Fassung blendete den Stift erst beim Zeigen ein, mit der
   Begruendung "zwanzig Pakete mit zwanzig dauerhaften Stiften sind
   Unruhe". Nutzer, unmittelbar danach: "Ich sehe den Stift zum
   bearbeiten nicht."

   Das ist der ganze Einwand gegen diese Sorte Sparsamkeit. Ein
   Bedienelement, das man erst findet, wenn man zufaellig mit der Maus
   darueberfaehrt, gibt es fuer den Nutzer nicht - und die Frage, die
   diesen Knopf ueberhaupt ausgeloest hat, lautete woertlich "Wie kann
   ich den Titel des Pakets ändern?". Auf genau diese Frage antwortet ein
   unsichtbarer Stift nicht.

   Ausserdem war die Zurueckhaltung inkonsequent: rechts in derselben
   Zeile stehen zwei dauerhaft sichtbare Schalter-Chips. Ein einzelner
   blasser Stift daneben ist kein Unruhefaktor.

   Zurueckhaltung passiert jetzt ueber die FARBE, nicht ueber die
   Sichtbarkeit: --color-text-faint im Ruhezustand, beim Zeigen kraeftiger
   mit Hintergrund. */
.pm-delivery-umbenennen {
  flex-shrink: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: none;
  color: var(--color-text-faint);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pm-delivery-umbenennen svg { width: 12px; height: 12px; }
.pm-delivery-umbenennen:hover,
.pm-delivery-umbenennen:focus-visible { background: var(--color-bg-alt); color: var(--color-text); }
.pm-delivery-titel {
  font-size: 12px;
  font-weight: 590;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pm-delivery-anzahl {
  font-size: var(--text-meta);
  color: var(--color-text-muted);
}
.pm-delivery-flags { display: flex; gap: 6px; }
/* Drei Zustände, nicht zwei: alle an, alle aus, GEMISCHT. Ohne den dritten
   sähe eine Lieferung, in der ein einziges Bild abweicht, gleichförmig aus
   - und ein Klick würde die Abweichung stillschweigend plattmachen. */
.pm-delivery-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: var(--text-meta);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pm-delivery-flag svg { width: 14px; height: 14px; }
.pm-delivery-flag:hover { border-color: var(--color-accent); }
.pm-delivery-flag.is-an {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-strong);
}
/* Gemischt bekommt einen eigenen Ton statt eines halbtransparenten "an" -
   ein blasses Blau läse sich als "an, aber ausgegraut". */
.pm-delivery-flag.is-gemischt {
  background: var(--color-warning-soft, #fdf3e0);
  border-color: var(--color-warning, #d97706);
  color: var(--color-warning-strong, #92590b);
}
.pm-delivery-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 10px 10px;
}

.pm-file-group { display: flex; flex-direction: column; gap: 6px; }
.pm-file-versions { display: flex; flex-direction: column; gap: 6px; }

/* hidden schlaegt jede Klasse (03.09.2026).
   -------------------------------------------------------------------------
   Das Browser-Standardblatt setzt fuer [hidden] display:none - aber jede
   Klassenregel mit eigenem display gewinnt dagegen, weil sie
   spezifischer ist. Ein Element mit display:flex bleibt also sichtbar,
   obwohl im JavaScript sauber hidden gesetzt wird.

   Nutzer 03.09.2026: "die Liste klappt ueberhaupt nicht auf oder zu. Sie
   ist einfach nur aufgeklappt." Genau das: .pm-delivery-body hatte
   display:flex und keine [hidden]-Begleitregel. Eine Zeile darueber stand
   die Begleitregel fuer .pm-file-versions - beim Bauen des neuen Blocks
   habe ich das display abgeschrieben und die Begleitregel nicht.

   Beim Nachsehen hatten .chat-draft-bar und .changelog-panel-item-try
   denselben Fehler, nur hat es dort noch niemand gemeldet. Drei von vier
   Stellen falsch heisst: nicht die Stellen sind das Problem, sondern dass
   es ueberhaupt eine Begleitregel braucht.

   Deshalb einmal global statt vier Mal einzeln. !important ist hier kein
   Holzhammer, sondern die Aussage, die das Attribut ohnehin schon macht:
   hidden heisst weg. */
[hidden] { display: none !important; }
.pm-file-row.is-version-child {
  margin-left: 26px;
  background: var(--color-bg);
  border-style: dashed;
}
.pm-file-row.is-version-child .pm-file-name { font-weight: 400; }
/* Das Versions-Abzeichen als Aufklapper - sieht aus wie das Abzeichen
   daneben, verhaelt sich aber wie ein Knopf. */
.pm-tag-version-btn {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font: inherit;
  cursor: pointer;
}
.pm-tag-version-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
.pm-tag-version-pfeil { display: inline-block; transition: transform var(--dur-fast) var(--ease); }
.pm-tag-version-btn[aria-expanded="true"] .pm-tag-version-pfeil { transform: rotate(90deg); }
/* Ziel-Zeile beim Ziehen einer neuen Datei drüber (Chat-Verlauf: "Für
   Previews möchte ich auch eine Versionierung, vielleicht kann man eine
   neue Version einfach auf die Kachel ziehen?") - deutlich auffälliger als
   der normale Hover, damit klar ist: das ERSETZT diese eine Datei, statt
   nur eine weitere danebenzulegen. */
.pm-file-row.is-version-dragover {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  box-shadow: 0 0 0 2px var(--color-accent) inset;
}
.pm-file-icon { display: flex; flex-shrink: 0; color: var(--color-text-faint); }
.pm-file-icon svg { width: 14px; height: 14px; }
/* Archiviertes Vimeo-Thumbnail statt des generischen Icons (Chat-Verlauf:
   "Kannst du theoretisch auch bei Vimeo manuell gesetzte Thumbnails
   extrahieren und bei uns im Zuordnen-Dialog und später im Previews-Masters
   Bereich integrieren bzw. darstellen?") - 16:9, dezent gerundet, mit
   Platzhalter-Hintergrund während des Ladens. */
.pm-file-thumb {
  flex-shrink: 0;
  width: 40px;
  height: 23px;
  object-fit: cover;
  border-radius: 4px;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
}
/* ~3x größer, nur im Zuordnen-Dialog (Chat-Verlauf: "Kannst du die
   Thumbnails im Zuordnungs-Dialog ca. 3x so groß machen?") - eigene
   Modifier-Klasse statt eines zweiten CSS-Blocks pro Kontext, damit die Basis
   .pm-file-thumb/.pm-file-icon-Regeln (Dateiliste) unangetastet bleiben. */
.pm-file-thumb-lg { width: 120px; height: 68px; border-radius: 6px; }
.pm-file-icon-lg {
  width: 120px;
  height: 68px;
  justify-content: center;
  align-items: center;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: 6px;
}
.pm-file-icon-lg svg { width: 28px; height: 28px; }

/* "Archiviert"-Tab (Chat-Verlauf: "'Archiviert' ... zeigt abgeschlossene
   Produktionen - dort aber Vimeo-ähnlich als Liste mit großem Thumbnail,
   Name, Datum, Länge, Versionen") - eigene, noch größere Thumbnail-Stufe als
   im Zuordnen-Dialog (dort .pm-file-thumb-lg/.pm-file-icon-lg, 120x68), für
   den "Bibliotheks"-Charakter der Liste. */
.pm-archived-thumb-col { width: 168px; }
.pm-archived-thumb-col .pm-file-thumb-lg,
.pm-archived-thumb-col .pm-file-icon-lg { width: 160px; height: 90px; }
/* Thumbnail-Auswahl-Button auf der Archiviert-Kachel (Chat-Verlauf: "Kannst
   du aus den Videos ein Thumbnail generieren? ... eine Auswahl an 10
   Thumbnails ... von denen ich eins als finales Thumbnail auswählen kann?")
   - permanent sichtbares Icon oben rechts auf dem Thumbnail statt eines
   Hover-Only-Overlays (gleiche Begründung wie beim iPhone-Mobile-UX-Audit:
   Sekundäraktionen bleiben konsistent sichtbar statt sich per Hover zu
   verstecken, was auf Touch-Geräten ohnehin nicht funktioniert). */
.pm-archived-thumb-wrap { position: relative; display: inline-block; }
.pm-thumb-pick-btn {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  padding: 0;
}
.pm-thumb-pick-btn svg { width: 14px; height: 14px; }
.pm-thumb-pick-btn:hover { background: rgba(0, 0, 0, 0.75); }

/* Thumbnail-Auswahl-Dialog (openThumbnailPickerModal() in
   previewsmasters.js) - Raster aus THUMBNAIL_CANDIDATE_COUNT Frame-
   Kandidaten, Klick markiert die Auswahl per Rahmen/Häkchen-artigem Akzent
   (gleiche is-selected-Konvention wie .pm-version-badge etc.). */
.pm-thumbnail-picker { min-height: 80px; }
.pm-thumbnail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px;
}
.pm-thumbnail-candidate {
  padding: 0;
  border: 2px solid var(--color-border);
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
  background: var(--color-bg-alt);
  line-height: 0;
}
.pm-thumbnail-candidate img { width: 100%; height: auto; display: block; }
.pm-thumbnail-candidate.is-selected { border-color: var(--color-accent); box-shadow: 0 0 0 2px var(--color-accent); }
.pm-archived-name {
  font-weight: 600;
  font-size: var(--text-body);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  max-width: 100%;
}
/* Bug-Report: "Kannst du die Listen-Ansicht innerhalb der Kacheln bei
   Laufend (also Thumbnail, Titel, darunter, Länge, Größe, Datum) so auch
   bei 'Archiviert' und 'Nicht zugeordnet' anwenden. Betrifft nur die Mobile
   Darstellung." - Subline unter dem Namen (siehe render() der 'name'-Spalte
   in ARCHIVED_COLUMNS/buildUnifiedPendingColumns() in previewsmasters.js),
   nur unterhalb des 720px-Breakpoints sichtbar: auf Desktop zeigen die
   eigenen date-/length-Spalten dieselbe Info bereits an, hier stünde sie
   sonst doppelt. */
.pm-archived-name-sub {
  display: none;
  font-size: var(--text-small);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Einspaltige Liste statt Kachel-Grid (Chat-Verlauf: "Ich denke besser wäre
   eine Liste, mit kleinem Thumbnail, Namen, Größe, Länge, usw. - alle
   untereinander, sortiert nach Uploaddatum.") - vorher ein
   auto-fill-Kachel-Grid (minmax(220px,1fr)), das lange Dateinamen in der
   schmalen Kachel abschnitt; jetzt volle Zeilenbreite je Eintrag. */
.pm-archived-previews {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 2px;
}
.pm-archived-preview-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  cursor: pointer;
  background: var(--color-surface);
  transition: border-color var(--dur-fast) var(--ease);
}
.pm-archived-preview-item:hover { border-color: var(--color-accent); }
/* Thumbnail-Auswahl direkt in der aufgeklappten Previews-Liste (Chat-Verlauf:
   "Unter 'Archiviert' im geöffneten Zustand kann ich aber kein Thumbnail
   mehr hinzufügen, kannst du auch einfach ein 'leeres' Thumbnail zeigen,
   wenn keins hinterlegt ist? Beim Klick drauf, kann man dann eins
   auswählen.") - gestrichelter Platzhalter statt des generischen Icons,
   solange kein eigenes/Vimeo-Thumbnail vorliegt; eigener Klick-Handler
   (stopPropagation, siehe renderArchivedExpanded()) statt des Zeilen-Klicks,
   der sonst das Video öffnen würde. */
.pm-archived-preview-thumb-wrap { flex-shrink: 0; display: flex; }
.pm-archived-preview-thumb-pickable { cursor: pointer; }
.pm-file-thumb-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
  border-style: dashed;
}
.pm-file-thumb-empty svg { width: 12px; height: 12px; }
.pm-archived-preview-thumb-pickable:hover .pm-file-thumb,
.pm-archived-preview-thumb-pickable:hover .pm-file-icon { border-color: var(--color-accent); }
/* "Laufend"-Zeile (fileRow(), Chat-Verlauf: "mach das Thumbnail-Icon rechts
   aus der Gruppe raus und setz es links hin anstatt dem Thumbnail") - hier
   ist .pm-file-thumb-empty selbst ein <button>, kein Wrapper-div wie oben
   bei der Archiviert-Liste, deshalb direkt statt über einen Eltern-Hover. */
.pm-file-thumb-empty:hover { border-color: var(--color-accent); color: var(--color-accent); }
/* Seit dem 28.08.2026 ist auch ein VORHANDENES Thumbnail ein Knopf, der in
   die Auswahl fuehrt (vorher gab es keinen Weg, ein einmal gesetztes Bild
   zu aendern). Der Knopf darf dabei nicht als Knopf auffallen - er soll
   weiterhin wie das Bild aussehen, das er zeigt; nur der Zeiger und der
   Rahmen beim Darueberfahren verraten, dass man klicken kann. */
.pm-file-thumb-set {
  padding: 0;
  overflow: hidden;
  cursor: pointer;
  line-height: 0;
}
.pm-file-thumb-set img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pm-file-thumb-set:hover { border-color: var(--color-accent); }
.pm-file-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Auflösungs-Badges in der "Versionen"-Spalte (Archiviert + Nicht
   zugeordnet, Chat-Verlauf: "Kannst du die Auflösungsversionen als Badges
   machen - klick darauf öffnet diese Version im Player") - jede Badge
   öffnet gezielt GENAU diese Auflösung, unabhängig vom Thumbnail (das
   öffnet immer die größte). */
.pm-version-badges { display: flex; flex-wrap: wrap; gap: 4px; }
.pm-version-badge {
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  padding: 4px 8px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-bg-alt);
  color: var(--color-text);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.pm-version-badge:hover { border-color: var(--color-accent); background: var(--color-surface); }

/* Strukturierter Aufklapp-Inhalt (Chat-Verlauf 2026-08-23, s.o. bei der
   .mobile-secondary-Unterdrückung): "nur darunter kommen dann die
   verfügbaren Auflösungen und nochmal darunter Previews - sofern
   vorhanden" - renderArchivedExpanded() in previewsmasters.js baut genau
   diese Reihenfolge, hier nur die Container-/Überschrift-Optik. */
.pm-archived-expanded {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-2) 2px;
}
.pm-archived-expanded-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.pm-archived-expanded-heading {
  font-size: var(--text-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
}
.pm-archived-expanded-footer {
  display: flex;
  justify-content: flex-end;
}

.pm-file-sub { font-size: var(--text-small); color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pm-file-tags { display: flex; gap: 4px; flex-shrink: 0; }
.pm-tag {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-accent-strong);
  background: var(--color-accent-soft);
  border-radius: 5px;
  padding: 2px 6px;
}
.pm-tag-lang { color: var(--color-text-muted); background: var(--color-bg-alt); }
.pm-tag-version { color: #fff; background: var(--color-text-faint); }
.pm-file-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
/* Chat-Verlauf: "Der Delete-Button braucht auch etwas weniger Platz, weniger
   Padding drumherum." - .btn-ghost.btn-sm ist die geteilte Basisgröße für
   Icon-Buttons überall in der App (32px Klickfläche); hier eigens verkleinert
   (nur der Löschen-Button in der Previews & Masters-Dateizeile), statt die
   globale .btn-sm-Regel anzufassen und damit ungewollt andere Icon-Buttons
   mit zu verkleinern. */
.pm-file-delete-btn { padding: 4px; }
/* Chat-Verlauf: "Können wir Einträge bei Previews & Masters auch im
   Nachhinein noch als File-Transfer 'markieren' bzw. einen externen Link
   dazu anlegen" - Teilen-Button färbt sich ein, sobald für diese Zeile
   bereits ein aktiver Link existiert (gleiche Konvention wie z.B.
   .pm-tab.is-active). */
.pm-file-actions .is-shared { color: var(--color-accent-strong); }

/* "Frühere Versionen"-Bereich - "grid-template-rows: 0fr -> 1fr"-Aufklapp-
   Trick, siehe .datatable-expanded-grid weiter oben für die ausführliche
   Erklärung. */
.pm-archived-wrap:not(.has-items) { display: none; }
.pm-archived-toggle {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  margin-top: 8px;
  padding: 4px 2px;
  border: none;
  background: transparent;
  color: var(--color-text-faint);
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
}
.pm-archived-toggle:hover { color: var(--color-text-muted); }
.pm-archived-toggle::before {
  content: '';
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 4px solid currentColor;
  border-top: 3px solid transparent;
  border-bottom: 3px solid transparent;
  transition: transform var(--dur-fast) var(--ease);
}
.pm-archived-toggle.is-expanded::before { transform: rotate(90deg); }
.pm-archived-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow) var(--ease);
}
.pm-archived-collapse.is-open { grid-template-rows: 1fr; }
.pm-archived-collapse > .pm-file-list { overflow: hidden; min-height: 0; padding-top: 6px; }

/* ---------- Zuordnen-Pop-up für FTP-Incoming-Dateien (Einzel-Datei, siehe
   openSingleAssignModal() in previewsmasters.js) ---------- */
.pm-assign-row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.pm-assign-meta { min-width: 0; flex: 1 1 220px; }
.pm-assign-name { font-weight: 600; font-size: var(--text-body); display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.pm-kind-switch { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-bg-alt); }
.pm-kind-switch-btn {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: var(--text-meta);
  font-weight: 600;
  padding: 5px 10px;
  border-radius: calc(var(--radius-sm) - 1px);
  cursor: pointer;
}
.pm-kind-switch-btn.is-active { background: var(--color-surface); color: var(--color-accent-strong); box-shadow: var(--shadow-sm); }

/* Video-Lightbox (Plyr) - zentriert wie .chat-image-modal, eigene
   max-width statt volle Modal-Breite. */
.modal-backdrop.pm-video-modal { align-items: center; }
/* Die Breite des Player-Fensters folgt dem SEITENVERHAELTNIS des Films
   (28.08.2026, Nutzer: "das Preview-Fenster passt sich nicht der
   tatsaechlichen Filmgroesse an", mit Screenshot: schwarze Balken links und
   rechts).

   Ursache war das Zusammenspiel zweier Regeln: das Video war auf
   width:100% gesetzt, seine Hoehe aber per max-height:72vh gedeckelt. Ist
   ein Film hoeher als dieses Fenster, klemmt der Browser die Hoehe ab -
   die BREITE bleibt aber bei 100 %. Das Bild passt sich per contain ein,
   und links und rechts bleibt Kasten uebrig. Sichtbar wurde das bei allem,
   was nicht ungefaehr 16:9 ist.

   --pm-video-ratio setzt openVideoModalWithSrc() aus videoWidth/videoHeight,
   sobald der Browser die Metadaten des Films kennt. Der Fallback 1.7778
   (16:9) gilt in dem Moment davor und wenn ein Film seine Masse nicht
   preisgibt. Damit ist die Breite nie groesser als das, was bei voller
   erlaubter Hoehe ueberhaupt gefuellt werden kann - der Kasten sitzt am
   Bild, egal ob quer, quadratisch oder hochkant. */
.modal.pm-video-modal {
  max-width: min(900px, 92vw, calc(72vh * var(--pm-video-ratio, 1.7778)));
}
.pm-video-modal-player { border-radius: var(--radius-md); overflow: hidden; background: #000; }

/* ---------- Portfolio (app/js/pages/portfolios.js) ----------
   Chat-Verlauf: umfangreiche Spezifikation für native Portfolios mit
   Teilen-Link + optionalem Passwortschutz - siehe api/portfolios.php +
   portfolio.php (öffentliche Kundenansicht, eigenes <style>, siehe dort). */
/* Chat-Verlauf: "Quickfix: Liste hängt zu weit oben." - renderListView()
   packt pageHeader() + Grid zusammen in EINEN gemeinsamen Wrapper-Div (wegen
   des onImported-Reloads), wodurch das globale ".main > * + *"-Spacing
   (style.css, sorgt sonst überall automatisch für Abstand nach dem
   Seitenkopf) nicht mehr greift - der Wrapper selbst ist ja nur EIN
   Kind von .main, pageHeader()/Grid sind Enkel. Lokale Kopie derselben
   Regel, direkt auf den Wrapper. */
.portfolio-list-view > * + * { margin-top: var(--space-5); }

.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
}
.portfolio-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.portfolio-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.portfolio-card-thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--color-bg-alt);
  display: flex;
  align-items: center;
  justify-content: center;
}
.portfolio-thumb-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.portfolio-thumb-icon { color: var(--color-text-faint); display: inline-flex; }
.portfolio-thumb-icon svg { width: 28px; height: 28px; }
.portfolio-status-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  font-size: var(--text-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 3px 8px;
  border-radius: var(--radius-full);
  background: rgba(20,21,43,0.72);
  color: #fff;
}
.portfolio-status-badge.is-published { background: var(--color-accent); }
.portfolio-card-body { padding: 12px 14px; }
/* Design-System-Cleanup 2026-08-16 (Phase 4a): Entity-Title-Größe an
   restliche App angeglichen (.pm-card-title, Produktions-Titel) - war
   13.5px hardcoded, jetzt --text-entity-title (14px) wie überall sonst. */
.portfolio-card-title { font-weight: 600; font-size: var(--text-entity-title); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.portfolio-card-sub { font-size: var(--text-meta); color: var(--color-text-faint); margin-top: 2px; }

.portfolio-detail-topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4); }
.portfolio-share-row { display: flex; gap: 8px; align-items: center; }
.portfolio-share-row input { flex: 1; padding: 8px 11px; border: 1px solid var(--color-border-strong); border-radius: var(--radius-sm); font-size: 12.5px; background: var(--color-bg-alt); color: var(--color-text-muted); }
.portfolio-password-row { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 12.5px; color: var(--color-text-muted); }
.portfolio-password-row svg { width: 15px; height: 15px; }

.portfolio-videos-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.portfolio-videos-header h3 { margin: 0; font-size: var(--text-entity-title); }

.portfolio-video-list { display: flex; flex-direction: column; gap: 6px; }
.portfolio-video-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}
.portfolio-video-row.is-dragging { opacity: 0.4; }
.portfolio-drag-handle { color: var(--color-text-faint); cursor: grab; display: inline-flex; flex-shrink: 0; }
.portfolio-drag-handle svg { width: 16px; height: 16px; }
.portfolio-video-row .portfolio-thumb-img,
.portfolio-video-row .portfolio-thumb-icon { width: 56px; height: 32px; border-radius: 4px; flex-shrink: 0; }
.portfolio-video-row .portfolio-thumb-icon { display: flex; align-items: center; justify-content: center; background: var(--color-bg-alt); }
.portfolio-video-meta { min-width: 0; flex: 1; }
.portfolio-video-name { font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.portfolio-picker-search {
  position: relative; display: flex; align-items: center; margin-bottom: 12px;
}
.portfolio-picker-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--color-text-faint); pointer-events: none; }
.portfolio-picker-search input {
  width: 100%; padding: 8px 11px 8px 32px; border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm); font-size: 12.5px; background: var(--color-surface); color: var(--color-text);
}
.portfolio-picker-list { display: flex; flex-direction: column; gap: 4px; max-height: 55vh; overflow-y: auto; }
.portfolio-picker-row {
  display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: var(--radius-sm); cursor: pointer;
}
.portfolio-picker-row:hover { background: var(--color-bg-alt); }
.portfolio-picker-row.is-selected { background: var(--color-accent-soft); }
.portfolio-picker-row .portfolio-thumb-img,
.portfolio-picker-row .portfolio-thumb-icon { width: 56px; height: 32px; border-radius: 4px; flex-shrink: 0; }
.portfolio-picker-row .portfolio-thumb-icon { display: flex; align-items: center; justify-content: center; background: var(--color-bg-alt); }

/* Vimeo-Import-Modal (Chat-Verlauf: "Kannst du noch die existierenden
   Portfolios von Vimeo importieren und bei uns integrieren?") - Zeilen sind
   hier NICHT klickbar (nur der Importieren-Button), daher eigener
   Cursor/Hover statt der auswählbaren .portfolio-picker-row-Variante. */
.portfolio-vimeo-row { cursor: default; align-items: flex-start; }
.portfolio-vimeo-row:hover { background: none; }
.portfolio-picker-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.portfolio-picker-row-status { font-size: var(--text-meta); color: var(--color-text-muted); }
.portfolio-vimeo-unmatched { margin: 4px 0 0; padding-left: 16px; font-size: var(--text-meta); color: var(--color-text-muted); }
.portfolio-vimeo-unmatched li { margin-bottom: 2px; display: flex; align-items: center; gap: 4px; }
.portfolio-vimeo-unmatched a { display: inline-flex; color: var(--color-text-faint); }
.portfolio-vimeo-unmatched svg { width: 12px; height: 12px; }

.pm-video-modal-player video { display: block; width: 100%; max-height: 72vh; }

/* Chat-Verlauf: "In der Desktop-Version kann ich Film nicht im Fullscreen
   anschauen, immer nur so wie im Screenshot" - Root Cause: obige
   max-height:72vh galt uneingeschränkt weiter, selbst wenn Plyr per
   Fullscreen-API in den echten Browser-Vollbildmodus wechselt (dabei wird
   Plyrs eigenes .plyr-Element zum Fullscreen-Element, nicht das Modal
   drumherum) - das Video blieb dadurch auf 72% der Bildschirmhöhe
   gedeckelt, ringsherum blieb nur Plyrs schwarzer Fullscreen-Hintergrund
   sichtbar (die großen schwarzen Balken oben/unten im Screenshot). Fix: im
   Fullscreen-Zustand die Höhenbegrenzung aufheben, damit das Video den
   ganzen Bildschirm ausfüllt - sowohl über Plyrs eigene
   .plyr--fullscreen-active-Zustandsklasse als auch über die native
   :fullscreen-Pseudoklasse (inkl. Safari-Präfix), damit es browserübergreifend
   zuverlässig greift. */
.pm-video-modal-player .plyr--fullscreen-active video,
.pm-video-modal-player :fullscreen video,
.pm-video-modal-player :-webkit-full-screen video {
  max-height: 100vh;
  width: auto;
  height: 100%;
}

/* Bugfix (Chat-Verlauf: "Die Anzeige des Client-Dashboard-Freigabe-Icons
   funktioniert nicht korrekt [...] wird nur teilweise angezeigt, aber nicht
   aktiv", NACHTRAG: "es wird immer noch nicht blau, wenn es aktiv ist. Es
   bleibt einfach immer grau. Technisch funktioniert es im Hintergrund, nur
   die Farbe ändert sich nicht."): der vorherige Opacity-Fix
   (.pd-simple-row-note/-delete weiter oben) hat das eigentliche Problem nicht
   getroffen - .pd-simple-row svg bzw. .expand-documents-row svg setzen die
   SVG-Farbe DIREKT auf dem <svg>-Element (var(--color-text-faint)) und
   gewinnen damit unabhängig von der Spezifität der Button-Regel gegen die
   per Vererbung wirkende Farbe von .pd-simple-row-note.is-client-shared /
   .pd-simple-row-delete:hover - eine direkt gesetzte Eigenschaft schlägt
   immer eine nur vererbte, egal wie spezifisch die Vorfahren-Regel ist.
   Exakt derselbe Bug wie zuvor schon auf der Assets-Seite (.documents-row
   svg, siehe Kommentar in assets.js) gefixt, hier aber im Produktions-
   Dashboard (.pd-simple-row) und in documentsSection() (.expand-documents-
   row, productions.js) erneut aufgetreten. Fix bewusst ganz am Dateiende,
   damit er bei gleicher Selektor-Spezifität unabhängig von der Position der
   o.g. Regeln immer zuletzt gilt: color:inherit statt fester Farbe, damit
   Hover-/Aktiv-Zustand des umschließenden Buttons wieder bis zum Icon
   durchschlägt. */
.pd-simple-row-note svg,
.pd-simple-row-delete svg {
  color: inherit;
}

/* ---------- Generische Filterleiste (.filter-bar) ----------
   Eingefuehrt mit dem Aktivitaetsprotokoll (pages/auditlog.js), aber bewusst
   NICHT nach der Seite benannt: dieselbe Leiste aus "Suchfeld + ein paar
   Dropdowns/Datumsfeldern ueber einer Tabelle" gibt es in der App schon
   mehrfach in seitenspezifischen Varianten (.receipt-filter-bar/
   .pm-toolbar, .archive-search-bar; .receipt-filter-selects ist bereits
   darin aufgegangen) - siehe den
   Layout-Consistency-Audit vom 16.08. ("Proposed Shared Patterns"). Das hier
   ist die gemeinsame Variante, auf die diese Einzelfaelle spaeter wandern
   koennen, statt eine vierte danebenzustellen.

   Steht am Dateiende und damit NACH den konsolidierten 640px-Bloecken - das
   ist hier unkritisch, weil alle Selektoren in diesem Abschnitt neu und
   sonst nirgends verwendet sind, es also keinen Spezifitaets-Gleichstand mit
   einer frueheren Regel geben kann (siehe CLAUDE.md, Stolperfalle 2). Neue
   MOBILE-Overrides fuer diese Klassen gehoeren trotzdem in den
   konsolidierten Block, nicht hierher. flex-wrap macht sie fuers Erste ohne
   eigenen Breakpoint schmalscreen-tauglich. */
.filter-bar {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: var(--space-4);
}

/* Alle Felder der Leiste teilen sich EINE Regel - ohne das rendern Browser
   <select>, <input type="date"> und <input type="search"> jeweils mit eigener
   OS-Chrome (andere Hoehe, andere Innenabstaende, anderer Hintergrund), und
   die Leiste sieht auch dann uneinheitlich aus, wenn die CSS-Werte identisch
   sind. Hoehe/box-sizing deshalb explizit, nicht dem Default ueberlassen.
   Werte bewusst wie bei .field weiter oben (gleiche Border, gleicher Radius,
   gleiche Schriftgroesse, --color-surface-2 als Hintergrund), damit die
   Filterleiste nicht zur dritten Feld-Optik neben Formularen und Tabellen
   wird. */
.filter-bar select,
.filter-bar input {
  box-sizing: border-box;
  height: 36px;
  padding: 0 11px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  font-family: inherit;
  line-height: normal;
  background: var(--color-surface-2);
  color: var(--color-text);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

/* Eigener Chevron statt der nativen Pfeil-Chrome - identisch zu .field select
   (siehe dortiger Kommentar zur Begruendung). */
.filter-bar select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  min-width: 170px;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239598ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 15px 15px;
}

/* Datum und Uhrzeit auch hier (03.09.2026). Die Hoehe stimmt in dieser
   Leiste schon, weil sie fest gesetzt ist - die AUSRICHTUNG aber nicht:
   Safari und iOS zentrieren den Wert eines Datumsfeldes, waehrend Suchfeld
   und Auswahlfeld daneben linksbuendig stehen. Auf dem Telefon faellt das
   besonders auf, weil die Leiste dort umbricht und die Felder untereinander
   stehen.
   Dritte Stelle mit derselben Regel (.field, .expand-approval-form, hier) -
   dass es drei sind, liegt daran, dass diese drei Kontexte ihre Felder
   ohnehin je eigen gestalten; e2e/formularfelder.cjs misst alle drei
   nebeneinander, damit die naechste Abweichung auffaellt statt gemeldet zu
   werden. */
.filter-bar input[type='date'],
.filter-bar input[type='time'],
.filter-bar input[type='datetime-local'] {
  -webkit-appearance: none;
  appearance: none;
  text-align: left;
  /* Gleiche Begruendung wie bei .field: WebKits Eigenbreite deckeln. Hier
     wiegt es sogar schwerer - die Filterleiste ist eine Flexbox, und ein
     Feld, das nicht schrumpfen kann, schiebt die ganze Leiste auf. */
  min-width: 0;
  max-width: 100%;
}

.filter-bar input[type='search'] { flex: 1 1 220px; min-width: 200px; }
.filter-bar input::placeholder { color: var(--color-text-faint); opacity: 1; }

/* Safari/Chrome geben Suchfeldern eine eigene Innenabstands- und
   Runden-Chrome plus ein zusaetzliches Loeschkreuz, das neben den uebrigen
   Feldern deplatziert wirkt - abschalten, das Zuruecksetzen laeuft ohnehin
   ueber den "Filter zuruecksetzen"-Button. */
.filter-bar input[type='search'] {
  appearance: none;
  -webkit-appearance: none;
}
.filter-bar input[type='search']::-webkit-search-decoration,
.filter-bar input[type='search']::-webkit-search-cancel-button,
.filter-bar input[type='search']::-webkit-results-button,
.filter-bar input[type='search']::-webkit-results-decoration {
  -webkit-appearance: none;
  appearance: none;
}

/* Datumsfelder sind schmaler als die Dropdowns (der Inhalt ist immer gleich
   lang), brauchen aber dieselbe Hoehe - und in Safari/Chrome ein
   zurechtgestutztes Kalender-Icon, das farblich zum Chevron der Selects
   passt statt in Systemblau danebenzustehen. */
.filter-bar input[type='date'] {
  min-width: 150px;
  padding-right: 8px;
}
.filter-bar input[type='date']::-webkit-calendar-picker-indicator {
  opacity: 0.55;
  cursor: pointer;
}
.filter-bar input[type='date']::-webkit-calendar-picker-indicator:hover { opacity: 1; }
/* iOS/Safari zentrieren den Text in Datumsfeldern vertikal nicht von selbst,
   sobald eine feste Hoehe gesetzt ist. */
.filter-bar input[type='date']::-webkit-date-and-time-value { text-align: left; }

/* Hover-Rahmen aus der frueheren .receipt-filter-selects-Variante uebernommen -
   die Leiste soll beim Ueberfahren zeigen, dass sie bedienbar ist. */
.filter-bar select:hover,
.filter-bar input:hover { border-color: var(--color-accent); }

.filter-bar select:focus,
.filter-bar input:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* Sichtbarer Link innerhalb von Fliesstext/Tabellenzellen. Die Basisregel
   "a { color: inherit; text-decoration: none; }" (ganz oben in dieser Datei)
   macht Anker absichtlich unsichtbar, weil die meisten Links in dieser App
   ganze Karten oder Buttons sind - fuer einen echten Textlink braucht es
   deshalb eine explizite Klasse. */
.link {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link:hover { color: var(--color-accent-hover); }

/* ---------- Aktivitaetsprotokoll (pages/auditlog.js) ---------- */
.audit-change-from { color: var(--color-text-muted); }
.audit-detail-list {
  display: grid;
  grid-template-columns: minmax(120px, auto) 1fr;
  gap: var(--space-2) var(--space-4);
  margin: 0;
}
.audit-detail-list dt {
  font-weight: var(--weight-semibold);
  color: var(--color-text-muted);
  font-size: var(--text-small);
}
.audit-detail-list dd { margin: 0; word-break: break-word; }
.audit-detail-json {
  margin: 0;
  padding: var(--space-2);
  background: var(--color-bg-alt);
  border-radius: var(--radius-sm);
  font-size: var(--text-small);
  white-space: pre-wrap;
  word-break: break-word;
}
.audit-more { display: flex; justify-content: center; margin-top: var(--space-4); }

/* Verlaufs-Abschnitt in Detail-/Expand-Panels (components/auditsection.js) -
   bewusst dieselbe Zeilen-Optik wie die Ansprechpartner-Liste darueber
   (.client-contacts-list), damit das Kunden-Panel nicht zwei verschiedene
   Listenstile uebereinander stapelt. */
.audit-section-list { display: flex; flex-direction: column; gap: var(--space-2); }
.audit-section-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--text-small);
}
.audit-section-action { font-weight: var(--weight-semibold); }
.audit-section-meta { color: var(--color-text-muted); }
.audit-section-detail { color: var(--color-text-muted); }

/* ---------- Anhänge + Emojis im Produktions-Chat-Widget ----------
   Nachgeruestet am 27.08. (Chat-Verlauf: "Können wir im Chat auch Anhänge
   erlauben?" / "Auch Emojis") - das Backend konnte beides seit dem 10.08.,
   nur dieses Widget hatte keine Bedienung dafuer. Darstellung und Picker
   kommen aus denselben Komponenten wie im grossen Chat
   (components/chatattachment.js, components/emojipicker.js), deshalb greifen
   dort auch dieselben .chat-*-Klassen; hier steht nur, was der engere
   Widget-Kontext zusaetzlich braucht. */

/* Das Emoji-Panel oeffnet nach oben (bottom: 100% + 8px, siehe
   .chat-emoji-panel) - im Widget ist darueber die Nachrichtenliste, also
   genau der richtige Platz. Nur die Breite muss sich dem schmaleren Widget
   beugen, sonst ragt sie seitlich heraus. */
.pd-chat-composer .chat-emoji-panel { width: min(280px, calc(100vw - 64px)); }

/* Anhaenge im Widget kleiner als im grossen Chat - die Nachrichtenliste ist
   hier nur ein Ausschnitt und soll nicht von einem einzelnen Bild
   ausgefuellt werden. */
.pd-chat-msg .chat-message-image img { max-height: 140px; }
.pd-chat-msg .chat-message-file { font-size: 12px; padding: 6px 8px; }

/* Datei ueber der Nachrichtenliste - dieselbe Ansprache wie die Dropzones
   der Assets-/Previews-Kacheln daneben. */
.pd-chat-list.is-dropping {
  outline: 2px dashed var(--color-accent);
  outline-offset: -2px;
  border-radius: var(--radius-md);
  background: var(--color-accent-soft);
}

/* Zaehler offener Kundenanmerkungen an einer Preview (Chat-Verlauf
   2026-08-27) - nutzt die vorhandene .pm-tag-Optik, faerbt sich aber ein,
   solange etwas offen ist. Ohne offene Anmerkungen bleibt der Zaehler
   neutral: dass es ueberhaupt Feedback gab, ist Information, aber keine
   Aufforderung. */
.pm-tag.pm-tag-open {
  background: var(--color-danger-soft);
  color: var(--color-danger);
  font-weight: 600;
}

/* "Feedback wird Aufgabe" (28.08.2026): der Zustandsanzeiger an einer
   Kundenanmerkung. Sieht aus wie die uebrigen .pm-tag-Abzeichen, ist aber
   ein <button> - deshalb muessen Rahmen, Hintergrund und Schriftfamilie
   eigens zurueckgesetzt werden, sonst zeichnet der Browser seinen eigenen
   Knopf darunter.

   Kein eigener Farbton: offen = die Akzentfarbe wie jedes andere Abzeichen,
   erledigt setzt die JS-Seite auf Gruen. Ein dritter Farbwert waere hier nur
   Dekoration - der interessante Unterschied ist offen/erledigt. */
button.pm-tag.pm-tag-todo {
  border: 1px solid transparent;
  font-family: inherit;
  cursor: pointer;
  line-height: 1.4;
}
button.pm-tag.pm-tag-todo:hover {
  filter: brightness(0.96);
}

/* Herkunftszeile auf der Aufgaben-Karte: der Wortlaut der Kundenanmerkung,
   aus der die Aufgabe entstanden ist (28.08.2026).

   Bewusst EINE Zeile mit Auslassungspunkten und dem vollen Text im Tooltip.
   Eine Karte, die den kompletten Kundentext zeigt, waere schnell dreimal so
   hoch wie die Aufgabe selbst - und das Board lebt davon, dass viele Karten
   gleichzeitig sichtbar sind. Die Zeile beantwortet "worauf antwortet das?",
   nicht "was stand da genau". */

/* "Offene Kundenanmerkungen" im Freigaben-Widget (28.08.2026).

   Farblich abgesetzt vom Verlauf darunter, weil es eine andere Sorte
   Information ist: der Verlauf ist Vergangenheit, das hier ist eine
   Arbeitsliste. Der linke Balken statt eines vollen Rahmens - drei Karten
   mit Rahmen ineinander sehen nach Fehler aus. */
.pd-open-comments {
  margin: 0 0 var(--space-4);
  padding: 10px 12px 10px 14px;
  border-left: 2.5px solid var(--color-accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--color-accent-soft);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.pd-open-comments-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-accent-strong);
}
.pd-open-comments-head svg { width: 13px; height: 13px; }
.pd-open-comment {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}
.pd-open-comment-meta {
  font-size: 11px;
  color: var(--color-text-muted);
}
/* Hoechstens drei Zeilen je Anmerkung: der volle Wortlaut steht im
   Feedback-Dialog der Fassung, hier geht es nur ums Wiedererkennen. */
.pd-open-comment-body {
  font-size: 13px;
  line-height: 1.45;
  white-space: pre-wrap;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Previews & Masters im Produktions-Dashboard (28.08.2026): das Widget zeigt
   jetzt dieselben zwei .pm-section-Ablageflaechen wie die Seite selbst, nur
   untereinander statt nebeneinander - die Widget-Spalte ist zu schmal fuer
   zwei Kacheln nebeneinander, und untereinander ist ohnehin die vom Nutzer
   gewuenschte Anordnung. Ausser der Stapelung wird hier bewusst NICHTS
   umdefiniert: sobald eine .pm-section-Regel sich aendert, soll das hier
   genauso aussehen. */
.pd-previews-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* ==========================================================
   RESPONSIVE - konsolidiert am 28.08.2026
   ----------------------------------------------------------
   Vorher lagen die @media-Bloecke ueber die ganze Datei verstreut: 18x bei
   640px, 14x bei 900px, 5x bei 720px, dazu Einzelfaelle. CLAUDE.md
   behauptete, sie stuenden bereits gesammelt am Dateiende - das stimmte
   nicht mehr und ist mit dieser Aenderung erst wahr geworden.

   REIHENFOLGE IST HIER BEDEUTUNG, nicht Geschmack: die Bloecke stehen nach
   absteigender Breite. Auf einem 500px-Fenster greifen 900px UND 640px -
   gewinnen soll der engere, also muss er weiter unten stehen. Wer hier
   etwas einfuegt, haelt sich an diese Reihenfolge.

   Drei Bloecke sind NICHT hierher gewandert, weil Selektoren daraus weiter
   oben in der Datei erneut vorkommen und die Kaskade sonst kippt - sie
   stehen an ihrer alten Stelle mit einer Begruendung daneben.

   BREAKPOINTS (Stand 28.08.2026, nachmittags): 640, 720, 900. Vorher sechs -
   860 (.login-*) und 1100 (.pd-grid) sind in den 900er-Block gewandert.

   720 BLEIBT, gegen den urspruenglichen Auftrag "auf 640 und 900
   reduzieren". Nachgemessen an der Produktionstabelle:
     - 720 -> 640: bei 660px Fensterbreite bliebe die Tabelle eine Tabelle,
       die Betraege brechen zweizeilig um. Klare Verschlechterung.
     - 720 -> 900: bei 800px ist die Tabelle heute tadellos gesetzt; sie
       dort in Karten zu verwandeln waere ebenfalls eine Verschlechterung.
   Drei Breakpoints mit klarer Aufgabe schlagen zwei mit einer
   Verschlechterung: 900 = Layout bricht um, 720 = Tabellen werden Karten,
   640 = Telefon.

   ACHTUNG beim Zusammenlegen: die zusammengelegten Regeln muessen in den
   900er-Block HIER UNTEN, nicht in den weiter oben stehen gebliebenen.
   Beim ersten Versuch landeten sie oben und wurden von spaeteren Regeln
   still ueberschrieben - gefunden hat das erst der Stil-Vergleich.
   ========================================================== */

/* ---------- @media (min-width: 901px) ---------- */
/* Schmale, eingeklappte Sidebar (nur Icons) - ausschließlich am Desktop.
   Unter 901px übernimmt weiterhin die bestehende sidebar-open-Overlay-Logik
   (siehe @media (max-width: 900px) weiter unten); ohne dieses min-width
   würde body.sidebar-collapsed dort per Klassen-Spezifität die mobile
   "grid-template-columns: 1fr"-Regel überstimmen und die Sidebar aus
   Versehen auch auf dem Handy als schmale Spalte statt als volles Overlay
   einblenden. */
@media (min-width: 901px) {
  body.sidebar-collapsed .app-shell { grid-template-columns: var(--sidebar-width-collapsed) 1fr var(--rail-width); }
  body.sidebar-collapsed .sidebar { padding-left: var(--space-2); padding-right: var(--space-2); }
  body.sidebar-collapsed .sidebar-brand { justify-content: center; padding-left: 0; padding-right: 0; }
  body.sidebar-collapsed .sidebar-brand-text,
  body.sidebar-collapsed .sidebar-search-label,
  body.sidebar-collapsed .sidebar-search-kbd,
  body.sidebar-collapsed .nav-group-label,
  body.sidebar-collapsed .nav-link-label,
  body.sidebar-collapsed .sidebar-user-block { display: none; }
  /* Eingeklappt zeigt der Toggle-Button selbst nach rechts (chevronRight,
     siehe collapseIcon in layout.js) statt in der Markenzeile zu sitzen -
     wandert dafür unter das Logo, sonst wäre die Markenzeile mit Logo +
     Button bei 68px Breite zu eng. */
  body.sidebar-collapsed .sidebar-brand { flex-direction: column; gap: 6px; }
  body.sidebar-collapsed .sidebar-search-trigger,
  body.sidebar-collapsed .nav-link { justify-content: center; padding-left: 0; padding-right: 0; }
  body.sidebar-collapsed .sidebar-footer { gap: 10px; padding-top: var(--space-4); }
  body.sidebar-collapsed .sidebar-footer-user,
  body.sidebar-collapsed .sidebar-footer-actions { justify-content: center; flex-wrap: wrap; }
}

/* ---------- @media (max-width: 900px) ---------- */
@media (max-width: 900px) {
  .production-tab-label { max-width: 140px; }

  /* Zusammengelegt 28.08.2026 (.pd-grid, war 1100px). */
  .pd-grid { grid-template-columns: 1fr; }

  /* Zusammengelegt 28.08.2026 (.login-*, war 860px). */
  .login-screen { grid-template-columns: 1fr; }
  .login-hero { display: none; }
}
@media (max-width: 900px) {
  /* .main-Padding sinkt hier auf var(--space-4) (16px) - Bleed/Innenabstand
     entsprechend nachgezogen (halbiert), siehe Kommentar oben. */
  .dashboard-page { margin: 0 calc(-1 * var(--space-2)); padding: var(--space-4) calc(var(--space-4) + var(--space-2)); }
}
@media (max-width: 900px) {
  .dashboard-tile.size-sm, .dashboard-tile.size-md { grid-column: span 6; }
}
@media (max-width: 900px) {
  .modal-backdrop.is-wide { padding-left: var(--space-3); }
}
@media (max-width: 900px) {
  /* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Live-Review
     mit Nutzer über Claude-in-Chrome, Fund: "Schriftgröße der Positionen ist
     immer noch riesig") - Root Cause war NICHT Mobile-Safari-Font-Boosting
     (text-size-adjust war bereits korrekt), sondern die weiter oben in
     dieser Datei dokumentierte `input, select, textarea { font-size:16px
     !important }`-Regel (siehe deren Kommentar): die ist absichtlich global
     und !important, damit iOS beim Antippen EINES Formularfelds nicht die
     ganze Seite reinzoomt. .catalog-inline-input ist hier aber kein
     Formularfeld, das nur beim aktiven Ausfüllen sichtbar ist, sondern der
     DAUERHAFTE Anzeige-Zustand einer ganzen Tabellenspalte (Position/
     Einheit) - bekam die 16px daher permanent statt nur beim Fokussieren.
     :not(:focus) lässt den iOS-Zoom-Schutz beim tatsächlichen Antippen/
     Bearbeiten unangetastet, gleicht nur den Ruhezustand wieder an die
     restlichen 13px der Tabelle an. Eigenes !important nötig, um die
     gleich-wichtige Regel oben per höherer Selektor-Spezifität zu schlagen. */
  .catalog-inline-input:not(:focus) { font-size: var(--text-body) !important; }
}
@media (max-width: 900px) {
  .main .kanban-board { grid-template-columns: 1fr; min-height: 0; }
}
@media (max-width: 900px) {
  .cal-body, .cal-body-day { grid-template-columns: 1fr; min-height: 0; }
  .cal-week { grid-template-columns: repeat(7, minmax(130px, 1fr)); overflow-x: auto; }
}
@media (max-width: 900px) {
  .booking-material-layout { grid-template-columns: 1fr; }
}
@media (max-width: 900px) {
  .board-toolbar { flex-wrap: wrap; max-width: calc(100vw - 32px); justify-content: center; }
}
/* Chat-Verlauf: "Dokumente (insbesondere Shotlist) sehen Mobile leider gar
   nicht gut aus ... Schriftgrößen teilweise riesig, Zeilen abgeschnitten -
   kannst du vor allem für Dokumente nochmal eine richtige Mobile-Seite
   aufbauen". Ursache: die App-weite mobile 16px-Zwangsschrift für
   Eingabefelder (siehe ganz oben in dieser Datei, @media(max-width:900px)
   "input, select, textarea { font-size: 16px !important; }", gedacht gegen
   iOS-Auto-Zoom) lief in den nur 32-200px schmalen Grid-Spalten der Tabelle
   über - dadurch wirkten Zellen riesig und Inhalte wurden abgeschnitten.
   Statt die Zwangsschrift für dieses eine Formular zu unterlaufen (das
   würde iOS-Auto-Zoom beim Fokussieren einer Zelle zurückbringen), wird die
   Tabelle unterhalb DERSELBEN Breite komplett zu gestapelten Karten
   umgebaut - jede Zeile eine eigene Karte, jedes Feld untereinander mit
   eigener Beschriftung (data-label, siehe shotRow() in shotdocs.js), exakt
   dasselbe Prinzip wie table.responsive-table an anderer Stelle dieser
   Datei. Gleicher Breakpoint wie die Zwangsschrift-Regel, damit es keinen
   Bereich dazwischen gibt, in dem die alte, zu schmale Spalten-Optik mit
   der bereits vergrößerten Schrift kollidiert. */
@media (max-width: 900px) {
  .shotdoc-badge-row { gap: 4px; }

  /* Spalten-Kopfzeile ergibt in der gestapelten Kartenansicht keinen Sinn
     mehr (jedes Feld trägt jetzt seine eigene Beschriftung) - !important,
     weil ".shotdoc-row:not(.shotdoc-section-row)" unten wegen höherer
     Selektor-Spezifität sonst gewinnen und die (ebenfalls zur Klasse
     .shotdoc-row gehörende) Kopfzeile trotzdem als Karte anzeigen würde. */
  .shotdoc-header-row { display: none !important; }

  /* Die äußere Box (Rahmen/Hintergrund) wandert von der ganzen Tabelle auf
     jede einzelne Zeilen-Karte (siehe unten) - sonst gäbe es eine große Box
     voller kleinerer Boxen ineinander. */
  .shotdoc-table { border: none; background: transparent; border-radius: 0; }

  /* Jede normale Zeile wird zu einer eigenständigen Karte - gleiches
     Grundmuster wie table.responsive-table tr weiter oben in dieser Datei
     (Chat-Verlauf: "sehr stark komprimieren ... Mehr Infos in eine Zeile
     packen und auch die einzelnen Tabellen-Einträge visuell stärker
     voneinander abheben"). Trennzeilen (.shotdoc-section-row) bleiben
     bewusst die schmale, durchgehende Balken-Optik (32px Griff / 1fr Label
     / 36px Löschen) - die ist auch auf dem Handy schon narrow-freundlich
     und muss nicht gestapelt werden. flex-wrap statt block: einzelne
     Zellen (siehe .shotdoc-cell unten) können sich dadurch mehrere pro
     Zeile teilen statt zwingend je eine eigene Zeile zu belegen. Farbiger
     linker Rand + Schatten statt nur dünner Rahmen grenzt jede Karte klar
     von der nächsten ab, kompensiert den jetzt viel knapperen Abstand
     zwischen den Karten (vorher var(--space-3)=12px, jetzt 6px). */
  .shotdoc-row:not(.shotdoc-section-row) {
    display: flex;
    flex-wrap: wrap;
    column-gap: 14px;
    border: 1px solid var(--color-border);
    border-left: 3px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    box-shadow: var(--shadow-sm);
    padding: 8px 10px;
    margin-bottom: 6px;
  }
  .shotdoc-row:not(.shotdoc-section-row):last-child { margin-bottom: 0; }
  .shotdoc-section-row { border-radius: var(--radius-md); margin-bottom: 6px; }

  /* Drag&Drop zum Umsortieren funktioniert auf Touch-Geräten grundsätzlich
     nicht (HTML5 Drag&Drop hat dort keine native Unterstützung) - der
     Griff wäre auf dem Handy ein totes, verwirrendes Bedienelement.
     visibility:hidden statt display:none, damit die Trennzeile (dort noch
     ein echtes 32px/1fr/36px-Grid, siehe .shotdoc-section-row oben) ihre
     Spaltenaufteilung behält, statt durch ein fehlendes Grid-Element zu
     verrutschen. Die Auswahl-Checkbox im Zusammenführen-Modus
     (.shotdoc-select-cell) bleibt dagegen sichtbar - Tippen auf eine
     Checkbox funktioniert auf Touch einwandfrei. */
  .shotdoc-grip { visibility: hidden; height: 0; overflow: hidden; }
  .shotdoc-select-cell { justify-content: flex-start; margin-bottom: var(--space-2); }

  /* Jede Zelle ein eigener Block mit ihrer Spalten-Beschriftung darüber
     (data-label, siehe shotRow() in shotdocs.js) - gleiche ::before-
     Konvention wie table.responsive-table td[data-label] weiter oben.
     Standard: volle Kartenbreite (flex-basis 100%) - sicherer Default für
     Textarea-/Bild-Zellen, die nicht schrumpfen dürfen. Padding/Label
     deutlich knapper als vorher (8px/10.5px/5px -> 5px/10px/3px). */
  .shotdoc-cell {
    flex: 1 1 100%;
    min-width: 0;
    padding: 5px 0;
    border-bottom: 1px solid var(--color-border);
  }
  .shotdoc-cell:last-child { border-bottom: none; }
  /* Kurze, in sich geschlossene Zelltypen dürfen sich mehrere pro Zeile
     teilen (Chat-Verlauf: "Mehr Infos in eine Zeile packen") - erkannt über
     ihr direktes Kind-Element, da normale Text-Zellen (Name, Perspektive,
     Länge, Tag/Zeit) alle als schlichtes <input> ohne Typ-Unterscheidung
     gerendert werden (siehe editableField() in shotdocs.js) und sich daher
     selbst potenziell mehrere pro Zeile teilen - nur Mehrzeilen-Text
     (Beschreibung, <textarea>) und Bild-Zellen bleiben zwingend volle
     Breite. */
  .shotdoc-cell:has(> input) { flex: 1 1 150px; }
  .shotdoc-cell:has(> .shotdoc-shotnr-cell),
  .shotdoc-cell:has(> .shotdoc-done-cell),
  .shotdoc-cell:has(> .shotdoc-progress-status) { flex: 0 1 auto; min-width: 90px; }
  .shotdoc-cell:has(> textarea),
  .shotdoc-cell:has(> .shotdoc-image-cell) { flex: 1 1 100%; }
  .shotdoc-cell[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-faint);
    margin-bottom: 3px;
  }
  /* Die rechte Trennlinie zwischen Spalten ergibt gestapelt keinen Sinn mehr
     (jede Zelle hat stattdessen ihre eigene untere Trennlinie, siehe oben) -
     betrifft sowohl reine Text-/Textarea-Zellen als auch die Zelltypen, die
     ihre eigene rechte Trennlinie mitbringen (Bild/Nr./Erledigt). */
  .shotdoc-cell > input, .shotdoc-cell > textarea,
  .shotdoc-cell .shotdoc-shotnr-cell,
  .shotdoc-cell .shotdoc-image-cell,
  .shotdoc-cell .shotdoc-done-cell {
    border-right: none;
  }

  /* Bild-Zellen (Moodbild, Result etc.) - die festen Desktop-Höhen (128px
     Standard, 108-268px je nach Result-Skalierungsstufe) ergeben auf voller
     Mobile-Kartenbreite ein sehr breites/kurzes Kästchen; object-fit:cover
     schnitt dabei sichtbar viel vom Bild ab (Chat-Verlauf: "wird das Bild
     seltsam beschnitten"). Feste Höhe durch ein Seitenverhältnis ersetzen
     (16:9, passt zu den meisten Renderings/Screenshots hier) + object-fit:
     contain statt cover, damit gar nichts mehr abgeschnitten wird - nur
     noch dezente Ränder bei abweichendem Format. !important, weil das
     bewusst alle Desktop-Höhen-Overrides (u.a. die drei Result-
     Skalierungsstufen, siehe .shotdoc-result-scale-*) unabhängig von deren
     Selektor-Spezifität überschreiben soll. */
  .shotdoc-image-thumb { height: auto !important; aspect-ratio: 16 / 9; }
  .shotdoc-image-thumb img { object-fit: contain; background: var(--color-bg-alt); }

  /* Größere Touch-Ziele für Text-/Textarea-Zellen (Apple/Google empfehlen
     mind. ~44px) - vorher auf feste 36px Mindesthöhe für die kompakte
     Desktop-Tabelle ausgelegt. */
  .shotdoc-row input, .shotdoc-row textarea { min-height: 44px; padding: 10px 4px; }

  /* Trennzeilen (Pause/Schwarzbild etc.) sollen die schmale, durchgehende
     Balken-Optik behalten (siehe Kommentar bei .shotdoc-section-row oben) -
     die 44px-Touch-Ziel-Regel direkt darüber ist für normale mehrzeilige
     Text-Eingabefelder gedacht und blähte die an sich schon kompakte
     Trennzeile unnötig zusätzlich auf (Chat-Verlauf: "Diese Zwischenzeilen
     sind immer noch riesig"). Die 16px-Zwangsschrift gegen iOS-Auto-Zoom
     (ganz oben in dieser Datei) lässt sich nicht umgehen, Innenabstand und
     Mindesthöhe aber gezielt zurücknehmen. */
  .shotdoc-section-label input { min-height: auto; padding: 5px 4px; }

  /* Lösch-Button wird zur vollen, klar beschrifteten "Zeile löschen"-Zeile
     am Kartenende statt eines schmalen, nur icon-großen Buttons am
     Grid-Rand (siehe .shotdoc-row-delete-label im JS - Text nur hier
     sichtbar, siehe display:none-Grundregel weiter oben in dieser Datei).
     :not(.shotdoc-section-row) ist hier entscheidend: der Lösch-Button der
     Trennzeile (Pause/Schwarzbild etc.) trägt dieselbe Klasse, aber KEIN
     eigenes Text-Label (nur ein Icon, siehe shotSectionRow() in shotdocs.js)
     - ohne den Ausschluss bekam er trotzdem die volle mobile Auffüllung
     (10px Padding + 8px margin-top + Rahmen), und weil .shotdoc-row als
     Grid mit align-items:stretch alle Zellen einer Zeile auf die höchste
     Zelle streckt, blähte das die GESAMTE ohnehin schon kompakte Balken-
     Zeile unsichtbar mit auf (Chat-Verlauf: "Wieso sind die Trennzeilen bei
     Renderlisten so riesig? ... Alle anderen Änderungen sehe ich, nur das
     nicht" - das war der eigentliche, bis hierhin übersehene Grund). */
  .shotdoc-row:not(.shotdoc-section-row) .shotdoc-row-delete-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    margin-top: var(--space-2);
    padding: 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    font-size: 12.5px;
    font-weight: 600;
  }
  .shotdoc-row-delete-label { display: inline; }
  /* Trennzeile behält ihren kompakten, nur icon-großen Lösch-Button (kein
     Text-Label vorhanden) - keine zusätzliche mobile Auffüllung nötig. */
  .shotdoc-section-row .shotdoc-row-delete-btn { padding: 7px; }
}
@media (max-width: 900px) {
  .inspiration-masonry { columns: 2 200px; }
}
@media (max-width: 900px) {
  /* Chat: Konversationsliste UND Thread nicht mehr übereinander gestapelt
     (Chat-Verlauf: "50% der Höhe geht drauf für Navigation und das
     Chat-Feld ist ganz weit unten") - stattdessen wie eine typische
     Messenger-App eine Master-Detail-Umschaltung: auf dem Handy ist immer
     nur EINE der beiden Ansichten sichtbar und nutzt dafür die volle Höhe.
     Öffnet man eine Konversation (selectConversation()/selectVirtualDm() in
     chat.js), blendet die is-thread-active-Klasse die Liste aus und zeigt
     den Thread; der neue Zurück-Pfeil im Thread-Header (.chat-back-btn)
     schaltet wieder zurück - siehe mobileThreadOpen/updateMobileView() in
     chat.js. Die feste (statt scrollende) Höhe kommt nicht mehr von .main
     selbst, sondern von .app-shell-col direkt darüber - siehe den eigenen
     Block dazu weiter unten und dessen Begründung. */
  /* padding-bottom erhöht + env(safe-area-inset-bottom) (Chat-Verlauf: "Das
     Senden Feld unten sitzt mir zu weit unten, ganz am Rand") - vorher nur
     var(--space-4)=16px, auf Geräten mit Home-Indicator (iPhone ohne
     Home-Taste) saß der Composer dadurch praktisch direkt auf dem
     System-Balken. max() statt einfacher Addition, damit auf Geräten OHNE
     Safe-Area-Inset (env(...) dann 0px) trotzdem der erhöhte Fixwert gilt,
     statt auf den alten, zu knappen Wert zurückzufallen. */
  /* dvh-Fallback-Muster siehe Kommentar bei .app-shell weiter oben. */
  .main:has(.chat-page) { display: flex; flex-direction: column; overflow: hidden; padding-bottom: max(var(--space-6), calc(var(--space-4) + env(safe-area-inset-bottom))); }
  .chat-page { flex: 1; min-height: 0; display: flex; grid-template-columns: 1fr; height: auto; }
  .chat-sidebar, .chat-thread { display: none; width: 100%; }
  .chat-page:not(.is-thread-active) .chat-sidebar { display: block; flex: 1; min-height: 0; max-height: none; }
  .chat-page.is-thread-active .chat-thread { display: flex; flex: 1; min-height: 0; height: auto; }
  .chat-back-btn { display: flex; }
  .chat-thread-header { display: flex; align-items: center; gap: 10px; }
}

/* ---------- Vollhöhen-Seiten: die Hülle misst, nicht eine Zahl ----------
   Bug-Report 02.09.2026: "Mobile: Ich kann nicht in einem Chat-Fenster
   vertikal scrollen, er scrollt immer nur die gesamte Seite, nicht den
   Chat-Verlauf."

   GEMESSEN auf dem iPhone 16 Pro (402x874), Chat-Seite, ein offener
   Produktions-Tab:
       Seitenhöhe 922px bei 874px Fensterhöhe  ->  die Seite scrollt
       Eingabefeld endet 15px UNTER dem sichtbaren Rand

   Ursache: .main:has(.chat-page) und .main:has(.board-page) trugen beide
   height: calc(100dvh - 60px). Die 60 waren die Höhe der mobilen Topbar -
   aber .app-shell-col enthält seit der Produktions-Tab-Leiste DREI Kinder
   (Topbar, .production-tab-bar, .main). Sobald auch nur ein Produktions-Tab
   offen ist, kommen 47px dazu, die niemand abzieht, und die Seite wird
   höher als das Fenster.

   Warum das ausgerechnet als "der Chat-Verlauf scrollt nicht" auffällt:
   renderMessages() in chat.js scrollt die Liste ans ENDE. Wischt man von
   dort nach oben, hat .chat-messages in dieser Richtung nichts mehr zu
   geben - der Browser reicht die Geste an den nächsten scrollbaren
   Vorfahren weiter, und das ist genau diese um 47px zu hohe Seite. Es sieht
   also exakt so aus, als würde "nur die gesamte Seite" scrollen.

   Der Wert wird deshalb nicht korrigiert, sondern abgeschafft: die Hülle
   selbst bekommt die Fensterhöhe, ihre Kinder teilen sie unter sich auf.
   Damit stimmt die Rechnung auch dann noch, wenn oben irgendwann eine
   vierte Leiste dazukommt. dvh-Fallback-Muster siehe .app-shell oben. */
@media (max-width: 900px) {
  .app-shell-col:has(.chat-page),
  .app-shell-col:has(.board-page) {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
  }
  /* height: auto hebt die 100dvh auf, die .main:has(.board-page) weiter oben
     unbedingt (also auch auf Mobile) setzt - ohne das bliebe .main hier
     volle Fensterhöhe UNTER der Topbar. Die Spezifität reicht dafür aus
     (0,3,1 gegen 0,2,0), unabhängig von der Reihenfolge. */
  .app-shell-col:has(.chat-page) > .main,
  .app-shell-col:has(.board-page) > .main { flex: 1; min-height: 0; height: auto; }
}

/* Telefon quer. GEMESSEN bei 874x390 (iPhone 16 Pro gedreht), Chat offen:
       Seiten-Überschrift  61px
       Thread-Kopfzeile    65px
       Eingabezeile        66px
       ------------------------
       für den Verlauf blieben  32px
   Zweiunddreißig Pixel sind keine Nachrichtenliste. Die Seiten-Überschrift
   ("Chat / Direktnachrichten und Team-Kanal.") ist davon das Entbehrlichste:
   sie wiederholt nur, was die Kopfzeile des Gesprächs ohnehin sagt.
   pointer: coarse grenzt es auf Geräte mit Finger ein - ein flaches, breites
   FENSTER am Rechner ist etwas anderes als ein gedrehtes Telefon, und dort
   soll die Überschrift stehen bleiben. */
@media (max-height: 500px) and (pointer: coarse) {
  .main:has(.chat-page) > .page-header { display: none; }
  /* Der Push-Hinweis ist eine einmalige Nachfrage, kein Inhalt - im
     Hochformat steht er weiterhin, quer würde er ein Fünftel des Verlaufs
     kosten. */
  .main:has(.chat-page) > .chat-push-banner { display: none; }
  /* Die 20px Abstand zwischen den Blöcken (.main > * + *) und das großzügige
     Innenpolster sind für eine ruhig scrollende Seite gedacht. Hier teilen
     sich drei Elemente 329px - da zählt jede Zeile. */
  .main:has(.chat-page) { padding: var(--space-2) var(--space-2) max(var(--space-2), env(safe-area-inset-bottom)); }
  .main:has(.chat-page) > * + * { margin-top: var(--space-2); }
}

/* ---------- @media (max-width: 720px) ---------- */
/* Responsive tables: on narrow screens, rows become stacked "cards". Applied
   via the `.responsive-table` class on the <table> element; each <td> needs
   a matching `data-label` attribute (columns with no header, e.g. actions,
   can omit it).
   Chat-Verlauf: "Ich bin mit den Mobile Ansichten nicht glücklich, diese
   brauchen viel zu viel Platz ... jede Spalte wird jetzt eine eigene
   Zeile ... Mehr Infos in eine Zeile packen und auch die einzelnen
   Tabellen-Einträge visuell stärker voneinander abheben. Aktuell wirkt es
   wie eine ewig lange Liste ohne Unterbrechung." / "Bei Kunden ... extrem
   viel ungenutzter Weißraum ... Das könnte alles in maximal zwei Zeilen und
   insgesamt 80% weniger vertikalen Platz." - komplett umgebaut: statt JEDE
   Spalte als eigener voller Block mit Label darüber (nur EIN Feld pro
   Zeile, riesige Gesamthöhe), ist die ganze Karte jetzt ein
   flex-wrap-Container - jedes Feld ein kompaktes "LABEL: Wert"-Inline-Paar,
   mehrere pro Zeile, automatisch umbrechend je nach verfügbarer Breite.
   Statt der vorherigen dünnen Trennlinien UNTER jedem einzelnen Feld (die
   den "eine endlose Liste"-Eindruck erzeugt haben) grenzt sich jetzt jede
   ganze Karte über Rahmen + Schatten + farbigen linken Rand klar von der
   nächsten ab. */
@media (max-width: 720px) {
  .card:has(table.responsive-table) { padding: var(--space-2); }
  /* Live-Review-Fund 2026-08-16: "Zu fakturieren" (generisches .card,
     padding:var(--space-4)=16px auf Mobile) und "Produktionsliste"
     (.card:has(table.responsive-table), padding:var(--space-2)=8px, siehe
     Regel direkt oben - absichtlich knapper, weil jede Tabellenzeile schon
     ihr eigenes gerahmtes Karten-Padding mitbringt) saßen direkt
     untereinander mit sichtbar unterschiedlichem Innenabstand. Beide treffen
     sich hier auf einen gemeinsamen Mittelwert (var(--space-3)=12px) -
     div.card (Element+Klasse) statt nur .card, damit diese Regel die
     :has()-Selektorspezifität von oben zuverlässig schlägt. */
  div.card.to-invoice-card,
  div.card.productions-table-card { padding: var(--space-3); }
  table.responsive-table thead { display: none; }
  table.responsive-table, table.responsive-table tbody { display: block; width: 100%; }
  /* :not(.datatable-expanded-row) - die Aufklapp-Detailzeile (siehe
     datatable.js toggleRow()) ist KEIN Feld-Set, sondern ein einzelnes,
     volle Breite beanspruchendes Panel - die muss beim flex-wrap-Umbau
     unten außen vor bleiben (siehe eigene Regel dafür weiter unten),
     sonst würde ihr einziges <td> nur noch content-breit statt 100%. */
  table.responsive-table tr:not(.datatable-expanded-row) {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 12px;
    row-gap: 2px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-left: 3px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-sm);
    margin-bottom: 6px;
    padding: 8px 10px;
  }
  table.responsive-table tbody tr:last-child { margin-bottom: 0; }
  table.responsive-table tbody tr:hover { background: var(--color-surface); }
  /* Gleiche Spezifität wie die tr-Grundregel oben (table+tr Typ, .responsive-
     table+.is-expanded Klasse) - gewinnt per Quellreihenfolge und stellt die
     App-weite "aufgeklappte Zeile hervorgehoben" Optik (.is-expanded, siehe
     weiter oben in dieser Datei) wieder her, die die Grundregel sonst
     überschreiben würde. */
  table.responsive-table tr.is-expanded { background: var(--color-accent-soft); }
  /* Aufklapp-Detailzeile bleibt volle Breite (siehe Kommentar oben) - direkt
     unter der zugehörigen Feld-Karte, ohne eigenen Kartenrahmen (die
     Feld-Karte bekommt beim Öffnen bereits .is-expanded, siehe style.css
     weiter unten für die Akzentfarbe). */
  table.responsive-table tr.datatable-expanded-row { display: block; width: 100%; }
  table.responsive-table td.datatable-expanded-cell { display: block; width: 100%; }
  /* Aktionen-Spalte (Bearbeiten/Löschen) hat kein Label - statt am
     Zeilenende zu kleben (Standard-Flex-Reihenfolge), schiebt sie sich per
     auto-Margin an den rechten Kartenrand, bricht bei Platzmangel sauber in
     eine eigene Zeile um. Sticky-Positionierung (Desktop-Horizontalscroll,
     siehe .col-actions oben) ergibt im Karten-Layout keinen Sinn mehr. */
  table.responsive-table td.col-actions {
    margin-left: auto;
    position: static;
  }
  table.responsive-table td {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    border-bottom: none;
    padding: 1px 0;
    /* Design-System-Cleanup 2026-08-16 (Phase 3): unverändert 13px, nur
       token-isiert (== --text-body, der häufigste Einzelwert im gesamten
       Stylesheet). */
    font-size: var(--text-body);
    line-height: 1.35;
  }
  table.responsive-table td.text-right { text-align: left; }
  /* mobileTruncate (siehe datatable.js buildRow()) - flex-basis:0 statt
     "auto" ist hier der entscheidende Kniff: Flexbox trifft seine Umbruch-
     Entscheidung anhand der HYPOTHETISCHEN Größe (bei "auto" die volle
     Inhaltsbreite), nicht anhand der tatsächlich nach dem Schrumpfen
     verbleibenden Breite - mit Basis 0 zählt die Spalte für den Umbruch wie
     "leer" und bleibt garantiert in derselben Zeile wie z.B. das Kürzel,
     wächst danach aber per flex-grow wieder auf die verfügbare Restbreite. */
  table.responsive-table td.mobile-truncate {
    flex: 1 1 0%;
    min-width: 70px;
    overflow: hidden;
  }
  table.responsive-table td.mobile-truncate .cell-value-truncate {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    vertical-align: bottom;
  }
  /* mobileSeparator (siehe datatable.js buildRow(), clients.js "Titel") -
     dezenter Trennpunkt statt der sonst üblichen TITEL:-Beschriftung
     (Chat-Verlauf: "Bei Kunden 'Kürzel' und 'Titel' weg einfach nur das
     Kürzel anzeigen, dann einen Trenner und rechts daneben Titel"). Eigenes
     ::before statt des Label-::before oben, weil data-label für diese Spalte
     bewusst leer bleibt (mobileNoLabel). */
  table.responsive-table td.mobile-separator::before {
    content: '·';
    color: var(--color-text-faint);
    font-weight: 400;
  }
  /* :not([data-label=""]) - sonst würde die label-lose Aktionen-/Chevron-
     Spalte einen nackten, bedeutungslosen Doppelpunkt zeigen. */
  /* Design-System-Cleanup 2026-08-16 (Phase 3): font-size auf --text-small
     token-isiert (10px -> 10.5px, kaum wahrnehmbarer Unterschied). Bleibt
     bewusst bei 700/uppercase statt --weight-semibold - das ist genau der
     "kleine Utility Label"-Fall, den Prompt Abschnitt 11 als legitime
     Uppercase-Nutzung nennt (Feldbezeichner wie "KUNDE:"/"DATUM:" vor einem
     Wert), kein Fall von Übergewichtung. Welche Spalten dieses Label
     überhaupt noch zeigen (statt mobileNoLabel zu setzen), wird pro Screen
     in Phase 4 entschieden - siehe Audit Abschnitt "Produktionen". */
  table.responsive-table td[data-label]:not([data-label=""])::before {
    content: attr(data-label) ':';
    font-size: var(--text-small);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--color-text-faint);
  }

  /* Priority+-Muster (Chat-Verlauf: "recherchieren wie mobile Ansichten von
     umfangreichen Tabellen normalerweise dargestellt werden ... ggfs auch
     per Accordion nur das aufklappen, was der User sehen möchte. Ansonsten
     nur minimale Infos sichtbar.") - alle Spalten außer den 1-2 wichtigsten
     (siehe .mobile-secondary/mobileOnlyToggle in datatable.js) sind
     zunächst versteckt und klappen erst beim Antippen der Karte auf. Der
     Mobile-only-Pfeil (siehe .mobile-only-toggle oben) wird hier wieder
     sichtbar - er ist die einzige Instanz, wo eine Karte OHNE eigenes
     renderExpanded-Panel trotzdem einen Aufklapp-Pfeil braucht. */
  table.responsive-table td.mobile-only-toggle,
  table.responsive-table th.mobile-only-toggle {
    display: inline-flex;
    align-items: center;
  }
  /* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Chat-
     Verlauf: "global bei allen aufklappbaren Listen den kleinen Pfeil
     entfernen und stattdessen den Content deutlich weiter nach links
     schieben") - betrifft beide Aufklapp-Varianten gleichermaßen, da
     mobileOnlyToggle-Zeilen die Klasse .mobile-only-toggle IMMER zusätzlich
     zu .expand-col tragen (siehe datatable.js displayColumns), ein
     Selektor auf .expand-col deckt daher automatisch auch den Fall oben ab.
     Kommt bewusst NACH der .mobile-only-toggle-Regel oben (gleiche
     Spezifität, Quellreihenfolge entscheidet), sonst würde die Pfeil-Spalte
     dort weiterhin reaktiviert. display:none statt nur visuell
     ausblenden - die Spalte fällt dadurch komplett aus dem flex-wrap-Fluss
     heraus, wodurch der nächste Inhalt (Titel) automatisch samt seinem
     column-gap nach links nachrückt, ganz ohne manuelle Margin-Korrektur.
     Aufklappen selbst bleibt unverändert erreichbar - der Klick-Handler
     hängt an der ganzen <tr>, nie am Pfeil-Icon selbst (siehe
     is-expandable-row/is-mobile-toggle-row in datatable.js). */
  table.responsive-table td.expand-col { display: none; }
  table.responsive-table td.mobile-secondary { display: none; }
  table.responsive-table tr.is-expanded td.mobile-secondary,
  table.responsive-table tr.is-mobile-open td.mobile-secondary {
    display: inline-flex;
  }
  table.responsive-table tr.is-mobile-toggle-row:hover,
  table.responsive-table tr.is-mobile-open { background: var(--color-accent-soft); }

  /* Batch 2 (Produktionsliste, iPhone Mobile-UX 2026-08-16, Finding 2.3) -
     sichtbare Reihenfolge auf der Karte war bisher Status->Nr.->Titel (reine
     Deklarationsreihenfolge der columns[] in productions.js), Titel ist aber
     das Feld, an dem man eine Produktion eigentlich erkennt und sollte
     zuerst stehen. `order` auf den einzelnen <td>s (die <tr> ist hier
     bereits display:flex, siehe Regel oben) ändert nur die VISUELLE
     Reihenfolge, nicht die DOM-/columns[]-Reihenfolge - die Desktop-Tabelle
     (kein flex-Layout) bleibt dadurch unverändert in ihrer bisherigen
     Spaltenreihenfolge. Alle übrigen Spalten bekommen ebenfalls eine
     explizite (höhere) Order, statt nur die ersten vier zu setzen - sonst
     würden sie beim Aufklappen der Karte (Standard-order:0) plötzlich VOR
     Titel/Status/Nr. springen. Scoped auf .productions-table-card, damit
     keine andere dataTable()-Seite mit zufällig gleichem data-label-Text
     mit betroffen ist.
     Design-System-Cleanup 2026-08-16 (Phase 4a): Titel/Status/Nr. haben jetzt
     mobileNoLabel:true (Entity First, kein "TITEL:"/"STATUS:"/"NR.:"-Präfix
     mehr), data-label ist dadurch "" statt Text - Selektoren daher auf die
     dafür eingeführten className-Hooks (.col-title/.col-status/.col-number)
     umgestellt, gleiche order-Werte wie zuvor.
     Design-System-Cleanup 2026-08-16 (Phase 4c-Nachbesserung, Bug-Report:
     "zentrale Informationen fehlen in der Listenansicht") - Kunde ist jetzt
     ebenfalls mobilePrimary (siehe columns[] oben) und direkt nach dem
     Titel einsortiert, alle nachfolgenden Order-Werte um 1 verschoben.
     Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Live-Review
     mit Nutzer über Claude-in-Chrome, Fund: "Product Rendering"-Karte sah
     komplett anders aus als kurztitelige Karten) - Root Cause: reines
     flex-wrap ohne erzwungene Zeilenumbrüche lässt den Browser Titel/Kunde/
     Status/Nr. je nach Textlänge JEDER Karte unterschiedlich auf Zeilen
     verteilen (der Status-Block kollidierte dabei u.a. mit dem für Desktop
     gedachten `.status-cell { min-width:180px }`). flex-basis:100% auf
     Titel/Status/Aktionen erzwingt jetzt für jede Karte identisch: Titel
     IMMER allein auf Zeile 1 (Ellipsis statt Umbruch, siehe .mobile-truncate),
     Kunde+Nr. IMMER zusammen auf Zeile 2 (order 2/3, kein eigenes
     flex-basis:100%, daher nebeneinander), Status IMMER allein auf Zeile 3,
     Aktionen IMMER allein auf Zeile 4 (rechtsbündig, siehe justify-content
     unten). Nr. dafür von order 4 auf order 3 vorgezogen (direkt hinter
     Kunde), Status von order 3 auf order 4 nachgerückt. */
  /* Live-Review-Fund 2026-08-16: Nr.+Kunde (in genau dieser Reihenfolge, wie
     zuvor eingerichtet) bilden die Kopfzeile oben links, Titel folgt darunter
     links als eigene volle Zeile. Status (Progress-Bar+Badge, siehe
     .status-cell weiter unten) und Aktionen teilen sich danach die letzte
     Zeile ("Icons unten rechts auf derselben Höhe wie Status-Badge") - Status
     bekommt dafür flex:1 statt flex-basis:100% (wächst auf die verfügbare
     Restbreite, ähnlich .to-invoice-main weiter oben), Aktionen bleiben
     content-breit und rechtsbündig, align-self:flex-end richtet sie am
     unteren Rand des (nun mehrzeiligen) Status-Blocks aus - exakt auf Höhe
     des Badges, das dort ja am Ende sitzt. */
  .productions-table-card td.col-number { order: 1; }
  .productions-table-card td.col-client { order: 2; }
  /* .col-title.mobile-truncate (statt nur .col-title) ist hier WICHTIG:
     table.responsive-table td.mobile-truncate (siehe oben, "Umbruch-
     Entscheidung anhand hypothetischer Größe") setzt flex-basis:0% mit
     höherer Spezifität (2 Elemente + 2 Klassen) als die reine .col-title-
     Regel (1 Element + 2 Klassen) - dadurch gewann bisher lautlos die
     0%-Basis und der Titel brach NICHT mehr in eine eigene Zeile um,
     obwohl hier flex-basis:100% stand (Bug-Report: "Da stimmt überhaupt
     nichts, das ist alles in einer Zeile."). Die zusätzliche
     .mobile-truncate-Klasse im Selektor hebt die Spezifität (2 Elemente +
     3 Klassen) über die konkurrierende Regel. */
  .productions-table-card td.col-title.mobile-truncate { order: 3; flex-basis: 100%; }
  .productions-table-card td.col-status { order: 4; flex: 1 1 0%; min-width: 0; }
  .productions-table-card td.col-actions { order: 5; align-self: flex-end; justify-content: flex-end; }
  /* Chat-Verlauf: "Kannst bei Produktionen (aufgeklappt) den Lead in die
     letzte Zeile setzen, zu Agentur, Musik, usw.?" - Lead saß bisher direkt
     neben Status/Aktionen (order 6, gleiche Zeile wie die Progress-Bar);
     jetzt ganz ans Ende sortiert, reiht sich dadurch (natürlicher
     Flex-Wrap, keine eigene flex-basis:100% nötig, genau wie bei Agentur/
     Musik/LTO) in deren letzte Zeile ein statt weiter oben zu stehen. */
  .productions-table-card td[data-label="Status setzen"] { order: 6; }
  .productions-table-card td[data-label="Agentur"] { order: 7; }
  .productions-table-card td[data-label="Musik"] { order: 8; }
  .productions-table-card td[data-label="LTO"] { order: 9; }
  .productions-table-card td[data-label="Lead"] { order: 10; }
  /* Titel ist der Haupt-Identifikator der Karte (siehe Kommentar oben) -
     deutlich stärker hervorgehoben als der Fließtext der übrigen Felder. */
  .productions-table-card td.col-title .cell-value-truncate { font-size: 15px; font-weight: var(--weight-semibold); }
  /* .status-cell (Badge+Progress-Bar+Stepper) ist als Kind eines
     inline-flex-<td> (siehe table.responsive-table td oben) standardmäßig
     nur content-breit, nicht automatisch so breit wie die eigene
     Zellen-Breite selbst - width:100% lässt die Progress-Bar (die selbst
     schon width:100% relativ zu .status-cell hat) dadurch auf die volle
     Breite der Status-Spalte strecken (Chat: "Kannst du die Progress-Bar
     auf die gesamte Breite der Kachel strecken?" - seit die Spalte sich die
     letzte Zeile mit den Aktions-Icons teilt, ist das die Kachelbreite
     abzüglich der Icons, nicht mehr die komplette Kachel). */
  /* Chat: "Die beiden Pfeile unter der Progress-Bar weg, den Status-Badge
     zentriert." + Folge-Fund "Status-Badge unter die Progress-Bar." - der
     Vor/Zurück-Stepper (Statuswechsel per Klick) ist auf der Kachel
     überflüssig, dafür gibt es bereits "Status setzen" (separates Feld,
     siehe order 7 oben); auf Mobile daher ausgeblendet. .status-cell selbst
     (bisher reiner Block, DOM-Reihenfolge Badge->Bar->Stepper, siehe
     statusCell() in productions.js) wird hier zum eigenen, vertikalen
     Flex-Container, damit sich die Bar per `order` VOR den Badge schieben
     lässt, ohne die DOM-/Desktop-Reihenfolge anzufassen. Badge sitzt in
     .status-top bisher an Position space-between (sinnvoll, wenn dort noch
     ein zweites Element wie der Prozentwert stünde) - center, da aktuell
     einziges Kind. Alles nur auf der Kachel (Desktop-Tabelle unverändert),
     daher scoped auf .productions-table-card. */
  .productions-table-card td.col-status .status-cell {
    width: 100%;
    display: flex;
    flex-direction: column;
  }
  .productions-table-card td.col-status .progress-track { order: 1; }
  .productions-table-card td.col-status .status-top { order: 2; justify-content: flex-start; margin-bottom: 0; margin-top: 7px; }
  .productions-table-card td.col-status .status-stepper { display: none; }

  /* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Live-Review
     mit Nutzer über Claude-in-Chrome, Fund: Status-Badge in der Rechnungs-
     wesen-Liste landete je nach Titel-/Kundenlänge mal rechts in derselben
     Zeile wie "Produktion", mal linksbündig in einer eigenen Zeile) - gleiche
     Lösung wie bei .productions-table-card: flex-basis:100% erzwingt für
     jede Karte identisch eine eigene Status-Zeile, justify-content:flex-end
     hält sie dabei konsistent rechtsbündig (Chat-Verlauf: "Status-Button
     sollte einheitlich positioniert sein, z.B. ganz rechts"). */
  .invoicing-table-card td.col-status { flex-basis: 100%; justify-content: flex-end; }
  /* Global-Layout-Consistency 2026-08-16 (Phase 1 Audit-Folgefix, Live-
     Review mit Nutzer über Claude-in-Chrome, Fund: Zeilen ohne PDF ("—"
     statt Icon-Button) rissen komplett anders um als Zeilen mit PDF) - Root
     Cause: der Icon-Button (.btn-ghost.btn-sm, ~32px inkl. Klickfläche) und
     der reine Platzhalter-Text "—" (nur wenige Pixel breit) beanspruchten
     unterschiedlich viel Platz auf derselben Flex-Zeile wie "Produktion"
     daneben - je nach Variante blieb für den Titel mal mehr, mal zu wenig
     Raum übrig, sodass er teils auf derselben Zeile wie das Icon startete,
     teils in eine komplett eigene, linksbündige Zeile fiel. Feste Breite auf
     .col-pdf macht beide Varianten identisch breit (der Button behält seine
     eigene Innen-Größe, das Icon zentriert sich per justify-content einfach
     in der schmaleren Spalte), .col-production bekommt den dadurch fest
     kalkulierbaren Rest der Zeile per flex-grow + min-width:0 (letzteres
     nötig, damit die zweizeilige Titel/Kunde-Box darin auch wirklich
     schrumpfen statt überlaufen kann). */
  .invoicing-table-card td.col-pdf { flex: 0 0 28px; width: 28px; min-width: 28px; justify-content: center; }
  .invoicing-table-card td.col-production { flex: 1 1 0%; min-width: 0; }
}
/* Chat-Verlauf 2026-08-23: "Das 'Produktion zuordnen' Popup bitte fullscreen
   machen, nicht nur klein in der Mitte." - bewusst nur unterhalb 720px echte
   Vollbild-Darstellung (siehe openReassignModal() in previewsmasters.js);
   am Desktop bleibt das bisherige kleine, zentrierte Modal unangetastet,
   dort war "klein in der Mitte" nie das Problem.

   02.09.2026: die Bedingung um den Telefon-quer-Fall erweitert. Ein iPhone
   16 Pro ist gedreht 874px BREIT - der Block griff dort also nicht, und das
   Spielfenster stand mit 483px Hoehe auf einem 390px hohen Bildschirm
   (gemessen). Auf dem Schirm sah man davon nur, dass unten etwas fehlt.
   Dieselbe Bedingung wie beim Spiele-Block weiter unten: schmal ODER
   flach-und-mit-dem-Finger. */
@media (max-width: 720px), (max-height: 500px) and (pointer: coarse) {
  .modal-backdrop.is-fullscreen { padding: 0; align-items: stretch; }
  .modal.is-fullscreen {
    max-width: none;
    width: 100%;
    height: 100%;
    max-height: none;
    border-radius: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    padding: 0;
  }
  .modal.is-fullscreen .modal-header {
    padding: var(--space-5) var(--space-5) var(--space-3);
    margin-bottom: 0;
    flex: 0 0 auto;
  }
  .modal.is-fullscreen .modal-footer {
    padding: var(--space-3) var(--space-5) var(--space-5);
    flex: 0 0 auto;
  }
  .modal.is-fullscreen .pm-picker {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0 var(--space-5);
    display: flex;
    flex-direction: column;
  }
}
@media (max-width: 720px) {
  .game-modal-body { flex-direction: column; }
  /* Die Seitenspalte hat seit dem 01.09.2026 eine eigene Flaeche statt einer
     Trennlinie links - die Regeln, die diese Linie zurueckgenommen haben,
     sind damit gegenstandslos. Auf schmal steht sie einfach darunter. */
  .game-leaderboard-panel { width: 100%; }
  .game-leaderboard-list { max-height: 200px; }
}

/* Ab hier gilt: Telefon. Und zwar in BEIDEN Lagen.
   ------------------------------------------------------------------
   Die Bedingung stand bis zum 02.09.2026 nur auf max-width: 720px. Ein
   iPhone 16 Pro quer ist 874px breit - der ganze Vollbild-Block darunter
   griff dort also nicht, und das Spiel fiel auf das Rechner-Layout zurueck.
   GEMESSEN bei 874x390:
       Spielfeld Snake   242x242   (28% der Breite, 62% der Hoehe)
       Bestenliste       363px breit, daneben  -> 42% des Bildschirms
   Quer war damit SCHLECHTER als hoch (348x348). Genau das Gegenteil dessen,
   was Querformat bringen soll.

   (max-height: 500px) and (pointer: coarse) ist die zweite Haelfte: flach
   UND mit dem Finger bedient. Ein flaches Fenster am Rechner ist etwas
   anderes als ein gedrehtes Telefon und bleibt aussen vor. */
@media (max-width: 720px), (max-height: 500px) and (pointer: coarse) {
  /* ---------- Spiele im Vollbild (02.09.2026) ----------
     Nutzer: "alle Spiele Mobile nicht gut aus, das Spielfeld ist winzig" ->
     "Games sollten Fullscreen laufen, mit kleinem x zum schließen".

     GEMESSEN auf einem iPhone 16 Pro (402x874) vor der Aenderung:
       Snake, Pong, Racer u.a.   308 px breit  (77% der Bildschirmbreite)
       2048, Minesweeper, Schach 174 px breit  (43%)  <- das "winzig"
     Danach: alle zwoelf bei 348 px (87%).

     Das Vollbild selbst kommt von .is-fullscreen (weiter oben), das es fuer
     die Produktionsauswahl schon gab. Das Spiel nimmt beide Klassen. */
  .modal.is-game { max-width: none; }
  .modal.is-game .game-modal-body {
    /* DAS ist der eigentliche Hebel. .game-modal-body traegt
       align-items:flex-start - am Rechner richtig (Feld und Bestenliste
       stehen oben buendig nebeneinander), auf schmal aber verheerend: die
       Richtung kippt auf column, die Querachse ist damit die WAAGERECHTE,
       und flex-start heisst dort "so schmal wie der Inhalt". Der Rahmen um
       das Spielfeld blieb deshalb bei 212 px stehen, egal was darin steht. */
    align-items: stretch;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    gap: var(--space-4);
    padding: 0 var(--space-4) var(--space-4);
  }
  /* Der Rahmen um das Feld darf im Vollbild bis an die Kanten - die 18px
     Polsterung und der 22px-Radius sind fuer ein Fenster gedacht, nicht
     fuer einen Bildschirm. */
  .modal.is-game .game-canvas-wrap {
    padding: 10px;
    border-radius: 14px;
  }
  /* Die drei Raster: volle Breite, keine automatische Aussenkante - im
     Vollbild gibt es ohnehin nichts mehr zu zentrieren. */
  .modal.is-game .game-canvas-wrap > .g2048-grid,
  .modal.is-game .game-canvas-wrap > .mine-grid,
  .modal.is-game .game-canvas-wrap > .chess-board {
    width: 100%;
    max-width: none;
    margin-inline: 0;
  }
  /* Das Canvas ebenso: --spiel-breite wird in games.js jetzt aus 74% der
     Fensterhoehe gerechnet, auf einem Telefon begrenzt danach die Breite. */
  .modal.is-game .game-canvas { max-width: 100%; }
  /* Kopfzeile schlank halten, damit vom Bildschirm moeglichst viel Spiel
     uebrig bleibt. Das Schliesskreuz ist das, was modal.js ohnehin baut -
     hier nur kleiner und rund, damit es im Vollbild nicht wie ein
     Formularknopf wirkt. */
  .modal.is-game .modal-header {
    padding: var(--space-3) var(--space-4);
    align-items: center;
  }
  .modal.is-game .modal-header h2 { font-size: 16px; }
  .modal.is-game .modal-header button {
    width: 32px;
    height: 32px;
    padding: 0;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
  }

  /* ---------- Bestenliste: waehrend des Spiels weg ----------
     Nutzer 02.09.2026: "Während des Spiels weg, danach zeigen". Auf dem
     Telefon ist sie der groesste einzelne Platzfresser (quer 363px = 42%
     des Bildschirms), und waehrend man spielt schaut ohnehin niemand hin.

     Der Schalter ist die Klasse .spielt am Fensterkoerper: sie steht
     genau dann NICHT da, wenn ein .game-overlay ueber dem Feld liegt - vor
     dem Start, nach Game Over, und bei Schach auch waehrend der
     Modus-Auswahl. Gesetzt wird sie von einem MutationObserver in
     openGameModal(); der ausfuehrliche Grund, warum das NICHT als
     :has(.game-overlay) direkt hier in CSS steht, steht dort. Kurz: der
     berechnete Wert blieb nach dem Entfernen des Overlays stehen. */
  .modal.is-game .game-leaderboard-panel { display: none; }
  .modal.is-game .game-modal-body:not(.spielt) .game-leaderboard-panel { display: block; }
}

/* ---------- Telefon quer: die Hoehe ist das knappe Gut ----------
   Hochkant begrenzt die Breite das Spielfeld, quer die Hoehe - deshalb
   raeumt dieser Block genau die waagerechten Baender ab, die hochkant
   unproblematisch sind. */
@media (max-height: 500px) and (pointer: coarse) {
  /* Quer stehen Feld und (falls sichtbar) Bestenliste nebeneinander: eine
     Liste UNTER dem Feld wuerde bei 390px Gesamthoehe beides erdruecken. */
  .modal.is-game .game-modal-body { flex-direction: row; align-items: stretch; }
  .modal.is-game .game-leaderboard-panel { width: 240px; overflow-y: auto; }
  /* Die Statistikzeile ("Dein Bestwert" / "Spitzenreiter") steht in der
     Bestenliste ohnehin nochmal - quer kostet sie nur Hoehe. */
  .modal.is-game .game-stats-bar { display: none; }
  /* Kopfzeile raus (Nutzer 02.09.2026: "Im Querformat brauchen wir oben die
     Headline mit dem Spielenamen nicht. Das nimmt nur Platz weg."). Welches
     Spiel laeuft, sieht man am Spiel - und quer sind 40px Kopfzeile rund
     ein Zehntel des Bildschirms.
     Sie wird NICHT ausgeblendet, sondern aus dem Fluss genommen: das
     Schliesskreuz darin ist der einzige Weg aus dem Vollbild heraus (Escape
     hat auf einem Telefon keine Taste). Es schwebt jetzt als runder Knopf
     ueber der oberen rechten Ecke - dunkel hinterlegt, damit es auch auf
     einem hellen Spielfeld zu sehen bleibt. */
  .modal.is-game .modal-header {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 60;
    margin: 0;
    padding: 6px;
  }
  .modal.is-game .modal-header h2 { display: none; }
  .modal.is-game .modal-header button {
    background: rgba(12, 14, 20, 0.55);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid rgba(255, 255, 255, 0.22);
    color: rgba(255, 255, 255, 0.9);
  }
  .modal.is-game .game-modal-body { padding: var(--space-2); gap: var(--space-2); }
  .modal.is-game .game-canvas-wrap { padding: 6px; border-radius: 10px; }

  /* Und die Umkehrung der wichtigsten Hochkant-Regel. Hochkant heisst
     "nimm die ganze Breite" - genau richtig, wenn die Breite das knappe Gut
     ist. Quer ist sie es nicht: bei 874px Breite wurde Snake damit 516px
     hoch auf einem 390px hohen Bildschirm, also groesser als der Schirm.
     Gemessen beim ersten Durchlauf dieses Umbaus; auf dem Bildschirm sah
     man davon nur, dass unten etwas fehlt.
     Quer fuehrt deshalb wieder die HOEHE. Beim Canvas uebernimmt das
     --spiel-breite (games.js rechnet es aus dem Seitenverhaeltnis und der
     Fensterhoehe abzueglich der 100px, die Kopfzeile, Polsterung und
     Punktestand-Zeile brauchen - die Aufstellung steht dort). Die drei
     quadratischen Bretter rechnen dieselben 66px hier in CSS ab; sie
     brauchen bewusst keine gemessene Variable, weil eine Messung an dieser
     Stelle schon einmal in Safari und Chromium auseinandergelaufen ist
     (siehe die dritte Fassung der Brett-Regel weiter oben). */
  .modal.is-game .game-canvas { max-width: min(100%, var(--spiel-breite, 100%)); }
  .modal.is-game .game-canvas-wrap > .g2048-grid,
  .modal.is-game .game-canvas-wrap > .mine-grid,
  .modal.is-game .game-canvas-wrap > .chess-board {
    width: min(100%, calc(100vh - 66px));
    /* margin-inline: auto ist hier RICHTIG, obwohl es am 01.09. der Fehler
       war: dort fehlte die ausdrueckliche Breite, und ohne die faellt ein
       Flex-Kind mit automatischer Aussenkante auf den Mindestinhalt
       zusammen. Mit width daneben zentriert es nur noch. */
    margin-inline: auto;
  }

  /* Eine sichtbare Kante ums Spielfeld (Nutzer: "man erkennt das Spielfeld
     nicht, es hat nach links und rechts visuell keine klare Grenze").
     Grund: kein Spiel hat das Seitenverhaeltnis eines liegenden Telefons
     (2,24) - das breiteste ist die Kampfarena mit 1,78, die Bretter sind
     quadratisch. Links und rechts bleibt also immer Platz, und der ist
     bisher genauso dunkel wie das Feld selbst: der Rahmen und die
     Zeichenflaeche gehen ineinander ueber.
     Die Kante nimmt die Farbe DIESES Spiels (--spiel, gesetzt auf
     .game-modal-body) - ein Strich in Grau waere an dieser Stelle wieder
     nur eine Linie mehr. */
  .modal.is-game .game-canvas,
  .modal.is-game .game-canvas-wrap > .g2048-grid,
  .modal.is-game .game-canvas-wrap > .mine-grid,
  .modal.is-game .game-canvas-wrap > .chess-board {
    box-shadow:
      0 0 0 1.5px color-mix(in srgb, var(--spiel, #22b8d1) 60%, transparent),
      0 0 34px -12px var(--spiel, #22b8d1);
  }

  /* ---------- Daumentasten quer: in die Seitenraender, nicht aufs Feld ----
     Nutzer 02.09.2026: "Außerdem überdeckt der Daumen oft Steuerelemente,
     die man dann natürlich auch nicht sieht. Wie gehen wir damit um?"

     Die Tasten lagen bis dahin UEBER der unteren Kante des Spielfelds - und
     genau dort steht bei Invaders das eigene Schiff, beim Platformer der
     Boden. Halbdurchsichtige Tasten helfen dagegen nichts: der Daumen
     selbst ist nicht durchsichtig.

     Der Platz dafuer ist schon da und lag bisher brach. Kein Spiel hat das
     Seitenverhaeltnis eines liegenden Telefons (2,24); das breiteste Feld
     ist die Kampfarena mit 1,78, die Bretter sind quadratisch. GEMESSEN bei
     874x390 blieben links und rechts zusammen:
         Kampfarena 270px · Pong 327px · Invaders 467px · Snake 521px
     Zwei Tasten nebeneinander brauchen 112px, ein Rand also 132 mit Luft -
     und weil quer die HOEHE das Feld begrenzt, kostet dieser reservierte
     Rand bei allen vier Spielen mit Tasten genau nichts: das breiteste von
     ihnen ist mit 574px immer noch schmaler als die verbleibenden 592.

     Deshalb auch nie mehr als zwei Tasten in einer Reihe (siehe die
     Aufteilung in daumenPad()-Aufrufen in games.js) - drei waeren 172px und
     passten nicht mehr neben das Feld. */
  .modal.is-game .game-pad {
    position: absolute;
    inset: 0;
    z-index: 30;
    align-items: center;
    padding: 0 8px;
  }
  /* Der reservierte Rand. Zwei Bedingungen daran, beide gemessen noetig:

     .hat-pad setzt daumenPad() nur, wenn wirklich Tasten entstanden sind -
     die acht Spiele ohne behalten die volle Breite.

     .spielt heisst "es wird gerade gespielt". Ohne diese Bedingung
     galt der Rand auch VOR dem Start, wo die Bestenliste noch danebensteht
     und nur 610px zur Verfuegung stehen: die Kampfarena fiel dort auf 187px
     Hoehe zusammen. Die Tasten sind vor dem Start ohnehin sinnlos, deshalb
     sind sie dann auch ausgeblendet - und der Rand wird erst gebraucht,
     wenn sie erscheinen. */
  .modal.is-game .game-modal-body:not(.spielt) .game-pad { display: none; }
  .modal.is-game .game-modal-body.spielt .game-canvas-wrap.hat-pad > .game-canvas,
  .modal.is-game .game-modal-body.spielt .game-canvas-wrap.hat-pad > .g2048-grid,
  .modal.is-game .game-modal-body.spielt .game-canvas-wrap.hat-pad > .mine-grid,
  .modal.is-game .game-modal-body.spielt .game-canvas-wrap.hat-pad > .chess-board {
    max-width: min(var(--spiel-breite, 100%), calc(100% - 264px));
  }
  .modal.is-game .game-modal-body.spielt .game-canvas-wrap.hat-pad > .game-score { padding-inline: 132px; }
}
@media (max-width: 720px) {
  /* Belege-Archiv-Karten: Bearbeiten/Löschen erst nach dem Aufklappen
     sichtbar (siehe mobileActionsSecondary in receipts.js) - Chat-Verlauf:
     "Die beiden Icons rechts unten bitte in kleine dezente Kreise setzen".
     Eigene Klasse (.receipt-row-actions) statt der geteilten .table-actions
     selbst, damit nur diese eine Seite betroffen ist. Volle Breite +
     flex-end schiebt die Kreise an den rechten Kartenrand ("unten rechts").
  */
  .receipt-row-actions { width: 100%; justify-content: flex-end; gap: 8px; }
  .receipt-row-actions button {
    width: 30px;
    height: 30px;
    padding: 0;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--color-bg-alt);
  }
  /* Vier aufgeklappte Sekundärzeilen (Kategorie / Zahlungsart+Privat /
     Hochgeladen von+Bezahlt / Status+Actions) unterschiedlich hoch, je
     nachdem ob eine Zeile ein Badge/Button (siehe .badge, .receipt-row-
     actions oben) oder nur Fließtext enthält (Chat-Verlauf: "Du hast beim
     aufklappen jetzt vier Zeilen ... kannst du diese vier Zeilen generell
     gleich hoch machen?"). Jede td.mobile-secondary ist ein eigenes
     Flex-Item im umbrechenden tr-Container (siehe Grundregel weiter oben) -
     eine identische min-height auf ALLEN Items sorgt automatisch dafür, dass
     jede umgebrochene Zeile exakt gleich hoch wird, unabhängig davon wie
     viele/welche Felder darauf landen. 30px = Höhe der Kreis-Icons oben
     (die von Natur aus tallest Element), damit die Actions-Zeile nicht
     zusätzlich wächst. Scoped auf .receipt-table (siehe tableClassName in
     datatable.js), betrifft also nur die Belege-Karten. */
  table.receipt-table td.mobile-secondary {
    align-items: center;
    min-height: 30px;
  }
}
@media (max-width: 720px) {
  table.responsive-table.pm-archived-table td .pm-archived-name-sub { display: block; }
  /* Kompaktes Thumbnail wie im "Laufend"-Tab (.pm-file-thumb, 40x23) statt
     der breiten Desktop-Größe (.pm-archived-thumb-col .pm-file-thumb-lg,
     160x90) - auf der schmalen Karte würde das große Thumbnail sonst den
     halben verfügbaren Platz beanspruchen. Nur innerhalb .pm-archived-table
     (Archiviert/Extern/Nicht zugeordnet), andere responsive-table-Listen
     bleiben unberührt. */
  table.responsive-table.pm-archived-table td.pm-archived-thumb-col {
    width: auto;
    flex: 0 0 auto;
  }
  table.responsive-table.pm-archived-table td.pm-archived-thumb-col .pm-file-thumb-lg,
  table.responsive-table.pm-archived-table td.pm-archived-thumb-col .pm-file-icon-lg {
    width: 44px;
    height: 44px;
  }
  table.responsive-table.pm-archived-table td.mobile-truncate {
    min-width: 0;
  }
  /* Bug-Report: "Das Thumbnail-auswählen Icon bitte mobil gar nicht
     anzeigen." - auf der kleinen 44x44-Kachel ist der Overlay-Button kaum
     präzise treffbar; Klick auf das Thumbnail öffnet ohnehin bereits direkt
     das Video (siehe buildThumbEl() in previewsmasters.js), Thumbnail
     auswählen bleibt auf Mobile ausschließlich über die Desktop-Ansicht
     bzw. das Aufklappen der Produktion erreichbar. */
  table.responsive-table.pm-archived-table .pm-thumb-pick-btn { display: none; }
  /* Chat-Verlauf 2026-08-23: "Die zugeklappte Darstellung ist super, aber
     aufgeklappt komplettes Chaos." - Root Cause: das generische .mobile-
     secondary-Aufdeckverhalten (siehe datatable.js) zeigt beim Aufklappen
     ZUSÄTZLICH zum eigenen renderExpanded()-Panel (siehe
     renderArchivedExpanded() in previewsmasters.js) automatisch jede
     Nicht-Primär-Spalte (Datum/Länge/Versionen/Zuordnen) - hier für
     .pm-archived-table gezielt unterdrückt, damit nur noch das strukturierte
     Panel (Auflösungen, dann Previews, dann optional der Zuordnen-Button)
     zu sehen ist. */
  table.responsive-table.pm-archived-table tr.is-expanded td.mobile-secondary {
    display: none;
  }
}

/* ---------- @media (max-width: 640px) ---------- */
/* Bug-Report: "Ne das ist überhaupt nicht gut so [...] Lass uns einfach nur
   eine schmale Leiste oben machen mit Status, Produktionsname und
   Ampelanzeige. Übersicht komplett weg, ggfs. nur per Button einblendbar."
   - nach Mockup-Vorabstimmung (Option A) ersetzt auf Mobile eine komplett
   eigenständige, schlanke Leiste (.pd-header-mobile-wrap, siehe render() in
   productiondashboard.js) den ganzen Desktop-Hero-Header samt Ring/Badge/
   Übersicht-Karte - kein Versuch mehr, denselben Header responsiv
   zusammenzustauchen (das war der vorherige, gescheiterte Anlauf). Desktop
   bleibt davon komplett unberührt. */
@media (max-width: 640px) {
  .pd-header.has-hero { display: none; }
}
@media (max-width: 640px) {
  .pd-header-mobile-wrap {
    display: block;
    background: var(--color-surface-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-4);
    overflow: hidden;
  }
}
@media (max-width: 640px) {
  .pd-widget-grip, .pd-widget-hide-btn, .pd-hidden-tray { display: none; }
}
@media (max-width: 640px) {
  .pd-activity-layout { flex-direction: column; }
}
@media (max-width: 640px) {
  .datatable-search { max-width: none; }
}
@media (max-width: 640px) {
  /* .main-Padding sinkt hier weiter auf var(--space-3) (12px). */
  .dashboard-page { margin: 0 -6px; padding: var(--space-4) 22px; }
}
@media (max-width: 640px) {
  .dashboard-top-row { flex-direction: column; }
}
@media (max-width: 640px) {
  .dashboard-tile.size-sm, .dashboard-tile.size-md, .dashboard-tile.size-lg { grid-column: span 12; }
}
/* Bug-Report (Screenshot): "In der Mobile Ansicht liegen die Ausgehend/
   Eingehend-Felder über der Headline 'Offene Rechnungen'." Ursache: der
   Switch ist per position:absolute oben rechts über den Titel gelegt (siehe
   .dashboard-invoices-switch oben), der Titel reserviert dafür per
   padding-right:150px Platz (siehe .dashboard-invoices-card .card-header h2
   oben) - auf schmalen Viewports ist die Pillen-Gruppe mit zweistelligen
   Zählern ("Ausgehend (10)") aber breiter als die reservierten 150px und
   überlappt den Titel. Ab 640px deshalb zurück in den normalen Fluss (unter
   den Titel, volle Breite, Buttons gleich breit) statt absolut positioniert. */
@media (max-width: 640px) {
  .dashboard-invoices-card .card-header h2 { padding-right: 0; }
  .dashboard-invoices-switch {
    position: static;
    display: flex;
    width: 100%;
    margin-top: var(--space-2);
  }
  .dashboard-invoices-switch-btn { flex: 1; text-align: center; }

  /* Wunsch: "Mach doch Mobile einfach Pfeile in rot/grün - statt den
     langen Wörtern" - Label ("Ausgehend (10)"/"Eingehend (2)") ausblenden,
     stattdessen den Pfeil (icons.js arrowUpRight/arrowDownRight, siehe
     dashboard.js) zeigen. Ausgehend = Rechnungen, die WIR stellen (Geld
     kommt rein) -> grün; Eingehend = Belege, die WIR noch bezahlen müssen
     (Geld geht raus) -> rot, jeweils per data-dir-Attribut am Button
     unterschieden (siehe dashboard.js). Aktiver Tab zusätzlich per
     Hintergrund-Tint hervorgehoben, damit der Zustand auch ohne den
     Zähler-Text erkennbar bleibt. */
  .dashboard-invoices-switch-label { display: none; }
  .dashboard-invoices-switch-icon { display: flex; align-items: center; justify-content: center; }
  .dashboard-invoices-switch-btn[data-dir="out"] { color: var(--color-success); }
  .dashboard-invoices-switch-btn[data-dir="in"] { color: var(--color-danger); }
  .dashboard-invoices-switch-btn[data-dir="out"].is-active { background: var(--color-success-soft); color: var(--color-success); }
  .dashboard-invoices-switch-btn[data-dir="in"].is-active { background: var(--color-danger-soft); color: var(--color-danger); }
}
@media (max-width: 640px) {
  .briefing-grid { grid-template-columns: 1fr; gap: var(--space-4); }
}
/* Bug-Report per Screenshot: "die ganzen Inputs unten einheitlich stylen"
   - auf Mobile wirkte das freie Wrap (Art+Datum in einer Zeile, Uhrzeit+
   Notiz+Button uneinheitlich groß in der nächsten) unruhig, weil jedes
   Feld nur auf seine eigene Content-Breite kommt. Ein Feld pro Zeile,
   alle exakt gleich breit (100%), macht die Reihenfolge klar lesbar statt
   optisch zufällig zusammengewürfelt. */
@media (max-width: 640px) {
  .expand-approval-form { flex-direction: column; align-items: stretch; }
  .expand-approval-form select,
  .expand-approval-form input[type="date"],
  .expand-approval-form input[type="time"],
  .expand-approval-form input[type="text"] {
    width: 100%;
    max-width: none;
  }
  .expand-approval-form button { width: 100%; justify-content: center; }
}
@media (max-width: 640px) {
  .inventory-report-row { flex-direction: column; align-items: flex-start; gap: 6px; }
  .inventory-report-actions .btn, .inventory-report-actions .btn-ghost { flex: 1; justify-content: center; }
  .inventory-count-item-name { font-size: 18px; }
  .inventory-qty-number { font-size: 44px; }
}
@media (max-width: 640px) {
  .quicksearch-backdrop { padding-top: 8vh; }
  /* Die frühere Regel hier blendete versehentlich das Falsche aus -
     :not(:first-child) ließ genau den Tastatur-spezifischsten Hinweis
     ("↑↓ navigieren") übrig und versteckte nur die anderen beiden. Die
     eigentliche Korrektur (kompletter Hint-Bereich weg, siehe
     .quicksearch-hint { display:none } in der konsolidierten
     @640px-Sektion weiter unten, UX-Audit 2026-08-16 Finding 2.8) ersetzt
     sie vollständig. */
}
@media (max-width: 640px) {
  .receipt-modal-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  /* Fortsetzung des Fixes oben: die Tabs dürfen jetzt nicht mehr intern
     umbrechen, müssen aber trotzdem irgendwohin, wenn sie zusammen breiter
     als der Bildschirm sind - statt die Leiste selbst zu schrumpfen
     (Original-Ursache) scrollt sie jetzt horizontal, die Pille bleibt dabei
     optisch exakt gleich (kein sichtbarer Scrollbalken). */
  .pm-tabs, .booking-view-tabs {
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .pm-tabs::-webkit-scrollbar, .booking-view-tabs::-webkit-scrollbar { display: none; }
}
/* Chat-Verlauf: "Mobile bitte nur eine Kachel pro Zeile bei 'Laufend'" - die
   feste 3-Spalten-Regel oben ist nur für Desktop gedacht; auf schmalen
   Bildschirmen wären drei Produktions-Kacheln nebeneinander viel zu schmal
   gequetscht. Muss NACH der Basis-Regel stehen (gleiche Selektor-
   Spezifität), sonst würde die spätere, media-lose Regel oben sie wieder
   überschreiben. */
@media (max-width: 640px) {
  .pm-grid { grid-template-columns: 1fr; }
}
/* Gemeinsamer Mobile-Fallback für alle "Karten-Grid"-Sicherheits-Kandidaten
   (siehe .entity-card-grid weiter oben bei .card/.grid-4 für die volle
   Herleitung) - EINE Stelle statt einer Einzel-Regel pro Seite, damit ein
   künftiges neues Karten-Grid diesen Mobile-Fix automatisch mitbekommt,
   sobald es die Klasse .entity-card-grid trägt, statt ihn (wie zuvor bei
   .documents-grid) versehentlich zu vergessen. */
@media (max-width: 640px) {
  .entity-card-grid { grid-template-columns: 1fr; }
}

/* ---------- @media (prefers-reduced-motion: reduce) ---------- */
/* Wer Bewegung abbestellt hat, bekommt keine. Das ist eine Systemeinstellung
   und keine Geschmacksfrage - fuer manche Menschen loesen bewegte
   Oberflaechen Uebelkeit oder Schwindel aus.
  
   Bewusst pauschal statt Regel fuer Regel: bis zum 28.08.2026 standen hier
   drei Einzelfaelle, waehrend der Rest der Datei munter weiter animierte.
   Eine Ausnahmeliste, die man bei jeder neuen Animation pflegen muesste,
   ist am Tag nach der Einfuehrung unvollstaendig. 0.01ms statt 0 (und nicht
   "none"), damit Ereignisse wie animationend/transitionend weiterhin
   feuern - Code, der darauf wartet, wuerde sonst haengen bleiben. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .main.page-enter { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .line-chart-path { animation: none; stroke-dashoffset: 0; }
  .line-chart-area { animation: none; opacity: 1; }
  .line-chart-dot { animation: none; opacity: 1; }
  .donut-chart-segment { transition: none var(--ease); }
}

/* ---- Widget "Kunden-Board": Besuchsauswertung (30.08.2026) ------------
   Konzept-Schritt 9. Die Gestaltung folgt einer Regel: der SATZ oben ist
   die Aussage, alles darunter ist der Beleg. Deshalb trägt nur er Farbe,
   und die Zeilen darunter bleiben ruhige Textzeilen - ein Widget aus lauter
   farbigen Zählern liest niemand, weil nichts mehr heraussticht. */
.pd-visit-status {
  font-size: 13px;
  line-height: 1.45;
  font-weight: 500;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  color: var(--color-text);
}
.pd-visit-status.is-warn {
  background: color-mix(in srgb, var(--color-warning) 14%, transparent);
  color: var(--color-text);
  box-shadow: inset 2px 0 0 0 var(--color-warning);
}
.pd-visit-status.is-ok { color: var(--color-text-muted); }
.pd-visit-status.is-idle { color: var(--color-text-muted); }

.pd-visit-list { margin-top: 10px; display: flex; flex-direction: column; }
.pd-visit {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 7px 0;
  border-top: 1px solid var(--color-border);
}
.pd-visit:first-child { border-top: 0; }
.pd-visit.is-off { opacity: 0.5; }
/* Ein Punkt statt eines Zustandswortes je Zeile: "war da" / "war noch nicht
   da" ist die einzige binäre Auskunft hier, und die verträgt ein Symbol. */
.pd-visit-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 6px; flex: none; }
.pd-visit-dot.is-seen { background: var(--color-success); }
.pd-visit-dot.is-unseen { background: var(--color-border-strong, var(--color-border)); box-shadow: inset 0 0 0 1px var(--color-text-faint); }
.pd-visit-dot.is-off { background: var(--color-text-faint); }
.pd-visit-main { flex: 1; min-width: 0; }
.pd-visit-name { font-size: 13px; font-weight: 500; }
.pd-visit-sub { font-size: 12px; color: var(--color-text-faint); margin-top: 1px; }
.pd-visit-anon { margin-top: 9px; font-size: 12px; color: var(--color-text-faint); }

.pd-visit-log { margin-top: 10px; }
.pd-visit-log > summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--color-text-muted);
  list-style: none;
}
.pd-visit-log > summary::-webkit-details-marker { display: none; }
.pd-visit-log > summary::before { content: '▸ '; }
.pd-visit-log[open] > summary::before { content: '▾ '; }
.pd-visit-log-list { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
.pd-visit-log-row { display: flex; gap: 9px; font-size: 12px; color: var(--color-text-muted); line-height: 1.45; }
/* Feste Breite für die Zeitangabe, damit die Sätze eine gemeinsame Kante
   haben - sonst franst die Liste rechts der Zeitspalte aus. */
.pd-visit-log-when { flex: none; width: 76px; color: var(--color-text-faint); }

/* ---- Darstellung: Hell / Dunkel / Automatisch (30.08.2026) -----------
   Nutzer: "Kann der Dark-Mode automatisch switchen? Angepasst ans System?"
   Dieselbe Popover-Mechanik wie die Farbwahl daneben, nur untereinander
   statt nebeneinander - hier stehen Wörter, keine Farbkreise. */
/* RECHTS ausgerichtet, nicht links (30.08.2026). Der Knopf sitzt am rechten
   Ende der Fusszeile, und die Seitenleiste hat overflow: hidden - links
   ausgerichtet (wie das Farbwahl-Popover, das schmal ist und links sitzt)
   ragte dieses hier ueber die Kante und wurde abgeschnitten: aus "Dunkel"
   wurde "Dunke". Genau der Fehler, vor dem der Kommentar am Spotify-Popover
   in layout.js warnt. */
.theme-popover { flex-direction: column; gap: 2px; padding: 6px; min-width: 156px; left: auto; right: 0; transform-origin: bottom right; }
.theme-mode-btn {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.theme-mode-btn:hover { background: var(--color-bg-alt); }
.theme-mode-btn.is-on { background: var(--color-accent-soft); color: var(--color-accent-strong); }
.theme-mode-btn svg { width: 15px; height: 15px; flex: none; }
/* Der Punkt sagt: hier entscheidet der Rechner, nicht du. Ohne ihn sähe der
   Automatik-Modus aus wie eine feste Wahl. */
.btn-logout.is-system { position: relative; }
.btn-logout.is-system::after {
  content: '';
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-accent);
}

/* ---- Vermietung: Auslastung und Buchungen in einer Karte (30.08.2026) ----
   Nutzer: "diese beiden kombinieren, ich will Platz effizienter nutzen."
   Der Donut liess ueber und unter sich Luft, die Buchungsliste darunter -
   nebeneinander fuellt das eine das Loch des anderen. */
.rental-split {
  display: flex;
  gap: var(--space-6);
  flex: 1;
  flex-wrap: wrap;
  align-items: flex-start;
}
.rental-utilization {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex: 1 1 320px;
  min-width: 0;
}
.rental-figures { flex: 1; min-width: 150px; }
/* Eine sichtbare Kante zwischen den beiden Haelften, damit es eine Karte mit
   zwei Gedanken bleibt und nicht ein Brei. Faellt weg, sobald umbrochen
   wird - dort trennt der Abstand. */
.rental-upcoming {
  flex: 1 1 300px;
  min-width: 0;
  padding-left: var(--space-6);
  border-left: 1px solid var(--color-border);
}
@media (max-width: 900px) {
  .rental-upcoming { padding-left: 0; border-left: 0; }
}
.rental-bookings { display: flex; flex-direction: column; }
