/* FlowSuite — Salon Design System
   Custom styles that Tailwind CDN cannot handle. Token reference: CLAUDE.md. */

/* ── Self-hosted fonts (OFL-licensed) ───────────────────────────── */
@font-face {
    font-family: 'Geist';
    src: url("/static/fonts/Geist-Regular.c7c6c5403943.woff2") format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Geist';
    src: url("/static/fonts/Geist-Medium.217fdb926a60.woff2") format('woff2');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Geist Mono';
    src: url("/static/fonts/GeistMono-Regular.aedf2d1c7257.woff2") format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Geist Mono';
    src: url("/static/fonts/GeistMono-Medium.7bf7a1dfc74d.woff2") format('woff2');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}

/* ── Body baseline ──────────────────────────────────────────────── */
html, body {
    font-family: 'Geist', ui-sans-serif, system-ui, sans-serif;
}

/* Theme-aware baseline (fs-Tokens schalten in .dark). Die Tailwind-Utility
   bg-fs-bg/text-fs-fg auf <body> gewinnt per Spezifitaet, dies ist der
   robuste Fallback fuer den Moment vor Stylesheet-Parse + Druck/Mail. */
body {
    background: var(--color-fs-bg, #F6F1EA);
    color: var(--color-fs-fg, #241B14);
}

/* ── Salon utility classes ──────────────────────────────────────── */
.tnum {
    font-variant-numeric: tabular-nums;
}

.placeholder-stripes {
    background-image: repeating-linear-gradient(
        135deg,
        rgba(0, 0, 0, 0.04) 0,
        rgba(0, 0, 0, 0.04) 1px,
        transparent 1px,
        transparent 8px
    );
}

.sidebar-transition {
    transition: width 220ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.nav-label-transition {
    transition: opacity 160ms ease;
    white-space: nowrap;
    overflow: hidden;
}

/* ── Trefferflaeche: gross fuer den Finger, unveraendert fuers Auge ─ */
/* Ein Zeichen darf klein aussehen und muss trotzdem zu treffen sein. Die drei
   Klassen legen eine unsichtbare Flaeche ueber das Element, ohne seine Optik,
   seinen Platzbedarf oder das Layout anzuruehren: `position: absolute` nimmt
   das Pseudo-Element aus dem Fluss, es wird also weder Flex- noch Gitterkind.

   DIE MASSE SIND GEMESSEN, NICHT GEWAEHLT (2026-08-23, staging.svede.de,
   390x844, `docs/qa/tippziel-klasse-2026-08-23.md`). Zu jedem betroffenen
   Baustein wurde per `elementFromPoint` abgetastet, wie weit seine Flaeche
   wachsen darf, bevor sie einem Nachbarn Tipper wegnimmt. Wer eine Zahl hier
   aendert, misst vorher nach — sonst oeffnet sich kuenftig ein Erklaertext,
   wo jemand ins Feld tippen wollte. */
.tippziel-44,
.tippziel-40,
.tippziel-44-senkrecht,
.tippziel-feldhilfe,
.tippziel-zellenknopf {
    position: relative;
}

/* Die Zusage: 44 x 44 px, mittig auf dem Element, gleich wie gross es ist.
   Gemessen frei an allen Fundstellen, an denen sie verwendet wird — das
   Zeilenmenue (32x32, ringsum >= 32 px frei), die Monatswechsel im Kalender
   (32x32, >= 36 px) und das Aufgaben-Kaestchen im Flow (16x16, >= 30 px).
   Beim Kaestchen sitzt sie auf der `<label>`, weil ein `<input>` als ersetztes
   Element gar kein Pseudo-Element erzeugt. */
.tippziel-44::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

/* 40 x 40 statt 44 — und der Grund steht hier, damit ihn niemand fuer eine
   Nachlaessigkeit haelt: Der Verwerfen-Knopf der Flow-Karte sitzt in einer
   umbrechenden Knopfzeile mit `gap-1`. Seine Nachbarn stehen also 4 px
   entfernt, gemessen nach oben genau 4. Eine 44er-Flaeche nimmt dem Knopf
   „Erledigt" darueber 2 px weg. */
.tippziel-40::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 40px;
    height: 40px;
    transform: translate(-50%, -50%);
}

/* 44 px HOCH, so breit wie das Element — mehr geht dort nicht, und das ist
   gemessen (2026-08-24, staging.svede.de, 390x844 und 1440x900,
   `docs/qa/tippziel-fenster-2026-08-24.md`). Die fuenf Knoepfe im Menue
   „Weitere Bedienelemente" stehen genau 4 px auseinander; ein 44er-Quadrat
   braucht 6 je Seite und griffe damit in die Flaeche des Nachbarn. Nach oben
   sind 21 px frei, nach unten 60 — die Hoehe kostet niemanden etwas.

   WARUM NICHT EINFACH 40 x 40, das waere symmetrisch und passte in die 4 px:
   Zwei benachbarte 40er-Flaechen treffen sich in der Luecke und beanspruchen
   sie BEIDE. Ein Tipp dazwischen loeste danach die Aktion eines der beiden
   Nachbarn aus statt gar keiner — aus totem Raum wuerde ein Fehlgriff. Sven
   hat am 2026-08-24 die senkrechte Fassung gewaehlt.

   `width: 100%` loest gegen die POLSTERBOX auf, ist bei einem Element mit
   Rahmen also 2 px schmaler als sein sichtbarer Kasten. Das ist die
   ungefaehrliche Richtung — die Flaeche wird kleiner, nie groesser.

   NICHT AN EIN ELEMENT, DAS UMBRECHEN KANN — dort wirkt sie nicht, und man
   sieht es nicht (TL-2698, gemessen 2026-09-06). Der Container-Block eines
   absolut gesetzten Nachfahren eines *inline* Elements wird aus dessen
   Fragmenten gebildet, und Chromium spannt ihn waagerecht von der linken Kante
   des ERSTEN bis zur rechten Kante des LETZTEN Fragments — zwei Kanten aus
   verschiedenen Zeilen. `width: 100%` loest dann gegen etwas auf, das mit der
   Ausdehnung des Elements nichts zu tun hat, und wird negativ auf null
   geklemmt. Nachgemessen ueber 28 Containerbreiten an derselben Beschriftung:
   7 einzeilige Lagen decken das Element zu 100 %, alle 21 umbrechenden zu
   0 %, 9 % oder 48 % — nie voll (`docs/qa/tippziel-wirkungsprobe-2026-09-06.md`).
   Heute trifft das keine Fundstelle: Jede der rund 40 traegt eine
   Anzeige-Klasse, ist Kind eines Flex-Kastens oder einwortig. Wer eine neue
   setzt, prueft es — `scripts/e2e/tor2_trn_antippflaechen.py` weist je Element
   `fragmente` aus, `tests/test_tippziel_wirkung.py` haelt die Kopplung.

   UND NICHT AN EINEN VERWEIS MITTEN IM SATZ (Sven, 2026-09-06, U-E46 Gruppe E
   — Weg (a), `docs/design/entscheidung-fliesstext-verweis-tippziel.md`). Dort
   ist eine 44 px hohe Flaeche kein toter Raum, den man gewinnt, sondern Text,
   den man verliert: Ein Tipp auf ein anderes Wort navigierte. Gemessen ueber
   acht Fundstellen laegen am Telefon 12.909,2 px2 fremder Text unter den
   Flaechen. Die Zugaenglichkeits-Richtlinien nehmen diese Bauform aus
   demselben Grund ausdruecklich von der Mindestgroesse aus. Wo ein solcher
   Verweis wirklich groesser werden soll, wird der SATZ umgebaut und der
   Verweis auf eine eigene Zeile gestellt — dann waechst er in Polsterung und
   gewinnt echten toten Raum. */
.tippziel-44-senkrecht::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 100%;
    height: 44px;
    transform: translate(-50%, -50%);
}

/* Das Hilfezeichen „?" am Feldnamen: 44 breit, 30 hoch, nach rechts versetzt.
   Rund geht hier nicht, und das ist der interessante Teil der Messung: Unter
   dem Zeichen beginnt nach 2 bis 8 px DAS FELD, das es erklaert, links nach
   4 bis 6 px seine Beschriftung. Nach rechts dagegen ist die Beschriftungs-
   zeile leer — an allen 30 Fundstellen mindestens 53 px bis zum Zeilenende.
   Der Platz ist rechts, nicht ringsum: eine symmetrisch um 12 px gewachsene
   Flaeche kollidierte an 30 von 30 Stellen und haette dem Feld die oberen
   8 px genommen. */
.tippziel-feldhilfe::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 30px;
    /* Mittig auf das Zeichen, dann 10 px nach rechts und 5 px nach oben
       versetzt. Daraus wird, auf das 16 x 16 px grosse Zeichen bezogen:
       4 px links, 24 px rechts, 12 px oben, 2 px unten — genau die vier
       gemessenen Grenzen.

       WARUM NICHT `top/right/bottom/left` MIT NEGATIVEN WERTEN, das war die
       erste Fassung: Ein absolut positioniertes Pseudo-Element bezieht sich
       auf die POLSTERBOX seines Elternteils, nicht auf dessen Rahmenbox. Das
       Zeichen traegt `border` — 1 px ringsum. Aus den vier Versaetzen wurden
       so 42 x 28 px statt 44 x 30, am Browser nachgemessen. Die Mitten-Form
       ist randfest: `left: 50%` trifft die Mitte der Polsterbox, und die ist
       auch die Mitte der Rahmenbox. */
    transform: translate(calc(-50% + 10px), calc(-50% - 5px));
}

/* Der Knopf „ändern" in einer Zelle des Auslastungsrasters: 44 breit,
   40 hoch, nach UNTEN versetzt. Gemessen am 2026-09-03 auf
   `staging.svede.de` mit `elementFromPoint`, Konto `e2e.wleitung`, bei
   390, 820 und 1440 px — der Knopf ist 39 x 16 px gross und steht
   192-mal auf der Seite (`docs/qa/tippziel-werkstatt-planung-2026-09-03.md`).

   Die vier gemessenen Grenzen, jeweils der SCHMALSTE Wert ueber alle drei
   Breiten: oben **4**, unten **26**, links **32**, rechts **60**.

   Daraus folgt die Form zwingend. Eine mittige Flaeche braucht die halbe
   Zunahme nach oben — 14 px bei 44, 12 px bei 40 —, und dort sind nur 4 px
   frei. Sie naehme dem Text darueber („X h geplant" bzw. „keine Kapazitaet")
   seine Zeile weg. Also asymmetrisch: 4 px nach oben, 20 px nach unten.

   WARUM 40 HOCH UND NICHT 44 — das ist die Entscheidung, nicht die Rechnung
   (Sven, 2026-09-03): Bei 44 waeren es 4 oben und 24 unten. Der naechste
   Knopf steht 26 px darunter und greift seinerseits 4 px nach oben; zwei
   benachbarte Flaechen ueberlappten sich dann um 2 px, und ein Tipp
   dazwischen loeste die Aktion einer FREMDEN Zeile aus. Das ist woertlich
   der Fehler, den `.tippziel-44-senkrecht` weiter oben verwirft: „aus totem
   Raum wuerde ein Fehlgriff". Bei 40 bleiben 26 - 20 - 4 = **2 px** Luecke,
   und die Flaechen beruehren sich nie.

   Die Hoehe waechst damit von 16 auf 40 px (Faktor 2,5), die Breite von 39
   auf 44. */
.tippziel-zellenknopf::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 40px;
    /* Mittig auf den Knopf, dann 8 px nach unten versetzt. Auf die 39 x 16 px
       bezogen ergibt das 4 px oben und 20 px unten — genau die gemessenen
       Grenzen. Der Knopf traegt weder Rahmen noch Polsterung, Polster- und
       Rahmenbox fallen also zusammen; die Mitten-Form ist trotzdem gewaehlt,
       weil sie das auch dann bliebe, wenn spaeter ein Rahmen dazukaeme
       (dieselbe Falle wie bei `.tippziel-feldhilfe`). */
    transform: translate(-50%, calc(-50% + 8px));
}

/* ── Alpine.js cloak (hide elements until Alpine initializes) ──── */
[x-cloak] {
    display: none !important;
}

/* ── Sidebar scrollbar ──────────────────────────────────────────
   War bis 2026-07-30 ein 4px breiter Balken in #3A2C1F — einem
   Salon-Braun aus dem retired Design, das auf der dunklen Leiste
   praktisch unsichtbar blieb. Damit gab es kein Signal, dass unter
   der Fensterkante noch Einträge kommen: gemessen lagen 19 von 38
   dort. Jetzt 8px in einem Ton, der sich vom Sidebar-Hintergrund
   abhebt.

   Das Sichtbarmachen ist die RÜCKFALLLINIE, nicht die Lösung — die
   trägt der Gruppen-Zuschnitt in _sidebar.html. Ein Menü, in dem man
   scrollen muss, um ein Modul zu finden, ist auch mit gutem Balken
   kaputt. */
.sidebar-scroll {
    scrollbar-width: thin;
    scrollbar-color: var(--color-fs-border-strong, #6B5B4D) transparent;
}

.sidebar-scroll::-webkit-scrollbar {
    width: 8px;
}

.sidebar-scroll::-webkit-scrollbar-track {
    background: transparent;
}

.sidebar-scroll::-webkit-scrollbar-thumb {
    background-color: var(--color-fs-border-strong, #6B5B4D);
    border-radius: 4px;
    /* Rand in Sidebar-Farbe: lässt den Balken schlanker wirken, ohne
       ihn wieder unsichtbar zu machen. */
    border: 2px solid var(--color-fs-sidebar-bg, #1A1613);
    background-clip: padding-box;
}

.sidebar-scroll::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-fs-fg-hint, #8A7B6D);
}

/* Verlauf am unteren Rand der Navigation: zeigt an, dass unterhalb
   der sichtbaren Fläche noch etwas kommt. Wird von Alpine nur dann
   eingeblendet, wenn der Inhalt wirklich überläuft — ein dauerhaft
   sichtbarer Verlauf würde den letzten Eintrag verwaschen, obwohl
   nichts folgt. */
.sidebar-fade {
    background: linear-gradient(
        to bottom,
        transparent,
        var(--color-fs-sidebar-bg, #1A1613)
    );
}

/* ── Hover-only behaviours (skip on touch devices) ─────────────── */
@media (hover: none) {
    aside[data-sidebar] {
        pointer-events: auto;
    }
}

/* ── Anmelde-Flaechen: Knopfbreite gehoert dem Layout (TL-1526) ── */
/* `allauth/elements/button.html` gibt jedem Knopf `fs-auth-btn w-full` mit —
   auf der Anmeldeseite richtig (schmales Formular, der Knopf fuellt es).
   `allauth/layouts/manage.html` setzt `auth-manage` auf den Inhaltsbereich der
   EINGELOGGTEN Flaechen; dort ist er so breit wie die App, und ein `w-full`-Knopf
   spannte sich ueber 1120 von 1216 px (gemessen 2026-08-23, sechs Knoepfe auf
   drei Seiten). Zwei Klassen schlagen die eine Tailwind-Utility, die Reihenfolge
   der Stylesheets spielt also keine Rolle. */
.auth-manage .fs-auth-btn {
    width: auto;
}

/* Dieselbe Regel eine Zeile weiter: Auch das Eingabefeld ist fuer das schmale
   Anmeldeformular gebaut (`.auth-input` ist `width:100%`). Im breiten
   Inhaltsbereich stand „E-Mail-Adresse" ueber 1056 px — ein Feld, dessen Breite
   nichts ueber seinen Inhalt sagt. 28rem ist die Breite des Anmeldeformulars. */
.auth-manage .auth-input,
.auth-manage input:not([type="checkbox"]):not([type="radio"]),
.auth-manage select,
.auth-manage textarea {
    max-width: 28rem;
}

/* ── Anmelde-Flaechen: der Drittanbieter als Bedienelement ─────── */
/* allauth liefert `elements/provider.html` als nacktes `<li><a>` aus — auf
   `/accounts/3rdparty/` stand „Microsoft" dadurch als Text ohne erkennbares
   Bedienelement (TL-1526). Die Vorlage ist ueberschrieben; diese Regel haelt nur
   die Aufzaehlungspunkte davon ab, neben den Knoepfen zu stehen. */
.fs-provider-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* ── Auth form inputs ──────────────────────────────────────────── */
/* Styles for Django-rendered form widgets inside .auth-form containers.
   Required because {{ field }} outputs raw <input> without Tailwind classes,
   and CDN Tailwind does not support @apply. */

.auth-form input[type="text"],
.auth-form input[type="email"],
.auth-form input[type="password"],
.auth-form input[type="number"],
.auth-form input[type="tel"],
.auth-form input[type="url"],
.auth-form input[type="search"],
.auth-form input[type="date"],
.auth-form input[type="datetime-local"],
.auth-form input[type="time"],
.auth-form input[type="month"],
.auth-form input[type="week"],
.auth-form select,
.auth-form textarea,
.auth-input {
    width: 100%;
    border-radius: 0.5rem;
    border: 1px solid var(--color-fs-input-border);
    padding: 0.625rem 0.75rem;
    font-size: 0.875rem;
    line-height: 1.25rem;
    color: var(--color-fs-fg);
    background-color: var(--color-fs-surface);
    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.auth-form input::placeholder,
.auth-input::placeholder {
    color: var(--color-fs-fg-hint);
}

.auth-form input:focus,
.auth-form select:focus,
.auth-form textarea:focus,
.auth-input:focus {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-fs-tenant) 30%, transparent);
    border-color: var(--color-fs-tenant);
}

/* Error state */
.auth-form .errorlist + input,
.auth-form input.is-invalid,
.auth-form :is(input, select, textarea)[aria-invalid="true"] {
    border-color: var(--color-fs-on-surface-danger);
}

.auth-form .errorlist + input:focus,
.auth-form input.is-invalid:focus,
.auth-form :is(input, select, textarea)[aria-invalid="true"]:focus {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-fs-on-surface-danger) 30%, transparent);
    border-color: var(--color-fs-on-surface-danger);
}

/* Django error list styling */
.auth-form .errorlist {
    list-style: none;
    padding: 0;
    margin: 0 0 0.25rem 0;
}

.auth-form .errorlist li {
    font-size: 0.875rem;
    color: var(--color-fs-danger-500);
}

/* ── Global form inputs (all app forms inside main content) ────── */
/* Styles for Django-rendered form widgets inside <main>.
   Works alongside @tailwindcss/forms plugin reset.
   Scoped to main to avoid interference with admin panel, sidebar, cluster.

   DIE TYPLISTE IST VOLLSTAENDIG ZU HALTEN (TL-4140). Tailwinds Preflight
   setzt jede Randbreite auf 0; einen Rand bekommt ein Feld hier nur, wenn
   sein Typ in dieser Liste steht. Bis zum 2026-09-29 fehlten
   `datetime-local`, `time`, `month` und `week` — gemessen an der
   Reiseabrechnung: Beginn und Ende standen als „tt.mm.jjjj, --:--" ohne
   jeden Kasten da, als Eingabefeld gar nicht erkennbar. Die Liste oben
   (.auth-form) fuehrt dieselben Typen. Waechter:
   tests/test_feldregel_typen.py haelt jeden Typ, den ein Formular oder eine
   Vorlage rendert, gegen beide Listen. */

main input[type="text"],
main input[type="email"],
main input[type="password"],
main input[type="number"],
main input[type="tel"],
main input[type="url"],
main input[type="search"],
main input[type="date"],
main input[type="datetime-local"],
main input[type="time"],
main input[type="month"],
main input[type="week"],
main select,
main textarea {
    width: 100%;
    border-radius: 0.5rem;
    border: 1px solid var(--color-fs-input-border);
    padding: 0.625rem 0.75rem;
    font-size: 0.875rem;
    line-height: 1.25rem;
    color: var(--color-fs-fg);
    background-color: var(--color-fs-surface);
    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* ── Ein gesperrtes Feld muss man SEHEN (TL-2043) ───────────────────────
   Django rendert `field.disabled = True` als `disabled`-Attribut, und der
   Server ignoriert dann jeden POST auf dieses Feld. Das ist die richtige
   Zusage — sie war nur unsichtbar: Bis zum 2026-08-29 hatte dieses
   Stylesheet **keine einzige** `:disabled`-Regel (gemessen, `grep -c` = 0),
   und die Feldregel darueber gilt unveraendert weiter. Am gesperrten Ende
   eines aktiven Vertrags gemessen: gleicher Hintergrund (`rgb(255,255,255)`),
   gleiche Schriftfarbe, gleicher Rand, `opacity: 1` wie am editierbaren
   Nachbarfeld. Der einzige Unterschied war der Mauszeiger — und ausgerechnet
   ein Datumsfeld traegt ohnehin `cursor: default`, auch wenn es offen ist.

   Ein Feld, das aussieht wie editierbar und Eingaben stillschweigend
   verwirft, ist genau die Fehlerform, gegen die dieses Regelwerk sonst
   gebaut ist: nichts schlaegt fehl, es passiert nur nichts.

   DIE KLASSE, NICHT DER EINZELFALL: `disabled = True` steht an elf Stellen
   in sieben Modulen (werkstatt, betreiber, discovery, accounts,
   customfields, sourcing, wishlist) — an allen war der Zustand bisher
   unsichtbar. Die Regel ist rein additiv: Sie greift nur, wo ein Feld
   ohnehin schon gesperrt ist.

   Tokens statt fester Farben, damit der Dark-Mode mitschaltet;
   `fs-fg-muted` auf `fs-surface-sunken` ist das etablierte Paar fuer
   „vorhanden, aber zurueckgenommen" (Design-System, Badge „Info/Neutral"). */
main input:disabled,
main select:disabled,
main textarea:disabled {
    background-color: var(--color-fs-surface-sunken);
    color: var(--color-fs-fg-muted);
    cursor: not-allowed;
    box-shadow: none;
}

/* Schmale Zahlen-/Mengeneingaben (Minuten am Werktisch, Menge in einer Zeile).

   Die Feldregel darueber setzt `width: 100%` ueber einen Typ-Selektor
   (Spezifitaet 0,0,1,1). Eine einzelne Utility-Klasse am Element (`w-20`,
   Spezifitaet 0,0,1,0) verliert dagegen — das Feld wird trotzdem breit, und
   zwar lautlos: nichts schlaegt fehl, die Zeile bricht nur um. Deshalb hier
   mit ausreichender Spezifitaet statt am Element. */
main input.eingabe-schmal {
    width: 5.5rem;
}

/* ── Pflicht-Wächter des Auswahlbausteins ───────────────────────────────
   `select_required` rendert seit TL-1371 neben dem Sternchen ein echtes
   Formularfeld, das der Browser prüfen KANN. Das versteckte Feld daneben
   (`type="hidden"`) kann es nicht: hidden-Felder sind von der
   Constraint-Validierung ausgenommen — gemessen im Chrome, `checkValidity()`
   meldet dort `true`, obwohl `required` dransteht. Der Marker versprach also
   an 72 Stellen etwas, das nichts aufhielt.

   Unsichtbar, aber NICHT `display:none`/`visibility:hidden`: ein Pflichtfeld,
   das der Browser nicht fokussieren kann, blockt das Absenden **stumm** und
   schreibt nur „An invalid form control … is not focusable" in die Konsole.

   **Die Höhe ist 1px und NICHT 0 — hier stand bis zum 2026-09-07 das
   Gegenteil, und es war falsch** (EK-23, TL-2778). Der Satz lautete: „Höhe 0
   bei voller Breite … heißt: die Blase erscheint direkt unter dem
   Auswahlfeld". Sie erscheint dort nicht. Chrome verankert die
   Constraint-Blase an einem Rechteck, und ein Rechteck mit Höhe 0 ist keines
   — gemessen am echten Block auf einer nackten Seite: bei `height: 0` **keine
   Blase**, bei `height: 1px` steht sie da, an derselben Stelle, mit demselben
   Layout (das Feld ist absolut positioniert und deckungsgleich unsichtbar).

   **Der Fehler blockte damit stumm, obwohl alles andere richtig war.** Der
   Nutzer klickt Speichern: kein POST, keine Meldung, keine Konsolenausgabe,
   die Adresse bleibt stehen. Gemessen am 2026-09-07 auf `staging.svede.de`
   an der Eingangsrechnungs-Maske — und die Konsole ist der Beleg, dass es
   NICHT am Fokus liegt: bliebe das Feld unfokussierbar, stünde dort „not
   focusable". `document.activeElement` steht nach dem Klick auf dem Wächter.

   Die drei anderen Eigenschaften sind gemessen unschuldig: `opacity: 0`,
   `pointer-events: none` und `position: absolute` lassen die Blase in jeder
   Kombination erscheinen, sobald die Höhe nicht 0 ist. Wer hier aufräumt,
   fasst deshalb `height` nicht an.

   Warum hier und nicht als Utility-Klassen am Element: die Feldregel oben
   greift über einen Typ-Selektor (`main input[type="text"]`, Spezifität
   0,0,1,1) und gäbe dem Wächter Padding, Rand und damit ~38 px Höhe — eine
   einzelne Utility (`h-0`, 0,0,1,0) verliert dagegen lautlos. Dieselbe Falle
   wie bei `eingabe-schmal` darüber. */
main input.pflicht-waechter,
input.pflicht-waechter {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    /* 1px, nicht 0 — sonst hat die Constraint-Blase keine Verankerungsflaeche
       und die Sperre wird stumm. Begruendung und Messung im Block darueber. */
    height: 1px;
    min-height: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    box-shadow: none;
    pointer-events: none;
}

/* ── Terminal: Finger statt Maus ────────────────────────────────────────
   Die Feldregel oben gilt fuer die ganze App und setzt `font-size: 0.875rem`
   sowie ein Padding, das auf etwa 38 px Hoehe hinauslaeuft. Am Tablet ist das
   zu klein: Doc 07 (Terminal-PWA) macht **44 px Trefferflaeche und eine
   Schriftstufe groesser** zur Auflage — und genau diese Auflage wurde an
   Eingabefeldern bisher lautlos unterlaufen. Eine Utility am Element
   (`text-base`, Spezifitaet 0,0,1,0) verliert gegen den Typ-Selektor der
   App-Regel (0,0,1,1), ohne dass etwas fehlschlaegt; dieselbe Mechanik wie bei
   `eingabe-schmal` eine Regel weiter oben.

   Gefunden beim Bau der Zaehlflaeche (W-6) — der ersten Terminal-Flaeche,
   deren Hauptbedienelement ein Eingabefeld ist (die Werkbank arbeitet mit
   Knoepfen). Die Regel wirkt auf JEDE Terminal-Flaeche, also auch rueckwirkend
   auf die Werkbank; ausserhalb von `.terminal-flaeche` aendert sie nichts.

   Bewusst KEINE `width` hier: die Breite bleibt Sache von `eingabe-schmal`
   bzw. der App-Regel, sonst haette diese Regel zwei Zustaendigkeiten. */
main.terminal-flaeche input[type="text"],
main.terminal-flaeche input[type="email"],
main.terminal-flaeche input[type="password"],
main.terminal-flaeche input[type="number"],
main.terminal-flaeche input[type="tel"],
main.terminal-flaeche input[type="url"],
main.terminal-flaeche input[type="search"],
main.terminal-flaeche input[type="date"],
main.terminal-flaeche select,
main.terminal-flaeche textarea {
    min-height: 44px;
    font-size: 1rem;
    line-height: 1.5rem;
}

/* Suchfelder in Werkzeugleisten. Sie sollen die Zeile nicht auffuellen, aber
   auch nicht so schmal sein, dass man den Suchbegriff nicht mehr liest. Auf
   schmalen Geraeten faellt die Breite auf die volle Zeile zurueck.

   **DIESER SATZ STAND HIER SCHON, WAEHREND DIE REGEL DAS GEGENTEIL TAT**
   (TL-2477, Befund WS-28, behoben am 2026-09-04). Unter 640 px stand
   `width: 10rem` — also **160 px statt 224**, und eben nicht die volle Zeile.
   Im Browser gemessen: bei 1280 px ein 224 px breites Feld (198 innen), bei
   390 px ein 160 px breites (134 innen). Der Platzhalter der Werkbank
   („Auftrag, Uhr oder Kunde…") braucht **188,5 px** und war dort abgeschnitten;
   die beiden Lager-Felder derselben Klasse tragen 44 und 46 Zeichen und ebenso.
   Ein Textproblem war das keines mehr — es liess sich mit keinem Text loesen.

   Die Klasse traegt drei Fundstellen in zwei Modulen, und alle drei haben
   dieselbe Bauform: ein Feld in einer Flex-Zeile neben einem oder zwei
   Knoepfen. `width: 100%` heisst deshalb genau das, was der Satz oben zusagt —
   in einer umbrechenden Zeile bekommt das Feld eine eigene Zeile, in einer
   nicht umbrechenden schrumpft es auf den Platz neben den Knoepfen. Das
   verlangt `min-width: 0` (ein Eingabefeld traegt eine `size`-basierte
   Mindestbreite, die das Schrumpfen sonst blockiert und die Zeile ueberlaufen
   laesst) und `shrink-0` an den Knoepfen daneben. */
main input.eingabe-suche {
    width: 14rem;
    max-width: 100%;
}

@media (max-width: 640px) {
    main input.eingabe-suche {
        width: 100%;
        min-width: 0;
    }
}

main input::placeholder,
main textarea::placeholder {
    color: var(--color-fs-fg-hint);
}

/* Ein Platzhalter in einem Zahlenfeld darf nie wie ein Wert aussehen (TL-347).
   Im Beleg stand grau eine „19" als Platzhalter, direkt daneben eine „1" als
   echter Wert — auf dem Bildschirm nicht zu unterscheiden. Wer die Zeile so
   speicherte, bekam sie mit 0,00 € heraus, und ab dann JEDE weitere Position
   ebenfalls. Die Kursivstellung ist das Merkmal, das die Farbe allein nicht
   liefert: Grau ist auch die Farbe eines deaktivierten Feldes. */
main input[inputmode="decimal"]::placeholder,
main input[inputmode="numeric"]::placeholder,
main input[type="number"]::placeholder {
    font-style: italic;
    opacity: 0.75;
}

main input:focus,
main select:focus,
main textarea:focus {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-fs-tenant) 30%, transparent);
    border-color: var(--color-fs-tenant);
}

/* Checkboxes
   Die drei Regeln gelten für `main` UND `.auth-form`. Die Anmeldeflächen liegen
   NICHT in `main` (`allauth/layouts/entrance.html` baut ein eigenes Gerüst), und
   ihr Kästchen „Angemeldet bleiben" stand deshalb ungestylt da: Systemfarbe statt
   Petrol, kein Fokusring. Aufgefallen bei TL-562 — dort als Nebensatz („ohne CSS-
   Klassen"), weil der eigentliche Befund die fehlende Beschriftung war. */
main input[type="checkbox"],
.auth-form input[type="checkbox"] {
    width: 1rem;
    height: 1rem;
    border-radius: 0.25rem;
    border: 1px solid var(--color-fs-input-border);
    accent-color: var(--color-fs-tenant);
    cursor: pointer;
}

main input[type="checkbox"]:focus,
.auth-form input[type="checkbox"]:focus {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-fs-tenant) 30%, transparent);
}

main input[type="checkbox"]:checked,
.auth-form input[type="checkbox"]:checked {
    background-color: var(--color-fs-tenant);
    border-color: var(--color-fs-tenant);
}

/* Optionsfelder — dieselbe Akzentfarbe wie die Kästchen (B-ui2; Website-Prüfung
   vom 2026-10-04, Nachtrag Nr. 4 zu Nr. 30). Bis dahin setzte dieses Stylesheet
   `accent-color` nur für Kästchen, und Optionsfelder standen in Browser-Blau
   (#0075FF) — gemessen in der Einrichtung, hell UND dunkel. Bewusst NUR Akzent und
   Zeiger: Breite, Höhe und Rundung bleiben die des Browsers. Die Kästchen-Regel
   darüber setzt `border-radius`; mitkopiert würde aus dem Kreis ein Quadrat.
   Wächter: tests/test_color_scheme.py. */
main input[type="radio"],
.auth-form input[type="radio"] {
    accent-color: var(--color-fs-tenant);
    cursor: pointer;
}

/* Error states for form fields

   DER FEHLERZUSTAND MUSS GEGEN DIE FELDREGEL GEWINNEN (TL-4150). Die
   Feldregel oben ist ungelayert und schlaegt damit jede Utility — auch ein
   `border-fs-danger-700` am Feld. Gemessen am 2026-09-29: Ein Feld in <main>
   mit roter Randklasse rendert 1px `fs-input-border`, der Nutzer sieht nicht,
   WELCHES Feld falsch ist. Der Fehler haengt deshalb an einem Zustand, den
   diese Datei selbst kennt: `aria-invalid="true"`. Django setzt ihn an jedes
   ungueltige Feld, das ueber das BoundField gerendert wird
   (_form_field.html, _form_checkbox.html); der Auswahl-Baustein setzt ihn an
   seinem Knopf (_custom_select.html) — und Vorlesesoftware sagt „ungueltig".

   Spezifitaet (0,1,2) wie die Feldregel, deshalb gewinnt die SPAETERE Stelle:
   dieser Block steht hinter der Feldregel und hinter `main input:focus`.
   Farbe: dasselbe Token wie der Fehlertext darunter, schaltet im Dunkelmodus
   mit (SC 1.4.11 verlangt 3:1 — hell 6,47 · dunkel 5,87 auf fs-surface).
   Die Farbe traegt den Fehler nicht allein: der Satz darunter nennt ihn.
   Waechter: tests/test_fehlerrand.py. */
main .errorlist + input,
main .errorlist + select,
main .errorlist + textarea,
main :is(input, select, textarea, button)[aria-invalid="true"] {
    border-color: var(--color-fs-on-surface-danger);
}

main .errorlist + input:focus,
main .errorlist + select:focus,
main .errorlist + textarea:focus,
main :is(input, select, textarea, button)[aria-invalid="true"]:focus {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-fs-on-surface-danger) 30%, transparent);
    border-color: var(--color-fs-on-surface-danger);
}

main .errorlist {
    list-style: none;
    padding: 0;
    margin: 0 0 0.25rem 0;
}

main .errorlist li {
    font-size: 0.875rem;
    color: var(--color-fs-danger-500);
}

/* ── Lead-Board Drag & Drop (SortableJS) ────────────────────────── */
/* Platzhalter-Karte während des Ziehens (ghostClass). */
.lead-card-ghost {
    opacity: 0.4;
}
/* Aktive Spalte hebt sich beim Drüberziehen leicht ab. */
.lead-column.sortable-drag-over {
    background-color: var(--color-fs-surface-sunken);
}

/* ── Klebende Aktionsspalte einer Liste (U-26) ──────────────────── */
/*
 * Die Spalte mit dem Zeilenmenü bleibt am rechten Rand stehen, wenn die
 * Tabelle waagerecht rollt. Ohne sie ist das Menü genau dann weg, wenn man es
 * am ehesten braucht: bei einer breiten Tabelle auf einem schmalen Schirm.
 * Beim Vorbild (zeitgenau) steht es bei x = 1163 fest.
 *
 * WARUM ALS CSS-KLASSE UND NICHT ALS SECHS TAILWIND-KLASSEN JE LISTE:
 * Es sind sechs zusammengehörige Angaben (Haftung, Rand, Ebene, Hintergrund
 * hell, Hintergrund beim Überfahren, Trennlinie). Sechsmal abgeschrieben
 * driften sie — und der Fehler wäre unauffällig: ein durchscheinender
 * Hintergrund fällt erst auf, wenn tatsächlich gerollt wird. Eine Klasse gibt
 * dem Guard außerdem einen Anker, an dem er JEDE Liste prüfen kann.
 *
 * DER HINTERGRUND IST PFLICHT, NICHT SCHMUCK: Eine klebende Zelle ohne eigene
 * Fläche lässt den darunter wegrollenden Zelleninhalt durchscheinen.
 * `var(--color-fs-*)` schaltet mit dem Dunkelmodus um — rohe Farben täten das
 * nicht.
 */
.zeilenmenue-kopf,
.zeilenmenue-zelle {
    position: sticky;
    right: 0;
    z-index: 10;
    border-left: 1px solid var(--color-fs-border);
}

.zeilenmenue-kopf {
    background-color: var(--color-fs-surface-sunken);
}

.zeilenmenue-zelle {
    background-color: var(--color-fs-surface);
}

/*
 * Die Zeile färbt sich beim Überfahren; die klebende Zelle trägt ihre eigene
 * Fläche und bekäme davon nichts mit — sie bliebe als heller Block in einer
 * eingefärbten Zeile stehen.
 */
tr:hover > .zeilenmenue-zelle {
    background-color: var(--color-fs-surface-sunken);
}

/*
 * Ein geöffnetes Menü muss über den Zeilen DARUNTER liegen. Alle klebenden
 * Zellen teilen sich `z-index: 10`; bei Gleichstand gewinnt die spätere im
 * Dokument — das Menü der ersten Zeile verschwände also hinter der zweiten.
 * `:has()` hebt genau die Zelle an, deren Menü offen ist.
 */
.zeilenmenue-zelle:has(details[open]) {
    z-index: 20;
}

/*
 * ── Eine BLASSE Zeile darf ihr Menue nicht einsperren (UX-102, TL-2934) ──
 *
 * DER FEHLER, gemessen am 2026-09-10 im Browser bei 390 px: Auf
 * `/belege/einstellungen/belegtexte/` waren „Bearbeiten" und „Einschalten" im
 * offenen Zeilenmenue per `elementFromPoint` NICHT erreichbar — darueber lag
 * die klebende Zelle der NAECHSTEN Zeile. Und zwar obwohl beide z-index
 * korrekt trugen: die eigene Zelle 20 (die `:has()`-Regel darueber greift),
 * die fremde 10, das Menuefeld 30.
 *
 * DIE URSACHE IST `opacity`, NICHT DER Z-INDEX. Die drei Listen mit
 * Zeilenmenue faerben eine abgeschaltete Zeile blass — `<tr class="…
 * opacity-60">` (`faktura/_belegtext_liste.html`,
 * `faktura/_mahnstufe_liste.html`, `settings/team/_liste.html`). Ein
 * `opacity` unter 1 erzeugt einen **eigenen Stapelkontext**: Das `z-index: 30`
 * des Menuefelds gilt dann nur noch INNERHALB dieser Zeile, und die Zeile
 * selbst traegt keinen z-index. Die naechste Zeile mit ihrer klebenden Zelle
 * (z-index 10) liegt damit ueber dem GANZEN Menue. Kein Wert an Menue oder
 * Zelle kann das heilen — der Deckel sitzt eine Ebene hoeher.
 *
 * WARUM DIE REGEL HIER STEHT UND NICHT DREIMAL IM TEMPLATE: Es ist ein
 * Klassenfehler des Musters, nicht der einzelnen Liste. `opacity` von der
 * Zeile auf ihre Zellen zu verschieben hiesse, es in jeder Vorlage an jeder
 * Datenzelle zu wiederholen — und der naechste Traeger macht es wieder falsch.
 * Solange das Menue offen ist, hebt die Zeile ihre Blaesse auf; danach ist sie
 * Zeichen fuer Zeichen wieder blass. Fachlich ist das sogar richtig: Wer das
 * Menue geoeffnet hat, arbeitet gerade an dieser Zeile.
 *
 * `opacity: 1` erzeugt KEINEN Stapelkontext — genau das ist der Hebel.
 */
tr:has(> .zeilenmenue-zelle details[open]) {
    opacity: 1;
}

/* ── Der rollende Tabellenrahmen SAGT, dass rechts noch etwas ist (LG-N8) ──
 *
 * DER BEFUND (Lagerist, Tor-2-Lauf vom 2026-09-01, §10 Punkt 3): „Nichts zeigt
 * an, dass rechts noch etwas ist. Kein Verlaufsschatten am Rand, kein Pfeil,
 * kein angeschnittenes Zeichen. … Wer nicht auf die Idee kommt zu wischen,
 * haelt die Liste fuer vollstaendig."
 *
 * GEMESSEN am 2026-09-03 auf staging (Stand von `origin/main`, also mit allen
 * Spalten-Reparaturen): 12 von 22 Listenflaechen rollen bei 390 px waagerecht,
 * bei 768/1024/1280 px KEINE einzige. Auf allen zwoelf belegte die Rollleiste
 * 0 px (Overlay, im Ruhezustand unsichtbar), `box-shadow`, `mask-image` und
 * `background-image` standen auf `none`. Der schaerfste Fall war
 * `/lager/fehlbestaende/`: drei vollstaendige Ueberschriften, ein ganz
 * sichtbarer Knopf „Klaeren" — und 105 px Ueberlauf.
 * Vollstaendig: docs/qa/rollhinweis-390px-2026-09-03.md
 *
 * WIE ES OHNE EINE ZEILE JAVASCRIPT GEHT — vier Verlaeufe, zwei Haftungsarten:
 *
 *   (1)(2) Die DECKFLAECHEN haften `local`, rollen also MIT dem Inhalt. Sie
 *          liegen an dessen linkem und rechtem ENDE.
 *   (3)(4) Die SCHATTEN haften `scroll`, stehen also fest am Rahmen — links
 *          und rechts an seiner Kante.
 *
 * Im Ruhezustand liegt Deckflaeche (1) genau ueber Schatten (3): links ist
 * nichts verborgen, also ist dort auch kein Schatten zu sehen. Rollt man nach
 * rechts, wandert (1) aus dem Bild und gibt (3) frei. Rechts umgekehrt. Der
 * Hinweis erscheint damit GENAU dann, wenn hinter der Kante wirklich etwas
 * liegt — ohne Messung, ohne Ereignis, ohne Zustand.
 *
 * DIE REIHENFOLGE IST DER TRICK: In `background-image` liegt der ERSTE Verlauf
 * obenauf. Die Deckflaechen muessen also vor den Schatten stehen, sonst deckt
 * nichts.
 *
 * WARUM DIE DECKFLAECHE BREITER IST ALS DER SCHATTEN (40 gegen 16 px): So
 * schweigt die Regel bei einem winzigen Ueberlauf. Gemessen rollten drei der
 * zwoelf Flaechen nur 11, 17 und 19 px — dort verdeckt die mitgerollte
 * Deckflaeche den Schatten weiterhin ganz, und ein Hinweis auf wenige
 * abgeschnittene Bildpunkte waere Rauschen statt Auskunft.
 *
 * NACHGEMESSEN statt gerechnet (2026-09-03, `/kunden/`, Fensterbreite in
 * 5-px-Schritten, Helligkeit der aeussersten zwei Bildpunkte ueber den
 * Koerperzeilen): Bei 21 px Ueberlauf und darunter steht dort in hell exakt
 * 255,0 und in dunkel exakt 32,3 — also die unberuehrte Flaeche, kein
 * Schatten. Ab 26 px 238,1 bzw. 49,0, ab 36 px 205,2 bzw. 82,1. Die Schwelle
 * liegt damit zwischen 21 und 26 px, in beiden Modi an derselben Stelle.
 *
 * DIE FARBEN SIND TOKEN, KEINE WERTE. Die Deckflaeche ist `fs-surface` — die
 * Flaeche, auf der diese Rahmen sitzen. Der Schatten ist `fs-border-strong`,
 * das Token fuer „betonte Kante"; beide schalten im Dunkelmodus mit. Ein rohes
 * `rgba(0,0,0,…)` waere dort unsichtbar, ein rohes Weiss in der Deckflaeche
 * ein heller Streifen auf dunklem Grund.
 *
 * `:has(> table)` grenzt auf Tabellenrahmen ein. Ein `overflow-x-auto` um
 * etwas anderes — eine Zeitleiste, ein Codeblock, ein Aufklappmenue — bleibt
 * unberuehrt. Kennt ein Browser `:has()` nicht, faellt die ganze Regel weg:
 * kein Hinweis, aber auch kein Schaden.
 *
 * WAS DIE REGEL NICHT ERREICHT, ausgesprochen statt unterstellt:
 *
 *   - Der Hintergrund eines Rahmens liegt HINTER seinem Inhalt. Wo eine Zelle
 *     eine eigene deckende Flaeche traegt, steht der Schatten nicht davor. Das
 *     betrifft die Kopfzeile (`bg-fs-surface-sunken`) und die klebende
 *     Aktionsspalte aus U-26 (`.zeilenmenue-zelle`). Unter dem Zeilenkoerper —
 *     dort, wo der Lagerist gelesen hat — ist er da.
 *   - Waehrend eine Rueckfrage offen ist, setzt die Regel weiter unten
 *     `overflow: visible`. Der Rahmen ist dann kein Rollbehaelter mehr, `local`
 *     verhaelt sich wie `scroll`, und die Deckflaechen liegen wieder auf den
 *     Schatten: kein Hinweis, solange das Panel offen steht. Folgenlos — in
 *     diesem Zustand liest niemand die Tabelle.
 */
.overflow-x-auto:has(> table) {
    background-image:
        /* (1) (2) Deckflaechen — rollen MIT dem Inhalt. */
        linear-gradient(to right, var(--color-fs-surface), var(--color-fs-surface) 60%, transparent),
        linear-gradient(to left, var(--color-fs-surface), var(--color-fs-surface) 60%, transparent),
        /* (3) (4) Schatten — stehen fest am Rahmen. */
        linear-gradient(to right, color-mix(in srgb, var(--color-fs-border-strong) 90%, transparent), transparent),
        linear-gradient(to left, color-mix(in srgb, var(--color-fs-border-strong) 90%, transparent), transparent);
    background-position: left center, right center, left center, right center;
    background-repeat: no-repeat;
    background-size: 40px 100%, 40px 100%, 16px 100%, 16px 100%;
    background-attachment: local, local, scroll, scroll;
}

/*
 * Auf Papier gibt es kein Rollen — dort waeren die vier Verlaeufe nur zwei
 * graue Streifen an jeder Tabelle.
 */
@media print {
    .overflow-x-auto:has(> table) {
        background-image: none;
    }
}

/*
 * Eine offene Rueckfrage darf nicht vom scrollenden Tabellenrahmen
 * abgeschnitten werden (U-24).
 *
 * `components/_rueckfrage.html` legt sein Panel `absolute` ueber die Zeile.
 * Steht die Tabelle — wie ueblich — in einem `overflow-x-auto`-Rahmen, dann
 * BESCHNEIDET der: sobald eine Achse nicht `visible` ist, macht die CSS-Norm
 * aus dem `visible` der anderen ein `auto`. Der Rahmen scrollt also auch
 * senkrecht und schneidet alles ab, was aus ihm herausragt.
 *
 * Gemessen am 2026-08-19 auf `/vertretung/` (erster Aufrufer des Bausteins):
 * vom Panel blieb ein wenige Pixel hoher Streifen stehen, Folge-Satz,
 * Pflichtgrund und beide Knoepfe waren unerreichbar — in hell wie dunkel.
 * Weder Suite noch HTTP-Status noch HTML-Grep sehen das; gefunden hat es das
 * BILD.
 *
 * Die Regel steht hier und nicht am einbindenden Template, weil sie jeden
 * kuenftigen Aufrufer betrifft: 96 Vorlagen tragen einen solchen Rahmen.
 *
 * NACHGEZOGEN 2026-08-19 (zweiter Aufrufer, `faktura/_mahn_aktion.html`): die
 * erste Fassung nannte nur `overflow-x-auto` — und lief prompt in dieselbe
 * Falle eine Ebene hoeher. Die Mahnliste steckt in einer Karte mit
 * `overflow-hidden`, und die schneidet unbedingt in BEIDEN Achsen ab; die
 * Tabelle darin traegt zusaetzlich `overflow-x-auto`. Gemessen im Baum:
 * 27 Vorlagen mit `overflow-x-auto`, 26 mit `overflow-hidden`, 15 mit
 * `overflow-y-auto`, 2 mit `overflow-auto` — die erste Fassung deckte also
 * knapp die Haelfte der Faelle. Jetzt stehen alle vier da.
 *
 * Der Preis von `overflow-hidden`: es klammert meist die abgerundeten Ecken.
 * Waehrend eine Rueckfrage offen ist, sind die Ecken einer solchen Karte
 * wieder eckig. Das ist der bewusst gewaehlte Handel — eine unsichtbare
 * Rueckfrage ist teurer als eine Ecke.
 * Sie wirkt nur, solange wirklich eine Rueckfrage offen ist — der Rahmen
 * behaelt sein waagerechtes Scrollen in jedem anderen Zustand.
 *
 * ERWEITERT 2026-08-23 (U-21-Rest): `data-ueberlagert` nimmt jeden weiteren
 * Aufklapper auf, dessen Panel sich UEBER den Inhalt legt. Erster Nutzer ist
 * der Spaltenwaehler — ein `<details>` OHNE `name`, das die Regel darum
 * bauartbedingt nie erfassen konnte. Ihn nachtraeglich `rueckfrage-` zu
 * taufen waere die falsche Loesung gewesen: `name` gruppiert `<details>`
 * (gleichnamige schliessen einander), der Name traegt also Verhalten und
 * nicht nur eine Kennung. Wer kuenftig einen ueberlagernden Aufklapper baut,
 * setzt das Attribut und ist gedeckt — ohne diese Datei anzufassen.
 *
 * Das Attribut steht bewusst NICHT an jedem `<details>` des Repos. Die
 * meisten sind Akkordeons, die im Fluss aufklappen; fuer sie waere
 * `overflow: visible` falsch — ein aufgeklappter Textabschnitt soll seine
 * rollende Karte nicht aufsprengen.
 *
 * DIE GRENZE DER REGEL, gemessen am 2026-08-23 (Chromium, Laborseite).
 * Die beiden Achsen verhalten sich VERSCHIEDEN, und darauf beruht, wo die
 * Regel eingesetzt werden darf:
 *
 *   senkrecht (`overflow-y-auto` + `max-height`): `scrollTop` faellt auf 0
 *       und kommt beim Schliessen NICHT zurueck — gemessen 298 -> 0 -> 0.
 *   waagerecht (`overflow-x-auto`, keine Groessenbegrenzung): `scrollLeft`
 *       faellt zwar auf 0, kehrt aber vollstaendig zurueck — gemessen
 *       171 -> 0 -> 171.
 *
 * Deshalb ist die Regel an einem Tabellenrahmen folgenlos und dort wirksam:
 * in der Kontrollprobe wurden 2 von 4 verdeckten Eintraegen anklickbar, ohne
 * dass eine Rollposition verloren ging.
 * An einem Modal-Panel mit `max-h-[90vh] overflow-y-auto` waere sie dagegen
 * SCHAEDLICHER als der Beschnitt: die Maske spraenge beim Oeffnen der Liste an
 * den Anfang zurueck und bliebe dort. Wer die Regel auf eine neue Flaeche
 * zieht, prueft zuerst, ob deren Rahmen SENKRECHT rollt.
 * In einem Modal mit `max-h-[90vh] overflow-y-auto` waere sie dagegen
 * SCHAEDLICHER als der Beschnitt: die Maske spraenge beim Oeffnen der Liste
 * an den Anfang zurueck. Deshalb loest der Auswahl-Baustein sein
 * Beschneidungsproblem NICHT hierueber, sondern indem er seine Liste in den
 * vorhandenen Platz einpasst (`hoeheAnpassen` in `_custom_select.html`).
 *
 * AUSGENOMMEN seit dem 2026-10-08 (TL-4526): eine Rueckfrage mit
 * `data-rf-im-fluss`. Ihr Panel ueberlagert nicht, es steht im Fluss des
 * Schrittmenues — und genau dessen Deckel (`max-h` + `overflow-y-auto`) soll
 * sie mitrollen lassen. Hoebe die Regel ihn auf, laege der Fuss der Rueckfrage
 * im klebenden Vorgangskopf wieder unter der Bildkante. Das ist zugleich die
 * senkrechte Grenze von oben, einmal richtig herum genutzt.
 */
.overflow-x-auto:has(details[name^="rueckfrage-"][open]:not([data-rf-im-fluss])),
.overflow-y-auto:has(details[name^="rueckfrage-"][open]:not([data-rf-im-fluss])),
.overflow-auto:has(details[name^="rueckfrage-"][open]:not([data-rf-im-fluss])),
.overflow-hidden:has(details[name^="rueckfrage-"][open]:not([data-rf-im-fluss])),
.overflow-x-auto:has(details[data-ueberlagert][open]),
.overflow-y-auto:has(details[data-ueberlagert][open]),
.overflow-auto:has(details[data-ueberlagert][open]),
.overflow-hidden:has(details[data-ueberlagert][open]) {
    overflow: visible;
}

/* ── Der Fokus einer klickbaren Tabellenzeile ist SICHTBAR (U-20, TL-1925) ──
 *
 * Das Haus-Muster der klickbaren Zeile traegt seit dem 2026-08-24
 * `focus:outline-none focus:ring-2 focus:ring-fs-tenant/30`. An jedem anderen
 * Element ist das richtig — an einem `<tr>` ist es fast wirkungslos: Tailwinds
 * `ring` malt einen `box-shadow`, und eine Tabelle steht unter
 * `border-collapse: collapse` (Tailwinds Preflight setzt das). Ein `<tr>`
 * erzeugt dann keinen eigenen Rahmenkasten, an dem ein Schatten haengen
 * koennte; gezeichnet werden nur die waagerechten Kanten.
 *
 * GEMESSEN am 2026-08-28 (Chromium, `/lager/inventur/`, 1440 px, dreifache
 * Bildpunktdichte): Die Regel greift — `boxShadow` enthaelt
 * `oklab(0.455 …/0.3) 0px 0px 0px 2px`, ist also da. Sichtbar bleibt davon
 * eine leicht getoente Trennlinie unter der Zeile, wo vorher eine graue stand.
 * Wer eine Liste mit der Tabulatortaste durchgeht, kann daran nicht ablesen,
 * auf welcher Zeile er steht. Der Weg war seit TL-1602 offen und die Rueckmeldung
 * dazu nicht.
 *
 * `outline` statt `box-shadow`, weil ein Umriss NICHT Teil des Kastenmodells
 * ist: Er wird auch dort gezeichnet, wo `border-collapse` den Kasten der Zeile
 * einzieht. `outline-offset: -2px` legt ihn nach innen, sonst schnitte ihn der
 * `overflow-x-auto`-Rahmen an der ersten und letzten Zeile an.
 *
 * DIE SPEZIFITAET IST DER GANZE TRICK, und sie ist nachgerechnet: Die Vorlagen
 * tragen `focus:outline-none`, in der gebauten Datei
 * `.focus\:outline-none:focus` — eine Klasse plus eine Pseudoklasse, (0,2,0).
 * `tr[tabindex]:focus-visible` ist ein Element plus ein Attribut plus eine
 * Pseudoklasse, (0,2,1), und gewinnt damit. Im Browser gegengeprueft:
 * `getComputedStyle(...).outline` meldet `oklch(0.455 0.058 215) solid 2px`.
 *
 * `:focus` steht als zweite Zeile daneben, weil ein programmatisches
 * `element.focus()` — das die Pruefskripte fahren — `:focus-visible` je nach
 * vorheriger Eingabeart NICHT ausloest. Eine Zusage, die nur bei echter
 * Tastatureingabe haelt, ist nicht messbar.
 *
 * Die Regel wirkt NUR auf `<tr>` mit `tabindex`, also genau auf die Zeilen,
 * die das Haus-Muster tragen. Eine Tabelle ohne das Muster kann sie nicht
 * treffen. Der Farbwert ist das Akzent-Token und schaltet mit dem Dunkelmodus.
 */
tr[tabindex]:focus-visible,
tr[tabindex]:focus {
    outline: 2px solid var(--color-fs-tenant);
    outline-offset: -2px;
}

/* ── Hilfebereich: gesetzte Prosa (Scheibe H-2b) ─────────────────
   Die Anleitung entsteht aus docs/module/** und bringt Ueberschriften, Listen,
   Tabellen und Zitate mit. Tailwind setzt Elemente von Haus aus zurueck, also
   bekommt genau dieser Container seine Typografie zurueck — gescopet auf
   .fs-prosa, damit nichts davon in den Rest der Anwendung laeuft.
   Ausschliesslich semantische fs-Tokens: die Flaeche schaltet im Dunkelmodus mit. */
.fs-prosa > *:first-child { margin-top: 0; }
.fs-prosa > *:last-child { margin-bottom: 0; }
.fs-prosa p { margin: 0.75rem 0; line-height: 1.65; }
.fs-prosa h2,
.fs-prosa h3,
.fs-prosa h4,
.fs-prosa h5,
.fs-prosa h6 {
  color: var(--color-fs-fg);
  font-weight: 500;
  line-height: 1.35;
  margin: 1.5rem 0 0.5rem;
}
.fs-prosa h2 { font-size: 1rem; }
.fs-prosa h3 { font-size: 0.9375rem; }
.fs-prosa h4,
.fs-prosa h5,
.fs-prosa h6 { font-size: 0.875rem; }
.fs-prosa strong { color: var(--color-fs-fg); font-weight: 500; }
.fs-prosa ul,
.fs-prosa ol { margin: 0.75rem 0; padding-left: 1.25rem; }
.fs-prosa ul { list-style: disc; }
.fs-prosa ol { list-style: decimal; }
.fs-prosa li { margin: 0.35rem 0; line-height: 1.6; }
.fs-prosa li > ul,
.fs-prosa li > ol { margin: 0.35rem 0; }
.fs-prosa a { color: var(--color-fs-tenant); text-decoration: underline; }
.fs-prosa code {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.8125em;
  background: var(--color-fs-surface-sunken);
  border-radius: 4px;
  padding: 0.1em 0.35em;
}
.fs-prosa blockquote {
  border-left: 2px solid var(--color-fs-border-strong);
  padding-left: 0.875rem;
  margin: 1rem 0;
  color: var(--color-fs-fg-muted);
}
.fs-prosa hr { border: 0; border-top: 1px solid var(--color-fs-border); margin: 1.5rem 0; }
/* Breite Tabellen rollen in ihrem eigenen Rahmen — der Seitenrumpf darf nie
   waagerecht rollen (CLAUDE.md → Layout). */
.fs-prosa table {
  display: block;
  overflow-x: auto;
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0;
  font-size: 0.8125rem;
}
.fs-prosa th,
.fs-prosa td {
  border: 1px solid var(--color-fs-border);
  padding: 0.4rem 0.6rem;
  text-align: left;
  vertical-align: top;
}
.fs-prosa th { background: var(--color-fs-surface-sunken); color: var(--color-fs-fg); font-weight: 500; }

/* ── Onboarding-Modus: welche Darstellung der Feldhilfe sichtbar ist ── */
/* Beide stehen im HTML, sichtbar ist immer genau eine. Vorher entschied das
   eine Template-Bedingung je Baustein — und deshalb musste der Umschalter im
   Bedien-Cluster die ganze Seite neu aufbauen, was auf jeder Formularseite das
   Getippte wegwarf (TL-2426). Die Klasse setzt `base.html` aus
   `user.erklaerungen_anzeigen`; der Knopf dreht sie beim Klick sofort um und
   meldet den neuen Stand nebenher an den Server.

   Die Regeln stehen hier und nicht in `tailwind.src.css`: Sie greifen an einer
   Klasse an der Seitenwurzel, gehören also zu den globalen Regeln wie das
   Formular-Theming darüber, und ein Utility, das nur an genau zwei Bausteinen
   sitzt, wäre keins. */
html:not(.erklaerungen-an) .feldhilfe-satz { display: none; }
html.erklaerungen-an .feldhilfe-zeichen { display: none; }

/* Die Kontextspalte einer Vorgangsakte — Auskunft oder Streifen (TL-2426).

   Dieselbe Bauform eine Ebene groesser: Beide Zustaende stehen im HTML, die
   Klasse an der Seitenwurzel entscheidet. Der Schalter braucht dafuer keinen
   Seitenaufbau mehr und wirft nichts Getipptes weg.

   `display` und NICHTS anderes. Die Auskunft traegt in der Auftragsakte zwei
   Handgriffe mit Auswahl- und freien Textfeldern; `visibility`, `opacity` oder
   eine Hoehe von 0 nehmen ein Formular nicht aus der Tabfolge — es waere
   unsichtbar, aber mit der Tabulatortaste erreichbar und mit der Eingabetaste
   absendbar. Das ist der Unterschied zur Feldhilfe darueber, wo beide Seiten
   reiner Text sind.

   Die Spezifitaet traegt ohne `!important`: (0,2,1) gegen die (0,1,0) der
   Tailwind-Utilities an denselben Elementen — eine Media Query erhoeht die
   Spezifitaet nicht.

   **Nur ab 64rem (`lg`) — U-27a.** Darunter steht die Auskunft oben als
   zugeklappte Kopfzeile, und die IST das Zuklappen. Gaelte die Vorliebe am
   Konto auch dort, saehe, wer die Spalte am Schreibtisch zugeklappt hat, am
   Telefon gar nichts: keine Auskunft, keinen Streifen (der ist unter `lg`
   per `max-lg:hidden` verborgen). */
@media (min-width: 64rem) {
  html.kontextspalte-zu .kontextspalte-auskunft { display: none; }
  html:not(.kontextspalte-zu) .kontextspalte-streifen { display: none; }
}

/* Die Kontextspalte klebt nur, solange sie ins Bild passt (TL-4740, Weg (ii)).

   Ab `lg` klebt die Huelle (`lg:sticky lg:top-20`). Ist sie hoeher als das
   Fenster unter ihrem Klebeabstand — meist, weil ein Handgriff aufgeklappt
   ist —, kam ihr Fuss erst am Seitenende ins Bild: Rollen bewegte die
   klebende Spalte nicht. Das Skript in `components/_kontextspalte.html` setzt
   dann `data-rollt-mit`, und die Spalte rollt mit der Seite wie jeder andere
   Inhalt. Passt sie wieder, nimmt es die Marke zurueck.

   **Ohne Skript bleibt alles wie zuvor** — die Marke setzt nur das Skript.
   **Ungelayert, wie die Regeln darueber:** `lg:sticky` liegt in
   `@layer utilities`, und eine ungelayerte Regel schlaegt jede gelayerte.
   **Verworfen: Weg (i)**, ein Rollrahmen an der Spalte. Er brachte den Fuss
   nur durch Rollen IN der Spalte herein und schnitte jedes absolut stehende
   Panel darin ab (die Klasse aus TL-4703/TL-4739). Gemessen auf staging am
   2026-10-11, Entscheidung flowsuite-39.

   **Der Rollabstand gehoert dazu.** Rollt die Spalte mit, kann ihr Inhalt unter
   die klebende Kopfleiste (56 px) geraten — beim Kleben (80 px) ging das nie.
   Ohne `scroll-margin-top` legte ein `scrollIntoView` einen Ausloeser der Spalte
   auf y = 16,5, UNTER die Leiste; ein Klick dorthin traf die Leiste (beim Bau
   gemessen). Der Abstand kommt aus derselben gemessenen Groesse wie
   `.fs-sprungziel` (`--fs-klebestreifen`), nicht aus einer eigenen Zahl —
   Anweisung flowsuite-39, damit es EINE Antwort auf „was rollt unter die
   Leiste" gibt. Ueber der Spalte liegt davon nur die Kopfleiste, der Abstand ist
   dort also grosszuegig; zu knapp ist er nie.
   **Spezifitaet null, in `@layer components`:** Felder tragen dort einen eigenen
   Rollabstand (KQ-1, flowsuite-d4) — der soll in der Spalte gewinnen, nicht von
   einer ID-Regel ueberstimmt werden. Es ist ein Rand am ELEMENT, kein Polster an
   einem rollenden Behaelter: Zwei Raender addieren sich nicht (gemessen von
   flowsuite-d4), ein Polster an der Spalte haette es getan. */
@media (min-width: 64rem) {
  #kontextspalte[data-rollt-mit] { position: static; }
}
@layer components {
    @media (min-width: 64rem) {
        :where(#kontextspalte[data-rollt-mit]) :where(*) {
            scroll-margin-top: calc(var(--fs-klebestreifen) + 1.25rem);
        }
    }
}

/* ── Handlung oder Navigation — die Wirkungsrichtung ist SICHTBAR (U-E38) ──

   KONTRAST fs-tenant auf fs-bg — hell 6,80 · dunkel 6,07
   KONTRAST fs-tenant auf fs-surface — hell 7,12 · dunkel 5,34

   **Der Anlass ist gemessen, nicht gestalterisch.** Im Tor-2-Lauf vom 2026-09-01
   fand keine Rolle an einem vollen Arbeitstag mehr als ein Drittel dessen, was
   fuer sie gebaut ist (Befund `A-1`). Die Nachmessung hat den teuersten Teil
   davon isoliert: Auf `/belege/offene-posten/` stehen „Zu den Verbindlichkeiten"
   (fuehrt woandershin) und „Als Excel-Tabelle" (tut etwas hier) nebeneinander —
   **wortgleich gestylt**. Auf der Mahnliste stehen drei Knoepfe derselben
   Gestalt in einer Reihe, von denen einer eine Handlung ist. Das Design-System
   kannte Primary, Secondary, Danger und Ghost, aber keine Unterscheidung
   zwischen *„das fuehrt woandershin"* und *„das tut etwas hier"*. Wer fragte
   „was kann ich auf dieser Seite tun", hatte **keine visuelle Klasse, nach der
   er suchen koennte**. Die Buchhalterin addierte 26 Zeilen mit dem
   Taschenrechner und schrieb: „Ich habe den Knopf schlicht nicht gesehen."

   **Warum nur die SEKUNDAER-Gestalt.** Primary und Danger sind bereits
   unterscheidbar, und der Befund nennt ausdruecklich den Gegenfall: Bei
   „Zahlungslauf vorbereiten" — dem einzigen farbigen Knopf seiner Seite — hat
   dieselbe Persona den Knopf nachweislich **gesehen** und ihn beim Namen
   genannt. Sie leiser zu machen loeste nichts und verloere etwas.

   **Zwei unabhaengige Kanaele, keiner davon der Zeiger.**

   | | Handlung | Navigation |
   |---|---|---|
   | Form  | Kasten: Fuellung + Rand | kein Kasten |
   | Farbe | `fs-fg` auf `fs-surface` | Akzent `fs-tenant` |

   Die **Form** traegt allein — sie ueberlebt Graustufen und Farbenblindheit;
   die Farbe ist der zweite Kanal, nicht der erste. Ein Zeigerzustand traegt
   **gar nichts**: Auf einem Tablet gibt es keinen, und U-20 hat dieselbe Lehre
   schon fuer die klickbare Zeile gezogen — das Aufleuchten ist die Lesehilfe,
   nicht die Aussage. Der Akzent bedeutet hier dasselbe wie dort: **der Weg**.

   **Kein Richtungspfeil**, obwohl er naheliegt: Die Mehrheit der
   Kopfzeilen-Navigation fuehrt ZURUECK („Zurueck zur Rechnung", „Zur
   Mahnliste", „Abbrechen"). Ein `→` waere dort eine Falschaussage, und ein
   Zeichen, das an der Haelfte seiner Fundstellen das Gegenteil behauptet, ist
   schlechter als keins.

   **Gleiche Metrik wie der Kasten daneben** (Innenabstand, Schriftgrad,
   Zeilenhoehe, und ein `1px` durchsichtiger Rand, der die Hoehe haelt). Ohne
   das saesse die Navigation zwei Pixel hoeher als ihr Nachbar und die
   Antippflaeche schruempfte — der Unterschied soll an der Gestalt haengen,
   nicht an der Groesse.

   Regel und Waechter: `tests/test_kopfzeile_gestalt.py`; Bestand:
   `docs/qa/kopfzeile_gestalt_bestand.json`. Entscheidung: U-E38 in
   `docs/design/calibre/06-bedienoberflaeche.md`.
   ───────────────────────────────────────────────────────────────── */

.fs-handlung,
.fs-navigation {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    line-height: 1.25rem;
    font-weight: 500;
    border-radius: 6px;
    border: 1px solid transparent;
    text-decoration: none;
    transition: background-color 150ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* Handlung — sie tut etwas HIER. Der Kasten ist die Aussage.

   Der Rand ist fs-button-border, NICHT fs-border. Das ist eine bewusste
   Abweichung von der Sekundaer-Gestalt in CLAUDE.md § Buttons, entschieden
   als U-E57 Weg (a) (Sven, 2026-09-07).

   Gemessen war der Kasten der SCHWAECHSTE Kanal der Regel, nicht ihr
   staerkster: mit fs-border bleibt er in Graustufen knapp ueber der
   Wahrnehmungsgrenze, waehrend die Regel sagt, die Form ueberlebe
   Graustufen allein (UX-301). fs-button-border erreicht die
   Bedienelement-Schwelle 3:1 gegen jeden Grund, auf dem der Knopf steht.

   Zahlen stehen hier bewusst keine: .css wird von keinem Kontrast-
   Waechter gelesen, eine Zahl an dieser Stelle waere ungeprueft. Sie
   stehen dort, wo sie nachgerechnet werden —
   tests/test_farbkontrast.py. */
.fs-handlung {
    color: var(--color-fs-fg);
    background: var(--color-fs-surface);
    border-color: var(--color-fs-button-border);
}

.fs-handlung:hover {
    background: var(--color-fs-surface-sunken);
}

/* Navigation — sie fuehrt WOANDERSHIN. Kein Kasten, Akzent. */
.fs-navigation {
    color: var(--color-fs-tenant);
    background: transparent;
}

/* Das Aufleuchten ist die Lesehilfe. Die Navigation unterstreicht statt sich zu
   fuellen — bekaeme sie hier denselben Grund wie die Handlung, waeren die
   beiden ausgerechnet unter dem Zeiger wieder ununterscheidbar. */
.fs-navigation:hover {
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* ── Der Rand eines Bedienelements ────────────────────────────────────
   U-E71 Frage 1, Weg (c2)+(g) — Sven, 2026-09-09. Messung:
   `docs/design/entscheidung-bedienelement-rand.md`.

   **Was sie ist.** Der eine Ort, an dem steht, wie die Kante eines
   Bedienelements aussieht. Bis hierher stand das in 444 Kopien: jedes
   `border-fs-border` in einer Klassenliste war eine eigene Behauptung
   darueber, und keine davon erreichte WCAG SC 1.4.11 (3:1 gegen jeden
   Grund, auf dem das Element steht). Gemessen lag `fs-border` bei 1,30
   gegen die Karte — der Rand war nicht bloss leise, er war das EINZIGE
   Merkmal des Knopfes: 93 % der 444 tragen dieselbe Fuellung wie ihr
   Grund, und das zweite Merkmal (`hover:bg-fs-surface-sunken`) gibt es
   auf einem Tablet nicht.

   **Sie setzt NUR den Rand — das ist die ganze Entscheidung.** Weg (c1)
   waere gewesen, `.fs-handlung` auszurollen; die Klasse setzt aber ueber
   ihren geteilten Block Innenabstand, Schriftgrad und Anzeigeart hart.
   Gemessen truegen 182 der 444 Elemente eigene Utilities, die dabei still
   umgesprungen waeren — darunter das `pr-10` des Auswahlfelds, das den
   Platz fuer sein Chevron haelt. Wer die Groesse vereinheitlichen will,
   soll das als Gestaltungsabsicht aussprechen, nicht als Nebenwirkung
   eines Kontrast-Fixes bekommen.

   **Warum `@layer components` und nicht ungelayert wie `.fs-handlung`.**
   `tailwind.dist.css` registriert die Schicht leer (`@layer components;`)
   und laedt vor dieser Datei; die Schicht liegt damit VOR `utilities`.
   Eine ungelayerte Regel schluege dagegen JEDE Utility — auch
   `focus:border-fs-tenant` und `hover:border-fs-border-strong`. Das
   Auswahlfeld verloere so seinen Fokusrand, also ausgerechnet das
   Merkmal, dessen Sichtbarkeit hier repariert wird. In der Schicht
   gewinnen die Zustands-Utilities weiterhin, der Grundzustand kommt von
   hier.
   Der Preis steht dazu: das Element muss `border-fs-border` VERLIEREN,
   sonst gewinnt die Utility. Das ist gewollt — es macht den Austausch
   sichtbar, statt ihn zu ueberdecken.

   **Warum eine Klasse und nicht `border-fs-button-border`.** Die Utility
   existiert im gebauten CSS nicht, solange sie in keiner Quelle steht —
   ein Sweep ohne Rebuild faerbte nichts (TL-261/TL-292). Und eine Regel
   an einem Ort laesst sich aendern; 444 Kopien nicht.

   Hier stehen bewusst KEINE Kontrastzahlen: `.css` wird von
   `tests/test_kontrast_deklaration.py` nicht gelesen, eine Zahl waere
   ungeprueft. Sie stehen in `tests/test_farbkontrast.py`.

   Waechter: `tests/test_bedienelement_rand.py`; Bestand:
   `docs/qa/bedienrand_bestand.json` — er darf nur schrumpfen.
   ───────────────────────────────────────────────────────────────── */
@layer components {
    .fs-bedienrand {
        border-color: var(--color-fs-button-border);
    }
}

/* ── .fs-sprungziel — der Anker landet UNTER dem klebenden Streifen ──
   TL-3644, gemessen auf `staging.svede.de` am 2026-09-21/22.

   **Der Fall.** Ein Wegweiser-Schritt springt per Anker auf eine Karte
   derselben Seite (`shared.schritte`, `wegweiser=True`). Der Sprung selbst
   traegt — gemessen `scrollY` 0 -> 1451 an
   `/belege/461/?tab=uebersicht#anzahlung-card`. Was danach nicht traegt, ist
   die Sicht: Die Ueberschrift der Karte liegt HINTER dem klebenden Streifen,
   und wer ankommt, sieht den Rumpf einer Karte ohne ihren Namen.

   **Der Streifen ist ZWEI Elemente, nicht eines** — und daran ist die erste
   Reparatur gescheitert (`scroll-mt-28`, 112 px, PR #2990):

       Kopfleiste     `sticky top-0  h-14`    ->  y   0 …  56   (fest)
       Vorgangskopf   `sticky top-14 …`       ->  y  56 … 173   (117 px, variabel)

   Gemessen war die Ueberschrift danach bei y 141 — **33 px zu hoch**. Wer nur
   die Kopfleiste rechnet, kommt auf 56 und setzt einen Wert, der doppelt zu
   klein ist. Die Zahl steht deshalb hier und nicht viermal im Markup.

   **Seit B-ui10 (TL-4417) wird der Streifen gemessen, nicht geschaetzt.**
   `static/js/klebestreifen.js` legt die Unterkante aller klebenden Teile mit
   `data-klebestreifen` als `--fs-klebestreifen` auf `<html>`; der Abstand ist
   diese Hoehe plus 1.25rem (20 px) Luft. Der Anlass war genau die Grenze, die
   hier bis dahin stand: *„Die Hoehe des Vorgangskopfs ist INHALTSABHAENGIG —
   ein gesperrter Schritt haengt seinen Grund darunter, und der Streifen
   waechst."* Gemessen auf staging (RA-2026-00042, 1440 px, B-wst5): Der Kopf
   trug ein Rezept und endete bei y 251, die Karte „Reklamation“ begann bei
   y 192 — ihre Ueberschrift war verdeckt.

   **Der Rueckfall ist der alte Wert:** ohne Skript gilt die Vorgabe aus
   `tailwind.src.css` (`--fs-klebestreifen: 10.75rem`), also `10.75rem + 1.25rem`
   = 12rem = 192 px — der gemessene typische Streifen (173) plus 19 px Luft.
   Unter `md` klebt der Vorgangskopf nicht (`max-md:static`); das Skript zaehlt
   ihn dort nicht mit, und der Abstand faellt auf Kopfleiste plus Luft.

   **Was diese Regel NICHT entscheidet:** wie hoch ein klebender Vorgangskopf
   werden darf (U-E100). Sie macht nur jedes Sprungziel unabhaengig von seiner
   Hoehe richtig.

   Waechter: `tests/test_wegweiser_heisst_wie_sein_ziel.py` —
   `test_das_sprungziel_landet_nicht_unter_der_klebenden_leiste`. Er verlangt
   genau DIESE Klasse und nicht irgendein `scroll-mt-*`: Ein `scroll-mt-1`
   erfuellte die Buchstaben der Regel und nichts von ihrem Zweck.
   ───────────────────────────────────────────────────────────────── */
@layer components {
    .fs-sprungziel {
        scroll-margin-top: calc(var(--fs-klebestreifen) + 1.25rem);
    }
}

/* ── Ein Feld, an dem der Browser hält, liegt UNTER dem klebenden Streifen ──
   Tor K Runde 4 `KQ-1` (TL-4764), gemessen am 2026-10-11.

   **Der Fall.** Ein leeres Pflicht-Auswahlfeld oben in einer Maske sprang beim
   Absenden unter die Kopfleiste; „Bitte eine Auswahl treffen." zeigte auf eine
   verdeckte Stelle (`/betreiber/neu/`, `/betreiber/vertraege/neu/`). Gemessen an
   der echten Seite: Beschriftung „Kunde" bei y = -57, Kopfleiste bis 56.

   **Die Ursache ist eine Browser-Eigenheit.** Chromium rollt das erste ungültige
   Feld beim Absenden an die OBERKANTE des Fensters — genau dorthin, wo der Streifen
   klebt. WebKit (das iPad am Tresen) rollt es in die MITTE und hat den Befund nicht.
   Beide halten `scroll-margin-top` ein (an einer nackten Seite gemessen); dieselbe
   Regel trägt also auch den Tab-Fokus, der sonst ebenfalls unter dem Streifen
   landet (WCAG 2.2, SC 2.4.11).

   **Der Abstand ist der gemessene Streifen** (`--fs-klebestreifen`, siehe
   `.fs-sprungziel`) plus Platz für die Beschriftung ÜBER dem Feld.

   **Das Wächterfeld braucht mehr, und das ist der Teil, den man nicht rät.** Der
   Browser hält am `input.pflicht-waechter` des Auswahlbausteins, und der sitzt mit
   1 px am UNTEREN Rand des ganzen Bausteins (Block darüber: dort verankert Chrome
   die Meldungsblase). Darüber liegen Beschriftung, Knopf und — im Erklärungen-Modus
   oder mit Fehler — weitere Zeilen. Ein fester Zusatz verfehlte genau diese Lage;
   der Wächter rollt deshalb in die Mitte des Fensters, wie WebKit es von sich aus
   tut, und nie weniger hoch als Streifen plus ein Baustein ohne Zusatzzeilen.

   **In einem rollenden Fenster** (Modal mit eigenem Rollbereich, keine Leiste darüber)
   gilt die Regel mit, und gemessen ist, dass sie dort nichts verdirbt: Chromium rollt
   ein verdecktes Feld beim Fokus ohnehin in die Mitte und verschiebt es nur um den
   halben Rand (96 px → 48 px tiefer); Feld und Beschriftung bleiben im Bild. Eine
   Ausnahme für Fenster wäre deshalb Aufwand ohne Gegenwert.

   **Bewusst NICHT an Knöpfen:** Die Optionen der Auswahlliste sind Knöpfe und
   werden per `scrollIntoView({ block: 'nearest' })` IN der Liste gerollt — ein
   Rand dort verschöbe die Tastaturnavigation. **Bewusst NICHT als
   `scroll-padding` am `<html>`:** es addierte sich mit `.fs-sprungziel`.

   Wächter: `tests/test_kq1_pflichtfeld_unter_kopfleiste.py` (im Browser, an den
   beiden Fundstellen und in einem rollenden Fenster).
   ───────────────────────────────────────────────────────────────── */
@layer components {
    :is(input, select, textarea) {
        scroll-margin-top: calc(var(--fs-klebestreifen) + 2.5rem);
    }

    input.pflicht-waechter {
        scroll-margin-top: max(calc(var(--fs-klebestreifen) + 2.5rem + 4.5rem), 50vh);
    }
}

/* ── .fs-auslassung — ein zu langer Platzhalter endet mit „…" (B-ui1) ─────
   Ein abgeschnittener Platzhalter ohne Auslassung liest sich wie ein ganzer
   Text: „Kommissio", „Überweisu", „Werksteil-Nr., E". Gemessen am 2026-10-06
   im Browser: `text-overflow: ellipsis` am Eingabefeld allein greift auf den
   Platzhalter NICHT — weder in Chromium 154 noch in WebKit 26.6 (der Engine
   des iPad-Safari am Tresen). Erst dieselbe Regel auf `::placeholder`, mit
   `overflow: hidden` und `white-space: nowrap`, setzt „…" in beiden.
   Firefox ist nicht gemessen.

   Die Auslassung ist der zweite Kanal, nicht der erste: Der volle Text gehört
   ins `aria-label` (Suchbaustein: `search_umfang`; Spaltenfilter: der
   Spaltenname), und der sichtbare Text soll ohnehin passen — Wächter
   tests/test_suchbaustein_platzhalter.py.
   ───────────────────────────────────────────────────────────────── */
@layer components {
    .fs-auslassung {
        text-overflow: ellipsis;
    }
    .fs-auslassung::placeholder {
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
    }
}

/* ── .fs-beschriftung — die Farbe einer Feldbeschriftung (B-ui7) ──────────
   Der kanonische Weg `components/_form_field.html` rendert seine Beschriftung
   in fs-fg-body; die von Hand geschriebenen Beschriftungen trugen gemessen
   140-mal fs-fg und 35-mal fs-fg-body (2026-10-06) — dieselbe Rolle, zwei
   Farben. Entschieden ist der Maßstab (flowsuite-39, Wellenplan B-ui7).

   **Eine Klasse statt eines Farbworts:** Wer die Farbe ändern will, ändert sie
   HIER, an einer Stelle, statt in 175 Vorlagen. Sie liegt in `@layer components`
   (wie `.fs-bedienrand`): Eine Zustands-Utility wie `group-hover:text-fs-fg`
   schlägt sie weiterhin, die Rückmeldung beim Überfahren bleibt also erhalten.
   Sie setzt NUR die Farbe — Größe und Gewicht bleiben am Element.

   KONTRAST fs-fg-body auf fs-surface — hell 10,40 · dunkel 12,54
   KONTRAST fs-fg-body auf fs-bg — hell 9,93 · dunkel 14,27
   KONTRAST fs-fg-body auf fs-surface-sunken — hell 9,31 · dunkel 14,88

   Wächter: tests/test_beschriftungsklasse.py.
   ───────────────────────────────────────────────────────────────── */
@layer components {
    .fs-beschriftung {
        color: var(--color-fs-fg-body);
    }
}

/* ── .fs-schrittformular — ein Schritt-Formular im Vorgangskopf (B-ui8) ──
   Ein Schritt, der mehrere Felder abfragt (Versand der Einsendung, Bestellung,
   Wareneingang), stand im Kopf in der schmalen Handlungsspalte (höchstens
   `max-w-sm`): gemessen 260 px breit und 510 px hoch, während links neben ihm
   die halbe Kopfhöhe leer blieb — und der Kopf klebt, verdeckte beim Rollen
   also mehr als die Hälfte des Bildes (2026-10-06, /einsendungen/1/ bei 1440).

   Die Kopffassung des Formulars trägt diese Klasse, und die Handlungsspalte
   in `components/_vorgangskopf.html` nimmt dann eine eigene Zeile über die
   volle Breite (`has-[.fs-schrittformular]:basis-full`). Die Felder stehen in
   einem Raster, das sich nach der Breite richtet statt nach einem Umbruchpunkt:
   am Schreibtisch drei bis vier nebeneinander, am Handy eins unter dem anderen.

   Erklärsatz, Rückfrage und Knopf stehen auf einer eigenen Zeile; der Knopf
   behält die Breite eines Kopfknopfs, statt sich über die ganze Seite zu
   ziehen. `max-width` statt `width`, weil die Vorlagen `w-full` tragen und eine
   Utility diese Schicht schlüge — `max-width` setzt dort niemand.

   Die Menüfassung desselben Schritts trägt die Klasse NICHT: Die Spalte prüft
   per `:has()` jeden Nachfahren, und ein Schritt im zugeklappten Menü zöge den
   Kopf sonst auf volle Breite, ohne dass ein Formular zu sehen ist.

   Wächter: tests/test_schrittformular_kopf.py.
   ───────────────────────────────────────────────────────────────── */
@layer components {
    .fs-schrittformular {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
        gap: 1rem 1.25rem;
        align-items: start;
    }
    .fs-schrittformular > :is(p, button, [data-volle-zeile]) {
        grid-column: 1 / -1;
    }
    .fs-schrittformular > button {
        justify-self: start;
        max-width: 20rem;
    }
}

/* ── .fs-meldeknopf-frei — der Fensterkopf haelt am Handy Platz frei ──
   U-E92, Weg (a), von Sven am 2026-09-11 entschieden.

   **Der gemessene Fall.** Auf `/ankauf/neu/` bei 390 px lag das „×" des
   Fensters bei x 314–346, der Melde-Knopf (Kaefer) bei x 334–378 — vom
   „×" blieben **20 von 32 px** zusammenhaengend frei, weniger als die
   44-px-Schranke aus TL-1546. Ab 820 px gibt es die Ueberlappung nicht.
   Die Mitte des „×" traf auf allen Breiten noch sich selbst; wer die
   rechte Ecke antippte, oeffnete das Melde-Fenster statt zu schliessen.

   **Warum der Kopf ausweicht und nicht der Knopf.** Der Melde-Knopf liegt
   seit TL-1363 mit `z-[70]` ueber JEDEM Fenster, und das ist seine
   Aufgabe: Einen Fehler IM Fenster kann man sonst weder melden noch
   abfotografieren. Ihn bei offenem Fenster zu versetzen naehme ihm genau
   die feste Lage, die TL-1363 gebracht hat. Der Kopf hat dagegen Platz.

   **Das Mass ist die BREITE des Melde-Knopfs** (`max-md:w-11` = 44 px),
   nicht sein voller Fussabdruck: Die 12 px Abstand zum Bildschirmrand
   bringt der Fensterrahmen ohnehin mit (Huelle `p-4` plus Panel-Polster,
   zusammen mindestens 16 px). Gemessen bleibt danach mehr als das
   44-px-Tippziel frei — nachgerechnet in
   `docs/qa/meldeknopf-fensterkopf-2026-09-11.md`.

   **Warum UNGELAYERT, anders als `.fs-bedienrand` darueber.** Die Regel
   muss ein `-mr-3` schlagen, das ein Fensterkopf zur optischen
   Ausrichtung traegt (`components/_fenster.html`) — eine Utility, und
   Utilities gewinnen gegen `@layer components`. Anders als beim
   Bedienrand gibt es hier auch nichts, was ein Zustand (`hover:`,
   `focus:`) an diesem Mass aendern duerfte: Ein Rand aendert im Zustand
   seine Farbe, ein Aussenabstand nicht. Am Schreibtisch setzt die Klasse
   gar nichts — dort gibt es die Ueberlappung nicht, und `-mr-3` gilt
   unveraendert weiter.

   Waechter: `tests/test_fensterkopf_meldeknopf.py` — er fordert die
   Klasse an JEDEM Schliessen-Knopf ein, der in einer Fenster-Huelle
   sitzt, nicht nur am Anlassfall.
   ───────────────────────────────────────────────────────────────── */
@media (max-width: 767px) {
    .fs-meldeknopf-frei {
        margin-right: 2.75rem;
    }
}

/* ── Print styles ───────────────────────────────────────────────── */
@media print {
    aside,
    [data-floating-cluster],
    [aria-label="Seitenleiste"] {
        display: none !important;
    }

    main {
        padding: 0 !important;
    }
}

/* ── UX-205: die eingeloggten Anmelde-Flaechen bekommen einen Abschnitt ───── */
/* Gemessen am 2026-09-07: Auf `/accounts/password/change/` standen Ueberschrift,
   Beschriftungen, Felder und die vier Passwortregeln als 10-11 Bloecke ohne EINE
   Karte direkt auf dem Seitenhintergrund. Es gab keinen Abschnitt, den man
   erkennen konnte — die Achse „Abschnitte unterteilt" war dort gar nicht bedient.

   Der Kasten haengt am `form` und NICHT am Inhaltsbereich, und das ist gemessen:
   Von den 20 Vorlagen mit diesem Rahmen rendern **8** ein `<form>` als direktes
   Kind des Inhaltsblocks (Passwort aendern/setzen, E-Mail-Adressen, E-Mail
   aendern, Telefon, Abmelden, Drittanbieter, Sitzungen) — genau die, denen der
   Kasten fehlt. `mfa/index.html` dagegen baut seine Flaeche aus
   `{% element panel %}`, und die Kacheln tragen ihren Rahmen seit TL-652 selbst.
   Ein Kasten um den ganzen Inhaltsbereich legte dort eine Karte um Karten.

   `> form` und nicht `form`: Ein Formular INNERHALB einer Panel-Kachel (etwa der
   Abschalt-Knopf der Authenticator-App) ist kein direktes Kind und bleibt
   unberuehrt. Die Auswahl haengt damit an der Bauform der Seite, nicht an einer
   Liste von Dateinamen, die beim naechsten allauth-Update veraltet.

   Die Werte sind die der Standard-Card aus CLAUDE.md („Cards"):
   bg-fs-surface, rounded-lg, border-fs-border, p-7. Sie stehen hier als
   Variablen statt als Tailwind-Klassen, weil die Vorlage aus dem allauth-Paket
   kommt und wir sie nicht fuer jede der acht Seiten ueberschreiben wollen. */
.auth-manage > form {
    background: var(--color-fs-surface);
    border: 1px solid var(--color-fs-border);
    border-radius: 0.5rem; /* rounded-lg */
    padding: 1.75rem; /* p-7 */
}

/* Die Regelliste im Hilfetext (siehe allauth/elements/fields.html). Django
   liefert sie als nacktes `<ul>`; ohne diese Regel stuenden vier Zeilen ohne
   Aufzaehlungszeichen und ohne Zeilenabstand aufeinander — gemessen 0 px. */
.fs-hilfeliste ul {
    list-style: disc;
    margin: 0;
    padding-left: 1.125rem;
}

.fs-hilfeliste li + li {
    margin-top: 0.25rem;
}

/* ── .fs-handlungsleiste — die Handlungsleiste eines Formulars klebt unten ──
   UX-6, Weg „CSS-Klasse + Partial stellt intern um", von Sven am 2026-09-18
   entschieden.

   **Der gemessene Fall.** Erhebung vom 2026-09-18 auf staging.svede.de
   (1440x900, Inhaber/in, 14 Module): 34 Anlegemasken tragen einen messbaren
   Absendeknopf, **12 davon liegen unter dem Falz** — 35 %. Schwerster Fall
   `/kunden/neu/` mit dem Knopf bei 2.342 px, also im DRITTEN Bildschirm.
   Zahlen und Methode: docs/qa/anlegemasken-absendeknopf-2026-09-18.md.

   **Warum eine Klasse und nicht das bestehende Partial.**
   `components/_speicherleiste.html` loest denselben Fall fuer
   Einstellungsseiten (UX-4) — es kennt aber genau EINEN Knopf, weil die
   einzige Handlung dort „Speichern" ist. Die gemessenen Anlegemasken tragen
   meist „Abbrechen" daneben, und ein Django-`{% include %}` hat keine Slots.
   Eine Klasse traegt beliebige Kinder.

   **Das Partial ist intern darauf umgestellt**, statt die Regel ein zweites
   Mal zu schreiben. Sonst stuenden zwei Fassungen derselben Idee im Haus —
   die Form, in der eine Aenderung an einer Stelle nachgezogen wird und an der
   anderen stehenbleibt. Dieselbe Lehre, die `notifications.views`
   ausdruecklich fuer den Dropdown-Kontext festhaelt.

   **`sticky` und nicht `fixed`, und das ist keine Stilfrage** (uebernommen aus
   UX-4): Ein `fixed`-Element haengt am Fenster und stuende auch dann da, wenn
   man laengst am Formular vorbei liest. `sticky bottom-0` haengt an seinem
   Formular — es klebt, solange das Formular im Bild ist, und loest sich
   danach. Auf einer kurzen Maske klebt es gar nicht und steht, wo es immer
   stand.

   **Der Hintergrund ist DECKEND, nicht durchscheinend.** Ohne ihn laufen die
   Felder darunter durch die Leiste hindurch. `--color-fs-bg` ist die
   Seitenflaeche selbst und schaltet im Dunkelmodus mit; eine Deckkraft
   darunter (`/80`) waere genau der Fehler, den der UX-4-Waechter verbietet.

   **Warum `@layer components`** — dieselbe Begruendung wie bei
   `.fs-bedienrand`: Die Schicht liegt vor `utilities`, damit gewinnt eine
   Zustands-Utility weiterhin gegen den Grundzustand. Ungelayert schluege die
   Regel jede Utility, auch ein `hidden` oder ein `md:static`, mit dem eine
   Maske ihre Leiste am kleinen Bildschirm bewusst anders fuehrt.

   **Sie setzt NIE Innenabstand, Ausrichtung oder Schriftgrad.** Das waere
   derselbe Fehler, den `U-E71` beim Bedienrand ausdruecklich vermieden hat:
   182 Elemente spraengen still um. Wer die Leiste anders ausrichten will,
   schreibt seine Utilities dazu — `flex justify-end gap-3` bleibt Sache der
   Vorlage.

   Waechter: tests/test_ux4_speicherleiste.py (dieselbe Datei, weil es
   dieselbe Zusage ist — sie hat nur zwei Zugaenge). */
@layer components {
    .fs-handlungsleiste {
        position: sticky;
        bottom: 0;
        z-index: 10;
        background-color: var(--color-fs-bg);
        border-top-width: 1px;
        border-top-color: var(--color-fs-border);
    }
}

/* DIE EINBLENDUNG DER ERKLAER-BLASE (B-hlp1, TL-1996, 2026-10-03).
   Die Blase in `components/_hilfe_zeichen.html` versteckt sich seit diesem Tag
   ueber die Klasse `hidden` statt ueber `x-show`, weil htmx beim Austausch den
   Inline-Stil eines Elements mit `id` auf den Server-Stand zuruecksetzt -- und
   damit das `display:none` von `x-show` wegnahm. `x-transition` braucht aber
   `x-show`; die Einblendung von 100 ms kommt deshalb von hier. Eine Animation
   startet jedes Mal neu, wenn ein Element von `display:none` in die Anzeige
   wechselt -- also bei jedem Oeffnen, ohne Zutun von Alpine. */
.feldhilfe-blase {
    animation: feldhilfe-einblenden 100ms ease-out;
}

@keyframes feldhilfe-einblenden {
    from { opacity: 0; }
    to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .feldhilfe-blase {
        animation: none;
    }
}

/* ── Freigabe-Stufenkette (approvals/_pipeline.html) — BB-F2 ─────────────────
   Ab 640 px waagerecht, darunter senkrecht. Bei 390 px lag die zweite Stufe
   außerhalb des Bildes: Die Kette rollte in einer Karte, die ihren Überlauf
   abschneidet, und die Seite meldete 0 px Überlauf (gemessen von flowsuite-28
   auf staging). UNGELAYERT, weil diese Regeln die Utilities der Vorlage
   (`min-w-[520px]`, `flex-col`, `mt-4`) überstimmen müssen. */
.fs-stufe {
  width: 128px;
}

@media (max-width: 639.98px) {
  .fs-stufenkette {
    flex-direction: column;
    min-width: 0;
  }
  .fs-stufenkette > .fs-stufe {
    width: 100%;
    flex-direction: row;
    align-items: flex-start;
    text-align: left;
    gap: 0.75rem;
  }
  .fs-stufenkette > .fs-stufe > .fs-stufe-text {
    align-items: flex-start;
  }
  .fs-stufenkette > .fs-stufe-linie {
    flex: none;
    width: 2px;
    height: 1rem;
    margin: 0.25rem 0 0.25rem 17px;
  }
}
