/* Die ruhende Form, an einer Stelle (#160)
   ========================================

   Es gibt kein Build-Werkzeug und keine Klassenbibliothek, also gibt es dieses
   Blatt: eine weitere Datei unter `public/`, die jede Oberfläche lädt — genau
   so, wie `interaction-feedback.css` entstanden ist. Die Trennlinie zwischen
   beiden ist erklärbar:

     ui-surfaces.css        Form   — Karte, Abzeichen, Menüfläche, Reiterleiste,
                                     Tabellenkörper, Leerzustand, Ablagehinweis,
                                     Knopf, Feld, Dialog, Segmentwahl.
     interaction-feedback.css Zustand — Ladeskelett, Toast, Sortierpfeil,
                                     Auswahlleiste, Hochladezeile, Dialoge.

   Farben ausschließlich über die kanonischen Token aus color-tokens.css
   (`var(--surface-1)`), die jede Schale als erstes lädt. Bis zum 2026-09-13
   stand hinter jedem Token eine Rückfallkette
   `var(--surface-1, var(--viewer-surface, var(--ws-surface)))` aus der Zeit,
   in der Viewer und Projektbereich eigene Farbwelten führten; seit die
   Aliasse selbst nur noch auf dieselben Token zeigen (#108), war die zweite
   Antwort tot, und `ColorTokenConsistencyTest` hält die geteilten Blätter
   jetzt frei davon (docs/ENTSCHEIDUNGEN.md, 2026-09-13).

   Beim Umstellen gilt: Klasse zuerst *zusätzlich* vergeben, dann die alten
   Deklarationen leeren, dann den alten Namen entfernen. `public/*.js` greift
   auf einige dieser Namen zu (`.mount-context-menu`, `.tag-picker-menu`,
   `.files-table`, `.file-drop-hint`) — wer sie ersetzt,
   statt sie zu ergänzen, bricht das Skript. */

/* --- Fokusring, einmal für alle Dokumente ------------------------------
   Stand bis zum 2026-09-13 in interaction-feedback.css — und das Blatt
   laden Stock-Laden, Anlieferung „geschlossen" und Urkunde nicht: Dort
   zeichnete der Browser seinen eigenen Ring (auto 1px, Offset 1). Die Regel
   gehört zum Fundament, weil `--focus-ring` „überall, mit outline-offset:
   2px" verlangt ist (docs/Oberflaeche/Farben-und-Thema.md, Kontrasttabelle)
   und dieses Blatt das einzige ist, das jede Schale lädt. */
button:focus-visible, .button-link:focus-visible, summary:focus-visible,
a:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* --- Nur für Hilfstechnik sichtbar ---------------------------------------
   Stand zweimal wortgleich in project-ui.css und viewer.css; `.visually-hidden`
   in client/src/tokens.css hat denselben Zweck, lebt aber hinter der
   Prozessgrenze des Electron-Clients und bleibt dort.

   `white-space: nowrap` kam aus der dritten serverseitigen Fassung
   (`.visually-hidden` in file-columns.css, ein einziger Aufrufer im Skript des
   Spaltenpanels). Es schadet keinem der vorhandenen Aufrufer — deren Inhalt
   schneiden `height: 1px` und `overflow: hidden` ohnehin weg —, verhindert aber
   den Fall, für den es dort stand: Ein Text, der umbricht, kann eine Zeile
   auseinanderziehen, bevor er unsichtbar wird. `clip` ist als veraltet markiert
   und wird trotzdem überall unterstützt; es bleibt, weil die vorhandenen
   Aufrufer daran hängen. */
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }

/* --- Fundament: Gewicht und Kleinschrift (2026-09-13) ----------------------
   Die Skala endet bei 600 (`--weight-semibold`), der Browser aber setzt
   Überschriften, `strong` und `b` auf 700 — und nichts im Bestand sagte ihm
   etwas anderes: Die Topleiste stand mit 700 über einem Seitenkopf mit 600,
   ein Dialogtitel, „Noch keine Einladungen", jedes `td strong`, 288 `strong`
   der Hilfe. TypographyTokenConsistencyTest verbietet Zahlen im Blatt und
   sieht die Vorgabe des Browsers nicht; deshalb steht sie hier ausgeschrieben,
   in dem Blatt, das jede Schale lädt. Spezifischere Regeln (`.help-body h4`
   mit `--weight-medium`, `.wordmark > b` mit `inherit`) gewinnen weiter.

   `small` ohne Größe fällt auf `smaller` — 11,67 px unter dem Benutzernamen,
   10,83 px an „optional", 10 px am Dateinamen im Viewer, alles außerhalb der
   Skala und unter dem Boden von 11. `--font-sm` ist die Fußnote der Tabellen;
   wo eine Fläche einen Boden hat (Außen: `--font-base`), hebt ihr Blatt es. */
h1, h2, h3, h4, strong, b { font-weight: var(--weight-semibold); }
small { font-size: var(--font-sm); }

/* --- Symbol ---------------------------------------------------------------
   Die Form zu `components/icon.blade.php` und `public/icons.svg`.

   Feste Pixelmaße statt `1em`: Ein Symbol steht neben Text in sechs
   verschiedenen Größen, und mitwachsende Symbole ergeben sechs verschieden
   große Symbole für dieselbe Sache. Zwei Größen reichen — 16 neben Fließtext
   und in Knöpfen, 20 in der Navigation und in Überschriften.

   `stroke: currentColor` ist der ganze Grund für den Bogen: Das Symbol nimmt
   die Farbe seiner Umgebung, ohne dass irgendeine Regel sie ein zweites Mal
   nennt — Hover, Fokus und Zustandsfarben gelten damit von selbst mit.
   `fill: none`, weil Lucide durchgehend gestrichen zeichnet.

   `flex: 0 0 auto` verhindert den häufigsten Fehler: In einer Flex-Zeile mit
   langem Text schrumpft ein Symbol sonst zu einem Strich. Der Versatz von
   -.15em setzt es auf die optische Mittellinie der Schrift; die Grundlinie
   allein stellt es zu tief. */
.icon { display: inline-block; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -.15em; }
.icon-16 { width: 16px; height: 16px; }
.icon-20 { width: 20px; height: 20px; }
/* Die drei Zeichen aus dem App-Icon (`mark-play`, `mark-m`, `mark-app`) sind
   Flächen, keine Striche: gefüllt statt gestrichen, sonst zeichnete `.icon`
   ihnen einen 2px-Rand um die Form. Das Dreieck heißt „abspielbar" und steht
   immer in der Marke — nie in der Linkfarbe, nie im Ton der Umgebung. Der
   zweite Selektor fängt das Dreieck auch dort, wo kein Aufrufer eine Klasse
   ans Symbol reichen kann (`config/navigation.php` über `<x-icon :name>` im
   Bereichsmenü): Was `#mark-play` zeigt, ist die Marke, ohne Ausnahme —
   seit dem 2026-09-13 auch die Füllung, damit kein Aufrufer sie über eine
   Klasse mitführen muss. Ein Knopf, der ein Medium abspielt, zeigt dagegen
   Lucides `play`/`pause` (Playbar, Sound-Bibliothek): Das Dreieck der Marke
   heißt „abspielbar", es ist kein Bedienzeichen (Marke.md). */
.icon-mark { fill: currentColor; stroke: none; }
.icon-play { color: var(--brand); }
.icon:has(> use[href$="#mark-play"]) { fill: currentColor; stroke: none; color: var(--brand); }

/* --- Wortmarke ------------------------------------------------------------
   Die Form zu `components/wordmark.blade.php`.

   Die Zweifarbigkeit ist die einzige Signatur der Marke: „Media" trägt das
   Blau des App-Icons (`--brand`, 5,87:1 auf `--canvas`) und nicht die
   Linkfarbe — ein Wort in Linkblau verspricht ein Ziel. Deshalb hängt sie
   allein an `.wordmark > b` und nie mehr an `h1 span`: Dort färbte sie jede
   Überschrift mit einem <span>, als wäre sie die Marke.

   Zwei Größen, dieselbe Laufweite: 17 im Bereichsmenü und in der
   Seitenleiste, 24 auf Login und Fehlerseiten. Das <b> erbt das Gewicht,
   damit „Media" nicht fetter steht als „Mounting". */
.wordmark { font-weight: var(--weight-semibold); letter-spacing: -.02em; color: var(--text); text-decoration: none; white-space: nowrap; }
.wordmark-m { font-size: var(--font-lg); }
.wordmark-l { font-size: var(--font-xl); }
.wordmark > b { font-weight: inherit; color: var(--brand); }
/* `a:hover` (app.css) zöge „Mounting" sonst in die Linkfarbe. Die Marke bleibt
   still, auch wenn sie als Link steht — sie ist der Ort, nicht der Weg. */
.wordmark:hover { color: var(--text); }

/* --- Seitenkopf -----------------------------------------------------------
   Die Form zu `components/page-header.blade.php`: Titel und Meta links,
   Aktionen rechts, dazwischen nichts (ENTSCHEIDUNGEN 2026-09-10).

   Kein Vorspann: 21 von 27 Seiten sagten unter dem Titel noch einmal, was der
   Titel sagt (Regel 11, Krug). Kein Eyebrow: den Bereich nennen Bereichsmenü
   und Seitenleiste. Keine Linie: Unter der Kopfzeile (`main > header`) liegt
   schon eine, und zwei Linien übereinander wären ein Rahmen, den niemand
   gezeichnet hat — der Abstand trennt den Kopf vom Inhalt.

   Die Aktionen brechen nicht um, weder untereinander noch im Wort
   („Regel / anlegen"), dieselbe Zusage wie an `.settings-intro`
   (`AdminSurfaceLayoutTest`). Wird die Zeile zu schmal, wandert die ganze
   Gruppe unter den Titel (`flex-wrap` am Kopf), nicht das Wort in die
   nächste Zeile. */
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; margin: 0 0 var(--space-5); }
.page-header-lead { display: grid; gap: var(--space-1); min-width: 0; }
/* Der Titel ist im Panel eine <h2> (die <h1> steht in der Topleiste); außen
   ist er mit `level="1"` die <h1> der Seite und trägt dann `--font-xl` — die
   Stufe der `h1` der Topleiste unter 600 px — auf 16-px-Lesetext wäre
   `--font-lg` (17) kein Titel mehr (2026-09-13, #357). */
.page-header :is(h1, h2) { margin: 0; font-size: var(--font-lg); font-weight: var(--weight-semibold); }
.page-header h1 { font-size: var(--font-xl); line-height: var(--leading-tight); }
.page-header-meta { margin: 0; color: var(--text-muted); font-size: var(--font-sm); }
.page-header-actions { display: flex; flex: 0 0 auto; flex-wrap: nowrap; align-items: center; gap: var(--space-2); }
.page-header-actions .button-link, .page-header-actions button { white-space: nowrap; }
/* --- Ablagehinweis beim Ziehen -------------------------------------------
   Stand wortgleich als `.file-drop-hint` (project-ui.css) und
   `.viewer-file-drop-hint` (viewer-files.css), 15 Deklarationen je Fassung.
   Verschieden war nur die Randfarbe im Zustand `is-drag-over`; die bleibt bei
   der jeweiligen Oberfläche. */
.ui-drop-hint { position: absolute; inset: 10px; z-index: 4; display: grid; gap: var(--space-1); place-content: center; border: 2px dashed transparent; border-radius: var(--radius-md); background: transparent; color: transparent; text-align: center; visibility: hidden; opacity: 0; pointer-events: none; transition: opacity .12s ease, background-color .12s ease, border-color .12s ease; }
.ui-drop-hint strong { font-size: var(--font-lg); }
.ui-drop-hint span { font-size: var(--font-sm); }
.is-drag-over > .ui-drop-hint { visibility: visible; background: color-mix(in srgb, var(--surface-selected) 94%, transparent); color: var(--text); opacity: 1; }

/* --- Fläche ---------------------------------------------------------------
   „--surface-1 + 1px solid var(--border) + Radius" stand an 31 Stellen; etwa
   sechzehn davon meinten dieselbe Karte. Der Radius war dabei über den Bestand
   verteilt ohne erkennbare Regel (6px 52×, 8px 34×, 5px 13×, 10px 10×, 7px 9×).
   Gewählt ist `--radius-md`, der Radius der Fläche (docs/Oberflaeche/Typografie-und-Form.md).

   Die Form zu `components/card.blade.php` (2026-09-10): Kopf (`.ui-card-head`)
   und Körper (`.ui-card-body`) tragen die Polsterung, die Fläche selbst keine —
   Login, Hilfe und Systemkacheln polstern seit jeher selbst. Eine Flächenebene
   je Seite: keine Karte in der Karte, keine Tabelle mit eigenem Rahmen darin
   (die hat `<x-table>`). */
.ui-card { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-1); color: var(--text); }
.ui-card > header, .ui-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--border); }
.ui-card > header h2, .ui-card > header h3, .ui-card-head h2, .ui-card-head h3 { margin: 0; font-size: var(--font-base); font-weight: var(--weight-semibold); }
.ui-card > header small, .ui-card-head small { color: var(--text-muted); }
.ui-card-body { padding: var(--space-5); }

/* --- Zeilen (2026-09-10; ohne Kasten seit 2026-09-11) ------------------------
   Die Form zu `components/row.blade.php`. `.ui-rows` ist kein Kasten mehr:
   kein Rahmen, keine Fläche, kein Radius — die Zeilen liegen direkt auf dem
   Grund, und nur eine Haarlinie trennt Zeile von Zeile, auch unter der
   letzten (sie schließt die Liste nach unten ab, wo vorher die Kante der
   Fläche stand). Tabelle und Callout bleiben Flächen; die Zeile ist es nicht
   (ENTSCHEIDUNGEN 2026-09-11, „Das Material").

   Flex statt Raster: Ein leeres Symbolfeld ließe in einem Raster seine Spalte
   samt Abstand stehen; im Flex steht nur da, was da ist.

   `a:hover` (app.css) ist (0,1,1) und zöge den Text eines Zeilenlinks ins
   Linkblau; `.ui-row-link:hover` steht deshalb mit eigener Farbe da. Die
   Hover-Fläche malt ein Pseudoelement um `--space-3` über den Text hinaus,
   damit sie den Titel nicht anstößt — und nicht die Zeile selbst mit
   negativer Marge, denn dann spränge die Haarlinie unter der Maus um 24px
   nach außen. Die Linie bleibt, wo sie ist; nur die Fläche wächst. */
.ui-row { display: flex; align-items: flex-start; gap: var(--space-3); min-width: 0; padding: var(--space-3) 0; border-bottom: 1px solid var(--border); color: var(--text); }
/* Das Symbol steht an der ersten Zeile, nicht in der Mitte des Blocks: Bei
   drei Zeilen Text rutschte es sonst neben den zweiten Satz und zeigte auf
   nichts. Die Handgriffe bleiben mittig — sie gehören der ganzen Zeile. */
.ui-row > .icon { flex: 0 0 auto; margin-top: 2px; color: var(--text-muted); }
/* Ein Fenster ist ein Block, kein Zeichen: Neben ihm steht der Text mittig,
   sonst hinge ein zweizeiliger Titel am oberen Rand eines 54px hohen Bilds.
   `.ui-cover` ist das Fenster samt Formular zum Titelbild — dieselbe Zeile. */
.ui-row:has(> .ui-frame), .ui-row:has(> .ui-cover), .ui-row:has(> .ui-frame-slot) { align-items: center; }
.ui-row-body { display: grid; flex: 1 1 auto; gap: var(--space-1); min-width: 0; }
.ui-row-body > strong { font-size: var(--font-base); font-weight: var(--weight-semibold); }
.ui-row-text { color: var(--text-muted); font-size: var(--font-sm); line-height: var(--leading-base); }
.ui-row-meta { color: var(--text-subtle); font-size: var(--font-sm); }
.ui-row-actions { display: flex; flex: 0 0 auto; align-self: center; align-items: center; gap: var(--space-2); }
.ui-row-link { position: relative; isolation: isolate; text-decoration: none; }
.ui-row-link:hover { color: var(--text); }
.ui-row-link:hover::before { content: ""; position: absolute; inset: 0 calc(-1 * var(--space-3)); z-index: -1; border-radius: var(--radius-sm); background: var(--surface-hover); }
/* `--text-subtle` trägt auf `--surface-hover` nur 4,27:1 — die gedimmten
   Stunden eines Timecodes wechseln unter der Maus eine Stufe heller. */
.ui-row-link:hover .tc > small { color: var(--text-muted); }
/* Ton nur am Titel, nur warn/danger; der Zustandspunkt kommt vom Aufrufer. */
/* Der Ton faerbt Titel *und* Symbol. Steht im Titel ein Link, gewinnt dort
   das Linkblau — dann traegt allein das Symbol die Stufe, und ohne diese
   zwei Regeln waere sie unsichtbar. Der Schlitz gibt die Farbe an sein
   Symbol weiter, damit eine Zeile ohne Fenster in einer Fensterliste die
   Stufe genauso zeigt. */
.ui-row[data-tone="warn"] > .ui-row-body > strong, .ui-row[data-tone="warn"] > .icon, .ui-row[data-tone="warn"] > .ui-frame-slot { color: var(--warning); }
.ui-row[data-tone="danger"] > .ui-row-body > strong, .ui-row[data-tone="danger"] > .icon, .ui-row[data-tone="danger"] > .ui-frame-slot { color: var(--danger-color); }

/* --- Fenster (2026-09-11) -----------------------------------------------------
   Die Form zu `components/frame.blade.php`: ein Fenster auf das Material,
   16:9 (`--frame-ratio`), immer auf `--window` — der einen Fläche unter dem
   Grund, denn ein Bildschirm ist dunkler als der Raum, auch im hellen Thema.
   Darin das letzte Standbild, das der Index gesehen hat, die Wellenform für
   Ton, sonst das M aus dem App-Icon in `--text-subtle` (5,76:1 auf
   `--window`) und ausdrücklich ohne `opacity`: mit .6 fiele es auf 2,7.

   `object-fit: contain`, nie `cover`: Hochkant steht mit Balken darin, wie
   auf jedem Monitor — und die Balken sind `--window`, nicht die Fläche, auf
   der das Fenster liegt. Der 1px-Innenring `--border` macht das leere
   Fenster auf `--canvas` erst zum Fenster (1,04:1 ohne ihn, unsichtbar).
   Bild und Wellenform bleiben 1px innerhalb des Rings, sonst läge ein
   16:9-Bild genau darauf und die Kante unter der Maus (Streifen) wäre weg.

   Drei Breiten nach Rolle, nie nach Gefühl: `--frame-w` in der Zeile,
   `--frame-w-s` in der Ereigniszeile, im Streifen die ganze Kachel. Das
   Fenster steht, blendet nicht ein, pulsiert nicht — ein serverseitig
   gerendertes Bild hat nichts anzukündigen. */
.ui-frame { position: relative; display: grid; flex: 0 0 auto; width: var(--frame-w); aspect-ratio: var(--frame-ratio); overflow: hidden; place-items: center; margin: 0; border-radius: var(--radius-sm); box-shadow: inset 0 0 0 1px var(--border); background: var(--window); color: var(--text-subtle); }
.ui-frame-s { --frame-w: var(--frame-w-s); }
/* Absolut, nicht im Raster: Als Rasterkind loeste `height: calc(100% - 2px)` gegen
   eine `auto`-Zeile auf, das Bild wurde so hoch wie es wollte, und `overflow:
   hidden` schnitt — ein Hochkant fuellte das Fenster wie `cover`. Mit `inset`
   hat die Hoehe einen Bezug, und `contain` zeigt die Balken in `--window`. */
.ui-frame > img { position: absolute; inset: 1px; width: calc(100% - 2px); height: calc(100% - 2px); object-fit: contain; }
/* Die Wellenform ist das Bild einer Tondatei, gezeichnet von waveform.js in
   das leere <svg>, in derselben Fläche wie ein Bild; bis dahin steht das M.
   Gedämpft wie eine Meta-Zeile (7,98:1) — sie ist Auskunft, kein Pegel. */
.ui-frame-wave { position: absolute; inset: 1px; display: block; width: calc(100% - 2px); height: calc(100% - 2px); color: var(--text-muted); fill: currentColor; stroke: none; }
/* Noch nichts gesehen: das M, 30 % der Fensterbreite. Kein Spinner, kein
   Ordner — das Fenster sagt „hier käme ein Bild", sobald indiziert ist.
   Gefüllt wie jedes Zeichen der Marke, auch wenn `<x-icon>` es als `.icon`
   (gestrichen, 2px) ausgibt: ein M als Umriss wäre ein anderes Zeichen. */
.ui-frame > .ui-frame-mark { width: 30%; height: auto; fill: currentColor; stroke: none; color: var(--text-subtle); }
/* Im Ton-Fall stehen M und Welle im selben Fenster; das M tritt erst zurueck,
   wenn frame-waveform.js die Kurve gezeichnet und `data-drawn` gesetzt hat —
   ohne JavaScript oder ohne Antwort bleibt es das Bild. */
.ui-frame:has(> .ui-frame-wave[data-drawn]) > .ui-frame-mark { display: none; }
/* Die Zustandsfarbe des Punkts steht hier, nicht nur in ui-shape.css: Der
   Projektbereich laedt jenes Blatt nicht, und ein Punkt, der immer grau ist,
   sagt nichts. Dieselbe Zuordnung wie `.shape-state[data-state]` dort. */
.ui-frame[data-state="ok"] { --shape-state: var(--success); }
.ui-frame[data-state="warn"] { --shape-state: var(--warning); }
.ui-frame[data-state="error"] { --shape-state: var(--danger-color); }
.ui-frame > .ui-frame-wave:not([data-drawn]) { display: none; }
/* Die Dauer am Fenster, sobald es Kartenbreite hat; in der Zeile steht sie
   im Text, nie beides. 78 % `--window` unter `--text` hält auf dem hellsten
   Bild noch 10:1 (gerechnet über Weiß) — in OKLCH gemischt, damit die Deckung
   auf jedem Bild dieselbe bleibt. */
.ui-frame-tc { position: absolute; right: var(--space-1); bottom: var(--space-1); padding: 1px var(--space-1); border-radius: var(--radius-sm); background: color-mix(in oklch, var(--window) 90%, transparent); color: var(--text); font-size: var(--font-xs); font-weight: var(--weight-medium); line-height: var(--leading-base); }
/* Ein Zustandspunkt am Fenster (Abnahme), oben links wie die Aufnahmeleuchte
   am Monitor — nur zusätzlich zum Wort in der Meta-Zeile und `aria-hidden`,
   denn 8px Farbe sind keine Auskunft (SC 1.4.1). Farbe aus `--shape-state`
   (ui-shape.css), damit die Zustandsfläche darüber sie setzt. */
.ui-frame-dot { position: absolute; top: var(--space-2); left: var(--space-2); width: 8px; height: 8px; border-radius: var(--radius-round); background: var(--shape-state, var(--text-subtle)); box-shadow: 0 0 0 2px color-mix(in oklch, var(--window) 70%, transparent); }
/* Ein leerer Schlitz in derselben Breite, damit Zeilen ohne Medium
   (Mitglied, Einstellung) in einer Fensterliste mit den Fensterzeilen
   fluchten; das 16px-Symbol steht mittig und behält die Farbe des
   Zeilensymbols. Ein Fenster vor „Datensicherung" gäbe es nicht. */
.ui-frame-slot { display: grid; flex: 0 0 auto; width: var(--frame-w-s); place-items: center; color: var(--text-muted); }
/* Das Titelbild eines Arbeitsbereichs (2026-09-11) füllt das Fenster:
   `cover` statt `contain`, weil der Server es schon auf 16:9 geschnitten hat
   und ein gesetztes Bild Fläche ist, kein Standbild mit Balken. Es bleibt
   wie jedes Bild 1px innerhalb des Rings. */
.ui-frame > .ui-frame-cover { object-fit: cover; }
/* Die Vorschau beim Überfahren (#352, Stufe 2). Das Sprite liegt als
   Hintergrund im Fenster und wird erst beim ersten Überfahren geladen
   (`frame-scrub.js` setzt `background-image`); solange nichts gesetzt ist,
   sagt diese Regel nur, dass es sich nicht kacheln soll. `contain` fehlt
   bewusst: `background-size` trägt hier das Raster (1000 % × 1000 %), und
   jede zweite Angabe überschriebe es.
   `.is-scrubbing` blendet das Standbild aus statt es zu überdecken — ein
   `<img>` über dem Hintergrund bliebe sonst obenauf, und man sähe die
   Kacheln nie. Kein Übergang, kein Einblenden: Das Bild soll dem Zeiger
   folgen, nicht ihm hinterherlaufen. */
.ui-frame[data-scrub] { background-repeat: no-repeat; }
.ui-frame[data-scrub].is-scrubbing > img { visibility: hidden; }
/* Wer weniger Bewegung will, bekommt keine. Das Skript bindet dann gar nicht
   erst (`frame-scrub.js`, `reduced()`); diese Regel ist der zweite Riegel für
   den Fall, dass die Klasse doch einmal gesetzt wurde — dann steht wieder das
   Standbild da, nicht ein halb aufgedecktes Sprite. */
@media (prefers-reduced-motion: reduce) {
    .ui-frame[data-scrub].is-scrubbing > img { visibility: visible; }
    .ui-frame[data-scrub].is-scrubbing { background-image: none !important; }
}
/* Die Kachel mit Initialen: der Leerzustand eines Arbeitsbereichs, wo weder
   Titelbild noch Standbild liegt. Creme (`--cover`, ungespiegelt wie
   `--window` — ein Bild wechselt nicht mit dem Thema), darauf `--on-cover`
   (15,93:1; `--text-subtle` trüge nur 2,88, deshalb nie gedämpft). Die
   Schrift misst 4 von 9 viewBox-Einheiten — sie wächst mit dem Fenster,
   ohne dass eine Pixelgröße je Breite gepflegt werden müsste. */
/* `--frame-initials-size` ist ein Maß im viewBox der Kachel (4 von 9
   Einheiten), keine Schriftgröße der Seite — deshalb kein Token der Skala
   und keine Zahl aus ihr; der Browser skaliert es mit dem Fenster. */
.ui-frame-initials { --frame-initials-size: 4px; position: absolute; inset: 1px; display: block; width: calc(100% - 2px); height: calc(100% - 2px); }
.ui-frame-initials > rect { fill: var(--cover); }
.ui-frame-initials > text { fill: var(--on-cover); font: var(--weight-semibold) var(--frame-initials-size) var(--font-sans); }
/* Der Griff zum Setzen des Titelbilds: ein Formular um das Fenster, die
   Dateiwahl als Label darüber, der Stift in der Ecke wie die Dauer
   (`.ui-frame-tc`, dieselbe Deckung, dieselbe Lesbarkeit). Sichtbar erst
   unter der Maus oder mit Fokus — der Stift ist ein Griff, kein Zustand, und
   stünde sonst in jeder Zeile. Kein Einblenden über `--motion-*` hinaus,
   keine Vergrößerung. Das Label liegt als Geschwister über der <figure>,
   weil ein Label nur Phrasing-Inhalt erlaubt und die figure keiner ist. */
.ui-cover { position: relative; display: grid; width: var(--frame-w); margin: 0; }
/* Fenster und Label in derselben Rasterzelle statt `inset: 0` am Label: Ohne
   JavaScript steht der Knopf „Bild setzen" als zweite Zeile darunter, und ein
   absolut gelegtes Label deckte ihn mit ab. */
.ui-cover > .ui-frame { grid-area: 1 / 1; }
.ui-cover-pick { grid-area: 1 / 1; z-index: 1; display: grid; padding: var(--space-1); border-radius: var(--radius-sm); cursor: pointer; opacity: 0; place-items: end; transition: opacity var(--motion-fast) var(--motion-enter); }
.ui-cover-pick > .icon { box-sizing: content-box; padding: 2px; border-radius: var(--radius-sm); background: color-mix(in oklch, var(--window) 90%, transparent); color: var(--text); }
.ui-cover:hover > .ui-cover-pick { opacity: 1; }
.ui-cover:focus-within > .ui-cover-pick { opacity: 1; outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Mit `cover-upload.js` schickt die Dateiwahl das Formular selbst ab; der
   Knopf bleibt für den Fall ohne JavaScript — dann steht er unter dem Fenster. */
.ui-cover[data-enhanced] > .ui-cover-submit { display: none; }
.ui-cover > .ui-cover-submit { justify-self: start; }
/* Am Telefon fällt „Bild entfernen" unter die Zeile: Neben einem 96px-Fenster
   und einem Handgriff blieben dem Text sonst 180px, und „Speicherbereich G"
   brach mitten im Wort um. Nur Zeilen mit Titelbild-Formular — sie sind die
   einzigen mit Fenster **und** Handgriff. */
@media (max-width: 600px) {
    .ui-row:has(> .ui-cover) { flex-wrap: wrap; }
    /* Basis 0 statt `auto`: Sonst umbräche der Text selbst unter das Fenster,
       sobald er breiter ist als der Platz daneben — nur der Handgriff soll fallen. */
    .ui-row:has(> .ui-cover) > .ui-row-body { flex-basis: 0; }
    .ui-row:has(> .ui-cover) > .ui-row-actions { flex-basis: 100%; justify-content: flex-end; }
    /* Dasselbe für jeden Handgriff mit Wort (#354): Auf /admin/services drückte
       „Keine neuen Aufträge abholen" den Dienstnamen auf 90px. Ein Symbolknopf
       oder ein Menü (`…`) bleibt neben der Zeile — er ist 30px breit, auch in
       einem Formular: Der Papierkorb eines Passkeys ist ein DELETE-Formular,
       gesperrt ein nackter Knopf, und beide Zustände stehen gleich (#487). */
    .ui-row:has(> .ui-row-actions > form button:not(.icon-action)), .ui-row:has(> .ui-row-actions > .button-link), .ui-row:has(> .ui-row-actions > button:not(.icon-action)) { flex-wrap: wrap; }
    .ui-row:has(> .ui-row-actions > form button:not(.icon-action)) > .ui-row-body, .ui-row:has(> .ui-row-actions > .button-link) > .ui-row-body, .ui-row:has(> .ui-row-actions > button:not(.icon-action)) > .ui-row-body { flex-basis: 0; }
    .ui-row:has(> .ui-row-actions > form button:not(.icon-action)) > .ui-row-actions, .ui-row:has(> .ui-row-actions > .button-link) > .ui-row-actions, .ui-row:has(> .ui-row-actions > button:not(.icon-action)) > .ui-row-actions { flex-basis: 100%; justify-content: flex-end; }
}

/* --- Timecode (2026-09-11) --------------------------------------------------
   Alles Zählbare — Dauer, Timecode, Größe, Anzahl — in Inter, nicht in Mono:
   Ein Schnittprogramm setzt Timecode in seiner Oberflächenschrift.
   `tabular-nums` stellt Stelle auf Stelle (auch den Doppelpunkt),
   `slashed-zero` trennt 0 von O bei 11px, "case" hebt den Doppelpunkt auf
   Ziffernmitte. `--font-mono` bleibt Kennung, Konsole und Pfad.
   Führende Stunden dürfen als <small> gedimmt werden — das Auge landet auf
   dem, was sich ändert —, sonst nichts. */
.tc { font-variant-numeric: tabular-nums slashed-zero; font-feature-settings: "case"; white-space: nowrap; }
.tc > small { color: var(--text-subtle); font: inherit; }

/* --- Streifen (2026-09-11) ----------------------------------------------------
   Eine Reihe Fenster in Kartenbreite, ein Streifen je Übersicht, vor den
   Zahlen. `auto-fill` ab `--frame-w-card` statt einer Medienabfrage: bei
   1440 sechs Kacheln, am Telefon zwei, ohne dass ein Haltepunkt es sagt.
   Die Kachel hat keinen Rahmen und keine Fläche — das Fenster ist die Form.
   Unter der Maus nur die Kante am Fenster, kein Schatten, keine
   Vergrößerung; Scrub durch den Clip ist Stufe 2. Die Zeilen darunter
   schneiden ab statt umzubrechen, damit sechs Kacheln eine Höhe haben. */
/* `min(...)`: Bei 375 px braeuchten zwei 168er-Spalten 348 px und bekaemen 343 —
   dann stuende eine Saeule aus sechs Postern vor der ersten Zahl. Die Spalte
   darf schmaler werden, das Raster bleibt zweispaltig. */
.ui-reel { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(var(--frame-w-card), calc(50% - var(--space-3))), 1fr)); gap: var(--space-3); }
.ui-reel-item { display: grid; gap: var(--space-2); min-width: 0; color: var(--text); text-decoration: none; }
.ui-reel-item > .ui-frame { width: 100%; transition: box-shadow var(--motion-fast) var(--motion-enter); }
.ui-reel-item:hover { color: var(--text); }
.ui-reel-item:hover > .ui-frame { box-shadow: inset 0 0 0 1px var(--border-strong); }
.ui-reel-body { display: grid; gap: 2px; min-width: 0; }
.ui-reel-body > strong { min-width: 0; overflow: hidden; font-size: var(--font-sm); font-weight: var(--weight-medium); line-height: var(--leading-base); text-overflow: ellipsis; white-space: nowrap; }
.ui-reel-body > small { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; min-width: 0; overflow: hidden; color: var(--text-subtle); font-size: var(--font-xs); line-height: var(--leading-base); }
/* Ehrlich leer: ein einziges Fenster mit dem M neben dem Leerzustand — sechs
   M-Fenster behaupteten sechs Einträge. Ein Zustand, kein Fehler. Der Text
   steht linksbündig neben dem Fenster wie eine Zeile, nicht zentriert wie in
   einer leeren Fläche; das Fenster ist hier das Bild, also kein zweites. */
.ui-reel-empty { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
.ui-reel-empty > .ui-frame { width: var(--frame-w-card); }
.ui-reel-empty > .empty-state { flex: 1 1 24ch; justify-items: start; padding: 0; text-align: left; }
.ui-reel-empty > .empty-state > .icon { display: none; }

/* --- Meter (2026-09-10) -----------------------------------------------------
   Ein Anteil als schmaler Balken: 6px, Pille, Spur vertieft, Füllung in der
   Zustandsfarbe der Umgebung (`--shape-state`, ui-shape.css) oder der Marke.

       <div class="ui-meter" role="meter" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-label="Belegt"><i style="width: 62%"></i></div>

   Kein Balken bei „nicht gemessen" oder „ausstehend" — das entscheidet der
   Aufrufer: Ein leerer Balken sähe aus wie „0 %", und das wäre eine Zahl, die
   niemand gemessen hat. `data-tone="warn|danger"` am Meter, wenn keine
   Zustandsfläche darüber die Farbe setzt.

   Ersetzt auf Dauer `.meter` (app.css), `.shape-meter` (ui-shape.css),
   `.workspace-metric-bar` (workspace.css) und `.system-sensor-bar`
   (system-gauges.css) — vier Balken für eine Sache. Hier steht der eine; die
   vier fallen im Rollout, sobald ihre Aufrufer umgezogen sind. */
.ui-meter { display: block; width: 100%; height: 6px; overflow: hidden; border-radius: var(--radius-pill); background: var(--surface-recessed); }
.ui-meter > i { display: block; height: 100%; border-radius: inherit; background: var(--shape-state, var(--brand)); }
.ui-meter[data-tone="warn"] { --shape-state: var(--warning); }
.ui-meter[data-tone="danger"] { --shape-state: var(--danger-color); }

/* --- Abzeichen und Pille --------------------------------------------------
   `border-radius: 999px` stand in 26 Regeln über 14 Dateien; zwölf davon waren
   dasselbe Bauteil — kleine Fläche, Großbuchstaben, 10–12px, Rand in einer
   Zustandsfarbe. `.library-badge` war der Beleg dafür, dass das nicht bloß
   Menge ist: es gab zwei Fassungen, in admin-settings.css und in
   sound-library.css, die einander nie begegnen, weil keine Seite beide
   Stylesheets lädt.

   Seit dem 2026-09-10 in normaler Schreibung, 11/500: Versalien nehmen dem Wort
   seine Form, und ein Abzeichen wird gelesen, nicht buchstabiert. */
/* `font-style: normal` gehört ins Bauteil und nicht an jede Fundstelle: Das
   Abzeichen steht mehrfach als `<em>` im Fließtext einer Tabellenzelle
   („Bibliothek"), und der Schrägschnitt kommt dort vom Element, nicht von der
   Form. Er wurde deshalb zweimal getrennt zurückgenommen — in
   admin-settings.css und in sound-library.css —, ohne dass die beiden Seiten
   sich je begegnet wären. */
.ui-badge { display: inline-grid; align-items: center; padding: 3px var(--space-2); border: 1px solid var(--border-strong); border-radius: var(--radius-pill); color: var(--text-muted); font-size: var(--font-xs); font-weight: var(--weight-medium); font-style: normal; letter-spacing: 0; line-height: var(--leading-base); text-transform: none; white-space: nowrap; }
/* Die vier `[data-state]`-Töne sind gefallen (#397, Gruppe B): dieselben vier
   Farben wie `.ui-badge.is-*` weiter unten, nur in einer zweiten
   Schreibweise — und mit genau einem Aufrufer, dem freien Platz im
   Projektbereich. Der nimmt jetzt `<x-badge :tone>` wie jedes andere
   Abzeichen; `accent` hatte nie einen. */

/* --- Rollenchip -----------------------------------------------------------
   Die Rolle eines Kontos ist eine Einordnung, kein Zustand — sie trägt
   deshalb ein Abzeichen und keinen Zustandspunkt. Sie stand aber in zwei
   Fassungen da: auf /admin/access als farbiger Chip (hier, vormals inline in
   der Ansicht auf `.access-page` beschränkt), auf /admin/desktop-client als
   graue Pille `.role` mit einer Sonderfarbe für Administratoren. Zwei
   Bauarten für dieselbe Auskunft waren der Befund; die Fassung mit der
   gepflegten Rollenfarbe hat gewonnen und steht seit #224 hier, wo beide
   Seiten sie lesen können. */
.role-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.role-chip { display: inline-flex; align-items: center; gap: var(--space-2); padding: 2px var(--space-2); border: 1px solid var(--role-color, var(--border-strong)); border-radius: var(--radius-pill); color: var(--text); font-size: var(--font-sm); font-weight: var(--weight-medium); white-space: nowrap; }
.role-chip::before { content: ''; width: 8px; height: 8px; border-radius: var(--radius-round); background: var(--role-color, var(--text-subtle)); }
.role-chip.is-implicit { border-style: dashed; color: var(--text-muted); }

/* --- Menüfläche -----------------------------------------------------------
   Acht Umsetzungen desselben Rezepts, mit zufälligen statt begründeten
   Abweichungen: z-index 10/30/40/60/80/90, Radius 7/8/10px, `--surface-1`
   gegen `--surface-2`. Der z-index ist hier zu einer Staffel geordnet:
   `--z-menu` (40) Menü im Inhalt · `--z-menu-top` (80) Menü in der obersten
   Zeile · der Dialog liegt im Top Layer. Die Sprossen heißen seit #407 beim
   Namen aus color-tokens.css statt als Zahl — dieselben Werte; nur so sieht
   man, dass Schublade (`--z-drawer`), Vorhang und Menü eine Reihe sind. Die
   Fläche bringt bewusst keine Verankerung mit (kein top/right/left): Wo ein
   Menü aufgeht, weiß nur die Stelle, an der es hängt.

   Ein Eintrag darf auch in einem `<form>` stecken. Ohne Build-Werkzeug und
   ohne Framework ist ein Menüpunkt, der etwas ändert, ein POST — das Abmelden
   im Kontomenü ist genau das. Der Kindkombinator allein erreicht diesen Knopf
   nicht, deshalb steht `> form > button` überall dort, wo `> button` steht.

   `:focus-visible` steht neben `:hover`, weil ein Menü auch mit der Tastatur
   bedient wird: `mount-context-menu.js` setzt den Fokus beim Öffnen auf den
   ersten Eintrag, und der bloße Fokusring sagt nicht, welche Zeile gemeint
   ist. */
.ui-menu-surface { position: absolute; z-index: var(--z-menu); display: grid; min-width: 190px; padding: var(--space-2); border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface-2); box-shadow: var(--shadow-2); }
/* Die zweite Stufe der Staffel, und der Grund, warum es sie geben muss: In der
   Adminschale liegt die oberste Zeile in einem `position: sticky`-Header und
   damit in einem eigenen Stapelkontext, in dem jeder Wert reicht. Im Viewer
   rendert dieselbe Zeile als `.viewer-topbar` — die hat weder `position` noch
   `z-index`, bildet also keinen Kontext, und die Menüs konkurrieren seitenweit
   gegen jede Schublade mit Vorhang (--z-backdrop 50, Blatt --z-drawer 60 —
   bis zum 2026-09-21 die Dateischublade des Viewers, heute die Seitenleiste
   der Adminschale unter 960px, admin-layout.css). Mit --z-menu (40) öffnete
   das Kontomenü hinter dem Grauschleier. */
.ui-menu-surface.is-topbar { z-index: var(--z-menu-top); }
.ui-menu-surface > a, .ui-menu-surface > button, .ui-menu-surface > form > button { display: flex; width: 100%; min-height: var(--control-h); align-items: center; justify-content: flex-start; gap: var(--space-2); padding: var(--space-2) var(--space-3); border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text); font: inherit; font-size: var(--font-md); text-align: left; text-decoration: none; cursor: pointer; }
.ui-menu-surface > a:hover, .ui-menu-surface > a:focus-visible, .ui-menu-surface > button:hover:not(:disabled), .ui-menu-surface > button:focus-visible, .ui-menu-surface > form > button:hover:not(:disabled), .ui-menu-surface > form > button:focus-visible { background: var(--surface-hover); }
.ui-menu-surface > button:disabled, .ui-menu-surface > form > button:disabled { cursor: not-allowed; opacity: .5; }
.ui-menu-surface > .danger, .ui-menu-surface > button.danger, .ui-menu-surface > form > button.danger { color: var(--danger-color); }
.ui-menu-surface > hr { width: 100%; margin: var(--space-1) 0; border: 0; border-top: 1px solid var(--border); }
.ui-menu-surface > form { margin: 0; }
.ui-menu-surface > a > .icon, .ui-menu-surface > button > .icon, .ui-menu-surface > form > button > .icon { flex: 0 0 16px; color: var(--text-subtle); text-align: center; }
/* Die tastbare Mindesthoehe zurueckholen.

   `app.css` hebt unter 760px jeden Knopf auf 44px — aber mit (0,0,1). Die
   Eintragsregel hier steht bei (0,1,1) und schlaegt sie, unabhaengig von der
   Ladereihenfolge. Vor der Zusammenlegung trugen die Menues der Projektseite
   gar keine Mindesthoehe, die Mobilregel griff also. Ohne diese Stufe waere
   die Zusammenlegung eine Verschlechterung genau dort, wo es weh tut: Im
   Dateimenue liegt „Loeschen" direkt unter „Vorschau neu erzeugen", und 36px
   mit 8px Abstand ist die Groesse, bei der ein Fehlgriff die Grenze zwischen
   harmlos und zerstoerend ueberschreitet. */
@media (pointer: coarse), (max-width: 760px) {
    .ui-menu-surface > a, .ui-menu-surface > button, .ui-menu-surface > form > button { min-height: var(--control-h-touch); }
}

/* --- Reiterleiste ---------------------------------------------------------
   Fünf Ausprägungen von „Reihe aus Links oder Knöpfen, einer davon aktiv".
   `> a` und `> button` stehen ausdrücklich in einer Regel: Dieselbe Leiste wird
   im Bestand einmal aus Knöpfen und einmal aus Links gebaut (Arbeitsbereiche in
   workspace/files gegen workspace/card-ingest), und eine Fassung, die nur eine
   der beiden Formen kennt, lässt die andere unformatiert stehen. */
.ui-tabs { display: flex; gap: var(--space-1); flex-wrap: wrap; align-items: center; border-bottom: 1px solid var(--border); }
.ui-tabs > a, .ui-tabs > button { display: inline-flex; min-height: var(--control-h); align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-3); border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--text-muted); font: inherit; font-size: var(--font-md); font-weight: var(--weight-medium); text-decoration: none; cursor: pointer; }
.ui-tabs > a:hover, .ui-tabs > button:hover { color: var(--text); }
.ui-tabs > .active, .ui-tabs > [aria-selected="true"], .ui-tabs > [aria-current="page"] { border-bottom-color: var(--brand); color: var(--text); }
/* Die tastbare Mindesthöhe gehört ins Bauteil, nicht in jede Oberfläche:
   `app.css` setzt sie unter 760px an `button`/`nav a` (0,0,1 bzw. 0,0,2), und
   `.ui-tabs > a` ist (0,1,1) und schlug sie — unabhängig von der
   Ladereihenfolge. Die Reiter der Hilfe waren damit auf dem Telefon 36px hoch,
   obwohl die Regel für alles andere gilt. */
@media (pointer: coarse), (max-width: 760px) { .ui-tabs > a, .ui-tabs > button { min-height: var(--control-h-touch); } }
/* Eine umgebrochene Reiterleiste mit Unterkante liest sich als Defekt: Die
   Linie laeuft dann quer durch die Seite, und darunter haengen zwei Reiter,
   die aussehen wie vergessen. Unter 600px scrollt die Reihe deshalb seitwaerts,
   statt umzubrechen (#234). `scroll-padding` und `-webkit-overflow-scrolling`
   bleiben aus: Der aktive Reiter steht in dieser Anwendung immer vorn. */
@media (max-width: 600px) {
    .ui-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: thin; }
    .ui-tabs > a, .ui-tabs > button { flex: 0 0 auto; }
}

/* --- Tabellenkörper -------------------------------------------------------
   Fünfmal dieselbe Geometrie: Zellenhöhe 42px, Polsterung 8–9px/12–14px,
   `border-bottom`, klebriger Kopf in Großbuchstaben mit `letter-spacing`,
   letzte Zeile ohne Linie. Dass diese Menge stört, stand schon im Bestand:
   interaction-feedback.css muss alle drei Namen aufzählen, um eine gemeinsame
   Eigenschaft zu setzen. */
/* Toene des Abzeichens (#224). Es gab drei Stile nebeneinander — gefuellt-rund
   gemischt geschrieben, Outline-Versalien, orange Outline-Versalien — und
   Gruen hiess auf der einen Seite „alles in Ordnung", auf der naechsten
   „ausgewaehlt". Deshalb: fuenf benannte Toene, und Auswahl ist blau wie
   Seitenleiste und Reiter, nie gruen. */
.ui-badge.is-info { border-color: color-mix(in srgb, var(--brand) 55%, transparent); color: var(--brand-hover); }
.ui-badge.is-success { border-color: color-mix(in srgb, var(--success) 55%, transparent); color: var(--success); }
.ui-badge.is-warning { border-color: color-mix(in srgb, var(--warning) 55%, transparent); color: var(--warning); }
.ui-badge.is-danger { border-color: color-mix(in srgb, var(--danger-color) 55%, transparent); color: var(--danger-color); }
.ui-badge.is-selected { border-color: var(--brand); background: var(--brand-soft); color: var(--brand-hover); }

/* Das Drei-Punkte-Menue einer Zeile (#224). Die Regel „eine Aktion → Textknopf,
   zwei oder mehr → Menue" gilt auf jeder Seite; die Grundregeln standen aber in
   project-ui.css, das nur die Projektansichten laden.

   Flaeche und Eintraege kommen aus `.ui-menu-surface` (#160). Eigen bleibt
   allein die Verankerung: Die gemeinsame Fassung setzt `position: absolute`,
   bringt aber bewusst kein top/right mit — woran ein Menue haengt, weiss nur
   seine Stelle. Der eigene z-index war einmal 10 und damit *unter* der
   klebrigen Kopfzeile der Adminschale (30); das Menue fuhr beim Scrollen
   darunter. */
.more-menu { position: relative; }
.more-menu > summary { list-style: none; cursor: pointer; }
.more-menu > summary::-webkit-details-marker { display: none; }
.more-menu-popover { top: calc(100% + 8px); right: 0; }

/* Die Hülle der Tabelle (2026-09-11, aus app.css): Rand, Radius, Fläche und
   das Scrollen in sich, damit eine breite Tabelle nie die Seite scrollt. Hier
   und nicht in app.css, weil die Viewer-Dokumente (Dateien, Sounds) app.css
   nicht laden können — es formt `aside`, `main`, `table{min-width:760px}` — und
   ihre Tabellen trotzdem dieselbe Hülle tragen sollen. */
.table-wrap { overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-1); }
.ui-table { width: 100%; border-collapse: collapse; }
.ui-table th, .ui-table td { height: 42px; padding: var(--space-2) var(--space-3); text-align: left; }
/* Doppelung: `.ui-table th` steht bewusst zweimal — oben das Zellenmaß, das
   Kopf und Rumpf teilen, hier Schrift, Fläche und Linie des Kopfes. Die stehen
   an `.ui-table th` (0,1,1) und nicht an `.ui-table thead th` (0,1,2) — das ist
   kein Schönheitsfehler, sondern die Voraussetzung dafür, dass eine Seite
   ihren Kopf überhaupt noch anders setzen kann. Jede örtliche Kopfregel heißt
   `.X th` und ist damit (0,1,1); gegen (0,1,2) verlor sie unabhängig von der
   Ladereihenfolge. Auf den zehn Tabellen mit `class="ui-table shape-table"` waren
   `background: transparent` und `letter-spacing: .09em` aus ui-shape.css
   deshalb wirkungslos: Ihr Kopf stand auf --surface-2 statt durchsichtig über
   der eingefassten Fläche (--surface-recessed) — ein helleres Band, das niemand
   bestellt hatte. Bei (0,1,1) gegen (0,1,1) entscheidet wieder die Reihenfolge
   der Blätter, also die Seite.

   Seit dem 2026-09-10 steht der Kopf in normaler Schreibung, 12/500
   (docs/Oberflaeche/Typografie-und-Form.md): Zehn Pixel Versalien mit Sperrung
   waren die am schlechtesten lesbare Schrift der Seite. `text-transform: none`
   und `letter-spacing: 0` stehen trotzdem ausgeschrieben da —
   `SharedSurfaceCompositionTest` prüft, dass diese Eigenschaften hier stehen
   und nicht an `.ui-table thead th`.

   Das Klebrige bleibt getrennt und darf (0,1,2) sein: Es ist eine Frage der
   Struktur, keine der Schrift, und keine Seite nimmt es zurück. Die Fläche
   gehört trotzdem zur Schriftregel — ein klebriger Kopf braucht sie, aber
   deckend machen kann eine Seite ihn auch selbst. */
.ui-table th { border-bottom: 1px solid var(--border); background: var(--surface-2); color: var(--text-subtle); font-size: var(--font-sm); font-weight: var(--weight-medium); letter-spacing: 0; text-transform: none; }
.ui-table thead th { position: sticky; z-index: 2; top: 0; }
.ui-table tbody td { border-bottom: 1px solid var(--border); }
.ui-table tbody tr:last-child td { border-bottom: 0; }

/* --- Leerzustand ----------------------------------------------------------
   `components/empty-state.blade.php` wird an achtzig Stellen aufgerufen — eine
   Komponente, vorbildlich. Ihr Aussehen aber stand dreimal und war bereits
   auseinandergelaufen: app.css 32px/46ch, workspace.css 30px/44ch (und ohne
   `margin-bottom` am Symbol), viewer.css 32px/42ch. Wer die Komponente änderte,
   sah das Ergebnis auf einer der drei Oberflächen und hielt es für erledigt.

   Gewählt ist die Fassung aus app.css: sie wirkte an den meisten Stellen, und
   46ch ist von den dreien die Breite, bei der ein zweizeiliger Satz nicht in
   drei Zeilen bricht. Der Name bleibt `.empty-state` — es gibt genau eine
   Komponente mit dieser Klasse, ein zweiter Name wäre nur ein zweiter Name.
   Farben brauchen hier keine eigene Angabe je Oberfläche: Es gibt nur die
   eine Token-Datei. */
.empty-state { display: grid; justify-items: center; gap: var(--space-2); padding: var(--space-7) var(--space-5); color: var(--text-muted); text-align: center; }
/* Das Symbol kommt seit #354 aus dem Bogen (`<x-icon>`, 24 px) — auch aus
   `ui-states.js`, das denselben Block nachlädt; seit #361 gibt es keinen
   Aufrufer mehr, der seinen Namen aus einer Symbolschrift holt. */
.empty-state > .icon { margin-bottom: var(--space-1); color: var(--border-strong); }
.empty-state strong { color: var(--text); font-size: var(--font-base); }
.empty-state p { max-width: 46ch; margin: 0; color: var(--text-muted); }
.empty-state .button-link { margin-top: var(--space-2); }
/* Fehler und Idle teilen die Form des Leerzustands; nur der Akzent
   unterscheidet die drei. Die Töne standen bis #397 in
   `interaction-feedback.css`, der Block selbst hier — ein Blatt, das nur
   dieses lädt (die Stock-Schale), zeigte den Fehlerblock grau. Der Titel im
   Fehlerzustand in der Zustandsfarbe selbst — 6,55:1 auf `--canvas` —, nicht
   in einem aufgehellten Rot, das in keiner Tabelle steht. */
.empty-state.state-error > .icon { color: var(--danger-color); }
.empty-state.state-error strong { color: var(--danger-color); }
.empty-state.state-idle > .icon { opacity: .8; }

/* --- Hilfe-Symbol mit Erklärung (2026-08-25) ------------------------------
   Das Gegenstück zu `public/help-hint.js` und `components/help-hint.blade.php`.
   Es steht hier und nicht in einem eigenen Blatt, weil jede Oberfläche, die ein
   Fachwort zeigt, dieses Blatt ohnehin lädt — ein zusätzliches `<link>` je
   Ansicht wäre eine Zeile, die man vergessen kann.

   Das Symbol ist ein gezeichnetes Fragezeichen und kein Glyph einer
   Symbolschrift. Der ursprüngliche Grund war das CDN: Die Schrift kam von
   außen, dieser Server steht im LAN, und ohne Internet bliebe ausgerechnet das
   Zeichen leer, das Hilfe verspricht. Vom 2026-09-10 bis zum 2026-09-13 lag
   Font Awesome im Haus, nur als Brücke; seit #361 ist die Schrift weg. Ein
   gezeichnetes Zeichen hat beide Umzüge überlebt, ein Glyph keinen von beiden.

   Alles ist Phrasing Content (`span`, `button`), damit der Hinweis auch in
   einer Überschrift, einer Tabellenzelle oder einem Absatz stehen darf. Die
   Blase setzt Schriftschnitt, Laufweite und Groß-/Kleinschreibung ausdrücklich
   zurück: In einem `.ui-table thead th` erbte sie sonst Versalien und stünde in
   Großbuchstaben da.

   `--z-popover` (100): über den Menüflächen (`--z-menu` 40, `--z-menu-top`
   80) der Staffel oben, unter dem Dialog im Top Layer — ein Hinweis erklärt
   eine Stelle, er verdeckt keinen Dialog. */
.ui-help { position: relative; display: inline-grid; vertical-align: middle; }
/* `min-height: 0` hebt die 44px-Regel von app.css bewusst auf: Sie ist für
   Knöpfe gedacht, die für sich stehen, und ein 44px hoher Kreis mitten in einer
   Überschrift zerrisse die Zeile. Die Trefferfläche kommt stattdessen vom
   `::after` unten — 15px trifft ein Finger nicht zuverlässig, aber wachsen darf
   dafür nur der Fangbereich, nicht die Form.
   `background` steht auch in der Hover-Regel, weil `button:hover` in app.css
   sonst die Markenfarbe darunterlegt. */
.ui-help-trigger { position: relative; display: inline-grid; width: 15px; height: 15px; min-height: 0; place-items: center; padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-round); background: transparent; color: var(--text-muted); font: inherit; font-size: var(--font-xs); font-weight: var(--weight-semibold); font-style: normal; letter-spacing: 0; line-height: var(--leading-none); text-transform: none; cursor: help; }
.ui-help-trigger::after { position: absolute; inset: -7px; border-radius: var(--radius-round); content: ""; }
.ui-help-trigger:hover, .ui-help-trigger:focus-visible, .ui-help-trigger[aria-expanded="true"] { border-color: var(--brand); background: transparent; color: var(--brand-hover); }
/* Doppelung: `.ui-help-trigger:focus-visible` steht bewusst zweimal — oben in
   der Gruppe, die Rahmen und Farbe aller drei Hervorhebungszustände setzt, hier
   allein der Ring, den nur die Tastatur auslöst. Er steht hier und nicht nur in
   interaction-feedback.css, weil die Anlieferungsseite dieses Blatt lädt und
   jenes nicht (#160). */
.ui-help-trigger:focus-visible { outline: 2px solid var(--brand-hover); outline-offset: 2px; }
.ui-help-bubble { --ui-help-shift: 0px; position: absolute; z-index: var(--z-popover); top: calc(100% + 8px); left: 50%; display: grid; width: max-content; max-width: min(320px, calc(100vw - 24px)); gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface-2); box-shadow: var(--shadow-2); color: var(--text-muted); font-size: var(--font-sm); font-style: normal; font-weight: var(--weight-regular); letter-spacing: normal; line-height: var(--leading-base); text-align: left; text-transform: none; white-space: normal; transform: translateX(calc(-50% + var(--ui-help-shift))); }
/* `hidden` allein verliert gegen das `display: grid` oben — deshalb ausdrücklich. */
.ui-help-bubble[hidden] { display: none; }
.ui-help-bubble strong { color: var(--text); font-size: var(--font-sm); font-weight: var(--weight-semibold); }
/* Nach oben geklappt, wenn unten kein Platz ist — gesetzt von help-hint.js. */
.ui-help[data-placement="top"] .ui-help-bubble { top: auto; bottom: calc(100% + 8px); }

/* --- Hinweisfläche --------------------------------------------------------
   Eine Bauart für „hier steht etwas, das du wissen solltest" (#224).

   Es gab vier: ein Banner mit farbigem Balken links (Übersicht, Worker), eine
   blaue Prosabox (Transkripte, Regeln, …), eine dünne grün/gelbe Zeile
   (Netzwerk) und eine Symbolbox in der Gefahrenkarte (Einstellungen). Jede war
   für sich in Ordnung; zusammen ergaben sie vier Sprachen für denselben Satz,
   und keine davon sagte durch ihre Form, wie ernst die Sache ist.

   Der Ton sagt es jetzt, und zwar mit denselben vier Farben, die Abzeichen,
   Zustandspunkt und Zustandsfläche schon benutzen — `--callout-tone` wird
   einmal je Kasten gesetzt, alles Farbige darin liest ihn. Eine fünfte Palette
   für Hinweiskästen gibt es ausdrücklich nicht.

   Der Balken steht links und nicht oben, weil ein Kasten neben einer Tabelle
   sonst wie deren Kopfzeile aussieht.

   Lange Erklärungen bekommen `fold`: Der Kasten wird zu `<details>`, sichtbar
   bleibt die Überschrift. Auf /admin/transcripts standen drei Absätze
   Fließtext über der ersten Tabelle — wer die Seite zum zwanzigsten Mal
   öffnet, scrollt daran vorbei, und wer sie zum ersten Mal öffnet, findet die
   Tabelle nicht. Zugeklappt ist die Vorgabe; `open` hält ihn offen. */
.ui-callout { --callout-tone: var(--info); display: grid; gap: var(--space-2); padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in srgb, var(--callout-tone) 32%, transparent); border-left: 3px solid var(--callout-tone); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--callout-tone) 9%, transparent); color: var(--text-muted); line-height: var(--leading-base); }
.ui-callout.is-success { --callout-tone: var(--success); }
.ui-callout.is-warning { --callout-tone: var(--warning); }
.ui-callout.is-danger { --callout-tone: var(--danger-color); }
.ui-callout > :is(p, ul, ol) { margin: 0; }
.ui-callout > * + * { margin-top: 0; }
.ui-callout code { overflow-wrap: anywhere; }
.ui-callout-title { display: flex; align-items: baseline; gap: var(--space-2); color: var(--text); font-size: var(--font-md); font-weight: var(--weight-semibold); }
/* `align-self: center`, weil die Zeile an der Grundlinie ausrichtet und ein
   Symbol aus dem Bogen keine hat — es säße sonst mit der Unterkante darauf,
   also zu hoch. Mittig in der Zeile stimmt für Schrift und Bogen. */
.ui-callout-title > .icon { align-self: center; color: var(--callout-tone); }

/* Die eingeklappte Fassung. `list-style: none` plus `::-webkit-details-marker`
   entfernt das Vorgabedreieck; vor dem Titel steht derselbe Winkel wie am
   Klappabschnitt (`.ui-fold-mark`, ui-shape.css — hier noch einmal, weil
   Viewer und Karten jenes Blatt nicht laden), und er dreht beim Öffnen: zwei
   Klappen auf einer Seite sollen sich nicht verschieden aufklappen. Bis zum
   2026-09-13 stand statt dessen ein „›" am rechten Rand (`::after`), und die
   zugeklappte Erklärung sah aus wie ein Warn-Callout mit Titel (#355). Das
   Ton-Symbol folgt dem Winkel und behält seine Farbe. */
details.ui-callout { gap: 0; }
details.ui-callout > summary { display: flex; align-items: baseline; gap: var(--space-2); color: var(--text); font-size: var(--font-md); font-weight: var(--weight-semibold); list-style: none; cursor: pointer; }
details.ui-callout > summary::-webkit-details-marker { display: none; }
details.ui-callout > summary > .icon { align-self: center; color: var(--callout-tone); }
details.ui-callout > summary > .ui-fold-mark { flex: 0 0 auto; color: var(--text-subtle); transition: transform var(--motion-fast) ease; }
details.ui-callout[open] > summary > .ui-fold-mark { transform: rotate(90deg); }
details.ui-callout > summary:hover { color: var(--callout-tone); }
.ui-callout-body { display: grid; gap: var(--space-2); margin-top: var(--space-2); }
/* Knöpfe unter dem Text eines Zustandsbands (#490, Slot `actions`). */
.ui-callout-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-1); }
/* Die Sammelleiste (Welle 4, #352): ein Callout ohne Symbol, links die Zählung
   der Auswahl, rechts die Knöpfe — sichtbar erst mit einer Auswahl. Sound-
   Bibliothek (`.sound-bulk`) und Lizenzpflege (`.licensing-bulk-bar`) lösten
   dieselbe Aufgabe in zwei Bauarten; jetzt eine. `[hidden]` braucht die Regel,
   weil `.ui-callout` ein Raster ist und das Attribut sonst verliert. */
.ui-callout.ui-bulk { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
.ui-callout.ui-bulk[hidden] { display: none; }
.ui-bulk-actions { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-2); }
.ui-bulk-actions form { margin: 0; }
@media (max-width: 760px) { .ui-callout.ui-bulk { grid-template-columns: minmax(0, 1fr); } }
.ui-callout-body > :is(p, ul, ol) { margin: 0; }
@media (prefers-reduced-motion: reduce) { details.ui-callout > summary > .ui-fold-mark { transition: none; } }

/* --- Nur-Lese-Wert --------------------------------------------------------
   Ein Wert, den man ansehen und kopieren, aber nicht ändern kann (#224).

   Auf /admin/settings, /admin/desktop-client und /admin/storage/proxies standen
   solche Werte in gerahmten Kacheln, die aussahen wie Eingabefelder — Rahmen,
   Innenabstand, Flächenfarbe, alles wie bei einem `input`. Wer dort hineinklickt
   und nichts tippen kann, hält die Seite für kaputt, nicht den Wert für fest.

   Deshalb: kein Rahmen, keine Feldfläche. Die Beschriftung steht klein darüber,
   der Wert darunter in der Textfarbe — die Form eines gedruckten Eintrags, nicht
   die eines Formulars. Werte, die jemand weitergeben muss (Server-Adresse,
   SMB-Ziel), bekommen `copy` und damit einen Kopierknopf; er ist die einzige
   Bedienung, die es hier gibt. */
.ui-readonly { display: grid; gap: 2px; align-content: start; min-width: 0; }
/* Die Beschriftung in normaler Schreibung (2026-09-11): 11 px Versalien mit
   Sperrung waren die am schlechtesten lesbare Schrift der Seite — und auf dem
   Stock-Vorgang, wo dieser Wert nach außen steht, die einzige. Dieselbe Form
   wie die Beschriftung der Kennzahl (`.stat small`). */
.ui-readonly > dt, .ui-readonly-label { color: var(--text-subtle); font-size: var(--font-sm); font-weight: var(--weight-medium); letter-spacing: 0; text-transform: none; }
.ui-readonly > dd, .ui-readonly-value { display: flex; align-items: center; gap: var(--space-2); min-width: 0; margin: 0; color: var(--text); font-size: var(--font-base); }
.ui-readonly-value > span { min-width: 0; overflow-wrap: anywhere; }
.ui-readonly.is-mono .ui-readonly-value > span { font-family: var(--font-mono); font-size: var(--font-md); }
.ui-readonly-copy { display: inline-grid; width: 24px; height: 24px; min-height: 0; flex: 0 0 auto; place-items: center; padding: 0; border: 1px solid var(--border); border-radius: var(--radius-sm); background: transparent; color: var(--text-subtle); font-size: var(--font-xs); cursor: pointer; }
.ui-readonly-copy:hover { border-color: var(--brand); background: transparent; color: var(--link); }
.ui-readonly-empty { color: var(--text-subtle); }
/* Mehrere Nur-Lese-Werte nebeneinander (#354): Vier Seitenstränge rufen die
   Klasse, ohne sie stapelten „Video-Encoder", „Encoder-Profil", „Qualität"
   senkrecht wie ein Formular ohne Felder. Umbruch statt Raster, weil die
   Werte verschieden breit sind (ein Pfad neben einer Zahl). */
.ui-readonly-row { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-6); }
.ui-readonly-row > .ui-readonly { flex: 0 1 auto; max-width: 100%; }
/* Fenster in der Tabellenzelle (#354): `<x-row>` in einer Zelle brächte Rahmen
   und Abstand doppelt, also ein schlichter Träger — Fenster links, Name und
   Pfad rechts. Ohne ihn stand das Fenster über dem Namen. */
.ui-table .cell-media, .table-wrap .cell-media { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.ui-table .cell-media > div, .table-wrap .cell-media > div { min-width: 0; overflow-wrap: anywhere; }

/* --- Zähler ---------------------------------------------------------------
   „Wie viele sind es" — an einer Stelle, in einer Form (#224).

   Gezählt wurde vorher auf fünf Arten: als Abzeichen neben der Überschrift
   (Zugänge), als Untertitel darunter (Projekte), als Metatext rechts
   (Speicherbereiche), rechts in der Abschnittszeile (Hintergrundaufträge) und
   als Pille neben Gruppenüberschriften (Worker & Dienste). Die Zahl selbst war
   überall dieselbe Auskunft.

   Ab jetzt: unmittelbar hinter der Überschrift, in gedämpfter Schrift, ohne
   Rahmen. Kein Abzeichen — ein Abzeichen ist eine Einordnung („Administrator",
   „Intern"), eine Anzahl ist keine. */
.ui-count { color: var(--text-subtle); font-size: var(--font-sm); font-weight: var(--weight-semibold); letter-spacing: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Der Trenner hängt am ERSTEN Zähler: Bricht die Zeile bei 390 px zwischen
   beiden um, bleibt der Punkt hinten statt eine Zeile mit „·" zu beginnen. */
.ui-count:has(+ .ui-count)::after { margin: 0 var(--space-2); color: var(--border-strong); content: "·"; }

/* --- Formularfeld (2026-09-10) ---------------------------------------------
   Die Form zu `components/form-field.blade.php`: Beschriftung über dem Feld,
   Hinweis und Fehler darunter, alles in einer Spalte. Beim Kästchen steht die
   Beschriftung rechts daneben; Hinweis und Fehler rücken unter den Text, nicht
   unter das Kästchen.

   `width: 100%` mit `min-width: 0` nimmt die 200px-Mindestbreite aus app.css
   zurück: Ein Feld füllt seine Spalte, es sprengt sie nicht. Das Kästchen
   bekommt `min-height: 0`, weil app.css jedem `input` die Höhe eines
   Bedienelements gibt — ein 36px hohes Kästchen ist keins. Der Fehlerabsatz
   (`.field-problem`, interaction-feedback.css) verliert seinen eigenen Abstand:
   den trägt hier das Raster.

   Der Kasten des Feldes steht seit 2026-09-11 auch hier, klassengebunden:
   app.css gibt ihn jedem `input`, aber die Viewer-Dokumente laden app.css
   nicht, und ein Feld im Freigabe-Dialog oder in der Filterspalte der Sounds
   soll derselbe Kasten sein. Dieselben Werte wie `input, select, textarea,
   button` in app.css — im Panel ändert die Regel nichts. */
.form-field { display: grid; gap: var(--space-1); min-width: 0; }
.form-field > label { color: var(--text); font-size: var(--font-base); font-weight: var(--weight-medium); }
.form-field > input, .form-field > select, .form-field > textarea { width: 100%; min-width: 0; min-height: var(--control-h); padding: var(--space-1) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-recessed); color: var(--text); font: inherit; }
/* Doppelung: `.form-field > textarea` steht in der Gruppe darüber mit dem
   Kasten; hier nimmt es allein das senkrechte Polster zurück auf 8.
   4 px senkrecht wie in app.css (Welle 3): 8 px hoben das Feld auf 38, zwei
   über `--control-h`; die Mindesthöhe trägt es, der Browser zentriert den
   Text. Das Textfeld behält 8, seine Höhe kommt nicht aus einer Zeile. */
.form-field > textarea { padding-block: var(--space-2); }
.form-field > .field-problem { margin: 0; }
.form-field-hint { margin: 0; color: var(--text-muted); font-size: var(--font-sm); line-height: var(--leading-base); }
.form-field.is-check { grid-template-columns: auto minmax(0, 1fr); column-gap: var(--space-2); align-items: center; }
.form-field.is-check > input { width: 16px; height: 16px; min-height: 0; margin: 0; padding: 0; accent-color: var(--brand); }
.form-field.is-check > label { font-weight: var(--weight-regular); }
.form-field.is-check > .form-field-hint, .form-field.is-check > .field-problem { grid-column: 2; }
/* Im Dialog (2026-09-11, #355): `.modal > div` in app.css ist die Kopfzeile —
   Titel links, Kreuz rechts, also `flex` mit `space-between` — und trifft
   jedes `<div>` im Raster, auch das Feld und die Hülle `[data-optional]` um
   ein Feld hinter einem Schalter. Ohne diese Zeile stand die Beschriftung
   neben dem Feld statt darüber, und das Kästchen, sein Wort und der Hinweis
   flogen an drei Kanten auseinander. Mit dem Elementnamen (0,2,1) gegen die
   Kopfzeile `.modal > div:has(> h2)` (0,1,2), die seit dem 2026-09-13 weiter
   unten in diesem Blatt steht — die Spezifität entscheidet, nicht die
   Reihenfolge; `gap` noch einmal, weil die Kopfzeile ihre 12px mitbrächte. */
.modal > div.form-field, .modal > div[data-optional] { display: grid; justify-content: normal; gap: var(--space-1); }
.modal > div.form-field.is-check { gap: var(--space-1) var(--space-2); }

/* Ein Schalter und das Feld dahinter sind eine Sache (#171): Das Feld rückt
   an seinen Schalter heran, die Gruppe rückt von der vorigen ab; bei gleichem
   Abstand überall liest man sechs lose Zeilen statt drei Einstellungen. Die
   Regel stand als `.modal label.check + [data-optional]` in app.css und traf
   das Bauteil nicht — sie gehört an das Feld, nicht an den Dialog, in dem es
   zufällig steht (2026-09-11, #355). `[data-optional]` ist die Hülle um das
   verborgene Feld (optional-fields.js), nicht das Feld selbst. */
.form-field.is-check:has(+ [data-optional]) { margin-top: var(--space-1); }
.form-field.is-check + [data-optional] { margin-top: calc(-1 * var(--space-2)); }

/* --- Seitenblättern (2026-09-11) ------------------------------------------
   Die Form zu `resources/views/vendor/pagination/mountingmedia.blade.php`:
   Zurück, Zahlen, Weiter als ruhige Knöpfe in einer Zeile, die Zahlen in
   `.tc`; die aktuelle Seite ist ein Wort im Text, kein Knopf, und ein
   ausgegrauter Rand ist keiner — `aria-disabled` dimmt nur. Am Ende der
   Liste, nicht im Kopf: Wer blättert, hat gelesen. Kein eigener Abstand nach
   oben: Den Rhythmus trägt das Raster der Seite (32px zwischen den Gruppen,
   12px im Abschnitt), und ein `margin-top` obendrauf machte aus 32 eine 44. */
.ui-pagination { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
.ui-pagination-pages { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
.ui-pagination .is-current, .ui-pagination .is-current:hover { background: transparent; color: var(--text); font-weight: var(--weight-semibold); cursor: default; }
.ui-pagination [aria-disabled="true"], .ui-pagination [aria-disabled="true"]:hover { background: transparent; color: var(--text-subtle); cursor: default; }
.ui-pagination-gap { display: inline-flex; align-items: center; padding: 0 var(--space-2); color: var(--text-subtle); }

/* --- Bedienelemente (2026-09-11) --------------------------------------------
   Die Form zu `components/button.blade.php`, klassengebunden und deshalb hier:
   app.css gibt jedem nackten `button` die Sekundärform, aber die vier
   Viewer-Dokumente (Viewer, Dateien, Sounds, Lizenzen) können app.css nicht
   laden — es formt `aside`, `main`, `nav a`, `table` und gibt jedem `input`
   200px Mindestbreite, was Filterspalte, Timecode-Feld und Lautstärkeregler
   zerlegte. Ihre nackten Knöpfe sind Werkzeuge (Reiter, Playbar, Stiftleiste)
   und behalten ihre eigene Form; ein Knopf, der ein Knopf ist, sagt eine dieser
   Klassen. Kein `button`-Elementselektor — der träfe die Werkzeuge mit
   `min-height: 36px`.

   Die Werte sind die aus app.css (2026-09-10): Primär ist, wer `.primary` sagt,
   und eine Ansicht sagt es genau einmal. Die Schriftfarbe steht auch am Hover,
   weil `a:hover` sie sonst bei einem `.button-link` ins Linkblau zöge.
   Hover-Schatten, Druck und Fokusring kommen aus interaction-feedback.css und
   treffen Klassen wie Elemente. Die Reihenfolge der Blätter spielt keine Rolle:
   Das Panel lädt dieses Blatt vor app.css, und app.css nennt `.button-link`
   nicht mehr — sonst schlüge sein `.button-link` (gleiche Spezifität, später
   im Text) das `.primary` von hier. */
/* Doppelung: hier die gemeinsame Gestalt aller fünf Knöpfe — Maß, Rand,
   Schrift —, darunter je Variante nur die drei Farben, die sie unterscheiden.
   Zusammengefaltet stünde das Maß fünfmal. */
.button-link, .primary, .secondary, .quiet, .danger { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-height: var(--control-h); padding: 0 var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text); font: var(--weight-medium) var(--font-md)/var(--leading-tight) var(--font-sans); text-decoration: none; white-space: nowrap; cursor: pointer; }
.button-link:hover, .secondary:hover { border-color: var(--border-strong); background: var(--surface-hover); color: var(--text); }
.primary { border-color: var(--brand); background: var(--brand); color: var(--on-brand); }
.primary:hover { border-color: var(--brand-hover); background: var(--brand-hover); color: var(--on-brand); }
.quiet { border-color: transparent; background: transparent; color: var(--text-muted); }
.quiet:hover { border-color: transparent; background: var(--surface-hover); color: var(--text); }
.danger { border-color: color-mix(in srgb, var(--danger-color) 48%, transparent); background: transparent; color: var(--danger-color); }
.danger:hover { border-color: var(--danger-color); background: color-mix(in srgb, var(--danger-color) 12%, transparent); color: var(--danger-color); }

/* --- Dialog (2026-09-13) ------------------------------------------------------
   Die Form zu `components/dialog.blade.php` und `public/confirm-dialog.js`,
   hier statt in app.css — aus demselben Grund wie Knopf und Feld (#360): Die
   Viewer-Dokumente und die Außenflächen laden app.css nicht und sollen
   `<x-dialog>` trotzdem rufen können, ohne den Dialog nachzubauen. Bis dahin
   stand die Grundregel einmal in app.css (#354), davor ein zweites Mal in
   admin-settings.css und ein drittes Mal in workspace.css.

   Die Maße: höchstens 520 breit, nie breiter als das Fenster minus Rand.
   `dvh` statt `vh` (#234): `100vh` misst am Telefon die Höhe *ohne* die
   eingeblendete Adressleiste — ein hoher Dialog reichte damit unter sie, und
   die Knopfzeile lag außerhalb des Sichtfelds. Die `vh`-Zeile bleibt als
   Rückfallebene für Engines ohne `dvh` darüber. Fläche `--surface-1`, Rand
   `--border-strong`, `--radius-lg`, `--shadow-3`, dahinter `--backdrop`
   (docs/Oberflaeche/Bauteile.md „Dialog"). Ein Dialog mit eigener Klasse
   (`.viewer-keys`, `.operation-dialog`) überschreibt, was er anders
   will, und erbt den Rest — vorher hatte er in den Viewer-Dokumenten keine
   Grundregel und in der Panel-Schale eine andere. */
dialog { inline-size: min(520px, calc(100vw - 32px)); max-inline-size: calc(100vw - 32px); max-block-size: calc(100vh - 32px); max-block-size: calc(100dvh - 32px); overflow: auto; padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-1); color: var(--text); box-shadow: var(--shadow-3); }
dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
.modal { display: grid; gap: var(--space-5); min-inline-size: 0; width: 100%; padding: var(--space-6); }
/* Die Kopfzeile aus Titel und Schließen-Knopf, mit Haarlinie darunter. Erkannt
   am `<h2>` darin, nicht an der Stellung: Vor ihr steht meist das versteckte
   Feld aus `@csrf`, und seit `<x-form-field>` (#354) ist auch ein Feld ein
   direktes `div` des Dialogs — eine Regel auf jedes `div` zog die Beschriftung
   neben ihr Feld statt darüber. Das Feld selbst steht weiter oben
   (`.modal > div.form-field`) mit (0,2,1) und schlägt diese (0,1,2) — nicht
   die Reihenfolge, die Spezifität entscheidet. */
.modal > div:has(> h2) { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding-bottom: var(--space-4); border-bottom: 1px solid var(--border); }
/* Der Leerzustand im Dialog behält sein zentriertes Raster (#171): Als die
   Kopfzeilenregel noch auf jedes `div` zielte, zog sie ihn zur Reihe
   auseinander — Symbol links, Text mittig, Knopf rechts, der Dialog dreimal
   so breit wie nötig. `WorkspaceSharesPageStateTest` liest die Regel. */
.modal > .empty-state { display: grid; justify-content: center; gap: var(--space-2); padding: var(--space-2) 0 2px; }
.modal h2 { margin: 0; font-size: var(--font-lg); }
.modal label { display: grid; gap: var(--space-2); }
.modal label.check { display: flex; align-items: center; gap: var(--space-2); }
.modal input, .modal select { min-width: 0; width: 100%; }
.modal textarea { width: 100%; min-width: 0; resize: vertical; padding: var(--space-2) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-recessed); color: var(--text); font: inherit; }
.modal fieldset { display: grid; gap: var(--space-2); min-width: 0; padding: 0; border: 0; }
.modal legend { padding: 0; color: var(--text); font-weight: var(--weight-semibold); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--space-2); padding-top: var(--space-4); border-top: 1px solid var(--border); }
.modal-delete { padding: 0 var(--space-6) var(--space-6); }
.modal-delete .danger { width: 100%; }
/* Ein Schalter und das Feld dahinter sind eine Sache (#171): das Feld rückt an
   seinen Schalter heran, die Gruppe rückt von der vorigen ab. Bei gleichem
   Abstand überall liest man sechs lose Zeilen statt drei Einstellungen. */
.modal label.check:has(+ [data-optional]) { margin-top: var(--space-1); }
.modal label.check + [data-optional] { margin-top: calc(-1 * var(--space-2)); }
/* Datum links, Vorauswahl rechts, je zur Hälfte (#171, expiry-presets.js). Das
   `min-width: 0` ist nicht Kosmetik: Eingabefelder haben in app.css eine
   Mindestbreite von 200px — zwei davon nebeneinander sprengten sonst den
   Dialog, statt sich die Zeile zu teilen. */
.field-with-preset { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.field-with-preset > * { width: 100%; min-width: 0; }
/* Das Kreuz der von Hand gebauten Dialogköpfe; `<x-dialog>` schreibt statt
   dessen `<x-button variant="icon">` (`.icon-action`). */
.close { min-height: var(--control-h-icon); padding: 0 var(--space-1); border: 0; background: none; color: var(--text-muted); font-size: var(--font-xl); line-height: var(--leading-none); }
@media (max-width: 760px) { .modal { padding: var(--space-5); } .modal-actions { justify-content: stretch; } .modal-actions :is(button, .button-link) { width: 100%; } }
/* Am Telefon nimmt der Dialog die Breite, die da ist. Die 520px oben sind eine
   Obergrenze für den Schreibtisch; bei 360px ließen sie einen Rand stehen, in
   dem nichts steht, während das Formular darin umbricht (#234). */
@media (max-width: 600px) { dialog { inline-size: calc(100vw - 16px); max-inline-size: calc(100vw - 16px); } .modal { padding: var(--space-4); } }
/* Das Blatt (`<x-dialog sheet>`, #412, seit #489 ein Bauteil; seit dem
   2026-10-02 trägt es nur das Ereignis im Änderungsprotokoll — die
   Kontoseite ist zurück bei mittigen Dialogen): ein Dialog, der am
   rechten Rand hereingleitet statt mittig aufzugehen — dieselbe Schale wie
   jeder Dialog des Hauses (oberste Ebene, Escape, Fokus zurück, `--backdrop`
   dahinter), nur an den Rand gerückt: `inset` mit `left: auto`, `margin: 0`
   gegen das `margin: auto` des Browsers, kein Radius, die Kante nur links.
   Die Höhe kommt aus `top: 0; bottom: 0`, nicht aus `100dvh` — das ist die
   Höhe des Fensters auch dann, wenn die Adressleiste des Telefons gerade
   einfährt (#234); dafür muss `block-size` wieder `auto` sein, denn der
   Browser gibt jedem `<dialog>` `fit-content`, und damit stünde das Blatt
   339 px hoch am oberen Rand. Breite `min(420px, 100vw)` wie die
   Kommentarschublade des Viewers (`.viewer-comments`, viewer.css).

   Das Blatt ist die Vorgabe und die Vollbildkarte unter 760 px die Ausnahme,
   nicht umgekehrt: Der Kanon der Schwellen kennt nur `max-width`
   (MobileContractTest), also muss die schmale Fassung die spätere sein.
   Überlagert, nicht geschoben: Ein Blatt, das die Seite schmaler schiebt,
   nimmt der Tabelle Breite, ohne dass `table-cards.js` davon erführe — das
   fragt das Fenster (`matchMedia`), nicht die Tabelle —, und eine Tabelle
   unter 760 px ohne Karte rollt seitwärts, was der Umbau gerade beendet hat.

   Bewegung als Eintrittsanimation an `[open]` wie die Rückfrage
   (`.confirm-dialog[open]`, interaction-feedback.css): Ein geschlossener
   `<dialog>` ist `display: none`, ein Übergang von dort bräuchte
   `@starting-style` und `allow-discrete` — für 180 ms Weg keine neue Technik.
   Gleiten von rechts (`translateX(100%)` → 0) und Einblenden des Vorhangs,
   beides mit den Bewegungstoken; unter 760 px gleitet die Karte denselben
   Weg, ein Bild statt zweier. Zurückgenommen je Bauteil, nicht pauschal
   (Muster in viewer-internal.css): Die pauschale Rücknahme in
   interaction-feedback.css kürzt auf .01 ms, und `animation: none` sagt, was
   gemeint ist. */
dialog.ui-sheet { position: fixed; inset: 0 0 0 auto; inline-size: min(420px, 100vw); max-inline-size: 100vw; block-size: auto; max-block-size: none; margin: 0; border-width: 0 0 0 1px; border-radius: 0; }
dialog.ui-sheet[open] { animation: ui-sheet-in var(--motion-menu) var(--motion-enter); }
dialog.ui-sheet[open]::backdrop { animation: ui-sheet-backdrop-in var(--motion-menu) var(--motion-enter); }
@keyframes ui-sheet-in { from { transform: translateX(100%); } }
@keyframes ui-sheet-backdrop-in { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
    dialog.ui-sheet[open], dialog.ui-sheet[open]::backdrop { animation: none; }
}
@media (max-width: 760px) {
    dialog.ui-sheet { inset: 0; inline-size: auto; max-inline-size: none; border-width: 0; }
}

/* --- Segmentwahl (2026-10-04) ------------------------------------------------
   Die Form zu `components/segmented.blade.php`: zwei bis vier Zustände in
   einer Pille, genau einer gewählt — gebaut für den Überschreibungseditor der
   Wirksamkeit (je Recht ✕ Entziehen · – Unberührt · ✓ Erlauben). Darunter
   echte Radios, versteckt wie in `.profile-choice` (admin-settings.css); das
   Bild aus `:has(> input:checked)` — ohne JavaScript, ein Tab-Halt je Pille,
   Pfeiltasten wie jede Radiogruppe. Das Radio bleibt im Dokument (kein
   `display: none`, sonst fände die Tastatur die Gruppe nicht) und verliert
   die 200 px und die Bedienhöhe aus app.css.

   Ein Ton je Segment, `--segment-tone`, wie `--callout-tone`: ungewählt
   färbt er das Zeichen, gewählt die Fläche. Rot ist hier Zustand, nicht
   Warnung — das Rot des Zustandspunkts eines entzogenen Neins auf derselben
   Seite (docs/ENTSCHEIDUNGEN.md, 2026-10-04). Grau ist `--text-muted`:
   `--border-strong` als Fläche trüge gegen die Pille 1,76:1 und schiede
   gewählt nicht von ungewählt. Auf der Fläche `--canvas`, nicht `--on-brand`
   (Farben-und-Thema.md, Ableitung 2): Die Töne kippen im hellen Thema,
   `--on-brand` nicht — 3,61 / 3,65 / 2,63 statt 5,05 / 4,99 / 6,92.

   Erzwungene Farben und Druck (Blöcke unter den Regeln): Beide werfen die
   Fläche weg, und dann sähe das gewählte Segment aus wie die anderen — im
   Kontrastmodus trägt es deshalb `Highlight`, im Druck behält es seinen
   Ton.

   Pille auf `--surface-2` wie ein Knopf (Ableitung 5), 30 hoch wie
   `.icon-action`, das Segment 32 breit; in der Tabelle nimmt die Zelle ihr
   Polster auf 4 zurück, damit die einzeilige Zeile 42 bleibt. Am groben
   Zeiger 44 (Block am Dateiende). Kein `overflow: hidden`: Der Fokusring
   liegt außen — die Rundung tragen erstes und letztes Segment. Nach dem
   Dialog, weil `.modal label` gleich spezifisch ist und eine Segmentwahl im
   Dialog eine bleiben soll. */
.ui-segmented { --segment-tone: var(--text-muted); display: inline-flex; align-items: stretch; min-height: var(--control-h-icon); vertical-align: middle; border: 1px solid var(--border-strong); border-radius: var(--radius-pill); background: var(--surface-2); }
.ui-segmented > label { position: relative; display: flex; align-items: center; justify-content: center; width: var(--control-h-bar); color: var(--segment-tone); cursor: pointer; transition: background-color var(--motion-fast) var(--motion-enter), color var(--motion-fast) var(--motion-enter); }
.ui-segmented > label:first-child { border-radius: var(--radius-pill) 0 0 var(--radius-pill); }
.ui-segmented > label:last-child { border-radius: 0 var(--radius-pill) var(--radius-pill) 0; }
.ui-segmented > label + label { border-left: 1px solid var(--border-strong); }
.ui-segmented > label:hover { background: var(--surface-hover); }
.ui-segmented > label:has(> input:checked) { background: var(--segment-tone); color: var(--canvas); }
.ui-segmented > label:has(> input:focus-visible) { z-index: 1; outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.ui-segmented > label > input { position: absolute; width: 1px; height: 1px; min-width: 0; min-height: 0; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.ui-segmented > .is-success, .ui-segmented-legend > .is-success { --segment-tone: var(--success); }
.ui-segmented > .is-danger, .ui-segmented-legend > .is-danger { --segment-tone: var(--danger-color); }
.ui-table td:has(> .ui-segmented) { padding-block: var(--space-1); }
@media (forced-colors: active) {
    .ui-segmented > label:has(> input:checked) { forced-color-adjust: none; background: Highlight; color: HighlightText; }
}
@media print {
    .ui-segmented > label:has(> input:checked) { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
/* Die Legende zur Pille, einmal über der Tabelle (der Aufrufer schreibt sie):
   das Zeichen im Ton, das Wort in der Schrift der Umgebung. */
.ui-segmented-legend { --segment-tone: var(--text-muted); }
.ui-segmented-legend > span { white-space: nowrap; }
.ui-segmented-legend .icon { color: var(--segment-tone); }

/* --- Tastbare Flächen ------------------------------------------------------
   Am Dateiende, und das ist keine Ordnungsfrage: Beide Grundregeln
   (`.ui-help-trigger`, `.ui-readonly-copy`) stehen im Text weiter oben, und
   eine Medienabfrage erhöht die Spezifität nicht. Davor gestellt wäre diese
   Zusage still wirkungslos — beim ersten Versuch war sie genau das: Die Breite
   wurde 44px, die Höhe blieb bei 24px (#234). */
@media (pointer: coarse), (max-width: 760px) {
    /* Die sichtbare Fläche des Hilfe-Symbols bleibt 15px — größer sähe es neben
       einer Überschrift wie ein Knopf aus. Getroffen wird die unsichtbare
       Fläche darum herum, und die wächst von 29px auf 44px. */
    .ui-help-trigger::after { inset: -15px; }

    /* Der Kopierknopf eines Nur-Lese-Wertes ist 24px — sichtbar genug neben dem
       Wert, tastbar nicht. Groß wird die Fläche, nicht das Symbol. */
    .ui-readonly-copy { width: var(--control-h-touch); min-width: var(--control-h-touch); height: var(--control-h-touch); min-height: var(--control-h-touch); }

    /* Eine Zeile, die ein Link ist, ist so hoch wie ein Knopf. */
    .ui-row-link { min-height: var(--control-h-touch); }

    /* Das Kreuz im Dialogkopf ist 30px — als Ziel zu klein. */
    .close { min-height: var(--control-h-touch); }

    /* Die Segmentwahl ist 30 hoch, das Segment 32 breit — als Ziel zu klein.
       Die Pille wächst auf die Berührungsstufe, das Segment auf ihre Breite. */
    .ui-segmented { min-height: var(--control-h-touch); }
    .ui-segmented > label { width: var(--control-h-touch); }

    /* Der Aufklapper eines Hinweiskastens ist ohne Polsterung nur so hoch wie
       seine Zeile — 20px, während der sichtbare Kasten dreimal so hoch wirkt.
       Wer daneben tippt, trifft nichts und hält den Kasten für kaputt. */
    details.ui-callout > summary { align-items: center; min-height: var(--control-h-touch); margin: calc(-1 * var(--space-3)) calc(-1 * var(--space-4)); padding: var(--space-3) var(--space-4); }
    details.ui-callout[open] > summary { margin-bottom: 0; }
}

/* Wie `.shape-tabs`: seitwärts scrollen statt umbrechen. */
@media (max-width: 600px) {
    .ui-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
    .ui-tabs::-webkit-scrollbar { display: none; }
    .ui-tabs > a, .ui-tabs > button { flex: 0 0 auto; }
}

/* Der Rechtsfuß der Außenflächen (`<x-legal-foot>`, 2026-09-11): leise,
   unten, mit Haarlinie — Impressum und Datenschutz als Klappen, im Laden als
   Links. `--text-subtle` und `--font-base`, nicht kleiner: Es ist eine
   Außenfläche, und was dort steht, muss jemand lesen können. */
.ui-legal-foot { display: grid; gap: var(--space-2); margin-top: var(--space-7); padding-top: var(--space-3); border-top: 1px solid var(--border); color: var(--text-subtle); font-size: var(--font-base); line-height: var(--leading-base); }
/* In einer Karte (`.login`) traegt das Raster den Abstand schon. */
.login > .ui-legal-foot { margin-top: var(--space-2); }
.ui-legal-foot > nav { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
/* `nav a` in app.css ist das Bereichsmenü (Polster, Rahmen, Hover-Fläche) —
   hier sind es drei Textlinks: Die Höhe bleibt als Zielgröße (dasselbe
   Token wie dort), alles andere geht zurück, sonst rutscht
   „Lizenzbedingungen" zehn Pixel neben den Satz. */
.ui-legal-foot > nav a { display: inline-flex; min-height: var(--control-h); padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; }
.ui-legal-foot a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.ui-legal-foot a:hover { border-color: transparent; background: none; color: var(--text); }
.ui-legal-foot p { margin: 0; }
.ui-legal-foot > details > summary { padding: var(--space-2) 0; cursor: pointer; }
.ui-legal-foot > details > summary:hover { color: var(--text); }
.ui-legal-foot-body { max-width: 72ch; padding: var(--space-2) 0 var(--space-3); color: var(--text-muted); }
