/* Gemeinsame Basis für alle Seiten der AI Console.
   Enthält nur, was auf allen Seiten identisch war: Farbpalette, Reset,
   Schriftart und die reservierte Scrollleiste. Seitenspezifisches Layout
   und semantische Zusatzfarben (--green/--red/--yellow) bleiben je Seite.
   Beide Namensschemata (--bg-panel und --panel usw.) sind als Alias
   hinterlegt, damit keine bestehende Regel umgeschrieben werden muss. */

/* ==================== Emoji-Schrift (konsolenweit) ====================
 *
 * WARUM (Olha-Feedback 27./29./30.07.2026, "some emojis are just white
 * frames", "the same emoji shows correctly on the phone but appears as an
 * empty white-framed box on the computer"): Emojis sind Textzeichen. Fehlt
 * ein Zeichen in der Emoji-Schrift des Zielsystems, zeigt der Browser ein
 * leeres Kaestchen. Windows 10 blieb bei Segoe UI Emoji auf dem Stand
 * Emoji 12.0, und Laenderflaggen fehlen dort in JEDER Version. Das trifft
 * auch EMPFANGENE Zeichen: was eine Kollegin vom Handy schickt, bleibt am
 * Rechner ein Kasten - mit Auswahllisten ist das nicht heilbar, nur mit
 * einer mitgelieferten Schrift.
 *
 * WARUM HIER UND NICHT NUR IM CHAT (Nachtrag Philipp 30.07.2026): der
 * Nachtlauf hatte die Schrift in einer eigenen frontend/emoji.css, die nur
 * chat.html einband - base.css war fuer einen anderen Agenten gesperrt.
 * Emojis stehen aber auch in STEX News, Tickets, Erfolgs Tracker und
 * Nachrichten; dort blieben die Kaestchen. base.css laedt JEDE Seite,
 * deshalb steht die Schrift jetzt hier und emoji.css ist entfallen. Weitere
 * Seiten mussten dafuer NICHT angefasst werden - genau das war der Punkt.
 *
 * WARUM SCHRIFT UND NICHT <img> (Olhas Vorschlag "image-based emojis"):
 * eine Schrift wirkt an JEDER Anzeigestelle zugleich - Eingabefeld, Picker,
 * Verlauf, Zitate, Vorschautexte, Kanalnamen, Statuszeilen - ohne dass
 * irgendein Text umgeschrieben wird. Eine Zeichen-zu-Bild-Ersetzung muesste
 * dagegen an jeder dieser Stellen laufen und wuerde beim naechsten neuen
 * Anzeigeort vergessen (bekannte Fehlerklasse in diesem Projekt); sie muesste
 * ausserdem HTML in escMd()-Ausgaben einfuegen, also genau dort, wo das
 * Escaping schuetzt.
 *
 * HERKUNFT UND LIZENZ: Noto Color Emoji, Version 2.051 (Upstream-Build
 * noto-emoji:20250818), SIL Open Font License 1.1 - Lizenztext liegt
 * daneben in fonts/LICENSE-OFL-1.1.txt, Bezugsquellen und Pruefsummen in
 * fonts/HERKUNFT.txt. Die OFL erlaubt Weitergabe und Einbettung und
 * verlangt KEINEN sichtbaren Namensnennungshinweis in der Oberflaeche
 * (anders als CC-BY/CC-BY-SA-Bestaende wie Twemoji oder OpenMoji - deshalb
 * bewusst nicht genommen). Der Lizenztext benennt keinen "Reserved Font
 * Name", der Familienname darf also unveraendert bleiben.
 *
 * FARBTECHNIK: COLRv1 (Vektor, Tabellen COLR v1 + CPAL) - kein CBDT-Bitmap.
 * Deshalb sind die Dateien klein und beliebig skalierbar. COLRv1 tragen
 * Chrome/Edge ab 98, Firefox ab 107, Safari ab 18. Aeltere Browser ignorieren
 * die Schrift und zeigen weiter die Systememojis - also genau den fruehreren
 * Zustand, keine Verschlechterung.
 *
 * GROESSE: 10 Teildateien, zusammen 2,0 MB, aufgeteilt per unicode-range.
 * Der Browser laedt NUR die Teile, in denen ein tatsaechlich angezeigtes
 * Zeichen liegt - eine Seite mit ein paar Smileys zieht typisch 0,1-0,4 MB,
 * nie die volle Menge. Teil 0 (0,7 MB) enthaelt allein die
 * Laenderflaggen-Ligaturen und wird nur geholt, wenn wirklich eine Flagge
 * im Text steht. Der Umzug in base.css kostet also NICHT 2,0 MB je Seite:
 * eine Seite ohne Emoji laedt keine einzige Teildatei.
 *
 * ?v=2051 IN DEN url(): Der Cache-Stempel aus deps._mit_cache_stempel()
 * greift nur fuer src=/href="-Verweise auf .js/.css im HTML, NICHT fuer
 * url(...) aus einem Stylesheet heraus. Ohne Stempel liefert die Middleware
 * fuer /static/* "no-cache", der Browser fragt die Schrift also bei jedem
 * Seitenaufruf neu nach (billiges 304, aber unnoetig). Das feste ?v= trifft
 * denselben Zweig der Middleware wie ein mtime-Stempel und macht die Dateien
 * damit dauerhaft cachebar ("immutable"). Der Wert ist die Schriftversion und
 * aendert sich nur, wenn die Dateien ausgetauscht werden - dann hier mit
 * hochzaehlen. base.css selbst bekommt seinen Stempel automatisch.
 *
 * CSP: keine Anpassung noetig. font-src faellt auf default-src 'self' zurueck
 * (middleware.py), und die Dateien kommen aus /static - also von der eigenen
 * Herkunft. Eine zusaetzliche font-src-Zeile waere wirkungsgleich; nach der
 * img-src/blob:-Regression vom 13.07.2026 wird am CSP-Kopf nichts ohne Not
 * angefasst. tests/test_chat_emoji_schrift.py haelt fest, dass die Richtlinie
 * das Laden weiter erlaubt und nichts darueber hinaus.
 *
 * DIE ZIFFERN-FALLE - vor jeder Aenderung am Schriftstapel lesen: Teildatei 2
 * deckte AUCH die Ziffern 0-9 (U+30-39) sowie #, *, (c), (R) und (TM) ab, weil
 * das die Basis der Tastenkappen- und Symbol-Emoji ist. Alle 15 Codepunkte sind
 * inzwischen aus der unicode-range der Teildatei ENTFERNT (die Ziffern am
 * 31.07.2026 zu Meldung 43, die uebrigen fuenf am 01.08.2026; Begruendung am
 * Block selbst): der Schutz ueber die Reihenfolge haengt daran, dass die
 * Textschrift davor auf dem Geraet aufloest - bei Roboto ist genau diese
 * Annahme gebrochen. Fuer diese Zeichen ist die Fehlerklasse damit strukturell
 * zu, unabhaengig von Geraet und Stapel.
 * Die Reihenfolge bleibt trotzdem verbindlich: Teildatei 2 deckt weiter Zeichen
 * ab, die genauso als normaler Text vorkommen - Pfeile (U+2194-2199, U+27A1,
 * U+2B05-2B07), U+203C und U+2049, das Info-Zeichen U+2139, Kartenfarben
 * (U+2660-2666), Haken und Kreuze (U+2714, U+2716, U+274C), U+3030. Die
 * Emoji-Familie muss deshalb HINTER allen Textschriften stehen - steht sie
 * vorn, zeichnet der Browser diese Zeichen in der ganzen Console in
 * Emoji-Optik. Und sie muss VOR
 * sans-serif stehen, weil eine generische Familie in manchen Browsern als
 * letzte Instanz gilt und die Suche dort abbricht. Das gilt fuer die
 * body-Regel unten UND fuer jeden eigenen Schriftstapel in einer Seite;
 * tests/test_chat_emoji_schrift.py prueft beides projektweit.
 * Die Reihenfolge allein genuegt aber NICHT: unmittelbar vor der
 * Emoji-Familie muss eine benannte Textschrift stehen, die auch dann
 * aufloest, wenn keine der systemabhaengigen davor vorhanden ist - sonst ist
 * die Emoji-Familie auf so einem Geraet die erste, die zusagt (Befund des
 * Browser-Smoke-Laufs 31.07.2026, Begruendung an der body-Regel unten).
 */

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.0.woff2?v=2051) format('woff2');
  unicode-range: U+1f1e6-1f1ff;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.1.woff2?v=2051) format('woff2');
  unicode-range: U+200d, U+2620, U+26a7, U+fe0f, U+1f308, U+1f38c, U+1f3c1, U+1f3f3-1f3f4, U+1f6a9, U+e0062-e0063, U+e0065, U+e0067, U+e006c, U+e006e, U+e0073-e0074, U+e0077, U+e007f;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.2.woff2?v=2051) format('woff2');
  /* EINZIGE Abweichung von der wortgleich uebernommenen Angabe des Anbieters
     (fonts/HERKUNFT.txt): die 15 Codepunkte, die diese Teildatei mit ganz
     normalem Text teilt, sind HIER von Hand entfernt - U+30-39 (Ziffern 0-9)
     am 31.07.2026, U+23 (#), U+2a (*), U+a9 ((c)), U+ae ((R)) und U+2122 ((TM))
     am 01.08.2026. Entscheidung Philipp nach Meldung 43 (Marco Fischer,
     Screenshot 31.07. 14:00: Ziffern auf Android in Emoji-Optik); der zweite
     Schritt schliesst dieselbe Fehlerklasse fuer die restlichen Zeichen. Damit
     kann die Emoji-Schrift keines dieser Zeichen mehr zeichnen - unabhaengig
     von Geraet und Reihenfolge im Stapel; vorher hing der Schutz daran, dass
     eine Textschrift vor der Emoji-Familie auf dem Geraet aufloest.
     Der Preis, bewusst bezahlt: die Tastenkappen 0-9 und # sowie (c) (R) (TM)
     erscheinen als normales Zeichen statt als Bild. Aus dem Emoji-Angebot
     (backend/app/chat/emojis.py) trifft das genau vier Zeichen: #, (c), (R),
     (TM) - die Ziffern-Tastenkappen und *-Keycap stehen dort nicht.
     U+fe0f und U+20e3 bleiben in der Angabe: sie sind Beiwerk anderer Folgen
     und ohne ihr Basiszeichen ohnehin unsichtbar.
     Rueckwanderung verhindert der Waechter tests/test_chat_emoji_schrift.py::
     test_doppelt_genutzte_zeichen_stehen_in_keiner_unicode_range. */
  unicode-range: U+200d, U+203c, U+2049, U+20e3, U+2139, U+2194-2199, U+21a9-21aa, U+23cf, U+23e9-23ef, U+23f8-23fa, U+24c2, U+25aa-25ab, U+25b6, U+25c0, U+25fb-25fe, U+2611, U+2622-2623, U+2626, U+262a, U+262e-262f, U+2638, U+2640, U+2642, U+2648-2653, U+2660, U+2663, U+2665-2666, U+2668, U+267b, U+267e-267f, U+2695, U+269b-269c, U+26a0, U+26a7, U+26aa-26ab, U+26ce, U+26d4, U+2705, U+2714, U+2716, U+271d, U+2721, U+2733-2734, U+2747, U+274c, U+274e, U+2753-2755, U+2757, U+2764, U+2795-2797, U+27a1, U+27b0, U+27bf, U+2934-2935, U+2b05-2b07, U+2b1b-2b1c, U+2b55, U+3030, U+303d, U+3297, U+3299, U+fe0f, U+1f170-1f171, U+1f17e-1f17f, U+1f18e, U+1f191-1f19a, U+1f201-1f202, U+1f21a, U+1f22f, U+1f232-1f23a, U+1f250-1f251, U+1f310, U+1f3a6, U+1f3b5-1f3b6, U+1f3bc, U+1f3e7, U+1f441, U+1f499-1f49c, U+1f49f-1f4a0, U+1f4ac-1f4ad, U+1f4b1-1f4b2, U+1f4b9, U+1f4db, U+1f4f2-1f4f6, U+1f500-1f507, U+1f515, U+1f518-1f524, U+1f52f-1f53d, U+1f549, U+1f54e, U+1f5a4, U+1f5e8, U+1f5ef, U+1f6ab, U+1f6ad-1f6b1, U+1f6b3, U+1f6b7-1f6bc, U+1f6be, U+1f6c2-1f6c5, U+1f6d0-1f6d1, U+1f6d7, U+1f6dc, U+1f7e0-1f7eb, U+1f7f0, U+1f90d-1f90e, U+1f9d1-1f9d2, U+1f9e1, U+1fa75-1fa77, U+1faaf;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.3.woff2?v=2051) format('woff2');
  unicode-range: U+200d, U+231a-231b, U+2328, U+23f0-23f3, U+2602, U+260e, U+2692, U+2694, U+2696-2697, U+2699, U+26b0-26b1, U+26cf, U+26d1, U+26d3, U+2702, U+2709, U+270f, U+2712, U+fe0f, U+1f302, U+1f321, U+1f392-1f393, U+1f3a9, U+1f3bd, U+1f3ee, U+1f3f7, U+1f3fa, U+1f451-1f462, U+1f484, U+1f489-1f48a, U+1f48c-1f48e, U+1f4a1, U+1f4a3, U+1f4a5, U+1f4b0, U+1f4b3-1f4b8, U+1f4bb-1f4da, U+1f4dc-1f4f1, U+1f4ff, U+1f508-1f514, U+1f516-1f517, U+1f526-1f529, U+1f52c-1f52e, U+1f550-1f567, U+1f56f-1f570, U+1f576, U+1f587, U+1f58a-1f58d, U+1f5a5, U+1f5a8, U+1f5b1-1f5b2, U+1f5c2-1f5c4, U+1f5d1-1f5d3, U+1f5dc-1f5de, U+1f5e1, U+1f5f3, U+1f6aa, U+1f6ac, U+1f6bd, U+1f6bf, U+1f6c1, U+1f6cb, U+1f6cd-1f6cf, U+1f6d2, U+1f6e0-1f6e1, U+1f6f0, U+1f97b-1f97f, U+1f9af, U+1f9ba, U+1f9e2-1f9e6, U+1f9ea-1f9ec, U+1f9ee-1f9f4, U+1f9f7-1f9ff, U+1fa71-1fa74, U+1fa79-1fa7b, U+1fa86, U+1fa8e-1fa8f, U+1fa91-1fa93, U+1fa96, U+1fa99-1faa0, U+1faa2-1faa7, U+1faaa-1faae;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.4.woff2?v=2051) format('woff2');
  unicode-range: U+265f, U+26bd-26be, U+26f3, U+26f8, U+fe0f, U+1f004, U+1f0cf, U+1f380-1f384, U+1f386-1f38b, U+1f38d-1f391, U+1f396-1f397, U+1f399-1f39b, U+1f39e-1f39f, U+1f3a3-1f3a5, U+1f3a7-1f3a9, U+1f3ab-1f3b4, U+1f3b7-1f3bb, U+1f3bd-1f3c0, U+1f3c5-1f3c6, U+1f3c8-1f3c9, U+1f3cf-1f3d3, U+1f3f8-1f3f9, U+1f47e, U+1f4e2, U+1f4f7-1f4fd, U+1f52b, U+1f579, U+1f58c-1f58d, U+1f5bc, U+1f6f7, U+1f6f9, U+1f6fc, U+1f93f, U+1f941, U+1f945, U+1f947-1f94f, U+1f9e7-1f9e9, U+1f9f5-1f9f6, U+1fa70-1fa71, U+1fa80-1fa81, U+1fa83-1fa85, U+1fa87-1fa8a, U+1fa94-1fa95, U+1fa97-1fa98, U+1faa1, U+1faa9, U+1fadf;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.5.woff2?v=2051) format('woff2');
  unicode-range: U+2693, U+26e9-26ea, U+26f1-26f2, U+26f4-26f5, U+26fa, U+26fd, U+2708, U+fe0f, U+1f301, U+1f303, U+1f306-1f307, U+1f309, U+1f310, U+1f3a0-1f3a2, U+1f3aa, U+1f3cd-1f3ce, U+1f3d5, U+1f3d7-1f3db, U+1f3df-1f3e6, U+1f3e8-1f3ed, U+1f3ef-1f3f0, U+1f488, U+1f492, U+1f4ba, U+1f54b-1f54d, U+1f5fa-1f5ff, U+1f680-1f6a2, U+1f6a4-1f6a8, U+1f6b2, U+1f6d1, U+1f6d5-1f6d6, U+1f6dd-1f6df, U+1f6e2-1f6e5, U+1f6e9, U+1f6eb-1f6ec, U+1f6f3-1f6f6, U+1f6f8, U+1f6fa-1f6fb, U+1f9bc-1f9bd, U+1f9ed, U+1f9f3, U+1fa7c;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.6.woff2?v=2051) format('woff2');
  unicode-range: U+200d, U+2615, U+fe0f, U+1f32d-1f330, U+1f336, U+1f33d, U+1f344-1f37f, U+1f382, U+1f52a, U+1f7e9, U+1f7eb, U+1f942-1f944, U+1f950-1f96f, U+1f99e, U+1f9aa, U+1f9c0-1f9cb, U+1fad0-1fadc;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.7.woff2?v=2051) format('woff2');
  unicode-range: U+200d, U+2600-2601, U+2603-2604, U+2614, U+2618, U+26a1, U+26c4-26c5, U+26c8, U+26f0, U+2728, U+2744, U+2b1b, U+2b50, U+fe0f, U+1f300, U+1f304-1f305, U+1f308, U+1f30a-1f30f, U+1f311-1f321, U+1f324-1f32c, U+1f331-1f335, U+1f337-1f33c, U+1f33e-1f344, U+1f3d4, U+1f3d6, U+1f3dc-1f3de, U+1f3f5, U+1f400-1f43f, U+1f490, U+1f4a7, U+1f4ae, U+1f525, U+1f54a, U+1f573, U+1f577-1f578, U+1f648-1f64a, U+1f6d8, U+1f940, U+1f980-1f9ae, U+1f9ba, U+1fa90, U+1faa8, U+1fab0-1fabf, U+1facd-1facf, U+1fae7;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.8.woff2?v=2051) format('woff2');
  unicode-range: U+200d, U+2640, U+2642, U+2695-2696, U+26f7, U+26f9, U+2708, U+2764, U+27a1, U+fe0f, U+1f33e, U+1f373, U+1f37c, U+1f384-1f385, U+1f393, U+1f3a4, U+1f3a8, U+1f3c2-1f3c4, U+1f3c7, U+1f3ca-1f3cc, U+1f3eb, U+1f3ed, U+1f3fb-1f3ff, U+1f430, U+1f466-1f469, U+1f46b-1f478, U+1f47c, U+1f481-1f483, U+1f486-1f487, U+1f48b, U+1f48f, U+1f491, U+1f4bb-1f4bc, U+1f527, U+1f52c, U+1f574-1f575, U+1f57a, U+1f645-1f647, U+1f64b, U+1f64d-1f64e, U+1f680, U+1f692, U+1f6a3, U+1f6b4-1f6b6, U+1f6c0, U+1f6cc, U+1f91d, U+1f926, U+1f930-1f931, U+1f934-1f93a, U+1f93c-1f93e, U+1f977, U+1f9af-1f9b3, U+1f9b8-1f9b9, U+1f9bc-1f9bd, U+1f9cc-1f9cf, U+1f9d1-1f9df, U+1fa70, U+1fa82, U+1fac3-1fac5, U+1fac8, U+1faef;
}

@font-face {
  font-family: 'Noto Color Emoji';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/static/fonts/NotoColorEmoji.9.woff2?v=2051) format('woff2');
  unicode-range: U+200d, U+2194-2195, U+2603, U+261d, U+2620, U+2639-263a, U+2665, U+26a1, U+26c4, U+270a-270d, U+2728, U+2763-2764, U+2b50, U+fe0f, U+1f31a-1f31f, U+1f32b, U+1f389-1f38a, U+1f3fb-1f3ff, U+1f440-1f450, U+1f463-1f465, U+1f479-1f47b, U+1f47d-1f480, U+1f485, U+1f48b-1f48c, U+1f493-1f49f, U+1f4a2, U+1f4a4-1f4a6, U+1f4a8-1f4ab, U+1f4af, U+1f525, U+1f573, U+1f590, U+1f595-1f596, U+1f5a4, U+1f5e3, U+1f600-1f644, U+1f648-1f64a, U+1f64c, U+1f64f, U+1f90c-1f925, U+1f927-1f92f, U+1f932-1f933, U+1f970-1f976, U+1f978-1f97a, U+1f9a0, U+1f9b4-1f9b7, U+1f9bb, U+1f9be-1f9bf, U+1f9d0, U+1f9e0-1f9e1, U+1fa75-1fa79, U+1fac0-1fac2, U+1fac6, U+1fae0-1fae6, U+1fae8-1faea, U+1faef-1faf8;
}

/* Formularelemente erben font-family nicht vom Dokument, sondern bekommen vom
   Browser eine eigene (system-ui). Ohne diese Zeile bliebe die Emoji-Schrift
   genau dort aus, wo Emojis am haeufigsten getippt werden: in Eingabefeldern
   und auf Knoepfen (z.B. .epgrid button im Chat-Picker setzt font-size, aber
   keine Familie). Nur die FAMILIE wird geerbt - Groesse und Gewicht bleiben,
   wo sie sind, damit sich am Aussehen nichts verschiebt (system-ui und der
   Stapel der body-Regel loesen sich auf beiden Zielsystemen ohnehin auf
   dieselbe Schrift auf: Segoe UI unter Windows, SF Pro unter macOS). Die
   Regel steht bewusst VOR der Formularfeld-Baseline weiter unten, die mit
   `font: inherit` dasselbe fuer die dort aufgezaehlten Feldtypen tut - sie
   ergaenzt also nur die uebrigen (schmuckloses <button>, Checkbox, Radio). */
button, input, textarea, select {
  font-family: inherit;
}

/* ================== Ende Emoji-Schrift ================== */

:root {
  /* Native Formularelemente (Auswahlmenü-Popups, Datepicker, Checkboxen) auf
     Betriebssystemebene ohne dies auf DUNKEL - sonst rendert der Browser sie
     mit hellem Chrome, obwohl die App selbst nur ein dunkles Theme kennt
     (wiederkehrender Fehler: bisher nur einzeln an Datumsfeldern geflickt,
     siehe frontend/admin.html .toolbar input[type="date"] - hier zentral für
     ALLE Seiten und alle Steuerelemente gelöst). */
  color-scheme: dark;
  --bg: #080C16;
  --bg-panel: #0C1220;
  --panel: #0C1220;
  --bg-input: #101829;
  --input: #101829;
  --border: #1C2740;
  --border-focus: #2D8CFF;
  --accent: #2D8CFF;
  --accent-hover: #4DA0FF;
  --accent-glow: rgba(45, 140, 255, 0.35);
  --glow: rgba(45, 140, 255, 0.35);
  --text: #EBF0FA;
  --text-dim: #8A96AD;
  --dim: #8A96AD;
  --text-faint: #5A6680;
  --faint: #5A6680;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* Kopfzeile bis header.js sie fertig aufgebaut hat unsichtbar halten (Layout-
   Platz bleibt reserviert, visibility statt display - kein Sprung). Ohne das
   sieht man bei jedem vollen Seitenaufruf kurz die volle, unzusammengefaltete
   Navigation (Dashboard/Schulung/Admin, Sprachumschalter, Abmelden), bevor
   header.js sie ins Hauptmenü verschiebt - das eigentliche "Ruckeln". Die
   Klasse steht direkt im HTML jeder Seite mit gemeinsamem Header (siehe
   header.js renderHeader(), das sie am Ende jedes Aufrufs wieder entfernt);
   Seiten ohne diese Klasse (z.B. datenschutz.html mit eigenem, statischem
   Header ohne Umbau) sind unberührt. */
header.js-header-pending { visibility: hidden; }

/* Baseline für Formularfelder OHNE seitenspezifische Klasse (.toolbar/.modal/
   etc. überschreiben das gezielt weiter). Wiederkehrender Fehler: Felder in
   neuen Containern (z.B. .feldflex) bekamen bisher keine der beiden Regeln
   und fielen auf den nativen Browser-Chrome zurück - mit color-scheme:dark
   zwar nicht mehr WEISS, aber ein unpassendes natives Grau statt var(--input).
   Diese Regel ist bewusst niedrig-spezifisch (Typ-Selektoren), jede Seite mit
   eigener Klasse gewinnt automatisch per Spezifität. */
input:not([type]), input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="tel"], input[type="url"], input[type="search"],
input[type="date"], input[type="datetime-local"], input[type="month"], input[type="week"],
input[type="time"], select, textarea {
  background: var(--input); border: 1px solid var(--border); border-radius: 8px;
  color: var(--text); font: inherit; font-size: 14px; padding: 9px 12px;
}
input:not([type]):focus, input[type="text"]:focus, input[type="email"]:focus,
input[type="password"]:focus, input[type="number"]:focus, input[type="tel"]:focus,
input[type="url"]:focus, input[type="search"]:focus, input[type="date"]:focus,
input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="week"]:focus,
input[type="time"]:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--glow);
}

/* Namensauswahl "Nachname, Vorname" statt einer numerischen ID-Eingabe
   (tickets.js nutzerWahlHtml). Hintergrund/Rahmen/Textfarbe kommen aus der
   Select-Baseline oben - hier bewusst NUR Breite und Padding, damit das Feld
   neben den Nachbarfeldern einer Aktionszeile sitzt und lange Namen nicht
   abgeschnitten werden. Kein Inline-Style, sonst fällt der Dark-Theme-Fix je
   Seite wieder auseinander. */
select.nutzerwahl { min-width: 190px; max-width: 100%; padding: 7px 10px; }

/* Locale-festes Datumsfeld (common.js datumsFeld): drei numerische Selects
   Tag/Monat/Jahr statt eines nativen <input type="date">, damit die Anzeige
   nicht in der Browser-Locale erscheint. Die Selects erben das Select-Styling
   oben; hier nur die Anordnung + etwas engeres Padding, damit die drei
   nebeneinander passen (auch mobil). */
/* flex-wrap: der Kalender-Knopf trägt seit 27.07.2026 eine sichtbare
   Beschriftung (siehe .df-kal) - in engen Hüllen (Filterzeilen mit von/bis,
   Tabellenzellen) rutscht er dadurch in eine zweite Zeile statt überzulaufen. */
.datumsfeld { display: inline-flex; flex-wrap: wrap; gap: 4px; vertical-align: middle; }
.datumsfeld select { padding: 9px 6px; }
/* HAUSREGEL Datumsfeld-Breite (27.07.2026, Rückmeldung Olha zu STEX News
   „Milestone dates are cut off" UND Marco Fischer zu Reisekosten „Felder für
   das Datum zu schmal" - dieselbe Ursache): Tag/Monat/Jahr behalten IHRE
   Inhaltsbreite.
   Warum es ohne diese Regel bricht: fast jede Seite setzt in ihrem
   Inline-<style> `.field select { width: 100% }`. Das Inline-<style> steht NACH
   base.css, bei gleicher Spezifität gewinnt es - dann wollen alle drei Selects
   je 100 % der (nur inhaltsbreiten) .datumsfeld-Hülle, das flex-shrink staucht
   sie auf ein Drittel und der Text verschwindet hinter dem Dropdown-Pfeil.
   Bisher gab es dagegen Einzel-Gegenmittel je Seite (nachrichten.html:
   `.rw-form .datumsfeld select { width: auto }`) - diese Regel ersetzt sie.
   !important ist bewusst gesetzt (neben Teil 2 unten die einzige Stelle in
   base.css, beide aus demselben Grund): auf admin.html greift
   `#viewPraxistests .field select` mit ID-Spezifität, die
   KEINE klassenbasierte Regel schlagen kann. Wie breit die drei Selects des
   Datumsfelds sind, ist auch keine Seiten-Entscheidung, sondern eine
   Eigenschaft des Bausteins - Seiten dürfen Hülle/Umgebung layouten, nicht die
   Selects darin. */
.datumsfeld select.df-t, .datumsfeld select.df-m, .datumsfeld select.df-j {
  width: auto !important;
}
/* HAUSREGEL Datumsfeld-Breite, TEIL 2: die HÜLLE (30.07.2026, Anlass: Meldung
   Marco Fischer zur Reisekostenabrechnung „Die Felder für das Datum sind zu
   schmal, um den Inhalt anzuzeigen").
   Erst die Einordnung, damit die Regel nicht doppelt gebaut wird: Marcos
   Screenshot ist vom 27.07.2026 15:01 und zeigt je EIN Zeichen pro Select
   („2⌄ 0⌄ 2⌄" statt 27/07/2026). Dieses Bild ist der Zustand VOR Teil 1 oben -
   damals war .datumsfeld noch ohne flex-wrap und die Selects standen auf
   `width: 100%`, wurden also auf ~44 px (Rahmen + Pfeil) zusammengeschoben.
   Teil 1 (28.07.2026 03:45) plus das flex-wrap oben beheben GENAU das: mit
   Inhaltsbreite + Zeilenumbruch ist keine Ziffer mehr verdeckt.
   Was Teil 1 NICHT behebt, ist die Ursache eine Ebene höher - die Hülle wird
   weiter gestaucht. Nachgerechnet an frontend/js/reisekosten.js: dort steht ein
   Datumsfeld neben einem <input type="time"> in derselben Flex-Zeile. Das
   Zeitfeld erbt aus dem Inline-<style> der Seite `.field input { width: 100% }`,
   seine Flex-Basis ist damit die GANZE Zeile (~500 px im 1fr-Track von .row2).
   Zusammen mit den ~300 px Inhaltsbreite der Hülle entsteht ein Defizit von
   ~300 px, das flex-shrink proportional verteilt: die Hülle bekommt ~185 px und
   das Zeitfeld ~310 px, obwohl die Zeile mit ~420 px für beide reicht. Weil ein
   Flex-Container erst Zeilen bildet und dann streckt/staucht, bricht die Hülle
   dabei in zwei bis drei Zeilen (TT MM / JJJJ / 📅 Kalender), statt die Selects
   zu stauchen. Lesbar, aber sichtbar kaputt - und sobald die Zeile schmaler
   wird als die Mindestbreite beider Felder, läuft sie aus dem Grid-Track heraus
   über das Nachbarfeld.
   Deshalb tragen Zeilen mit Datumsfeld + Nachbarfeld .df-zeile statt eines
   Inline-Styles. Die Klasse deckt beide Richtungen ab:
   (1) der Nachbar fordert nicht mehr die ganze Zeile, sondern nimmt seine
       natürliche Breite (Zeitfeld ~110 px). Ohne dieses künstliche Defizit
       behält die Hülle ihre Inhaltsbreite, ohne dass ihr jemand das Schrumpfen
       verbieten muss;
   (2) wird die Zeile WIRKLICH zu eng (schmales Fenster), bricht die ZEILE um
       und stapelt Datum über Zeit - die Hülle wird also erst gestaucht, wenn
       auch das nicht mehr reicht, und bricht dann wie vorgesehen intern um.
   !important wie bei Teil 1 und aus demselben Grund: `.field input` steht im
   Inline-<style> NACH base.css und hat dieselbe Spezifität. Der
   Kind-Kombinator lässt den versteckten Input des Bausteins bewusst außen vor
   (der liegt in .datumsfeld, nicht direkt in .df-zeile).
   Bewusst KEINE Regel, die .datumsfeld generell am Schrumpfen hindert
   (flex-shrink: 0 / min-width: max-content): rund zehn Stellen setzen den
   Baustein absichtlich in echte Enge (Grid-Tracks von 140-230 px in
   gespraechsbewertung/health-benefit/auftraege, `flex: 0 0 220px` in admin) -
   dort ist der Umbruch aus Teil 1 die gewollte Antwort, ein Schrumpfverbot
   würde daraus einen Überlauf machen. */
.df-zeile { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
.df-zeile > input, .df-zeile > select { width: auto !important; }
/* HAUSREGEL Datumsfeld-Breite, TEIL 3: min-width (30.07.2026, Fund aus
   derselben Prüfung wie Teil 2).
   Teil 1 hält `width` auf der Inhaltsbreite - `width: auto` neutralisiert aber
   KEIN `min-width`. Eine Seitenregel, die den Feldern eine Mindestbreite gibt,
   unterläuft die Hausregel deshalb von der anderen Seite: sie macht die drei
   Selects nicht zu schmal, sondern zu breit. Einzige solche Stelle im Frontend
   ist frontend/pruefprotokoll.html mit `.filter select { min-width: 170px }`
   (gedacht für die langen Aktions-/Nutzer-Auswahlen der Filterzeile) - dort
   werden Tag, Monat und Jahr je 170 px breit, das Datumsfeld also gut 500 px,
   und schiebt die übrigen Filterfelder aus der Zeile.
   Gegenmittel ist die Rückkehr zum Standardwert, nicht `min-width: 0`: `auto`
   ist der Initialwert von min-width und ergibt für ein Flex-Element (die
   Selects liegen immer im inline-flex der .datumsfeld-Hülle) die automatische
   Mindestgröße, also die Inhaltsbreite. Mit `0` dürfte flex-shrink sie in engen
   Hüllen unter die Inhaltsbreite stauchen - genau der Zustand aus Marcos
   Screenshot, den Teil 1 behoben hat.
   Bewusst NUR die drei Selects des Bausteins (Klassen df-t/df-m/df-j): die
   Mindestbreite der übrigen Filterfelder von pruefprotokoll.html ist eine
   Seiten-Entscheidung und bleibt unberührt. Wie breit Tag/Monat/Jahr sind, ist
   dagegen eine Eigenschaft des Bausteins - dieselbe Begründung wie bei Teil 1:
   Seiten dürfen Hülle und Umgebung layouten, nicht die Selects darin. Sonst
   entsteht wieder ein Gegenmittel je Seite, und die Breitenfrage läuft zum
   dritten Mal auseinander.
   !important aus dem Grund von Teil 1: die Seitenregeln stehen im
   Inline-<style> NACH base.css, und `.filter select` gewinnt bei gleicher
   Spezifität ohnehin über die Reihenfolge. */
.datumsfeld select.df-t, .datumsfeld select.df-m, .datumsfeld select.df-j {
  min-width: auto !important;
}

/* Kalender-Popup zum Datumsfeld (common.js, U09/H03): eigener Monatsraster-
   Popup statt des nativen (Browser-Locale-)Datepickers. Der 📅-Knopf sitzt
   neben den Selects, der Popup haengt am body (position:absolute mit
   Seitenkoordinaten, common.js klemmt ihn an den Viewport). */
/* Beschriftung "📅 Kalender"/"📅 Calendar" statt nur des Icons (Rückmeldung
   Olha 27.07.2026): der Knopf muss OHNE Tooltip erkennbar sein - auf
   Touch-Geraeten erscheint nie einer. Zusaetzlich Akzentfarbe fuer Rahmen und
   Text, damit er sich von den drei Selects daneben absetzt. */
.df-kal { background: transparent; border: 1px solid var(--accent); border-radius: 8px;
          color: var(--accent); cursor: pointer; font: inherit; font-size: 13px;
          padding: 0 10px; line-height: 1; white-space: nowrap;
          display: inline-flex; align-items: center; gap: 5px; }
.df-kal:hover { background: var(--input); }
.df-kal-text { font-weight: 600; }
.df-kal-popup { position: absolute; z-index: 70; background: var(--panel);
                border: 1px solid var(--border); border-radius: 10px; padding: 10px;
                width: 256px; box-shadow: 0 10px 30px rgba(0, 0, 0, .45); }
.df-kal-kopf { display: flex; align-items: center; justify-content: space-between;
               margin-bottom: 6px; }
.df-kal-titel { font-size: 13.5px; font-weight: 600; color: var(--text); }
.df-kal-nav { background: transparent; border: 1px solid var(--border); color: var(--text);
              border-radius: 6px; width: 26px; height: 26px; cursor: pointer;
              font: inherit; font-size: 14px; line-height: 1; }
.df-kal-nav:hover { border-color: var(--accent); }
.df-kal-nav:disabled { opacity: .35; cursor: default; border-color: var(--border); }
.df-kal-gitter { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.df-kal-wt { text-align: center; font-size: 11px; color: var(--dim); padding: 3px 0; }
.df-kal-tag { background: transparent; border: none; border-radius: 6px;
              color: var(--text); font: inherit; font-size: 12.5px; padding: 5px 0;
              cursor: pointer; text-align: center; }
.df-kal-tag:hover { background: var(--input); }
.df-kal-heute { box-shadow: inset 0 0 0 1px var(--accent); }
.df-kal-gewaehlt, .df-kal-gewaehlt:hover { background: var(--accent); color: #fff; }

/* Locale-feste Datei-Upload-UI (common.js uploadFeldHtml, Muster tickets.js
   A5): verstecktes natives Input + lokalisierter Knopf + Datei-Liste mit
   ✕-Entfernen. --red mit Fallback: die semantischen Zusatzfarben liegen je
   Seite, nicht in base.css (siehe Kopfkommentar). */
.cupl { display: inline-flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.cupl-input { position: absolute; width: 1px; height: 1px; opacity: 0;
              pointer-events: none; }
.cupl-btn { background: transparent; border: 1px solid var(--border); color: var(--text);
            border-radius: 8px; padding: 7px 14px; cursor: pointer; font: inherit;
            font-size: 13px; }
.cupl-btn:hover { border-color: var(--accent); }
.cupl-liste { display: flex; flex-wrap: wrap; gap: 6px; }
.cupl-datei { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px;
              color: var(--text); border: 1px solid var(--border); border-radius: 6px;
              padding: 3px 8px; }
.cupl-weg { background: transparent; border: none; color: var(--red, #F87171);
            cursor: pointer; font: inherit; font-size: 13px; padding: 0 2px;
            line-height: 1; }
.cupl-weg:hover { color: #fff; }
.cupl-leer { font-style: italic; color: var(--dim); font-size: 12.5px; }

/* Platz für die Scrollleiste immer reservieren – sonst springt das Layout
   beim Wechsel zwischen scrollenden und kurzen Seiten. Hintergrund schon
   hier (nicht nur bei body) gegen den kurzen weißen Blitz beim Navigieren
   zwischen Seiten (siehe auch <meta name="color-scheme" content="dark"> in
   jeder Seite - das ist das eigentliche Signal an den Browser für den
   Übergangs-Frame, DIESE Regel ist die zusätzliche Absicherung). */
html { scrollbar-gutter: stable; background: var(--bg); }

/* 'Noto Color Emoji' HINTER allen Textschriften und VOR sans-serif - die
   Reihenfolge ist tragend, siehe Ziffern-Falle im Abschnitt "Emoji-Schrift"
   am Dateianfang.

   NACHTRAG 31.07.2026 (Meldung 43): Fuer die ZIFFERN haengt der Schutz nicht
   mehr an dieser Reihenfolge - sie stehen nicht mehr in der unicode-range der
   Emoji-Schrift. Fuer #, *, (c), (R) und (TM) tut er es weiter, deshalb bleibt
   der folgende Absatz in Kraft.

   Arial ist der VERSCHLUSS der Ziffern-Falle (Browser-Smoke-Lauf 31.07.2026):
   Die Reihenfolge allein schuetzt nur, solange EINE der fuenf Angaben davor auf
   dem Geraet aufloest. Tut keine davon es, war die naechste Familie bisher die
   Emoji-Schrift - gemessen kamen 15 von 17 Zeichen der Probe
   "0123456789 #* (c)(R)(TM)" aus ihr. sans-serif hilft dort nicht: es steht
   dahinter und wird nur noch fuer Zeichen erreicht, die die Emoji-Schrift
   nicht hat.

   Warum eine BENANNTE Schrift und nicht system-ui: Eine generische Familie
   darf hier nicht stehen - genau deshalb ist sans-serif ans Ende verbannt (in
   manchen Browsern gilt eine generische Familie als letzte Instanz und die
   Suche bricht dort ab). Stuende system-ui vor der Emoji-Familie, waere in
   ebendiesen Browsern das Emoji wieder ein Kaestchen, und wir tauschten einen
   Fehler gegen einen zweiten. Dazu ist system-ui juenger als die benannten
   Schriften (Firefox erst ab 92), waehrend eine benannte Familie in JEDEM
   Browser dieselbe Regel hat: vorhanden oder uebersprungen.

   Warum genau Arial - und warum NUR ein Glied: Arial liegt unter Windows und
   macOS vor und loest unter Linux ueber die Metrik-Aliase von fontconfig
   (30-metric-aliases.conf, binding="same") auf Liberation Sans/Arimo/Nimbus
   Sans auf; keine dieser Schriften bringt echte Emoji mit, die Emoji-Schrift
   behaelt sie also alle. 'DejaVu Sans' war als zweites Glied vorgesehen (die
   Grundschrift der meisten Linux-Installationen) und ist im Browser-Lauf
   durchgefallen: DejaVu Sans hat fuer einen Teil der Emoji EIGENE, einfarbige
   Umrisse (U+1F600 zum Beispiel) und haette sie der Farbschrift weggenommen -
   aus dem Fehler waere ein zweiter geworden. Wer hier ein weiteres Glied
   ergaenzen will, prueft es zuerst gegen
   tests/browser/test_browser_schrift.py::test_emoji_kommt_aus_der_mitgelieferten_schrift.
   Rest-Risiko, bewusst so: ein Linux ohne jede Arial-Metrikschrift faellt
   weiter in die Falle - dort hat die Oberflaeche aber ueberhaupt keine
   Textschrift des Stapels, und jedes generische Glied waere die Emoji-Schrift
   selbst in Gefahr.

   Am Aussehen der heutigen Geraete aendert das nichts: Arial wird nur
   erreicht, wenn Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI' UND
   Roboto alle nicht aufloesen - Windows gewinnt weiter mit Segoe UI,
   macOS/iOS mit -apple-system, Android/ChromeOS mit Roboto.
   Geprueft im echten Browser: tests/browser/test_browser_schrift.py
   (test_ziffern_bleiben_text_auch_ohne_die_benannten_textschriften). */
body {
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
               Arial, 'Noto Color Emoji', sans-serif;
}

/* Rechte-Vorschau (docs/AI-Console-Rechte-Konzept.md Abschnitt 6, Phase 3
   Teil B, Mechanik in common.js initRechteVorschau()). z-index bewusst weit
   über allem Bestehenden (bisher max. 60 in den Seiten) - die Vorschau muss
   IMMER ganz oben liegen, unabhängig davon, was die jeweilige Seite sonst an
   Overlays/Menüs kennt. */
.rvs-overlay {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(4, 8, 16, .6);
  pointer-events: none;
}
.rvs-highlight {
  position: relative; z-index: 501;
  outline: 3px solid #FFD27A;
  outline-offset: 2px;
  border-radius: 10px;
  box-shadow: 0 0 0 6px rgba(255, 210, 122, .22);
}
.rvs-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 502;
  background: #101829;
  border-bottom: 1px solid rgba(255, 210, 122, .5);
  color: #EBF0FA;
  padding: 12px 20px;
  display: flex; align-items: center; gap: 18px;
  font-size: 13.5px;
}
.rvs-banner span { flex: 1; }
.rvs-banner button {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, .3);
  color: #EBF0FA;
  border-radius: 8px;
  padding: 6px 14px;
  cursor: pointer;
  font: inherit;
  font-size: 12.5px;
  white-space: nowrap;
}
.rvs-banner button:hover { border-color: rgba(255, 255, 255, .6); }

/* Nachrichten-Dropdown-Eintrag (header.js): Hover-Effekt per CSS statt
   Inline-onmouseover (CSP-Härtung - script-src ohne 'unsafe-inline'). */
.nc-eintrag:hover { background: var(--input); }

/* Baseline für .btn/.tabs (Feedback Philipp 19.07.2026): neue Seiten (u. a.
   health-benefit.html) nutzten class="btn"/"tabs" im JS, definierten die
   Styles aber nicht im eigenen <style>-Block - die Knöpfe fielen aufs graue
   native Browser-Chrome zurück. Werte identisch zur etablierten Referenz
   (abrechnung-ua-geo.html); Seiten mit eigener .btn-Definition gewinnen
   weiterhin, weil ihr <style> NACH base.css geladen wird. */
.tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.btn { border-radius: 8px; padding: 8px 16px; font: inherit; font-size: 13.5px;
       cursor: pointer; background: transparent; border: 1px solid var(--border); color: var(--dim); }
.btn:hover { color: var(--text); border-color: var(--accent); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.btn.primary:hover { color: #fff; }
.btn.small { padding: 5px 11px; font-size: 12.5px; }

/* ============ HAUSREGEL Aktionsspalte klebt rechts (31.07.2026) ============
 *
 * WARUM (Meldung 52, Marco Fischer zu Administration → Meldungen: "es gibt
 * keinen sichtbaren horizontalen Scrollbalken ... außer ich scrolle an das Ende
 * der Liste nach ganz unten", dazu ausdrücklich "ggf. wäre eine flächendeckende
 * Anpassung sinnvoll"): Tabellen scrollen im Haus INNERHALB ihres Kastens
 * (overflow-x: auto auf .panel/.tablewrap, so in 31 Seiten). Damit sitzt die
 * Scrollleiste am UNTEREN Kastenrand - bei 30 Zeilen weit unter dem Falz. Was
 * dabei zuerst verschwindet, ist die letzte Spalte, und das ist in 20 Seiten
 * genau der Aktionsknopf (Öffnen/Bearbeiten/Speichern): die Kernaktion war
 * unbedienbar, ohne dass die Oberfläche darauf hinweist. Dieselbe Fehlerklasse
 * ist als Meldung 50 (abgeschnittene Namen im Erfolgs-Tracker-Lauf) ein zweites
 * Mal gemeldet worden und war in mein-success-tracker.html:60-72 schon einmal
 * für EINE Tabelle geflickt - deshalb hier zentral (Entscheidung Philipp,
 * Variante A), wie bei den Datumsfeld-Hausregeln oben.
 *
 * DREI SCHICHTEN:
 * 1. Die Aktionsspalte klebt am rechten Rand des Scrollports (position: sticky).
 *    Der Knopf ist damit IMMER erreichbar, unabhängig von der Scrollposition -
 *    das ist die Schicht, die den Fehler behebt, und sie wirkt in jedem Browser.
 * 2. Die Scrollleiste der Tabellenkästen wird eingefärbt und damit dauerhaft
 *    sichtbar statt als ausgeblendete Overlay-Leiste (scrollbar-color/-width;
 *    im Haus bisher nur einmal in admin.html .tabs verwendet, kein ::-webkit-
 *    scrollbar-Styling irgendwo, also kein Konflikt).
 * 3. Randverlauf als Hinweis "hier geht es weiter": Schatten links an der
 *    klebenden Spalte, NUR wenn der Kasten tatsächlich nach rechts scrollen kann
 *    (@container scroll-state). Rein zusätzlich - fehlt die Unterstützung
 *    (derzeit Chromium-only), bleiben 1 und 2 unberührt.
 *
 * WARUM OPT-IN STATT td:last-child PAUSCHAL: Die Regel wirkt konsolenweit auf
 * JEDE Tabelle, sie darf also keine Spalte festkleben, die Inhalt trägt
 * (Summen-, Matrix-, Kalender- und Rasterspalten). Angesprochen wird deshalb nur
 * die letzte Zelle, die als Aktionszelle erkennbar ist: die im Haus etablierten
 * Klassen .actions (170 Vorkommen) und .aktionen (26) oder ein Knopf mit
 * data-aktion DIREKT in der Zelle (so baut u. a. die gemeldete Meldungstabelle
 * ihre Öffnen-Spalte, ohne Klasse). Die Gegenprobe für die drei Matrix-/
 * Rasterbauten der Konsole (Rechte-Matrix admin.js rchZelle, Schichtraster
 * schichtplanung/mein-schichtplan renderZelle, Urlaubskalender kal-tabelle):
 * keine dieser Zellen enthält einen Knopf, keine trägt eine der Klassen - die
 * Regel greift dort also nicht. [colspan] bleibt ausgenommen: das sind die
 * ganzzeiligen Zellen (Gruppenkopf, "keine Einträge"), die nichts verdecken.
 *
 * WARUM TABELLEN OHNE ÜBERLAUF UNBERÜHRT BLEIBEN: sticky ohne scrollenden
 * Vorfahren verschiebt nichts, die Zelle bleibt an ihrem Platz. Sichtbar wäre
 * nur die deckende Fläche - und die ist var(--panel), also genau die Farbe, auf
 * der Tabellen hier liegen (geprüft: alle 31 .panel-Definitionen und alle
 * Modal-Kästen setzen background: var(--panel), ebenso table.liste in
 * auftraege/vorkasse). Der Schatten aus Schicht 3 erscheint nur bei echtem
 * Überlauf. Eine Tabelle, die heute passt, sieht damit unverändert aus.
 *
 * WARUM DIE FLÄCHE EIN PSEUDOELEMENT IST UND NICHT background AUF DER ZELLE:
 * alle Tabellen der Konsole laufen mit border-collapse: collapse. Die Zeilen-
 * trennlinien gehören dort der Tabelle, nicht der Zelle, und eine positionierte
 * (sticky) Zelle wird ÜBER ihnen gezeichnet - ein deckender Zellhintergrund
 * würde die Trennlinie in der Aktionsspalte wegradieren, also am rechten Rand
 * über die ganze Liste einen Bruch erzeugen. Das ::before deckt deshalb inset
 * 1px oben/unten weniger ab und lässt die Linien stehen; z-index: -1 legt es
 * hinter den Zellinhalt (die sticky Zelle ist ein eigener Stacking-Kontext).
 * BEKANNTE EINSCHRÄNKUNG: ein Hover-/Zebra-Ton, den eine Seite auf tr oder td
 * legt, liegt hinter dieser Fläche und fehlt damit in der klebenden Spalte. Wo
 * das auffällt, wiederholt die Seite ihn als background-image AUF diesem
 * ::before (Muster in admin.html bei tbody tr:hover).
 *
 * KEIN z-index auf der klebenden Zelle: sie wird als positioniertes Element
 * ohnehin nach ihren Nachbarn gezeichnet, liegt also über dem wegscrollenden
 * Inhalt. Ein z-index würde nur die Overlays/Popups der Seiten (z-index 20-70)
 * unter die Spalte holen können.
 */
:is(td.actions, td.aktionen, td:has(> button[data-aktion])):last-child:not([colspan]) {
  position: sticky;
  right: 0;
}
:is(td.actions, td.aktionen, td:has(> button[data-aktion])):last-child:not([colspan])::before,
table:has(> tbody > tr > td.actions:last-child)
  > thead > tr > th:last-child:not([colspan])::before,
table:has(> tbody > tr > td.aktionen:last-child)
  > thead > tr > th:last-child:not([colspan])::before,
table:has(> tbody > tr > td:last-child > button[data-aktion])
  > thead > tr > th:last-child:not([colspan])::before {
  content: ""; position: absolute; inset: 1px 0; z-index: -1;
  background: var(--panel);
}
/* Die Kopfzelle klebt mit, sonst wandert die Spaltenüberschrift unter den
   Knöpfen weg. Ein eventuelles sticky top der Seite (tickets.html,
   urlaubsplanung.html) bleibt erhalten - hier wird nur die rechte Achse
   gesetzt.
   DREI GETRENNTE SELEKTOREN statt einem verschachtelten: die Tabellen-
   Bedingung ist selbst ein :has(), und :has() darf nicht in :has() stehen. In
   der Kurzform `table:has(> tbody > tr > :is(…, td:has(> button…)):last-child)`
   verwirft die nachsichtige Auswertung von :is() dieses Glied stillschweigend -
   die Regel griff dann nur noch bei .actions/.aktionen, und in der Meldungs-
   liste (Aktionszelle OHNE Klasse, nur mit Knopf) wanderte die Überschrift beim
   Scrollen unter die Öffnen-Knöpfe. Ausgeschrieben braucht der Knopf-Fall kein
   inneres :has(): die Kette `td:last-child > button[data-aktion]` sagt dasselbe.
   Opt-in unverändert - kein pauschales th:last-child. */
table:has(> tbody > tr > td.actions:last-child)
  > thead > tr > th:last-child:not([colspan]),
table:has(> tbody > tr > td.aktionen:last-child)
  > thead > tr > th:last-child:not([colspan]),
table:has(> tbody > tr > td:last-child > button[data-aktion])
  > thead > tr > th:last-child:not([colspan]) {
  position: sticky;
  right: 0;
}
/* Schicht 2: sichtbare Scrollleiste an den Tabellenkästen. Absichtlich nicht
   global (die Seiten-Scrollleiste bleibt Systemvorgabe) - angesprochen wird der
   Kasten, der eine Tabelle unmittelbar enthält. Scrollt er nicht, hat die Regel
   keine Wirkung. */
:has(> table) {
  scrollbar-width: thin;
  scrollbar-color: var(--dim) var(--input);
}
/* Schicht 3: Randverlauf nur bei echtem Überlauf nach rechts. */
@supports (container-type: scroll-state) {
  :has(> table) { container-type: scroll-state; }
  @container scroll-state(scrollable: right) {
    /* Die Kopfzelle steht hier ohne die Tabellenbedingung: das ::before gibt es
       nur, wo die Regel oben es mit content erzeugt hat (Aktionsspalten) - ohne
       content entsteht kein Pseudoelement, die Zeile trifft also nichts anderes. */
    :is(td.actions, td.aktionen, td:has(> button[data-aktion])):last-child:not([colspan])::before,
    th:last-child:not([colspan])::before {
      box-shadow: -14px 0 16px -12px rgba(0, 0, 0, .75);
    }
  }
}
/* ================== Ende HAUSREGEL Aktionsspalte ================== */

/* ========== HAUSREGEL linke Klebespalte, Opt-in per Klasse (03.08.2026) ==========
 *
 * WARUM (Meldung 62, Olha zum Erfolgs-Tracker-Lauf: "if you see employee names
 * then you do not see what is in the right side. If you scroll to the right
 * corner you do not see the name of employees"; Meldung 80 zur Feedback-Liste:
 * "on the very bottom there is a scroll but then the date and the person to whom
 * feedback was given is not visible"): die Hausregel oben hält die AKTIONSSPALTE
 * fest, aber nicht die Zeilenkennung. Wer nach rechts scrollt, sieht Zahlen und
 * Felder ohne zu wissen, zu WEM sie gehören - der Fehler ist damit nur halb
 * behoben.
 *
 * WARUM OPT-IN UND NICHT PAUSCHAL td:first-child: die Regel würde konsolenweit
 * wirken. Erste Spalten tragen im Haus auch Inhalt, der nicht kleben darf
 * (Matrix-/Rasterzellen, Summenzeilen, Kalendertage) - und eine klebende Spalte
 * verschiebt beim Scrollen sichtbar Inhalt. Angefordert wird sie deshalb von der
 * Tabelle selbst, Hausmuster .rch-recht (admin.html) / .kal-name
 * (urlaubsplanung.html), nur zentral statt je Seite neu gebaut:
 *   .klebe-links   - die erste Spalte klebt links (Zeilenkennung = Spalte 1),
 *   .klebe-links2  - die ersten ZWEI Spalten kleben (Kennung erst in Spalte 2,
 *                    wie in der Feedback-Liste: Datum, dann Mitarbeiter).
 * Verwender: js/success-tracker.js (Lauf-Mitarbeitertabelle, Feedback-Liste).
 *
 * WARUM ::before UND KEIN background AUF DER ZELLE: identisch zur Hausregel
 * oben - bei border-collapse: collapse gehören die Zeilentrennlinien der
 * Tabelle, eine positionierte Zelle wird ÜBER ihnen gezeichnet, ein deckender
 * Zellhintergrund radiert sie in der klebenden Spalte weg. inset: 1px 0 lässt
 * sie stehen, z-index: -1 legt die Fläche hinter den Zellinhalt.
 *
 * WARUM :not([colspan]): die ganzzeiligen Zellen bleiben ausgenommen. Beide
 * Tabellen klappen Detailzeilen auf (Rechenweg, Feedback-Detail, Entscheidungs-
 * und Antragsformulare) - eine ganzzeilige Zelle als erste Spalte würde beim
 * Scrollen mitwandern und ihren Inhalt aus dem Kasten schieben.
 *
 * WARUM DIE ERSTE SPALTE BEI .klebe-links2 EINE FESTE BREITE HAT: sticky kennt
 * keine "Breite des Vorgängers", der Versatz der zweiten Spalte muss eine Zahl
 * sein. Beide lesen dieselbe Variable, damit weder Lücke noch Überlappung
 * entsteht; 106px trägt ein DD.MM.YYYY-Datum ohne Umbruch (10 Zeichen +
 * Zellpolster). Eine Seite mit breiterer erster Spalte setzt --klebe-spalte1
 * selbst. Der Browser-Smoke prüft die Deckung (Versatz == gezeichnete Breite),
 * damit die Zahl nicht stillschweigend auseinanderläuft.
 *
 * KEIN z-index: die klebende Zelle ist positioniert und wird ohnehin nach ihren
 * nicht positionierten Nachbarn gezeichnet. Trifft sie auf die (ebenfalls
 * klebende) Aktionsspalte, gewinnt diese - sie steht später im Markup, und die
 * Aktion darf nicht verdeckt werden.
 */
table:is(.klebe-links, .klebe-links2)
  > :is(thead, tbody) > tr > :is(th, td):first-child:not([colspan]) {
  position: sticky;
  left: 0;
}
table.klebe-links2 { --klebe-spalte1: 106px; }
table.klebe-links2
  > :is(thead, tbody) > tr > :is(th, td):first-child:not([colspan]) {
  width: var(--klebe-spalte1);
  min-width: var(--klebe-spalte1);
  white-space: nowrap;
}
table.klebe-links2
  > :is(thead, tbody) > tr > :is(th, td):nth-child(2):not([colspan]) {
  position: sticky;
  left: var(--klebe-spalte1);
}
table:is(.klebe-links, .klebe-links2)
  > :is(thead, tbody) > tr > :is(th, td):first-child:not([colspan])::before,
table.klebe-links2
  > :is(thead, tbody) > tr > :is(th, td):nth-child(2):not([colspan])::before {
  content: ""; position: absolute; inset: 1px 0; z-index: -1;
  background: var(--panel);
}
/* Kante am rechten Rand der klebenden Gruppe: sie sagt, wo der scrollende Teil
   beginnt (Vorbild .rch-recht in admin.html). Nur an der LETZTEN klebenden
   Spalte - zwischen Datum und Name wäre sie ein Trennstrich ohne Bedeutung. */
table.klebe-links
  > :is(thead, tbody) > tr > :is(th, td):first-child:not([colspan]),
table.klebe-links2
  > :is(thead, tbody) > tr > :is(th, td):nth-child(2):not([colspan]) {
  border-right: 1px solid var(--border);
}
/* ============== Ende HAUSREGEL linke Klebespalte ============== */

/* ===== HAUSREGEL höhenbegrenzter Tabellenkasten, Opt-in (04.08.2026) =====
 *
 * WARUM (Meldung 62, Olha zum Erfolgs-Tracker-Lauf: "the slider used to scroll
 * left and right is located only at the very bottom of the table, making it
 * impossible to view the data on the right side in the middle or at the
 * beginning of the table"; Meldung 80 wörtlich dasselbe für die Feedback-Liste;
 * Meldung 52, Marco: "außer ich scrolle an das Ende der Liste nach ganz
 * unten"): Schicht 2 der Hausregel oben hat die waagerechte Scrollleiste nur
 * EINGEFÄRBT. "Dauerhaft sichtbar" ist nicht "erreichbar" - die Leiste sitzt am
 * Fuß des Kastens, und der Kasten ist so hoch wie die Liste. Bei einer
 * Zeilenhöhe von rund 990 Pixeln (Kennzahlen-Spalte, Meldung 100) liegt sie
 * einen ganzen Bildschirm unter dem Tabellenkopf. Olhas Einwand war berechtigt.
 *
 * WAS DIE REGEL TUT: Der Kasten bekommt eine eigene Höhe. Die waagerechte
 * Leiste sitzt damit am Fuß des SICHTFENSTERS statt am Fuß der Liste. Und weil
 * der Kasten jetzt selbst senkrecht scrollt, bleibt die Kopfzeile stehen - ohne
 * sie stünden Zahlen ohne Überschrift da, sobald man weiterliest.
 *
 * KEINE ZWEITE LEISTE OBEN: ausdrücklich verworfen (Entscheidung Philipp, schon
 * beim Fix zu Meldung 80) - ein zweites Bedienelement für dieselbe Achse.
 *
 * WARUM OPT-IN UND NICHT :has(> table): der Selektor trifft AUCH die Zelle der
 * Rechenweg-Detailzeile, die eine innere Tabelle als direktes Kind hat
 * (js/success-tracker.js renderRechenwegZeile) - eine Höhengrenze samt
 * klebender Kopfzeile wäre dort falsch. Angefordert wird sie deshalb von der
 * Seite, Hausmuster .klebe-links oben.
 *
 * WARUM EIN EIGENER KASTEN UND NICHT .panel SELBST (am 04.08.2026 in Chromium
 * beide Wege gebaut und gemessen): .panel trägt Polster (18px oben) und die
 * Überschrift der Karte. Ein sticky-Kopf klebt NICHT am Rand des Kastens,
 * sondern unterhalb seines Polsters - gemessen 19 Pixel tiefer (1px Rahmen +
 * 18px Polster). Der wegscrollende Inhalt scheint dann in diesem Streifen ÜBER
 * der Kopfzeile durch, und die Überschrift der Karte scrollt mit. Der Kasten ist
 * deshalb ein eigenes, polsterloses div INNERHALB der Karte; Überschrift,
 * Filter und Erfassungsformulare bleiben davor stehen.
 *
 * WARUM z-index AUF DER KOPFZELLE (anders als bei den zwei Regeln oben): hier
 * treffen zwei klebende Achsen aufeinander. Die Kopfzelle und die linke
 * Klebespalte kleben beide; ohne z-index folgt die Malreihenfolge dem Markup,
 * und tbody kommt nach thead - die Namensspalte läge beim senkrechten Scrollen
 * ÜBER der Kopfzeile. 2 bleibt weit unter den Overlays der Seiten (20-70).
 *
 * DIE DECKFLÄCHE ALS ::before: gleiche Begründung wie in beiden Regeln oben -
 * bei border-collapse: collapse gehören die Trennlinien der Tabelle, eine
 * positionierte Zelle wird über ihnen gezeichnet, ein deckender
 * Zellhintergrund radiert die Kopflinie weg. inset: 1px 0 lässt sie stehen.
 * Trifft die Kopfzelle zusätzlich die Aktionsspalten-Regel (sticky right), gilt
 * beides: sticky kennt zwei Achsen, und die Eigenschaften stehen in getrennten
 * Regeln mit denselben Werten.
 */
.tabellenkasten {
  /* svh statt vh wegen der einklappenden Adressleiste auf Mobilgeräten; vh
     bleibt als Rückfall für Engines ohne svh stehen. */
  max-height: 78vh;
  max-height: 78svh;
  overflow: auto;
}
.tabellenkasten > table > thead > tr > th {
  position: sticky;
  top: 0;
  z-index: 2;
}
.tabellenkasten > table > thead > tr > th::before {
  content: ""; position: absolute; inset: 1px 0; z-index: -1;
  background: var(--panel);
}
/* MALREIHENFOLGE IN DER ECKE (Gegenprüfung 05.08.2026, Regression zu 03.08.):
 * z-index: 2 oben trifft ALLE Kopfzellen des Kastens, also auch die der linken
 * Klebespalte. Bei gleichem z-index gewinnt die im Markup SPÄTERE - beim
 * Rechtsscrollen schoben sich damit die wandernden Überschriften über die
 * stehenden ("Override" über "NAME" in der Lauf-Tabelle, "Text" über "Datum" in
 * der Feedback-Liste), genau in der Situation, für die die Klebespalte gebaut
 * wurde (Meldungen 62 und 80).
 *
 * DREI EBENEN, und alle drei werden gebraucht:
 *   auto (Datenzellen der Klebespalte) - positioniert, liegt damit ohnehin über
 *         dem NICHT positionierten scrollenden Inhalt. Bewusst OHNE z-index:
 *         eine Zahl hier holte die Klebespalte auch über die klebende
 *         Aktionsspalte (rechts, ebenfalls ohne z-index), und dort gilt
 *         weiterhin "die Aktion darf nicht verdeckt werden".
 *   2    - Kopfzeile über allem Inhalt (Regel oben, unverändert).
 *   3    - die EINGEFRORENE Kopfzelle: sie klebt auf beiden Achsen, ist also die
 *         Ecke, und muss über den scrollenden Kopfzellen UND über allem Inhalt
 *         liegen.
 * Kollision mit der klebenden Aktionsspalte (deren Kopfzelle bleibt bei 2) wäre
 * nur in einem Fenster möglich, das schmaler ist als beide klebenden Gruppen
 * zusammen - unterhalb von 760 Pixeln schaltet die Konsole auf Mobil. */
.tabellenkasten > table:is(.klebe-links, .klebe-links2)
  > thead > tr > th:first-child:not([colspan]),
.tabellenkasten > table.klebe-links2
  > thead > tr > th:nth-child(2):not([colspan]) {
  z-index: 3;
}
/* ========== Ende HAUSREGEL höhenbegrenzter Tabellenkasten ========== */

/* Datei-Auswahlfelder ("Datei auswählen"): der native Knopf ist sonst auf
   ALLEN Seiten Browser-Standard-Grau (gleiche Fehlerklasse wie die
   Formularfeld-Baseline oben - zentral statt je Seite geflickt). */
input[type="file"] { color: var(--dim); font: inherit; font-size: 13px; }
/* Höhe: der innenliegende Auswahl-Knopf bringt eigene Höhe mit - bekommt das
   Datei-Feld von einer Seiten-Regel (z. B. .formzeile input) das volle
   Feld-Padding, wird sein Kästchen höher als die Nachbarfelder (Feedback
   Philipp 19.07.2026, zuerst auf /meine-abrechnung und im Lohnarchiv).
   html-Präfix erhöht die Spezifität über die Typ+Klasse-Regeln der Seiten,
   damit diese Korrektur ÜBERALL gewinnt. */
html input[type="file"] { padding: 3px 6px; }
input[type="file"]::file-selector-button {
  border-radius: 8px; padding: 5px 11px; font: inherit; font-size: 12.5px;
  cursor: pointer; background: transparent; border: 1px solid var(--border);
  color: var(--dim); margin-right: 10px;
}
input[type="file"]::file-selector-button:hover { color: var(--text); border-color: var(--accent); }

/* ================== Sprungziel-Hervorhebung (Querschnitt) ==================
   Meldungen 53/54/57 (Olha, 31.07.2026): mehrere Checklisten-Zeilen führen
   bewusst auf DIESELBE Seite und unterscheiden sich nur im Ziel INNERHALB der
   Seite. Ein reines scrollIntoView ist als Wirkung nicht erkennbar - die
   Bedienerin sieht dieselbe Überschrift, denselben Status-Badge, dieselben
   Nachbarpanels und meldet "absolutely the same content".

   Deshalb hier EINE Klasse für alle Seiten (Vorbild .rvs-highlight oben, das
   aber zur Rechte-Vorschau samt Overlay/z-index gehört und dafür nicht
   verallgemeinert werden darf): wer per Deep-Link an ein Panel springt, setzt
   .sprungziel-aktiv auf das Zielpanel und entfernt sie nach dem Abklingen
   wieder. Kein z-index, kein Overlay - die Seite bleibt bedienbar.
   Verwender: js/success-tracker.js, js/abrechnung-ua-geo.js. */
.sprungziel-aktiv {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 10px;
  animation: sprungziel-abklingen 2.4s ease-out 1;
}
@keyframes sprungziel-abklingen {
  0%   { box-shadow: 0 0 0 8px var(--accent-glow); }
  60%  { box-shadow: 0 0 0 8px var(--accent-glow); }
  100% { box-shadow: 0 0 0 0 rgba(45, 140, 255, 0); }
}
/* Wer Bewegung abgestellt hat, bekommt die Umrandung ohne Puls - die
   Ankunft bleibt sichtbar, nur ohne Animation. */
@media (prefers-reduced-motion: reduce) {
  .sprungziel-aktiv { animation: none; }
}

/* ================== Bildvergrößerung .lightbox (Querschnitt) ==================
   HERKUNFT: stand wortgleich in frontend/chat.html (Block „E2 (F17/F18)",
   Zeilen 496-500) und war damit nur auf der Chat-Seite zu haben. Am 04.08.2026
   brauchte STEX News dasselbe Overlay für Meldung 98 (Bilder in Artikeln nicht
   öffenbar) und baute ersatzweise ein eigenes - js/stexnews.js hält den Grund
   fest: base.css lag außerhalb der damaligen Dateihoheit. Zwei Fassungen
   derselben Sache sind die häufigste Fehlerklasse dieses Projekts, deshalb
   liegen die Regeln jetzt hier und werden von den Seiten nur noch benutzt.

   UNVERÄNDERT ÜBERNOMMEN: gleiche Eigenschaften, gleiche Werte, gleiche
   Reihenfolge. Die einzige Abhängigkeit ist var(--dim) aus :root oben (die
   Konsole hat nur ein dunkles Theme, color-scheme: dark); der Rest ist absolut.
   Die Chat-Seite sieht damit identisch aus - ihr eigenes <style> enthält keine
   Regel, die .lightbox, ihr <img> oder .lbname trifft, kann die hier stehenden
   also auch nicht überschreiben, obwohl es nach base.css geladen wird.

   z-index 70: derselbe Wert wie vorher, und bewusst über allem, was auf den
   Seiten kleben kann - insbesondere über der klebenden Kopfzeile des
   Hausmusters .tabellenkasten oben (z-index: 2). Eine Bildvergrößerung, die
   hinter einer Tabellenkopfzeile liegt, wäre der Fehler; das Overlay hängt
   zudem an <body> und steht damit im Wurzel-Stapelkontext.
   Verwender: js/chat.js::lightboxOeffnen. */
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.82); z-index: 70;
            display: flex; flex-direction: column; align-items: center;
            justify-content: center; gap: 10px; padding: 24px; cursor: zoom-out; }
.lightbox img { max-width: 96vw; max-height: 88vh; border-radius: 10px; }
.lightbox .lbname { color: var(--dim); font-size: 12.5px; }

/* ========== Kalender: Feiertag / Brückentag / Wochenende (Querschnitt) ==========
   ANLASS: Meldung 142 (Diana, 06.08.2026) - „Feiertage im Kalender farblich
   nicht erkennbar". Im Monatsraster der Urlaubsplanung (js/urlaub-kalender.js
   ::ukMonatsGridHtml) stand der Feiertagsname nur als graue Kleinschrift
   (.mk-fr) in einer Zelle mit Werktags-Hintergrund - die Legende darüber zeigte
   aber einen Farbchip „Feiertag" (.kal-frei). Die Legende versprach also eine
   Farbe, die das Raster nie auftrug.

   WARUM HIER UND NICHT JE SEITE: es gab bereits ZWEI Fassungen desselben
   Begriffs - .kb-feiertag/.kb-brueckentag (rot/bernstein, mail-kalender.html)
   und .kal-frei/.kal-bt (grau/blau, doppelt in urlaubsplanung.html UND
   mein-urlaub.html, obwohl beide denselben Renderer benutzen). Eine dritte
   danebenzustellen ist in diesem Projekt die häufigste Fehlerklasse (siehe
   .lightbox oben). Die Farbe steht deshalb EINMAL hier als Merkmal, und die
   Seiten benutzen sie nur noch. Die betroffenen Regeln sind in den drei
   Seiten-<style>-Blöcken entfernt worden - sie werden nach base.css geladen
   und hätten diese hier sonst bei gleicher Spezifität überstimmt.

   FARBWAHL: rot = Feiertag und bernstein = Brückentag sind die Werte, die der
   Mail-Kalender schon trug; die Urlaubsplanung zieht nach (blaues B ->
   bernsteinfarbenes B). Beide Töne kommen im Urlaubskalender auch als
   Abwesenheitsfarbe vor (rot = unbezahlt, bernstein = halber Tag), sind dort
   aber DECKEND und tragen eine andere Form: Abwesenheiten sind Balken
   (.mk-bar) bzw. deckende Matrixzellen, Feiertag/Brückentag sind getönte
   TAGESFLÄCHEN mit farbigem Innenrahmen. Wochenende bleibt bewusst neutral
   (blaugrau, ohne Rahmen) - damit unterscheiden sich die drei Zustände
   voneinander und nicht nur vom Werktag, was Meldung 142 ausdrücklich verlangt.

   NUR EIN THEMA: die Konsole kennt ausschließlich das dunkle Theme
   (color-scheme: dark in :root, kein prefers-color-scheme-Zweig irgendwo im
   Bestand). Die Töne sind über color-mix auf den jeweiligen Untergrund
   gemischt statt hart gesetzt, kippen also mit --panel/--bg mit, falls je ein
   helles Theme dazukommt. */
:root {
  --kal-feiertag: #F87171;
  --kal-brueckentag: #FFD27A;
  --kal-wochenende: #64748B;
}

/* Mail-Kalender (js/mail-kalender.js): Eintragsbeschriftungen */
.kb-feiertag { color: var(--kal-feiertag); }
.kb-brueckentag { color: var(--kal-brueckentag); }
/* Brückentag als reiner Planungshinweis (Festlegung Philipp 27.07.2026):
   dezent - der Tag ist ein Arbeitstag, es ist kein Urlaub gebucht. */
.kb-brueckentag.kb-bt-hinweis { color: var(--dim); }

/* Urlaubskalender (js/urlaub-kalender.js), beide Ansichten und beide Seiten.
   Reihenfolge = Vorrang: Feiertag schlägt Brückentag schlägt Wochenende
   schlägt Nachbarmonat. */
.mk-other { background: color-mix(in srgb, var(--border) 12%, transparent); }
.mk-other .mk-daynum { color: var(--faint); }
.kal-we, .mk-we { background: color-mix(in srgb, var(--kal-wochenende) 26%, transparent); }
.mk-bt-tag { background: color-mix(in srgb, var(--kal-brueckentag) 22%, transparent);
             box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--kal-brueckentag) 70%, transparent); }
.kal-frei, .mk-feiertag {
  background: color-mix(in srgb, var(--kal-feiertag) 26%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--kal-feiertag) 70%, transparent);
}
.mk-fr { color: var(--kal-feiertag); font-weight: 600; }
.kal-bt { color: var(--kal-brueckentag); font-weight: 800; }
.kal-bt.kal-bt-hinweis { color: var(--dim); }

/* ---- Monatsnavigation des Rasters (Meldung 143, Diana 06.08.2026) ----
   Die Pfeile und „Heute" wanderten je nach Länge des Monatsnamens („Mai 2028"
   gegen „September 2028", englisch nochmal anders). Wer zweimal blätterte,
   traf beim dritten Klick „Heute" und verlor den eingestellten Monat - das ist
   Datenverlust im Bedienfluss, kein Schönheitsfehler. Das Label bekommt
   deshalb ein FESTES Maß; alles rechts davon steht damit still. Die Breite ist
   am längsten Namen beider Sprachen bemessen (September/Dezember bzw.
   September/November) mit Reserve - der Browser-Lauf
   tests/browser/test_browser_urlaub_feiertage_141_143_0608.py blättert durch
   alle zwölf Monate in DE und EN und vergleicht die Pixelposition. */
.mk-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.mk-head b { font-size: 15px; }
.mk-monatslabel { display: inline-block; min-width: 200px; text-align: center; }
