/* ============================================================================
   emha.app — gemeinsames Designsystem
   ============================================================================

   Diese Datei ist die einzige Quelle für das Aussehen aller Apps. Was hier
   steht, wird in keiner App noch einmal geschrieben.

   Die Vereinbarung, in drei Sätzen:

     1. Vor einer neuen Ansicht zuerst in die Designvorlage sehen:
        https://auth.emha.app/shared/design.html
     2. Gibt es die Sache dort, wird die Klasse benutzt — nicht nachgebaut.
     3. Braucht man sie öfter als einmal, kommt sie hierher. Eine eigene Regel
        in der App schreibt man nur für das, was wirklich nur diese eine
        Ansicht braucht.

   Warum das hier so ausführlich steht: die Leiter aus Farb-, Abstands- und
   Schriftvariablen gab es vorher schon, aber niemand stand darauf. Gemessen am
   2026-09-16 waren im Bestand:

     · `main { max-width; margin:0 auto; padding }` neunmal kopiert, mit drei
       verschiedenen Breiten und zwei verschiedenen Innenabständen
     · sechs Klassennamen (.convert-card, .go-card, .lookup-card, .share-card,
       .speak-card, .pass-card) für genau das, was .card schon war
     · .btn-icon fünfmal einzeln definiert, jedes Mal leicht anders
     · .status-ok/-warn/-fail dreimal, mit drei verschiedenen Grüntönen
     · .tabs/.tab-btn viermal, unter demselben Namen mit zwei völlig
       verschiedenen Aussehen
     · 31 verschiedene font-size-Rohwerte, obwohl es eine Token-Leiter gab
     · stackweit genau eine einzige h1-Regel — alle übrigen Überschriften waren
       Browservorgabe

   Aufbau dieser Datei:

     1  Variablen         Farbe, Zustand, Schrift, Abstand, Radius, Tiefe, Bewegung
     2  Grundschicht      rohe Elemente: body, Überschriften, Absätze, Links, Code
     3  Aufbau            main, .stack, .cluster, .toolbar, .grid-auto, .view-header
     4  Formular          Eingabefelder, Auswahl, .field, .hint, .field-error
     5  Bausteine         .btn, .icon-btn, .card, .tabs, .chip, .badge, .data-table,
                          .facts, .list-row, .dialog, .callout, .empty-state
     6  Zustand           Laden, Leere, Fehler, Erfolg
     7  Rahmen            Kopfleiste, Kontomenü
     8  Bewegung & Druck
     9  Schriftdateien

   ============================================================================ */


/* ============================================================================
   1  VARIABLEN
   ========================================================================= */

:root {
  /* Native Bedienelemente (Ankreuzfelder, Datumsauswahl, Bildlaufleisten) folgen
     dem Thema. Ohne diese Angabe malte Chrome sie im Dunklen weiß. */
  color-scheme: light;
  /* Ankreuzfeld/Auswahlknopf unausgewählt: graue Fläche, Rand 3:1 gegen Karte. */
  --control-bg: #e9edf1;
  --control-border: #7b8794;
  /* --- Flächen -------------------------------------------------------------
     Drei Ebenen, mehr nicht: die Seite, eine Fläche darauf, und ein Feld, das
     darin vertieft liegt. Wer eine vierte Ebene braucht, braucht in Wahrheit
     eine andere Anordnung. */
  --bg: #f3f5f7;                /* die Seite (2026-09-22: vom Dashboard-Redesign übernommen, kühleres Blaugrau statt warmem Salbeigrau) */
  --card: #fcfdfd;              /* Karte, Panel, Dialog */
  --border: #dce2e8;            /* Haarlinie, 1px, nie dicker */

  --text: #171b20;
  --muted: #586573;             /* Nebentext, Beschriftungen, Einheiten */

  /* --- Akzent --------------------------------------------------------------
     Kabelblau. --accent färbt Text und Linien, --accent-solid füllt Flächen
     mit weißer Schrift darauf. Im hellen Thema gleich, im dunklen nicht —
     deshalb sind sie nicht austauschbar. */
  --accent: #1f4e79;            /* 7,9:1 auf --bg */
  --accent-solid: #1f4e79;      /* 8,7:1 gegen weiße Schrift */
  --accent-solid-hover: #163a5c;

  /* Getönte Auflage für Zeilen unter dem Zeiger und für die ausgewählte Zeile.
     Vorher hat sich das jede App als rgba(31,78,121,0.07) selbst hingeschrieben
     — mit der Folge, dass eine Änderung am Akzent hier nicht mitkam. */
  --accent-wash: rgba(31, 78, 121, 0.07);
  --accent-wash-strong: rgba(31, 78, 121, 0.11);

  /* --- Zustandsfarben ------------------------------------------------------
     Vier Aussagen, mehr gibt es nicht: es läuft, sieh hin, es ist kaputt, wir
     wissen es nicht. Alle vier sind gegen --card auf mindestens 4,5:1 geprüft.

     Der Warnton war vorher stackweit #b8860b — auf Kartengrund sind das 3,0:1
     und damit unter AA. In doku, lookup und status stand echte Warnmeldung in
     dieser Farbe. #8a6400 sagt dasselbe und ist lesbar (5,0:1).

     Nie Farbe allein. Jede Zustandsanzeige trägt zusätzlich ein Zeichen, eine
     Form oder ein Wort — sonst ist sie für farbfehlsichtige Nutzer keine
     Aussage, sondern eine Behauptung. */
  --ok: #3f7d4e;                /* 4,8:1 */
  --warn: #856009;              /* 5,6:1 (2026-09-22: vom Dashboard-Redesign übernommen) */
  --danger: #b42332;            /* 6,4:1 (2026-09-22: vom Dashboard-Redesign übernommen) */
  --danger-solid: #b42332;      /* Fläche mit weißer Schrift (6,4:1) */
  --info: var(--accent);
  --neutral: var(--muted);

  /* --- Schrift -------------------------------------------------------------
     Plex Sans für alles. Plex Mono ausschließlich für Werte, die man
     vergleicht, abtippt oder diktiert: Adressen, Prüfsummen, Messwerte,
     Portnummern, Befehlszeilen. Mono als Gestaltungsmittel für Beschriftungen
     wäre Dekoration und ist hier nicht gemeint. */
  --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Acht Stufen. Sie stammen aus dem, was im Bestand ohnehin schon am
     häufigsten stand — die Leiter ist also nicht erfunden, sondern aufgeräumt.
     Wer eine neue Größe braucht, braucht fast immer eine vorhandene. */
  /* Unterhalb der Leseschrift. NUR für Beschriftung, die auf einer Blende oder
     einem Etikett steht und nicht gelesen, sondern abgezählt wird — Portnummern,
     der Namensstreifen einer Geräteblende. Nie für Fließtext oder Bedienelemente.
     Vorher standen dafür in doku vier verschiedene Werte nebeneinander (0.6rem,
     0.62rem, 0.68rem, 10px) und meinten alle dasselbe. */
  --fs-3xs: 0.625rem;           /*  10px — Blenden-/Etikettenbeschriftung */
  --fs-2xs: 0.6875rem;          /*  11px — Portnummern, Mikro-Beschriftung */
  --fs-xs:  0.75rem;            /* 12px — Tabellenkopf, Meta, Einheiten */
  --fs-sm:  0.85rem;            /* 14px — Nebentext, dichte Tabellen, Reiter */
  --fs-base: 0.95rem;           /* 15px — Fließtext, Eingabefelder */
  --fs-md:  1.05rem;            /* 17px — h3, Dialogüberschrift */
  --fs-lg:  1.25rem;            /* 20px — h2 */
  --fs-xl:  1.5rem;             /* 24px — h1 */
  --fs-2xl: 2rem;               /* 32px — die eine große Zahl, die Startseite */

  --lh-tight: 1.2;              /* Überschriften, große Zahlen */
  --lh-snug: 1.35;              /* dichte Listen, Tabellenzellen */
  --lh-base: 1.55;              /* Fließtext */

  --fw-normal: 400;
  --fw-medium: 500;             /* Messwerte in Mono */
  --fw-semibold: 600;           /* Überschriften, Schaltflächen, Namen */
  --fw-bold: 700;

  /* Zeilenlänge. Über 80 Zeichen verliert das Auge beim Rücksprung die Zeile. */
  --measure: 68ch;

  /* --- Abstände ------------------------------------------------------------
     4 · 8 · 12 · 16 · 24 · 32 · 48 · 64. Keine krummen Zwischenwerte. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  /* --- Radien --------------------------------------------------------------
     Der Radius sagt, wie groß eine Fläche ist — nicht, wie freundlich sie sein
     soll. Klein für Felder, mittel für Flächen, groß für das, was über der
     Seite liegt. Alles auf denselben Radius zu setzen nimmt der Anordnung
     genau diese Information. */
  --radius-sm: 8px;             /* Eingabefeld, Schaltfläche, kleine Kachel */
  --radius-md: 10px;            /* Karte, Panel, Tabellenrahmen */
  --radius-lg: 14px;            /* Dialog, Menü — was über der Seite schwebt */
  --radius-pill: 999px;         /* Etikett, Filter, Fortschritt */

  /* --- Tiefe ---------------------------------------------------------------
     Drei Stufen, passend zu den drei Radien. Im hellen Thema trägt der
     Schatten, im dunklen der Rand — ein Schatten auf fast schwarzem Grund ist
     schlicht nicht sichtbar und macht die Fläche nur schmutzig. */
  --shadow-sm: 0 1px 2px rgba(23, 27, 32, 0.04), 0 1px 3px rgba(23, 27, 32, 0.05);
  --shadow-md: 0 2px 4px rgba(23, 27, 32, 0.04), 0 8px 16px -8px rgba(23, 27, 32, 0.14);
  --shadow-lg: 0 8px 24px rgba(23, 27, 32, 0.12);
  --shadow-elevated: var(--shadow-lg);   /* alter Name, bleibt gültig */

  /* --- Bewegung ------------------------------------------------------------
     Bewegung ist eine Antwort, keine Verzierung. Sie zeigt, was sich gerade
     geändert hat und wohin etwas gegangen ist. Eine Einblendung auf jeder
     Karte und ein Aufleuchten bei jedem Überfahren sind genau das Gegenteil:
     Bewegung ohne Aussage.

     Beim Laden einer Ansicht gibt es genau einen Moment, nicht fünf. */
  --dur-1: 120ms;               /* Zustand am Ort: Farbe, Rand, Haken */
  --dur-2: 180ms;               /* Öffnen, Einblenden, Ankommen */
  --dur-3: 320ms;               /* größere Wege, selten */
  --ease-out: cubic-bezier(0.2, 0.7, 0.3, 1);     /* kommt an und bleibt stehen */
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

  /* --- Seitenbreite --------------------------------------------------------
     Jede App setzt --page-max einmal auf :root und schreibt nie wieder eine
     eigene main-Regel. Drei Werte decken alles ab:

       --w-form  560px   ein einzelnes schmales Formular (profile)
       --w-tool  640px   ein Werkzeug auf einer Karte (convert, lookup, pass,
                         share, speak, labels)
       --w-page 1280px   Arbeitsbereich mit Listen und Tabellen (customers,
                         tickets, know, go, status, runbooks, emergency,
                         settings) — seit 2026-09-23 eine Breite für alle,
                         vorher 960px, zu schmal für die Tabellen

     doku setzt einen eigenen, breiteren Wert: eine Schrankansicht ist so
     breit, wie der Schrank ist. */
  --w-form: 560px;
  --w-tool: 640px;
  --w-page: 1280px;
  --page-max: var(--w-page);

  /* Höhe der Kopfleiste. Steht als Token da, weil sich etwas darunter daran
     ausrichten muss: die Kundenzeile der Shell klebt auf dem Telefon bei
     top: var(--header-h) und wäre bei top: 0 exakt hinter der Kopfleiste
     verschwunden — genau der Fehler, den diese Zahl als Literal an zwei Stellen
     provoziert hat. */
  --header-h: 64px;

  /* Fläche der Tabellenköpfe: zwischen Seitengrund und Karte. Deckend, weil
     ein stehenbleibender Kopf (.is-sticky) sonst die Zeilen durchscheinen ließe. */
  --table-head-bg: color-mix(in srgb, var(--bg) 65%, var(--card));

  /* Feine Punktrasterung des Seitenhintergrunds — das Patchfeld hinter allem.
     Sie darf nur in wirklich leeren Bereichen auffallen und nie mit Inhalt
     konkurrieren. */
  --texture-dot: rgba(23, 27, 32, 0.07);
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --control-bg: #2b343f;
  --control-border: #75828f;
  --bg: #14191f;                /* 2026-09-22: vom Dashboard-Redesign übernommen */
  --card: #1d242d;
  --border: #36414d;

  --text: #eef0ea;
  --muted: #afbdca;

  --accent: #5b9bd5;            /* 6,0:1 auf dunklem --bg */
  --accent-solid: #2f6fae;      /* 5,3:1 gegen weiße Schrift */
  --accent-solid-hover: #24578c;
  --accent-wash: rgba(91, 155, 213, 0.10);
  --accent-wash-strong: rgba(91, 155, 213, 0.16);

  --ok: #6aae7c;                /* 5,9:1 */
  --warn: #e6c271;              /* 9,2:1 (2026-09-22: vom Dashboard-Redesign übernommen) */
  --danger: #ff9b9e;            /* 7,8:1 (2026-09-22: vom Dashboard-Redesign übernommen) */
  --danger-solid: #c62f3d;      /* Fläche mit weißer Schrift (5,2:1) — --danger ist hier Textfarbe */

  /* Im Dunklen tragen die Ränder, nicht die Schatten. --shadow-sm ist
     absichtlich leer: ein Schatten unter einer Karte auf #14191f ist nicht zu
     sehen, er trübt nur die Kante. */
  --shadow-sm: none;
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45);

  --texture-dot: rgba(255, 255, 255, 0.055);
}

/* --- Semantische Alias-Schicht (Ops-Console, 2026-09-22) ---------------------
   Zeigt nur auf bestehende Werte, ersetzt nichts. Neue Bausteine (Shell,
   Sidepanel, Toast, Filterleiste) benutzen diese Namen; der Bestand bleibt
   unverändert auf --bg/--card/--text/--muted/--border/--accent-solid/--ok/
   --warn/--danger.

   Warum überhaupt zwei Namen für dieselbe Farbe: die älteren Namen sagen, was
   etwas IST (--card, --muted), die neuen sagen, wofür es DA ist
   (--surface-raised, --text-secondary). Umbenennen hätte jede App angefasst;
   die Alias-Schicht kostet nichts und macht neuen Code lesbar.

   Vier Einträge sind KEIN Alias, sondern echte neue Werte — sie schließen
   Lücken, gegen die drei Apps heute mit hartem Hex-Fallback anschreiben
   (doku/public/styles.css:1188,1199,1200,1202,1207; tickets/public/styles.css:199,303).
   Sobald sie hier existieren, greifen die Fallbacks dort nie mehr und die Farben
   sind wieder die der Marke — ohne einen einzigen Diff in jenen Apps. */
:root {
  --surface: var(--bg);
  --surface-raised: var(--card);
  --surface-subtle: color-mix(in srgb, var(--card) 55%, var(--bg));
  --text-primary: var(--text);
  --text-secondary: var(--muted);
  --text-muted: var(--muted);
  --border-default: var(--border);
  --action-primary: var(--accent-solid);
  --action-primary-hover: var(--accent-solid-hover);
  --status-ok: var(--ok);
  --status-warn: var(--warn);
  --status-danger: var(--danger);
  --status-info: var(--info);
  --focus-ring: var(--accent);

  /* Echte neue Werte (kein Alias, weil es im Bestand keinen zweiten Rahmen-/
     Grauton gibt). Alle über color-mix aus vorhandenen Tokens abgeleitet, also
     in beiden Themen automatisch richtig — keine Wiederholung im Dark-Block. */
  --border-strong: color-mix(in srgb, var(--text) 28%, var(--border));
  --surface-2: color-mix(in srgb, var(--text) 8%, transparent);
  --success: var(--ok);          /* deckt doku var(--success, #2e9e5b) ab */
  --warning: var(--warn);        /* deckt doku var(--warning, #c98a00) ab */
  --shadow-2: var(--shadow-lg);  /* alter Name aus tickets, zeigt auf die echte Stufe */

  /* --- Schrift auf gefärbter Fläche ----------------------------------------
     ZWEI Token, weil es zwei Flächen gibt und keine Farbe auf beiden trägt.
     Gemessen:

                         --accent            --accent-solid
       hell   weiß        8,66:1              8,66:1
       dunkel weiß        2,96:1  ← durchfällt 5,24:1
       dunkel --bg        6,07:1              3,43:1  ← durchfällt

     Im hellen Thema sind beide Töne derselbe Wert, im dunklen nicht: --accent
     ist dort ein heller Blauton für Text und Linien, --accent-solid ein
     dunklerer zum Füllen. Ein gemeinsames Token müsste im Dunkeln entweder auf
     der einen oder der anderen Fläche unter 4,5:1 fallen — deshalb steht am
     Namen, wozu er gehört, und nicht nur, was er ist. */
  --accent-contrast: #fff;       /* NUR auf --accent-solid (gefüllte Knöpfe, .badge-solid) */
  --on-accent: #fff;             /* NUR auf --accent (getönte Flächen, hover-Füllungen) */
}
:root[data-theme="dark"] {
  --on-accent: var(--bg);        /* auf dem hellen --accent trägt im Dunkeln die Seitenfarbe */
}


/* ============================================================================
   2  GRUNDSCHICHT — wie ein rohes Element aussieht, wenn niemand etwas dazu sagt
   ----------------------------------------------------------------------------
   Diese Schicht ist ein Auffangnetz, kein Diktat. Zwei Regeln machen sie
   ungefährlich:

     · Nur nackte Elementselektoren, Spezifität (0,0,1). Jede vorhandene Regel
       einer App mit Klasse oder Id gewinnt weiterhin. Die Schicht greift also
       ausschließlich dort, wo sonst gar nichts stünde.

     · Nie eine Pseudoklasse in einer Regel, die von Komponenten überstimmt
       werden soll. `a:visited` hat Spezifität (0,1,1) und schlägt damit
       `.btn-ghost` (0,1,0) — Folge war, dass jede besuchte Schaltfläche blau
       wurde. Nur `a { color: … }` schreiben; das Verlauf-Lila kommt trotzdem
       nicht zurück, weil Autorenregeln die Browservorgabe schon aufgrund ihrer
       Herkunft schlagen, unabhängig von der Spezifität.
   ========================================================================= */

* { box-sizing: border-box; }

/* `hidden` muss immer verstecken. Komponenten mit eigenem display (.field, .field-row, …) schlugen
   das UA-Stylesheet sonst — Felder, die ein Skript per el.hidden = true ausblendet, blieben
   sichtbar (runbooks-Zeitplan 2026-09-24). Druckregeln, die [hidden] bewusst zeigen, sind
   spezifischer und ebenfalls !important, sie gewinnen weiter. */
[hidden]:not([hidden="until-found"]) { display: none !important; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  color: var(--text);
  /* Ein Hauch Akzent im Grund, damit weiße Karten sich ohne Schatten abheben
     (Übersicht-Runde 2026-09-22, vorher nur in customers/doku/tickets). Gemischt
     wird mit dem deckenden --accent, nicht mit --accent-wash: dessen Transparenz
     machte den Grund durchscheinend, und im dunklen Thema schien der weiße
     Browser-Hintergrund durch. */
  background-color: color-mix(in srgb, var(--bg) 97%, var(--accent));
  background-image: radial-gradient(circle, var(--texture-dot) 1.5px, transparent 1.5px);
  background-size: 28px 28px;
  /* Bewusst KEIN background-attachment: fixed. Ein fester Hintergrund sieht beim
     Scrollen zwar ruhiger aus, zwingt den Browser aber dazu, das 28px-Raster bei
     jedem Scroll-Frame über die volle Seitenhöhe neu zu rastern — auf einer
     langen Seite (die Designvorlage ist über 8000px hoch) ist das teuer und
     bringt nichts ein. Die Punkte laufen mit; sie sollen ohnehin nicht
     auffallen. */
  -webkit-text-size-adjust: 100%;
}

/* --- Überschriften ---------------------------------------------------------
   Das hier fehlte vollständig. Stackweit gab es genau eine h1-Regel, alles
   andere war Browservorgabe: h1 als 2em fett mit 0.67em Außenabstand oben und
   unten. Das ist für eine Anwendungsansicht zu groß und steht zu weit von dem
   weg, was es überschreibt — der Titel schwebte über der Seite, statt sie
   anzuführen.

   Vier Stufen, jede deutlich unterscheidbar, alle mit engem Abstand nach unten:
   eine Überschrift gehört zu dem, was unter ihr steht, nicht zu dem darüber.
   Deshalb hat sie oben viel und unten wenig Abstand. */
h1, h2, h3, h4, h5, h6 {
  margin-block: 0 var(--space-2);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  text-wrap: balance;
}
h1 { font-size: var(--fs-xl); letter-spacing: -0.012em; }
h2 { font-size: var(--fs-lg); letter-spacing: -0.006em; }
h3 { font-size: var(--fs-md); }
h4 { font-size: var(--fs-base); }
h5, h6 { font-size: var(--fs-sm); color: var(--muted); }

/* Eine Überschrift, die auf Text folgt, braucht Abstand nach oben. Eine, die
   eine Fläche anführt, nicht — deshalb :not(:first-child) statt margin-top auf
   allen. */
h2:not(:first-child), h3:not(:first-child) { margin-top: var(--space-5); }

p { margin-block: 0 var(--space-3); }
p:last-child { margin-bottom: 0; }

/* Zeilenlänge nur dort begrenzen, wo ein <p> wirklich Fließtext ist.
   Die erste Fassung hatte max-width einfach auf `p` stehen — und damit auch auf
   jedem <p>, das in Wahrheit ein Anordnungselement ist. Live nachgewiesen an der
   Gruppenbeschriftung einer Pillenreihe: sie trägt `flex: 0 0 100%`, um einen
   Zeilenumbruch zu erzwingen, wurde von den 68ch aber auf halbe Breite gedeckelt
   und stand dann neben den Pillen statt darüber. Drei benannte Umgebungen statt
   des nackten Elements treffen denselben Text und können das nicht anrichten. */
main > p,
section > p,
.card > p { max-width: var(--measure); }

ul, ol { margin-block: 0 var(--space-3); padding-inline-start: 1.4em; }

small { font-size: var(--fs-xs); }

code, kbd, samp, pre {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

hr {
  border: none;
  border-top: 1px solid var(--border);
  margin-block: var(--space-5);
}

/* Links. Ohne diese Regel fällt jeder Link, den keine Komponente einfärbt, auf
   das Browserblau zurück — auf dem dunklen Kartengrund unlesbar. Die
   Unterstreichung bleibt die Browservorgabe: sie ist das einzige Merkmal, an
   dem ein Link auch ohne Farbwahrnehmung als Link erkennbar ist. */
a { color: var(--accent-solid); text-underline-offset: 2px; }

/* :where() und nicht `:root[data-theme="dark"] a`. Das ist derselbe Fehler wie
   seinerzeit bei a:visited, nur eine Ebene höher: ein Vorfahrenselektor mit
   Attribut zählt (0,1,1) und schlägt damit JEDE Komponentenregel mit einer
   Klasse. Im dunklen Thema hat das jedes <a class="…"> blau eingefärbt —
   .module auf der Startseite, .list-row, .account-item, .btn-ghost als Link.
   Auf der Startseite waren dadurch alle Werkzeugnamen blau statt in Textfarbe;
   am 2026-09-16 im Browser gesehen, nicht vermutet.

   :where() trägt null zur Spezifität bei. Die Regel bleibt damit (0,0,1),
   gewinnt gegen den nackten Elementselektor darüber (spätere Regel bei
   gleicher Spezifität) und verliert gegen jede Komponente — genau so herum. */
:where(:root[data-theme="dark"]) a { color: var(--accent); }

/* Sichtbarer Tastaturfokus überall. Nichts darf den Fokus unterdrücken, ohne
   ihn zu ersetzen. */
:focus-visible {
  outline: 2px solid var(--accent-solid);
  outline-offset: 2px;
}
:root[data-theme="dark"] :focus-visible { outline-color: var(--accent); }
/* Eingabefelder: Rahmen direkt an der Kante statt 2px davor. Mit Abstand ragte
   er 4px in die Zeile darunter und lag dort auf Hinweistexten. */
:where(input, select, textarea):focus-visible { outline-offset: 0; }

/* --- Textwerkzeuge --------------------------------------------------------- */

.muted { color: var(--muted); }
.mono { font-family: var(--font-mono); }

/* Tabellarische Ziffern für alles, was untereinander verglichen wird oder sich
   live ändert. Proportionale Ziffern lassen eine Spalte bei jeder
   Aktualisierung sichtbar zappeln. */
.num { font-variant-numeric: tabular-nums; }

/* Kleine Bezeichnung über einer Gruppe von Bedienelementen. Bewusst sparsam:
   eine gesperrte Versalzeile über jeder Überschrift ist die verbreitetste
   Verzierung ohne Inhalt. Hier trägt sie Information — sie benennt eine Gruppe,
   die sonst keinen Namen hätte, so wie die Beschriftung über einem Feld am
   echten Patchfeld. */
.section-label {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 var(--space-2);
}

.hidden { display: none !important; }

/* Versteckt ein Element optisch, lässt es aber im Fokuszug und im
   Barrierefreiheitsbaum — für Text, den nur ein Screenreader hört, und für ein
   echtes Bedienelement, das eine gestaltete Hülle optisch ersetzt. Nie
   `hidden`/`display:none` dafür: beides entfernt das Element vollständig, und
   genau das darf bei einem bedienbaren Element nicht passieren. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Der Sprung über die Kopfleiste. Nicht `.sr-only`, sondern wirklich nur aus
   dem Bild geschoben: ein Element mit `clip: rect(0,0,0,0)` bleibt beim Fokus
   unsichtbar, und genau dann muss es zu sehen sein. Erstes Element im <body>,
   vor der Kopfleiste — sonst springt man mit der Tabulatortaste erst durch
   Navigation, Kundenchip, Suche, Thema und Konto, bevor der Inhalt kommt.
   Fügt app-shell.js selbst ein, wenn die Seite keinen hat. */
.skip-link {
  position: absolute;
  left: var(--space-3);
  top: -100%;
  z-index: 100;
  background: var(--surface-raised);
  color: var(--text-primary);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-sm);
  text-decoration: none;
  transition: top var(--dur-1) var(--ease-out);
}
.skip-link:focus { top: var(--space-3); }


/* ============================================================================
   3  AUFBAU
   ========================================================================= */

/* Der Seitenrahmen. Eine Regel für alle — jede App sagt nur noch, wie breit sie
   ist, indem sie --page-max auf :root setzt. */
main {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-7) var(--space-4) var(--space-8);
}

@media (max-width: 640px) {
  main { padding-top: var(--space-5); }
}

/* Senkrechter Rhythmus. Ersetzt die hunderte einzelner margin-top-Angaben:
   Abstand gehört zwischen die Dinge, nicht an die Dinge. */
.stack > * + * { margin-top: var(--stack-gap, var(--space-4)); }
.stack-sm { --stack-gap: var(--space-2); }
.stack-lg { --stack-gap: var(--space-6); }

/* Waagerechte Gruppe, die umbricht statt zu überlaufen. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Werkzeugleiste: links der Inhalt, rechts die Aktionen, auf schmalen Schirmen
   untereinander. */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}
.toolbar > .spacer { flex: 1; }
.toolbar > h2, .toolbar > h3 { margin: 0 auto 0 0; }

/* Filterleiste über einer Liste: eine ruhige Fläche, Felder in Fingerhöhe. */
.filter-bar {
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--card);
}
.filter-bar :where(input, select) { min-height: 44px; }

/* Raster, das sich selbst füllt. --col-min steuert, ab wann umbrochen wird. */
.grid-auto {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--col-min, 220px), 1fr));
  gap: var(--space-3);
  align-items: start;
}

/* Kopf einer Ansicht: Brotkrume und großer Titel links, Aktionen rechts unten
   bündig, darunter eine Linie. Seit 2026-09-23 die eine Fassung für alle Apps —
   vorher stand dieselbe Überschreibung als redesign.css dreimal einzeln in
   customers/doku/tickets und ein viertes Mal in know. */
.view-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border);
}
.view-header h1 {
  margin: 0;
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  letter-spacing: -.04em;
}
.view-header p { max-width: 60ch; margin: var(--space-2) 0 0; color: var(--muted); }
.view-header :where(.actions, .toolbar) { margin-left: auto; }
/* Direkt über Reitern nur EINE Linie (die der Reiterleiste), nicht zwei. */
.view-header:has(+ .tabs) { border-bottom: 0; padding-bottom: 0; }
@media (max-width: 640px) {
  .view-header { align-items: flex-start; flex-direction: column; }
  .view-header :where(.actions, .toolbar) { margin-left: 0; }
}
.view-header .crumb {
  color: var(--muted);
  font-size: var(--fs-sm);
  margin-bottom: 2px;
}
.view-header .crumb a { color: var(--muted); }
.view-header .crumb a:hover { color: var(--accent); }

/* Eine Reihe von Bedienelementen, die auf schmalen Schirmen untereinander
   rutscht statt abgeschnitten zu werden. */
@media (max-width: 640px) {
  .responsive-row {
    flex-direction: column;
    align-items: stretch;
  }
  .responsive-row > select { max-width: none !important; }
}


/* ============================================================================
   4  FORMULAR
   ----------------------------------------------------------------------------
   Nur Langform-Eigenschaften. Eine Kurzform (`background`, `padding`) setzt
   beim Anwenden alle ihre Langformen auf den Ausgangswert zurück — genau daran
   ist die Chevron-Grafik des <select> schon einmal zerbrochen, weil fast jede
   App eine eigene `<bereich> input, <bereich> select { background: … }`-Regel
   hat, die dabei background-image/-position/-size mit weggeworfen hat.
   ========================================================================= */

/* :where() und nicht :is(). Das sieht nach Haarspalterei aus und ist der
   Unterschied zwischen "greift nur, wenn sonst nichts dasteht" und "schlägt
   alles":

     :is()    übernimmt die Spezifität seines stärksten Arguments. [type="search"]
              ist eine Attributauswahl und zählt wie eine Klasse — der Selektor
              war damit (0,1,1) und hat JEDE App-Regel mit einer Klasse
              überstimmt. Genau das ist passiert: in doku lag der linke
              Innenabstand der beiden Suchfelder bei 11px statt bei 32px, weil
              .header-search-input (0,1,0) verloren hat — und das Lupensymbol
              saß sichtbar auf dem Platzhaltertext. Am 2026-09-16 im Browser
              gesehen. Dasselbe galt für input:not([type]).

     :where()  trägt null bei. Der Selektor ist (0,0,1), also genau das, was der
               Abschnitt oben verspricht.

   Wer also eine Basisregel für rohe Elemente schreibt: NIE :is(), immer
   :where(). */
input:where(
    [type="text"], [type="search"], [type="password"], [type="email"], [type="number"],
    [type="tel"], [type="url"], [type="date"], [type="time"], [type="datetime-local"],
    [type="month"], [type="week"]
  ),
input:where(:not([type])),
textarea {
  font-family: inherit;
  font-size: var(--fs-base);
  line-height: 1.4;
  padding-block: 0.55rem;
  padding-inline: 0.7rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  /* --bg und nicht --card: das Feld liegt damit sowohl auf einer Karte als auch
     auf der Seite erkennbar vertieft, statt mit einem der beiden zu verschmelzen. */
  background-color: var(--bg);
  color: var(--text);
}

textarea { min-height: 6rem; resize: vertical; }

/* opacity:1 ist nötig, weil Firefox den Platzhalter sonst zusätzlich abdunkelt
   und er dann doppelt blass ist. */
::placeholder { color: var(--muted); opacity: 1; }

input:disabled, textarea:disabled, select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Ankreuzfeld und Auswahlknopf selbst gezeichnet (2026-09-23, Nutzerwunsch):
   der native Kasten war im Dunklen unausgewählt knallweiß. Jetzt ist er in
   beiden Themen eine graue, leicht vertiefte Fläche mit deutlichem Rand;
   ausgewählt trägt er die Akzentfarbe UND ein Zeichen (Haken bzw. Punkt), damit
   der Zustand nicht nur an der Farbe hängt.
   WCAG 2.5.8: 13px sind mit dem Finger nicht sicher zu treffen. Die Beschriftung
   daneben ist der eigentliche Zielbereich, deshalb genügen 16px.
   Bewusst MIT Attributgewicht (0,1,1), wie die frühere Regel: mehrere Apps
   haben `input { width: 100%; margin-top: … }` für ihre Textfelder, das sonst
   aus jedem Kästchen einen Balken machen würde. */
input[type="checkbox"], input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  /* Einige Apps setzen `width: auto` (für das native Kästchen gedacht). Ohne
     natives Aussehen wäre das 0px — die Mindestmaße halten es sichtbar. */
  min-width: var(--check-w, var(--check-size, 16px));
  min-height: var(--check-h, var(--check-size, 16px));
  flex: none;
  margin: 0;
  position: relative;
  display: inline-block;
  vertical-align: -0.2em;
  border: 1.5px solid var(--control-border);
  background-color: var(--control-bg);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
/* Maße mechanisch festnageln: App-Regeln wie `.dialog-card input { width: 100% }` oder
   `.x input { flex: 1 }` haben dasselbe Gewicht wie die Regel oben, stehen aber später und machten
   aus Kästchen einen Balken über die ganze Breite (doku, Dialog „Regel anlegen", 2026-09-24).
   Eine andere Größe (große Abhakfelder, Tabellenauswahl) setzt die App NUR über
   `--check-size: 24px` am Feld oder Eltern-Element — nie über width/height (UI-Playbook,
   CI-Prüfung scripts/test/ui-conventions.test.js). --check-w/--check-h nur für den Schalter. */
input[type="checkbox"], input[type="radio"] {
  width: var(--check-w, var(--check-size, 16px)) !important;
  height: var(--check-h, var(--check-size, 16px)) !important;
  flex-grow: 0 !important;
  flex-shrink: 0 !important;
  flex-basis: auto !important;
}
input:where([type="checkbox"]) { border-radius: 4px; }
input:where([type="radio"]) { border-radius: 50%; }
input:where([type="checkbox"])::before {
  content: "";
  position: absolute;
  inset: 17%;
  transform: scale(0);
  background-color: #fff;
  /* Haken als Umriss, damit er die Farbe von background-color übernimmt. */
  clip-path: polygon(14% 44%, 0 59%, 40% 100%, 100% 22%, 85% 8%, 38% 70%);
}
input:where([type="radio"])::before {
  content: "";
  position: absolute;
  inset: 22%;
  border-radius: 50%;
  transform: scale(0);
  background-color: #fff;
}
/* Zustände mit demselben Attributgewicht wie die Grundregel oben (plus Pseudoklasse), sonst
   gewinnt deren background-color und ein angekreuztes Feld sieht leer aus (weißer Haken auf
   hellem Grund) — so vom 23.09. bis 24.09. in allen Apps. */
input[type="checkbox"]:checked, input[type="radio"]:checked,
input[type="checkbox"]:indeterminate {
  background-color: var(--accent-solid);
  border-color: var(--accent-solid);
}
input:where([type="checkbox"], [type="radio"]):where(:checked)::before { transform: scale(1); }
input:where([type="checkbox"]):where(:indeterminate)::before {
  transform: scale(1);
  inset: calc(50% - 1px) 22%;
  clip-path: none;
}
input[type="checkbox"]:hover:not(:disabled), input[type="radio"]:hover:not(:disabled) { border-color: var(--accent); }
input:where([type="checkbox"], [type="radio"]):where(:disabled) { opacity: 0.5; cursor: not-allowed; }

/* Ankreuzfeld mit Beschriftung: <label class="check"><input type="checkbox"> Text</label>. Die
   Beschriftung ist die Klickfläche (WCAG 2.5.8), deshalb mindestens 32 px hoch. */
.check { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--fs-sm); min-height: 32px; cursor: pointer; }
.check[hidden] { display: none; }

/* Schalter (an/aus mit sofortiger Wirkung): <label class="switch-line"><input type="checkbox"
   role="switch"> Beschriftung</label>. Gleiches Feld, andere Form — der Haken entfällt, dafür
   wandert der Knopf; der Zustand steht zusätzlich im Wort der Beschriftung bzw. aria-checked. */
.switch-line { display: flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--fs-sm); }
.switch-line input[type="checkbox"] {
  --check-w: 40px;
  --check-h: 22px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong, var(--border));
  background: var(--surface-2, var(--bg));
  transition: background var(--dur-1) var(--ease-out);
}
.switch-line input[type="checkbox"]::before { content: none; }
.switch-line input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform var(--dur-1) var(--ease-out);
}
.switch-line input[type="checkbox"]:checked { background: var(--accent-solid); border-color: var(--accent-solid); }
.switch-line input[type="checkbox"]:checked::after { transform: translateX(18px); background: #fff; }
.switch-line input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .switch-line input[type="checkbox"], .switch-line input[type="checkbox"]::after { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  input:where([type="checkbox"], [type="radio"]) { transition: none; }
}
@media (forced-colors: active) {
  input:where([type="checkbox"], [type="radio"]) { appearance: auto; -webkit-appearance: auto; }
  input:where([type="checkbox"], [type="radio"])::before { content: none; }
}

/* Das OS-eigene <select> ignoriert Rand- und Hintergrundangaben vollständig —
   also die native Grafik abschalten und den Chevron selbst zeichnen. Die
   !important-Angaben sind eng auf genau die Eigenschaften begrenzt, die den
   Chevron überhaupt erscheinen lassen; Farbe, Schrift und Rand bleiben frei,
   damit eine App sie weiterhin anpassen kann. Der Farbwert im SVG ist eine
   wörtliche Kopie von --text, weil eine CSS-Variable in einer data-URI nicht
   ausgewertet wird. */
select {
  appearance: none !important;
  -webkit-appearance: none !important;
  font-family: inherit;
  font-size: var(--fs-base);
  padding: 0.55rem 2.3rem 0.55rem 0.7rem;
  padding-right: 2.3rem !important;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background-color: var(--bg);
  color: var(--text);
  background-repeat: no-repeat !important;
  background-position: right 0.75rem center !important;
  background-size: 16px !important;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23171b20' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") !important;
}
:root[data-theme="dark"] select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23eef0ea' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") !important;
}

/* --- Die aufgeklappte Liste (2026-09-23) ------------------------------------
   Bisher zeigte jedes <select> beim Aufklappen die nackte Betriebssystem-Liste:
   weiß, eckig, ohne Rand. Chromium (Chrome, Edge ab 135) lässt die Liste mit
   `appearance: base-select` gestalten — ohne Skript und ohne dass eine App ihr
   <select> umbauen muss: Werte setzen, Optionen austauschen und change-Ereignisse
   bleiben genau so, wie sie sind. Firefox und Safari zeigen weiter ihre eigene
   Liste; der geschlossene Knopf sieht dort wie oben aus.
   Nur einzeilige Auswahl: `multiple`/`size` bleiben Listenfelder. */
select:where(:not(:disabled)) { cursor: pointer; }
select:where(:hover:not(:disabled)) { border-color: var(--control-border); }
@supports (appearance: base-select) {
  select:where(:not([multiple]):not([size])),
  select:where(:not([multiple]):not([size]))::picker(select) {
    appearance: base-select !important;
  }
  select:where(:not([multiple]):not([size])) {
    /* Den Chevron zeichnet jetzt ::picker-icon, das Hintergrundbild entfällt. */
    background-image: none !important;
    padding-right: 0.7rem !important;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    /* Einzeilig wie der native Knopf: ein langer Eintrag wird abgeschnitten, nicht zweizeilig. */
    white-space: nowrap;
    overflow: hidden;
  }
  select:where(:not([multiple]):not([size]))::picker-icon {
    content: "";
    width: 16px;
    height: 16px;
    margin-left: auto;
    background-color: var(--muted);
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") center / contain no-repeat;
    transition: transform var(--dur-1) var(--ease-out);
  }
  select:where(:not([multiple]):not([size])):where(:open)::picker-icon { transform: rotate(180deg); }
  select:where(:not([multiple]):not([size])):where(:open) { border-color: var(--accent-solid); }

  ::picker(select) {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--card);
    color: var(--text);
    box-shadow: var(--shadow-lg);
    padding: 4px;
    margin-block: 4px;
    max-height: min(22rem, 60vh);
    overflow-y: auto;
  }
  select:where(:not([multiple]):not([size])) option {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0.5rem 0.65rem;
    border-radius: var(--radius-sm);
    font-size: var(--fs-base);
    color: var(--text);
    background-color: transparent;
    cursor: pointer;
  }
  select:where(:not([multiple]):not([size])) option:where(:hover, :focus-visible) {
    background-color: var(--accent-wash-strong);
    outline: none;
  }
  select:where(:not([multiple]):not([size])) option:where(:checked) {
    font-weight: var(--fw-semibold);
    color: var(--accent);
  }
  select:where(:not([multiple]):not([size])) option::checkmark {
    content: "✓";
    order: 1;
    margin-left: auto;
    color: var(--accent);
  }
  select:where(:not([multiple]):not([size])) option:where(:disabled) { opacity: 0.5; cursor: not-allowed; }
  select:where(:not([multiple]):not([size])) optgroup { padding-top: var(--space-1); }
  select:where(:not([multiple]):not([size])) optgroup::before {
    display: block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    padding: 0.35rem 0.65rem;
  }
  @media (prefers-reduced-motion: reduce) {
    select:where(:not([multiple]):not([size]))::picker-icon { transition: none; }
  }
}

/* --- Das Feld als Einheit --------------------------------------------------
   Beschriftung, Eingabe, Hinweis, Fehler. Sechs Apps hatten dafür jeweils eine
   eigene .field-Regel. */
/* 6px Abstand, der Hinweis darunter noch einmal 4px weiter weg: der
   Fokusrahmen liegt außen um das Feld und darf den Text darunter nicht
   berühren (Nutzermeldung 2026-09-23: „blaue Ränder überlappen den Text"). */
.field { display: flex; flex-direction: column; gap: 6px; }
.field > :where(input, select, textarea) + :where(.hint, .field-error, small, p),
.field > :where(div) + :where(.hint, .field-error) { margin-top: 4px; }
.field > label,
.field-label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-snug);
}
.field > input, .field > select, .field > textarea { width: 100%; }

/* Ein Hinweis direkt unter einem Feld, auch außerhalb von .field. */
:where(input, select, textarea) + :where(.hint, small.muted, p.muted) { margin-top: var(--space-2); }

.hint {
  font-size: var(--fs-xs);
  color: var(--muted);
  line-height: var(--lh-snug);
  max-width: var(--measure);
}

/* Ein Fehler sagt, was schiefging und was zu tun ist. Er entschuldigt sich
   nicht und bleibt nie vage. Das Zeichen davor trägt die Aussage mit, damit sie
   auch ohne Farbwahrnehmung ankommt. */
.field-error {
  font-size: var(--fs-xs);
  color: var(--danger);
  font-weight: var(--fw-semibold);
}
/* Nur wenn wirklich ein Fehler dasteht. Fast jeder Dialog schreibt sein
   Fehlerfeld leer auf Vorrat ins Markup (`<div class="field-error"></div>`,
   allein in doku 22 Mal) und füllt es erst, wenn etwas schiefgeht. Ohne diese
   beiden Zeilen stand in jedem dieser Dialoge von Anfang an ein nacktes rotes
   „!" — im Beziehungs-Dialog gleich zweimal untereinander, und es sah aus, als
   sei schon beim Öffnen etwas kaputt. Live gemeldet, 2026-09-16.

   `:empty` greift nur bei wirklich leerem Inhalt: schon ein Leerzeichen im
   Markup zählt als Textknoten. Deshalb wird das Feld im Markup ohne Inhalt
   geschrieben und beim Zurücksetzen mit `textContent = ""` geleert, nicht mit
   einem Leerzeichen. */
.field-error:empty { display: none; }
.field-error:not(:empty)::before { content: "! "; }

.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"],
.field select[aria-invalid="true"] { border-color: var(--danger); }

/* Mehrere Felder nebeneinander, die bei Platzmangel umbrechen. */
.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--col-min, 220px), 1fr));
  gap: var(--space-4);
  align-items: start;
}

/* Thematische Gruppe in einem langen Formular, damit fünfzehn Felder nicht als
   ein undifferenziertes Raster lesen. <fieldset> + <legend>, weil ein
   Screenreader die Gruppe dann auch ansagt. */
.field-group { border: none; padding: 0; margin: 0 0 var(--space-6); min-width: 0; }
.field-group > legend {
  width: 100%;
  padding: 0 0 var(--space-2);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--text);
}

/* Eingabefeld mit Knöpfen daneben (aufdecken, kopieren, erzeugen). */
.input-with-actions { display: flex; align-items: center; gap: var(--space-1); }
.input-with-actions > input { flex: 1; min-width: 0; }


/* ============================================================================
   5  BAUSTEINE
   ========================================================================= */

/* --- Schaltflächen ---------------------------------------------------------
   Pro Ansicht genau eine primäre Schaltfläche. Sind zwei gleich wichtig, ist
   keine von beiden wichtig.

   Die Beschriftung sagt, was passiert — "Speichern", nicht "Absenden" — und
   behält denselben Namen durch den ganzen Vorgang: was "Veröffentlichen" heißt,
   meldet danach "Veröffentlicht". */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  padding: 0.7rem 1.1rem;
  min-height: 44px;             /* WCAG 2.5.8 */
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-1) var(--ease-out),
              border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out);
}
/* Viele Bedienelemente sind in Wahrheit ein <a> (Navigation). Ohne das hier
   tragen sie die Unterstreichung der Browservorgabe für Links — auf der
   Startseite live gesehen an der Pille unter "Zuletzt verwendet", die als
   unterstrichener Link in einer Pille saß.

   Jede neue Komponente, die auch als <a> vorkommen kann, gehört in diese Liste. */
.btn, .icon-btn, .btn-icon, .chip, .badge, .tab-btn { text-decoration: none; }

.btn-primary { background: var(--accent-solid); color: #fff; border-color: var(--accent-solid); }
.btn-primary:hover { background: var(--accent-solid-hover); border-color: var(--accent-solid-hover); }

.btn-ghost { background: var(--card); border-color: var(--border); color: var(--text); }
.btn-ghost:hover { border-color: var(--accent-solid); }
.btn-secondary { background: var(--card); border-color: var(--border); color: var(--text); }
.btn-secondary:hover { border-color: var(--accent-solid); }

.btn-danger { background: transparent; border-color: var(--border); color: var(--danger); }
.btn-danger:hover { border-color: var(--danger-solid); background: var(--danger-solid); color: #fff; }

.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; }

/* Kleine Variante für Zeilenaktionen — unter 44px, deshalb nur dort, wo direkt
   daneben noch ein größeres Ziel für dieselbe Sache liegt (etwa die anklickbare
   Zeile selbst). */
.btn-sm {
  padding: 0.35rem 0.7rem;
  min-height: 32px;
  font-size: var(--fs-sm);
}

/* Beschriftungsloser Knopf in der Kopfleiste: 44×44, eckig. Nur der
   Kontoknopf — ein Mensch — ist rund; alles andere liest als Instrument. */
.icon-btn {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin: 0;
  transition: border-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.icon-btn:hover { border-color: var(--accent-solid); }

/* Kleiner Zeichenknopf innerhalb einer Zeile oder eines Feldes (aufdecken,
   kopieren, löschen). Rahmenlos, damit eine Tabellenzeile nicht zur Knopfleiste
   wird. War vorher in fünf Apps einzeln definiert — doku, know, labels, lookup
   und pass, jedes Mal leicht anders. */
.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid transparent;
  cursor: pointer;
  font-size: var(--fs-base);
  line-height: 1;
  padding: 0.3rem 0.45rem;
  min-width: 30px;
  min-height: 30px;
  border-radius: var(--radius-sm);
  color: var(--muted);
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.btn-icon:hover { background: var(--accent-wash); color: var(--text); }
.btn-icon:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-icon.is-active,
.btn-icon.active { color: var(--accent-solid); }
/* Ein Zeichenknopf, der etwas wegnimmt, sagt das schon im Ruhezustand — nicht
   erst, wenn der Zeiger darauf liegt. Vorher hat sich das jede Stelle als
   `style="color:var(--danger)"` selbst hingeschrieben (allein in doku viermal). */
.btn-icon.is-danger { color: var(--danger); }
.btn-icon.is-danger:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }

/* Laufender Vorgang an der Schaltfläche selbst: der Knopf bleibt stehen, sagt
   was er tut und lässt sich nicht ein zweites Mal drücken. Ein Knopf, der
   während des Speicherns einfach nichts tut, sieht aus wie ein Knopf, der
   kaputt ist. */
.btn[aria-busy="true"], .btn.is-busy { pointer-events: none; opacity: 0.8; }
.btn[aria-busy="true"]::before, .btn.is-busy::before {
  content: "";
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

/* --- Flächen --------------------------------------------------------------- */

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  box-shadow: var(--shadow-sm);
}
.card-sm { padding: var(--space-4); }

/* Kopfzeile einer Karte: Titel links, Aktionen rechts. */
.card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.card-head > :is(h1, h2, h3, h4) { margin: 0; letter-spacing: -.02em; }

/* Anklickbare Zeile in einer Liste — Kunde, Schrank, Eintrag. Kein Schatten:
   eine Liste aus zwanzig schwebenden Karten ist keine Liste mehr. */
.list { display: flex; flex-direction: column; gap: var(--space-2); }
.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 56px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-4);
  color: var(--text);
  text-decoration: none;
  transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.list-row:hover { border-color: var(--accent-solid); background: var(--accent-wash); }
.list-row .name { font-weight: var(--fw-semibold); }
.list-row .meta { color: var(--muted); font-size: var(--fs-sm); margin-top: 2px; }
.list-row .actions { display: flex; gap: var(--space-2); flex-shrink: 0; }

/* Hinweisfläche am Rand des Textflusses. Der Streifen links ist die
   Portmarkierung des Patchfelds, nicht Dekoration — sie ist der Grund, warum
   eine Reihe solcher Blöcke wie ein Feld auf einer Leiste liest und nicht wie
   eine Reihe SaaS-Kacheln. */
.callout {
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--card);
  padding: var(--space-3) var(--space-4);
  font-size: var(--fs-sm);
}
.callout-warn { border-left-color: var(--warn); }
.callout-danger { border-left-color: var(--danger); }
.callout-ok { border-left-color: var(--ok); }

/* --- Reiter ----------------------------------------------------------------
   Zwei Bauformen, und die Regel, wann welche:

     .tabs              Unterstrich. Gliedert eine Ansicht in Abschnitte.
                        Bis etwa sechs Einträge, eine Zeile. Sie ordnen die
                        Seite und konkurrieren nicht mit den Schaltflächen
                        daneben.

     .tabs.is-pills     Pillen. Für eine Auswahl aus vielen oder gruppierten
                        Möglichkeiten, die in mehreren Zeilen steht — dort wäre
                        ein Unterstrich sinnlos, weil die Linie nur unter der
                        letzten Zeile läge.

   Vorher hatten vier Apps .tabs/.tab-btn mit zwei verschiedenen Aussehen unter
   demselben Namen. Jetzt ist das Aussehen eine ausgesprochene Entscheidung und
   keine Frage, welche Datei zuletzt geladen wurde.

   Das Tastaturverhalten (Pfeiltasten, Pos1/Ende, wanderndes tabindex) kommt aus
   shared/tablist.js und hängt nur an role="tablist"/role="tab", nicht an diesen
   Klassennamen. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-4);
}
/* Eine Reiterleiste direkt unter dem Titel braucht Luft nach oben — sonst klebt
   sie an der Überschrift, die ohnehin wenig Abstand nach unten hat, weil sie zu
   dem gehört was folgt. In lookup live gesehen. */
:is(h1, h2) + .tabs,
.view-header + .tabs { margin-top: var(--space-5); }
/* min-height 44 ist keine Optik, sondern die Zielgröße für den Finger
   (WCAG 2.2, 2.5.8). Der Reiter war mit 8px/12px Innenabstand rund 39px hoch —
   auf dem Telefon trifft man daneben, und daneben heißt hier: eine andere
   Ansicht. */
.tab-btn {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  min-height: 44px;
  padding: var(--space-2) 2px var(--space-3);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--muted);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active,
.tab-btn[aria-selected="true"] {
  color: var(--text);
  border-bottom-color: var(--accent-solid);
}
:root[data-theme="dark"] .tab-btn.active,
:root[data-theme="dark"] .tab-btn[aria-selected="true"] { border-bottom-color: var(--accent); }
.tab-btn:focus-visible { outline-offset: -2px; border-radius: 3px; }
/* Reiter als Links (Bereichswechsel per Adresse, z. B. go): gleiche Optik, aktiv über aria-current. */
a.tab-btn { display: inline-flex; align-items: flex-end; text-decoration: none; }
a.tab-btn[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent-solid); }
:root[data-theme="dark"] a.tab-btn[aria-current="page"] { border-bottom-color: var(--accent); }

.tabs.is-pills { border-bottom: 0; gap: var(--space-2); }
.tabs.is-pills .tab-btn {
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--card);
  padding: 0.5rem 0.9rem;
  min-height: 44px;
  margin-bottom: 0;
}
.tabs.is-pills .tab-btn:hover { border-color: var(--accent-solid); }
.tabs.is-pills .tab-btn.active,
.tabs.is-pills .tab-btn[aria-selected="true"] {
  border-color: var(--accent-solid);
  background: var(--accent-wash-strong);
  color: var(--text);
}
/* Fachliche Gruppierung innerhalb einer Pillenreihe: die Beschriftung erzwingt
   einen Umbruch vor sich, damit jede Gruppe eine eigene Zeile beginnt — ohne
   die flache Knopfliste aufbrechen zu müssen, die für die Pfeiltastenbedienung
   eine einzige role="tablist" bleiben muss. */
.tab-group-label {
  /* flex: 0 0 100% und nicht width: 100%. Ein Flex-Element darf standardmäßig
     schrumpfen (flex-shrink: 1), also hat die Breitenangabe allein den Umbruch
     nicht erzwungen — die Beschriftung stand neben den Pillen statt darüber.
     Live in der Designvorlage gesehen; die Regel stammt ursprünglich aus lookup
     und hatte den Fehler dort schon. */
  flex: 0 0 100%;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin: var(--space-3) 0 2px;
}
.tab-group-label:first-child { margin-top: 0; }

/* --- Filter-Pillen ---------------------------------------------------------
   Sieht aus wie ein Reiter in Pillenform und ist doch etwas anderes: ein Filter
   schaltet keine Ansicht um, er engt eine Liste ein, und mehrere davon können
   gleichzeitig an sein. Deshalb aria-pressed und nicht aria-selected. */
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
/* Zwei Größen in einem Element: die Pille bleibt optisch kompakt (rund 34px, so
   dicht wie eine Filterreihe sein muss, damit acht davon nebeneinander passen),
   das anfassbare Ziel ist über ::before trotzdem 44px hoch. Nur senkrecht
   vergrößert — waagerecht würde die unsichtbare Fläche die Nachbarpille
   überdecken und der Finger träfe den falschen Filter.
   ::before ist leer und aria-unsichtbar: es ist ein Ziel, kein Inhalt. */
.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 0.4rem 0.8rem;
  font: inherit;
  font-size: var(--fs-xs);
  color: var(--text);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.chip::before {
  content: "";
  position: absolute;
  inset-block: -6px;
  inset-inline: 0;
}
.chip:hover { border-color: var(--accent); }
.chip[aria-pressed="true"] { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; }
.chip-count { opacity: 0.7; font-variant-numeric: tabular-nums; }
.chip[aria-pressed="true"] .chip-count { opacity: 0.85; }
.chip-group-label { font-size: var(--fs-xs); color: var(--muted); }

/* --- Etiketten -------------------------------------------------------------
   Ein Zustand in Wortform. Immer mit Zeichen: die Farbe verstärkt die Aussage,
   sie ersetzt sie nicht.

   Ersetzt fünf getrennte Zustandsvokabulare allein in doku (.status-fail,
   .dev-dot.is-online, .health-bar.is-warn, .sec-row.is-bad, .health-chip)
   sowie .status-ok/-warn/-fail in lookup und status. */
.badge {
  --state: var(--neutral);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 0.55rem;
  border-radius: var(--radius-pill);
  border: 1px solid color-mix(in srgb, var(--state) 40%, transparent);
  background: color-mix(in srgb, var(--state) 10%, transparent);
  color: var(--state);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  line-height: 1.6;
  white-space: nowrap;
}
/* Kontrast im hellen Thema (axe, serious — gefunden 2026-09-22 in der neuen
   status-Übersicht, wo viele Etiketten nebeneinander stehen).
   Die Zustandsfarben sind gegen --card als VOLLFLÄCHE geprüft (Kommentar bei
   --ok/--warn/--danger). Im Etikett steht dieselbe Farbe aber als Schrift auf
   einer nur 10 % getönten Auflage derselben Farbe — und dort reichte sie nicht:

     gemessen auf --card / auf --bg      danger 3,90 / 3,47
                                         ok     4,11 / 3,66
                                         warn   4,43 / 3,98

   78 % Zustandsfarbe plus 22 % Textfarbe macht daraus 5,38/4,79 (danger),
   5,50/4,89 (ok), 5,82/5,22 (warn) — überall über 4,5:1, auch auf dem
   dunkleren Seitengrund, und die Farbe bleibt als Aussage erkennbar.

   Nur im hellen Thema: dort ist --text fast schwarz und dunkelt ab. Im dunklen
   Thema sind die Zustandsfarben helle Pastelltöne auf dunklem Grund und liegen
   bereits bei 4,83–6,79 — eine Beimischung des dort fast weißen --text würde
   sie aufhellen und den Kontrast wieder senken. */
.badge { color: color-mix(in srgb, var(--state) 78%, var(--text)); }
:root[data-theme="dark"] .badge { color: var(--state); }
.badge-ok { --state: var(--ok); }
.badge-warn { --state: var(--warn); }
.badge-danger { --state: var(--danger); }
.badge-info { --state: var(--accent-solid); }
:root[data-theme="dark"] .badge-info { --state: var(--accent); }

/* Gefüllte Bauform. Die getönte oben ist die Vorgabe — sie ordnet sich einer
   Tabellenzeile unter, und genau das will man bei einem Zustand, den man im
   Vorbeigehen liest. Gefüllt ist für die andere Sorte: eine harte
   Kennzeichnung, die sich nicht unterordnen soll ("Bot", "weitergeleitet"),
   weil sie ändert, wie die ganze Zeile zu lesen ist.

   Schriftfarbe: im hellen Thema weiß, im dunklen die Seitenfarbe. Die
   Zustandsfarben sind im Dunklen helle Pastelltöne — weiße Schrift darauf käme
   auf 2,7:1 und wäre unlesbar; dieselbe Fläche mit dunkler Schrift liegt bei
   6,5–7,9:1. Deshalb dreht sich die Schriftfarbe mit dem Thema und nicht mit
   dem Zustand. */
.badge-solid {
  background: var(--state);
  border-color: var(--state);
  color: #fff;
}
:root[data-theme="dark"] .badge-solid { color: var(--bg); }

/* Zustand als Text in einer Zelle, ohne Rahmen. Das Zeichen davor kommt aus dem
   Markup (✓ ! – ), nicht aus CSS: es soll beim Kopieren der Zeile mitgehen. */
.status-ok { color: var(--ok); font-weight: var(--fw-semibold); }
.status-warn { color: var(--warn); font-weight: var(--fw-semibold); }
.status-fail { color: var(--danger); font-weight: var(--fw-semibold); }
.status-unknown { color: var(--muted); font-weight: var(--fw-semibold); }

/* Punkt als Zustandsanzeige. Gefüllt = meldet sich. Hohl = meldet sich nicht
   mehr. Feiner Ring = war noch nie da. Die Form trägt die Aussage mit. */
.dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
  box-sizing: border-box;
  background: var(--muted);
}
.dot.is-ok { background: var(--ok); }
.dot.is-warn { background: var(--warn); }
.dot.is-fail { background: var(--danger); }
.dot.is-stale { background: transparent; border: 2px solid var(--muted); }
.dot.is-never { background: transparent; border: 1.5px solid var(--border); }

/* --- Tabelle ---------------------------------------------------------------
   Dichte Datentabelle. Keine Zebrastreifen: bei Haarlinien zwischen den Zeilen
   sind sie überflüssiges Rauschen. Der Kopf bleibt beim Scrollen stehen, Zahlen
   stehen rechts und tabellarisch, damit man Größenordnungen an der Stellenzahl
   erkennt statt sie lesen zu müssen. */
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  background: var(--card);
}
.data-table th,
.data-table td {
  text-align: left;
  padding: 0.875rem var(--space-4);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.data-table th {
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  white-space: nowrap;
}
/* Spaltenköpfe (nur thead — ein <th scope="row"> im Körper ist ein Wert, siehe
   unten): leicht abgesetzte Fläche, Versalien mit etwas Sperrung. */
.data-table thead th {
  background: var(--table-head-bg);
  text-transform: uppercase;
  letter-spacing: .03em;
}
/* Trägt die erste Spalte den Namen der Zeile, gehört dort ein
   <th scope="row"> hin — damit eine Vorlesehilfe jede Zelle der Zeile diesem
   Namen zuordnen kann. Optisch ist das aber kein Spaltenkopf, sondern der
   wichtigste Wert der Zeile: also Lesegröße und Textfarbe, nicht die gedämpfte
   Kopfzeilenschrift. Ohne diese Regel stand der Gerätename kleiner und blasser
   da als seine eigene IP-Adresse. */
.data-table tbody th {
  color: var(--text);
  font-size: var(--fs-sm);
  white-space: normal;
}

/* Stehenbleibender Tabellenkopf — ausdrücklich zum Anschalten und nicht als
   Vorgabe: die Kopfleiste der App klebt selbst schon oben, und ein Tabellenkopf
   mit top:0 verschwände beim Scrollen einfach dahinter. --stick-top sagt, wie
   weit unterhalb er stehenbleiben soll (Vorgabe: Höhe der Kopfleiste), oder 0,
   wenn die Tabelle in einem eigenen Scrollbereich sitzt. */
.data-table.is-sticky th {
  position: sticky;
  top: var(--stick-top, var(--shell-header-h, var(--header-h)));
  z-index: 1;
  background: var(--table-head-bg);
  box-shadow: 0 1px 0 var(--border);
}
.data-table tbody tr:hover { background: var(--accent-wash); }
.data-table td.mono,
.data-table td .mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
.data-table td.num,
.data-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table tbody tr:last-child > :is(td, th) { border-bottom: 0; }

/* Lange Freitextspalten auf eine Zeile kürzen, statt die Zeilenhöhe zu
   sprengen. Der volle Text bleibt über title und beim Öffnen erreichbar. */
.data-table td.cell-clamp {
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Waagerecht scrollbar auf schmalen Schirmen — die einzige Ausnahme von
   "nichts scrollt waagerecht", und genau dafür gedacht. */
.table-wrap { overflow-x: auto; }

/* --- Faktenliste -----------------------------------------------------------
   Bezeichnung links, Wert rechts, feine Trennlinien. Bewusst kein Raster aus
   Kästchen: eine Faktenliste liest man von oben nach unten, und Spalten
   zwingen das Auge dabei ins Zickzack. */
.facts { margin: 0; }
.facts > div,
.fact {
  display: flex;
  gap: var(--space-4);
  align-items: baseline;
  padding: var(--space-2) 0;
  border-top: 1px solid var(--border);
  font-size: var(--fs-sm);
}
.facts > div:first-child,
.fact:first-child { border-top: 0; padding-top: 0; }
.facts dt { flex: 0 0 11rem; color: var(--muted); font-size: var(--fs-xs); }
.facts dd { margin: 0; flex: 1; min-width: 0; overflow-wrap: anywhere; }
.facts dd.mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
.facts dd.is-empty { color: var(--muted); }
@media (max-width: 520px) {
  .facts > div, .fact { flex-direction: column; gap: 0; }
  .facts dt { flex: none; }
}

/* --- Datenblatt ------------------------------------------------------------
   Dieselben Wertepaare, andere Aufgabe: Beschriftung ÜBER dem Wert, mehrere
   Spalten nebeneinander. Für zwanzig kurze Angaben zu einem Gerät (Name, Typ,
   IP, Seriennummer …) ist das die richtige Form — die Faktenliste darüber wäre
   dafür zwanzig Zeilen lang. Umgekehrt gehört ein langer Wert in die
   Faktenliste und nicht hierher.

   overflow-wrap: anywhere ist hier nicht Kosmetik: eine SHA-256-Prüfsumme ist
   64 Zeichen ohne eine einzige Umbruchstelle und schiebt sich sonst quer über
   die Nachbarspalte. Genau das ist in doku passiert, nachdem beim
   Zusammenführen des Systems auffiel, dass .field-grid dort ZWEI verschiedene
   Dinge bezeichnet hat — ein Formularraster und dieses Datenblatt. Das
   Formularraster heißt in doku weiterhin .field-grid, das Datenblatt jetzt
   hier und einmal. */
.facts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-3) var(--space-5);
  margin: 0;
}
.facts-grid > div { min-width: 0; }
.facts-grid dt { font-size: var(--fs-xs); color: var(--muted); margin-bottom: 1px; }
.facts-grid dd {
  margin: 0;
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}
.facts-grid dd.mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
.facts-grid dd.is-empty { color: var(--muted); }

/* --- Dialog ----------------------------------------------------------------
   Liegt über der Seite, also größter Radius und stärkster Schatten. */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-7) var(--space-4);
  overflow-y: auto;
  animation: fade-in var(--dur-1) var(--ease-out);
}
.dialog-card {
  position: relative;
  width: 100%;
  max-width: 460px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-5);
  animation: dialog-in var(--dur-2) var(--ease-out);
}
.dialog-card > h2 { margin: 0 0 var(--space-4); font-size: var(--fs-md); }
.dialog-card .actions {
  margin-top: var(--space-5);
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  flex-wrap: wrap;
}

/* Aufklappmenü — Konto, Werkzeuge, Zeilenmenü. Dieselbe Ebene wie der Dialog. */
.menu {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2);
  z-index: 200;
  animation: menu-in var(--dur-1) var(--ease-out);
}
.menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.6rem;
  color: var(--text);
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--fs-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
}
.menu-item:hover { background: var(--accent-wash); color: var(--accent); }
.menu-item:focus-visible { background: var(--accent-wash); color: var(--accent); }

/* „⋯ Mehr"-Menü im Ansichtskopf (shared/more-menu.js): alles, was nicht täglich gebraucht wird.
   Geöffnet setzt more-menu.js die Liste fest ans Fenster (position: fixed) und klappt sie nach oben
   auf, wenn unten kein Platz ist — die Werte hier gelten nur als Rückfall ohne Skript. */
.more-menu { position: relative; display: inline-block; }
.more-menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  min-width: 14rem;
  max-width: min(22rem, calc(100vw - 2rem));
  max-height: min(70vh, calc(100dvh - 6rem));
  overflow-y: auto;
}
.more-menu-list.is-left { right: auto; left: 0; }
.more-menu-list[hidden] { display: none; }
.menu-sep { height: 1px; background: var(--border); margin: var(--space-2) 0; }
.menu-label {
  padding: 0.35rem 0.6rem 0.2rem;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.menu-item .menu-hint { display: block; font-size: var(--fs-xs); color: var(--muted); }

/* Aufklappbarer Abschnitt für Seltenes innerhalb einer Ansicht („Erweitert", Checkliste,
   Verknüpfungen …): <details class="fold"><summary>Titel <span class="fold-count">2</span></summary>…</details> */
.fold {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--card);
}
.fold > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  font-weight: 600;
  list-style: none;
  border-radius: var(--radius-lg);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(-45deg);
  transition: transform var(--dur-1) var(--ease-out);
  flex: none;
}
.fold[open] > summary::before { transform: rotate(45deg); }
.fold > summary:hover { color: var(--accent); }
.fold-count { font-weight: 400; color: var(--muted); font-size: var(--fs-sm); }
.fold > .fold-body { padding: 0 var(--space-4) var(--space-4); }

/* Vorschlagsliste für <input list> (shared/combobox.js, geladen von app-shell.js).
   Sieht aus wie die aufgeklappte <select>-Liste: Karte, Rand, Schatten, Zeilen
   mit Hover- und Tastaturmarkierung. popover = Top-Layer, liegt auch über Dialogen. */
.combo-pop {
  position: fixed;
  inset: auto;
  margin: 0;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-base);
}
.combo-opt {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 0.5rem 0.65rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.combo-opt:hover, .combo-opt.is-active { background: var(--accent-wash-strong); }
.combo-opt.is-active { outline: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); }
.combo-hint { font-size: var(--fs-xs); color: var(--muted); }

/* Einklappbare Karte (shared/fold.js): <section class="card" data-fold="…">.
   Kopf bleibt stehen, Knopf mit Chevron rechts; eingeklappt verschwindet die
   Trennlinie unter dem Kopf. Vorher nur in tickets, seit 2026-09-23 für alle. */
.fold-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex-wrap: nowrap; }
/* Die Überschrift gibt nach, Knöpfe im Kopf und der Klappknopf bleiben in der Zeile. */
.fold-head > :where(h2, h3) { flex: 1 1 auto; min-width: 0; }
.fold-head > .fold-toggle { margin-left: auto; }
.fold-head > .fold-title { cursor: pointer; }
section.is-folded > .fold-head { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }
.fold-toggle { width: 32px; height: 32px; }
.fold-toggle svg { transition: transform var(--dur-1) var(--ease-out); }
.fold-toggle[aria-expanded="false"] svg { transform: rotate(-90deg); }
@media (prefers-reduced-motion: reduce) { .fold-toggle svg { transition: none; } }
@media print { .fold-card-body[hidden] { display: block !important; } }
/* In einer .stack-Karte wandert der Inhalt in .fold-card-body; der Abstand
   zwischen den Kindern muss dort weitergelten. */
.stack > .fold-card-body > * + * { margin-top: var(--stack-gap, var(--space-4)); }


/* ============================================================================
   6  ZUSTAND — laden, leer, fertig
   ========================================================================= */

/* Statt eines Kringels die Form dessen, was gleich kommt. Dazu gehört immer ein
   .sr-only-Text mit der eigentlichen Ansage ("Lädt Geräte …"), weil das
   Flimmern für einen Screenreader nichts bedeutet. */
.loading-state { padding: var(--space-3) 0; }
.skeleton-line,
.skeleton-block {
  background: linear-gradient(90deg, var(--border) 25%, var(--card) 37%, var(--border) 63%);
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.4s ease infinite;
  border-radius: var(--radius-sm);
}
.skeleton-line { height: 0.9rem; margin: 0.5rem 0; }
.skeleton-block { height: 4rem; margin: 0.75rem 0; }
@keyframes skeleton-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* Ein leerer Bildschirm ist eine Aufforderung, keine Entschuldigung. Er sagt,
   was als Nächstes zu tun ist, und hat den Knopf dafür gleich dabei. */
.empty-state {
  display: grid;
  place-content: center;
  justify-items: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  background: var(--card);
  text-align: center;
  color: var(--muted);
}
.empty-state .icon { color: var(--muted); margin: 0 auto var(--space-3); display: block; }
.empty-state h2 { font-size: var(--fs-md); color: var(--text); margin: 0 0 var(--space-1); }
.empty-state p { margin: 0 auto; font-size: var(--fs-sm); max-width: 46ch; }
.empty-state p + .btn,
.empty-state p + .cluster { margin-top: var(--space-4); }

/* Kurze Rückmeldung nach einer Aktion, direkt an der Stelle, an der sie
   ausgelöst wurde. Sie verschwindet von selbst und hält nichts auf. */
.flash {
  font-size: var(--fs-sm);
  color: var(--ok);
  font-weight: var(--fw-semibold);
  animation: fade-in var(--dur-1) var(--ease-out);
}
.flash-error { color: var(--danger); }


/* ============================================================================
   7  RAHMEN — Kopfleiste und Kontomenü
   ========================================================================= */

.app-header {
  position: sticky;
  top: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  /* Seiten, deren <body> den Inhalt selbst mittig setzt, würden die Leiste
     sonst auf Inhaltsbreite schrumpfen statt sie über die Seite zu spannen. */
  width: 100%;
  align-self: stretch;
  min-height: var(--header-h);
  padding: 0 clamp(var(--space-4), 4vw, var(--space-7));
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.app-header .brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-base);
  color: var(--text);
  text-decoration: none;
}
.app-header .brand .sep { color: var(--muted); font-weight: var(--fw-normal); }
/* Die Klickfläche war die reine Zeilenhöhe (rund 23px). padding-block vergrößert
   sie auf 44, das negative margin-block nimmt den Platz wieder zurück — die
   Leiste bleibt 64px hoch, das Ziel ist trotzdem groß genug. */
.app-header .brand a {
  color: inherit;
  text-decoration: none;
  padding-block: 0.7rem;
  margin-block: -0.7rem;
}
/* Die Unterstreichung beim Überfahren ist nicht Zierde: im Kopf erben alle
   Segmente dieselbe Farbe, damit die Leiste ruhig bleibt — ohne sie ist einem
   Segment nicht anzusehen, ob es ein Weg zurück oder nur eine Ortsangabe ist. */
.app-header .brand a:hover { color: var(--accent); text-decoration: underline; }
/* Das letzte Segment ist der Ort, an dem man steht, und kein Ziel. Gedämpft
   gesetzt, damit der Unterschied zu den anklickbaren Segmenten davor auch im
   Ruhezustand sichtbar ist. */
.app-header .brand [aria-current="page"] { color: var(--muted); font-weight: var(--fw-normal); }
.app-header nav { display: flex; align-items: center; gap: var(--space-3); }
.app-header .ghost-link {
  color: var(--muted);
  text-decoration: none;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.app-header .ghost-link:hover { color: var(--accent); }

.account-menu { position: relative; }
.account-btn {
  font-weight: var(--fw-bold);
  font-size: var(--fs-sm);
  background: var(--accent-solid);
  color: #fff;
  border: none;
  border-radius: 50%;
}
.account-dropdown {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  width: 220px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2);
  z-index: 60;
  animation: menu-in var(--dur-1) var(--ease-out);
}
.account-info { padding: 0.4rem 0.6rem 0.7rem; }
.account-name { font-weight: var(--fw-semibold); font-size: var(--fs-sm); }
.account-email { font-size: var(--fs-xs); color: var(--muted); word-break: break-all; }
.account-dropdown hr { margin: 0.3rem 0; }
.account-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.6rem;
  color: var(--text);
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--fs-sm);
  text-decoration: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.account-item:hover { background: var(--accent-wash); color: var(--accent); }
/* Ein Eintrag, der etwas löscht oder abmeldet, hebt sich vom Rest des Menüs ab. */
.account-item.is-danger,
.menu-item.is-danger { color: var(--danger); }
.account-item.is-danger:hover,
.menu-item.is-danger:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }

@media (max-width: 640px) {
  .app-header {
    height: auto;
    flex-wrap: wrap;
    row-gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
  }
  .app-header nav { flex-wrap: wrap; row-gap: var(--space-2); }
}


/* ============================================================================
   7b  KONSOLE — Bausteine, die erst entstehen, wenn 15 Werkzeuge eine
       gemeinsame Oberfläche werden (Ops-Console, 2026-09-22)
   ----------------------------------------------------------------------------
   Alles hier benutzt die Alias-Namen aus Abschnitt 1 und funktioniert dadurch
   in beiden Themen ohne eigenen Dark-Block. Die App-Shell selbst (Kopfzeile,
   Palette, Sheet-Inhalt) steht in shared/app-shell.css — hier steht nur, was
   auch eine App ohne Shell einzeln benutzen kann.
   ========================================================================= */

/* --- Toast -----------------------------------------------------------------
   Rückmeldung zu etwas, das gerade passiert ist, aber nicht an der Stelle
   steht, an der man hinsieht (Speichern im Hintergrund, Kundenwechsel). Für
   Rückmeldung AM Auslöser bleibt .flash richtig — ein Toast für etwas, das
   direkt unter dem Zeiger steht, ist eine Wanderung des Blicks ohne Grund.
   Die Region ist EINMAL auf der Seite, role="status"/aria-live="polite", und
   app-shell.js legt sie an, falls sie fehlt. */
.toast-region {
  position: fixed;
  bottom: var(--space-5);
  right: var(--space-5);
  z-index: 210;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: min(360px, calc(100vw - 2 * var(--space-5)));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-left: 3px solid var(--status-ok);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: var(--space-3) var(--space-4);
  font-size: var(--fs-sm);
  color: var(--text-primary);
}
.toast-warn { border-left-color: var(--status-warn); }
.toast-danger { border-left-color: var(--status-danger); }
.toast-info { border-left-color: var(--accent); }
/* Mehrzeilige Meldungen (\n) aus stackDialog.notice/error bleiben mehrzeilig. */
.toast, .stack-modal-text { white-space: pre-line; }
/* Gefahr-Knopf als Hauptaktion eines Bestätigungsdialogs (stackDialog.confirm). */
.btn-danger-solid { background: var(--danger-solid); border-color: var(--danger-solid); color: #fff; }
.btn-danger-solid:hover { filter: brightness(1.08); }
@media (prefers-reduced-motion: no-preference) {
  .toast { animation: fade-in var(--dur-2) var(--ease-out); }
}
/* Der Kundenchip sitzt auf schmalen Schirmen unter der Kopfleiste, die
   Assistenten-Schaltfläche unten rechts — der Toast weicht ihr aus. */
@media (max-width: 640px) {
  .toast-region {
    left: var(--space-3);
    right: var(--space-3);
    bottom: calc(1.5rem + 52px + var(--space-3));
    max-width: none;
  }
}

/* --- Teilausfall -----------------------------------------------------------
   Eine Ansicht, die aus fünf Quellen zusammenläuft, hat einen Zustand, den es
   vorher nicht gab: sie stimmt, aber nicht vollständig. Das ist weder Fehler
   noch Erfolg, und darum weder .callout-danger noch stilles Weglassen — sonst
   hält man eine halbe Liste für die ganze. Immer mit Zahl ("3 von 5") und
   immer mit einem Weg zurück (erneut versuchen). */
.degraded-banner {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  background: color-mix(in srgb, var(--status-warn) 12%, var(--surface));
  border: 1px solid var(--status-warn);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-sm);
  color: var(--text-primary);
}
.degraded-banner-icon { display: inline-flex; color: var(--status-warn); flex-shrink: 0; }

/* --- Filterleiste ----------------------------------------------------------
   Beschriftung, Pillen, Zurücksetzen — in dieser Reihenfolge. Das Zurücksetzen
   steht rechts außen und ist immer da, auch wenn nichts gefiltert ist: ein
   Knopf, der auftaucht, sobald man ihn braucht, wird genau dann gesucht. */
.filterbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border-default);
}
.filterbar-label { font-size: var(--fs-xs); color: var(--text-secondary); }
.filterbar-reset { margin-left: auto; }
@media (max-width: 640px) {
  .filterbar-reset { margin-left: 0; }
}

/* --- Sidepanel -------------------------------------------------------------
   Detail neben dem Zusammenhang, statt Detail anstelle des Zusammenhangs. Ein
   Dialog nimmt die Liste weg, aus der man kommt; das Panel lässt sie stehen.
   Trotzdem modal (role="dialog" aria-modal="true" + Fokusfalle aus
   shared/dialog.js), weil dahinter nichts bedient werden soll. */
.sidepanel-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.4);
}
.sidepanel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(420px, 100vw);
  z-index: 201;
  background: var(--surface-raised);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
}
.sidepanel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-default);
}
.sidepanel-head h2 { margin: 0; font-size: var(--fs-md); }
.sidepanel-body { padding: var(--space-4); overflow-y: auto; }
.sidepanel-foot {
  margin-top: auto;
  padding: var(--space-4);
  border-top: 1px solid var(--border-default);
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  flex-wrap: wrap;
}
@media (max-width: 640px) { .sidepanel { width: 100vw; } }
@media (prefers-reduced-motion: no-preference) {
  .sidepanel { animation: sidepanel-in var(--dur-2) var(--ease-out); }
}

/* --- Sheet -----------------------------------------------------------------
   Das Menü von unten. Auf dem Telefon ist der untere Rand die Stelle, die der
   Daumen erreicht — ein Aufklappmenü oben rechts ist dort die schlechteste
   denkbare Position. Gleiche Regeln wie beim Dialog: Fokusfalle, Escape,
   Scroll-Sperre am <body>. */
.sheet-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.5);
}
.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 201;
  max-height: 80vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-raised);
  border-top: 1px solid var(--border-default);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-lg);
  padding: var(--space-3) var(--space-4) max(var(--space-4), env(safe-area-inset-bottom));
}
.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
}
.sheet-head h2 { margin: 0; font-size: var(--fs-md); }
.sheet-section { margin-top: var(--space-3); }
.sheet-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-2) var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  text-decoration: none;
  font-size: var(--fs-base);
  background: none;
  border: 0;
  width: 100%;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}
.sheet-item:hover { background: var(--accent-wash); color: var(--accent); }
.sheet-item[aria-current="page"] { background: var(--accent-wash-strong); font-weight: var(--fw-semibold); }
@media (prefers-reduced-motion: no-preference) {
  .sheet { animation: sheet-in var(--dur-2) var(--ease-out); }
}

/* --- Aufmerksamkeitszeile --------------------------------------------------
   Eine Zeile der Attention Queue: links ein Streifen und ein Wort für die
   Dringlichkeit, in der Mitte Objekt/Problem/Zusammenhang, rechts der eine
   Schritt, der etwas daran ändert.

   Der Streifen ist Farbe und trägt deshalb nichts allein. Das Zeichen und das
   Wort daneben tragen die Aussage: ● kritisch, ! Warnung, i Info. Wer die
   Farbe nicht sieht, liest dasselbe. */
.attention-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border-default);
  border-left: 3px solid var(--text-secondary);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}
.attention-row + .attention-row { margin-top: var(--space-2); }
.attention-row.is-critical { border-left-color: var(--status-danger); }
.attention-row.is-warning { border-left-color: var(--status-warn); }
.attention-row.is-info { border-left-color: var(--status-info); }
.attention-severity {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  min-width: 6.5rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-secondary);
}
.attention-row.is-critical .attention-severity { color: var(--status-danger); }
.attention-row.is-warning .attention-severity { color: var(--status-warn); }
.attention-row.is-info .attention-severity { color: var(--status-info); }
.attention-sign { font-size: var(--fs-sm); line-height: 1; }
.attention-main { flex: 1 1 18rem; min-width: 0; }
.attention-object { font-weight: var(--fw-semibold); font-size: var(--fs-base); }
.attention-problem { margin: 2px 0 0; font-size: var(--fs-sm); }
.attention-meta {
  margin: var(--space-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--text-secondary);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
}
.attention-action { flex: 0 0 auto; margin-left: auto; }
@media (max-width: 640px) {
  .attention-action { margin-left: 0; width: 100%; }
}

/* --- Speicherstand ---------------------------------------------------------
   Ein Formular, das von selbst speichert, muss sagen, wo es damit steht. Die
   drei Zustände heißen im Text "Speichert …", "Gespeichert ✓" und "Speichern
   fehlgeschlagen ✕" — mit Zeichen, nicht nur in Farbe. Das Element trägt
   role="status", damit der Wechsel auch angesagt wird. */
.autosave-status {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-xs);
  color: var(--text-secondary);
}
.autosave-status.is-saved { color: var(--status-ok); }
.autosave-status.is-error { color: var(--status-danger); font-weight: var(--fw-semibold); }

/* --- Dichte Tabelle --------------------------------------------------------
   Zum Anschalten, nicht als Vorgabe: dichter heißt mehr Zeilen im Blick und
   weniger Ruhe. Richtig für eine Liste, die man überfliegt; falsch für eine,
   in der man liest. */
.data-table.is-compact th,
.data-table.is-compact td { padding: 0.3rem 0.5rem; }
.data-table.is-compact { line-height: var(--lh-snug); }

/* --- Zeilen-Link -----------------------------------------------------------
   Wenn eine Tabellenzeile ein Objekt IST, ist die ganze Zeile der Weg dorthin,
   nicht nur das eine Wort in der ersten Spalte. Der Link spannt über ::after
   eine unsichtbare Fläche über die Zeile; Tastaturfokus und Vorlesereihenfolge
   bleiben am Text, weil der Link dort steht. Andere Bedienelemente derselben
   Zeile werden angehoben, sonst lägen sie unter der Fläche.

   `> :is(td, th)` und nicht nur `> td`: die erste Zelle einer Datenzeile ist
   oft `<th scope="row">` — so macht es status. Stand der Link dort, bekam die
   Zeile keinen Bezugsrahmen, während `.row-link::after` trotzdem galt. Die
   Fläche bemaß sich dann am nächsten positionierten Vorfahren, und weil es
   keinen gab, am Ansichtsfenster: jede Zeile legte eine bildschirmgroße
   unsichtbare Klickfläche über die Seite, die letzte gewann. Ein Klick
   irgendwo öffnete den zuletzt gerenderten Eintrag.

   Deshalb ist die Fläche jetzt an denselben Kontext gebunden: ein `.row-link`
   außerhalb einer Tabellenzeile spannt gar keine mehr auf. Eine Regel, die nur
   zusammen mit einer zweiten stimmt, muss dieselbe Bedingung tragen. */
.data-table tbody tr:has(> :is(td, th) > .row-link) { position: relative; cursor: pointer; }
.row-link { color: var(--text-primary); text-decoration: none; font-weight: var(--fw-semibold); }
.row-link:hover { color: var(--accent); text-decoration: underline; }
.data-table tbody tr:has(> :is(td, th) > .row-link) .row-link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.data-table tbody tr:has(> :is(td, th) > .row-link) :where(button, input, select, a:not(.row-link)) {
  position: relative;
  z-index: 1;
}

/* --- Kartenliste -----------------------------------------------------------
   Der Ersatz für eine Tabelle auf einem Schirm, auf dem sie nicht lesbar ist.
   Zwei Wege, je nachdem, was die App ohnehin hat:

     .card-list                  eigenes Markup, wenn die Ansicht sowieso
                                 clientseitig gebaut wird
     .data-table.is-cards        dieselbe Tabelle, unter 640px als Karten —
                                 jede Zelle braucht data-label="Spaltenname",
                                 sonst steht der Wert ohne Beschriftung da,
                                 sobald der Tabellenkopf weg ist. */
.card-list { display: flex; flex-direction: column; gap: var(--space-3); }
.card-list-item {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  padding: var(--space-3) var(--space-4);
}
.card-list-title { font-weight: var(--fw-semibold); font-size: var(--fs-base); }
.card-list-field {
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
  align-items: baseline;
  padding: 2px 0;
  font-size: var(--fs-sm);
}
.card-list-field > .label { color: var(--text-secondary); font-size: var(--fs-xs); flex: 0 0 auto; }

@media (max-width: 640px) {
  /* :is(td, th) überall, weil die Namensspalte einer Datenzeile ein
     <th scope="row"> sein soll — sonst fiele genau sie aus der Kartenansicht. */
  .data-table.is-cards,
  .data-table.is-cards tbody,
  .data-table.is-cards tbody tr,
  .data-table.is-cards tbody :is(td, th) { display: block; }
  .data-table.is-cards thead { display: none; }
  .data-table.is-cards tbody tr {
    border: 1px solid var(--border-default);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
    padding: var(--space-3) var(--space-4);
  }
  .data-table.is-cards tbody tr + tr { margin-top: var(--space-3); }
  .data-table.is-cards tbody :is(td, th) {
    display: flex;
    gap: var(--space-3);
    justify-content: space-between;
    align-items: baseline;
    border-bottom: 0;
    padding: 2px 0;
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
    text-align: left;
  }
  .data-table.is-cards tbody :is(td, th)[data-label]::before {
    content: attr(data-label);
    color: var(--text-secondary);
    font-size: var(--fs-xs);
    flex: 0 0 auto;
  }
}


/* ============================================================================
   8  BEWEGUNG UND DRUCK
   ========================================================================= */

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes dialog-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to { opacity: 1; transform: none; }
}
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
/* Kommt aus der Richtung, in der es liegt — das Panel von rechts, das Sheet von
   unten. Der Weg ist kurz (16px, nicht die volle Breite): er sagt, woher etwas
   kam, ohne dass man ihm zusehen muss. */
@keyframes sidepanel-in {
  from { opacity: 0; transform: translateX(16px); }
  to { opacity: 1; transform: none; }
}
@keyframes sheet-in {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: none; }
}

/* Der eine Moment beim Ankommen. Nicht auf jeder Karte einzeln — eine Kette
   nacheinander einschwebender Abschnitte ist das verbreitetste Erkennungsmerkmal
   einer erzeugten Seite und verzögert außerdem das Lesen.

   animation-fill-mode ist absichtlich `backwards` und nicht `both`: mit `both`
   bliebe die Schlussstellung dauerhaft angewandt, und eine dauerhaft gesetzte
   transform-Eigenschaft macht main zum Bezugsrahmen für jedes position:fixed
   darin — Dialoge und Zeilenmenüs säßen danach falsch. Mit `backwards` gilt nur
   die Anfangsstellung vor dem Start, danach nichts mehr. */
@media (prefers-reduced-motion: no-preference) {
  main { animation: view-in var(--dur-2) var(--ease-out) backwards; }
  @keyframes view-in {
    from { opacity: 0; transform: translateY(4px); }
  }
}

/* Was "reduzierte Bewegung" wirklich heißt: keine Bewegung — nicht: keine
   Rückmeldung. Die frühere Regel hier war ein `*, *::before, *::after`-Blanket
   und hat jede Animation UND jede Übergangsdauer stackweit auf 0,01ms gesetzt.
   Damit verschwand auch das, was gar keine Bewegung ist: der Farbwechsel eines
   Filters beim Umschalten, der Rahmen eines fokussierten Feldes, das Aufblenden
   einer Erfolgsmeldung. Für jemanden, der Bewegung nicht verträgt, sind genau
   diese Zustandswechsel die einzige verbleibende Rückmeldung — sie wegzuwerfen
   ist keine Rücksicht, sondern ein zweiter Schaden. WCAG 2.3.3 meint Bewegung
   (Weg, Parallaxe, Laufschrift), nicht Zustand.

   Deshalb gezielt: die layoutbewegten und dekorativen Keyframes aus, das
   Schimmern des Skeletts durch eine ruhige Fläche ersetzt, Opazitäts-Antworten
   nur verkürzt, Farb- und Fokusübergänge unangetastet. */
@media (prefers-reduced-motion: reduce) {
  main, .dialog-card, .overlay, .account-dropdown, .menu, .sheet, .sidepanel,
  .toast, .skeleton-line, .skeleton-block,
  *[class*="parallax"] {
    animation: none !important;
  }
  .skeleton-line, .skeleton-block { background: var(--border); }
  /* Kurze Opazitäts-Rückmeldung bleibt sichtbar, nur schneller statt bewegt. */
  .flash, .flash-error, .btn[aria-busy="true"]::before, .btn.is-busy::before {
    transition-duration: 120ms;
  }
  /* Fokus-/Statuswechsel (Farbe, Rahmen, outline) behalten ihre normale Dauer —
     darum steht hier bewusst keine Zeile dafür. */
  html { scroll-behavior: auto; }
}

@media print {
  /* Die Shell ist Weg, kein Inhalt — auf Papier gibt es keinen Weg. Sie steht
     hier mit, weil sonst jede App sie einzeln wieder ausblenden müsste
     (emergency und runbooks drucken ihre Ansichten wirklich). */
  .app-header, .no-print,
  .shell-nav, .shell-customer-bar, .shell-palette, .shell-palette-overlay,
  .toast-region, .sheet, .sheet-overlay, .skip-link { display: none !important; }
  body { background: #fff; color: #000; }
  .data-table, .data-table thead th, .empty-state { background: none; }
  main { max-width: none; padding: 0; }
  a { color: inherit; text-decoration: none; }
  .card, .list-row, .data-table { box-shadow: none; }
}


/* ---------------------------------------------------------------------------
   IBM Plex Sans + Mono, selbst gehostet aus shared/fonts/.
   Ersetzt den frueheren @import von fonts.googleapis.com: das hielt die Seite an
   Google fest (DSGVO) und war der Grund, warum die CSP nicht scharf geschaltet
   werden konnte - googleapis.com stand weder in style-src noch gstatic in font-src.
   Lokal ausgeliefert deckt das bestehende "https://auth.emha.app" in beiden
   Direktiven alles ab. Pfade sind relativ zu dieser Datei.
   --------------------------------------------------------------------------- */
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
  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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3twJwl1FgtIU.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3twJwlRFgtIU.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3twJwl9FgtIU.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgtIU.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgg.woff2) format('woff2');
  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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3vAOwl1FgtIU.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3vAOwlRFgtIU.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3vAOwl9FgtIU.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3vAOwl5FgtIU.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/-F6qfjptAgt5VM-kVkqdyU8n3vAOwlBFgg.woff2) format('woff2');
  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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxTKYbABA.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxaKYbABA.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxdKYbABA.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxRKYbABA.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxQKYbABA.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2) format('woff2');
  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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxTKYbABA.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxaKYbABA.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxdKYbABA.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxRKYbABA.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxQKYbABA.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2) format('woff2');
  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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxTKYbABA.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxaKYbABA.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxdKYbABA.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxRKYbABA.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxQKYbABA.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2) format('woff2');
  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;
}
/* cyrillic-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxTKYbABA.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxaKYbABA.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxdKYbABA.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxRKYbABA.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxQKYbABA.woff2) format('woff2');
  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;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2) format('woff2');
  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;
}

/* Logos in Druckansichten (shared/print-brand.js): IT-Betrieb links, Kunde rechts, nur im Druck. */
.print-brand { display: none; }
@media print {
  .print-brand { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin: 0 0 12px; min-height: 0; }
  .print-brand img { max-height: 44px; max-width: 45%; object-fit: contain; }
  .print-brand img[hidden] { display: none; }
  .print-brand .print-brand-customer { margin-left: auto; }
}
