/* Die Bewegungs-Token standen hier und stehen jetzt in color-tokens.css, bei
   den übrigen Maßen; dieses Blatt wird immer nach der Token-Datei geladen
   (ColorTokenConsistencyTest hält die Reihenfolge). Bis zum 2026-09-20 stand
   hier außerdem ein `:root` mit drei Aliassen — `--interactive-hover`,
   `--interactive-pressed`, `--interactive-focus` —, je ein zweites Wort für
   `--surface-hover`, `--surface-selected` und `--focus-ring`, mit fünf
   Lesern in zwei Blättern; die lesen jetzt die Namen selbst, und ein Alias
   außerhalb der Token-Datei ist seither ein Fund (#397, Gruppe K). */

/* Meldungen liegen auf der Sprosse der Hilfeblase, wie `.viewer-toast` im
   Viewer — nicht auf 130: Dort steht der Warnrahmen, und eine Meldung darüber
   verdeckte in schmalen Fenstern die Ladeanzeige (ENTSCHEIDUNGEN 2026-09-24). */
.toast-region { position: fixed; z-index: var(--z-popover); top: 18px; right: 18px; display: grid; gap: var(--space-2); width: min(360px, calc(100vw - 36px)); pointer-events: none; }
.toast { padding: var(--space-3) var(--space-3); border: 1px solid var(--border-strong); border-left: 3px solid var(--success); border-radius: var(--radius-md); background: var(--surface-2); box-shadow: var(--shadow-3); color: var(--text); font-size: var(--font-md); font-weight: var(--weight-medium); animation: feedback-enter var(--motion-menu) var(--motion-enter); }
.toast-error { border-left-color: var(--danger-color); color: var(--text); }

/* Ein geändertes Feld zeigt das, bevor gespeichert wird — sonst muss man sich
   merken, was man angefasst hat. */
label.is-changed, .field.is-changed { position: relative; }
label.is-changed > span:first-child::after, .field.is-changed > label::after { margin-left: var(--space-2); color: var(--info); content: "•"; }
.field-problem { margin: var(--space-1) 0 0; color: var(--danger-color); font-size: var(--font-sm); }
.field-problem[hidden] { display: none; }
.form-actions { display: flex; align-items: center; gap: var(--space-3); }
.form-actions [data-form-status] { color: var(--text-muted); font-size: var(--font-sm); }

/* Die Checkbox als Schalter: grau ist aus, Blau ist an — projektweit, weil
   diese Datei jede Oberfläche lädt (beide Layouts, Viewer, Sounds, Licensing,
   Login). Der Grundsatz: Im Formular schaltet ein Kästchen, in einer Tabelle
   wählt es aus — Tabellen (Dateilisten, Auswahlspalten, „alle auswählen")
   bleiben deshalb native Kästchen, ebenso die Auswahllisten außerhalb von
   Tabellen (.member-picker, die Trefferlisten des
   Lizenz-Assistenten, die Achsen-Chips) und die Muster, die ihr Kästchen
   selbst verstecken oder selbst zeichnen (.sound-filter-tag,
   .column-panel-switch). `appearance:none` nimmt den Schalter zugleich aus
   den globalen Eingabefeld-Regeln heraus (app.css: min-width 200px, mobil
   min-height 44px) — width/min-width/height/min-height stehen deshalb
   ausdrücklich hier. */
input[type="checkbox"]:not(table *, .sound-filter-tag *, .column-panel-switch *, .member-picker *, .licensing-media-results *, .wizard-chosen-row *, .licensing-axis-value *) {
    appearance: none; -webkit-appearance: none; position: relative; flex: 0 0 auto;
    width: 40px; min-width: 40px; height: 22px; min-height: 22px; margin: 0; padding: 0;
    border: 1px solid var(--border-strong); border-radius: var(--radius-pill); background: var(--border-strong);
    cursor: pointer; transition: background var(--motion-fast) var(--motion-enter), border-color var(--motion-fast) var(--motion-enter);
}
input[type="checkbox"]:not(table *, .sound-filter-tag *, .column-panel-switch *, .member-picker *, .licensing-media-results *, .wizard-chosen-row *, .licensing-axis-value *)::after {
    content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
    border-radius: var(--radius-round); background: var(--text); box-shadow: var(--shadow-1);
    transition: transform var(--motion-fast) var(--motion-enter);
}
input[type="checkbox"]:not(table *, .sound-filter-tag *, .column-panel-switch *, .member-picker *, .licensing-media-results *, .wizard-chosen-row *, .licensing-axis-value *):checked { border-color: var(--brand); background: var(--brand); }
input[type="checkbox"]:not(table *, .sound-filter-tag *, .column-panel-switch *, .member-picker *, .licensing-media-results *, .wizard-chosen-row *, .licensing-axis-value *):checked::after { transform: translateX(18px); }
/* Die Ausnahme braucht ihr Gegenstück: In einer Tabelle wählt ein Kästchen aus
   und bleibt Kästchen — aber ohne die appearance-Regel oben greift dort wieder
   die globale Eingabe-Mindestbreite (200px, app.css). In einer 44px-Spalte mit
   Kürzungsmuster (max-width:0 unten) war das Kästchen damit unsichtbar
   weggeschnitten — die Auswahlspalte im Projektbereich stand leer. */
table input[type="checkbox"] {
    width: 16px; min-width: 16px; height: 16px; min-height: 16px; margin: 0;
    accent-color: var(--brand); vertical-align: middle; cursor: pointer;
}

/* Tabellenanatomie: Der Kopf bleibt stehen, Zahlen sind spaltenweise
   vergleichbar, und ein langer Dateiname bricht das Layout nicht — er wird
   gekürzt, bleibt über `title` aber vollständig lesbar. */
.file-table thead th, .files-table thead th, .table-wrap thead th { position: sticky; top: 0; z-index: 2; background: var(--surface-recessed); }
.file-date, .list-count { font-variant-numeric: tabular-nums; }
/* „vor 3 Tagen" trägt das genaue Datum im `title`. Der Fragezeichen-Zeiger ist
   der einzige Hinweis darauf, dass dort etwas zu holen ist — eine gepunktete
   Unterstreichung wäre in einer ganzen Spalte davon nur Unruhe. */
.file-date time[title] { cursor: help; }
.file-size { font-variant-numeric: tabular-nums; text-align: right; }
.file-table td, .files-table td { max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Verwaltungstabellen kuerzen nicht (#224). `max-width: 0` meldete jeder Zelle
   die Mindestbreite null; die Spalten wurden daraufhin nach ihrem kleinsten
   Inhalt verteilt und kuerzten mit Ellipse — auf /admin/shares gleichzeitig in
   24 Zellen, waehrend rechts daneben dreihundert Pixel leer blieben. Der
   Dateibrowser oben behaelt die Ellipse: Er hat `table-layout: fixed` und
   verwaltbare Spaltenbreiten, ein langer Dateiname darf dort nicht die
   Aufteilung sprengen.

   Die Regel dafuer ist jetzt eine Spaltenpriorisierung statt einer Kuerzung:
   `.cell-tight` bleibt so schmal wie ihr Inhalt (Zahlen, Daten, Zustaende,
   Aktionen), `.cell-grow` bekommt den Rest (Namen). Was dennoch zu lang ist,
   bricht um — es verschwindet nicht. */
/* `overflow-wrap: anywhere` stand hier einen Tag lang fuer alle Zellen und
   war schlimmer als das, was es ersetzen sollte: Es setzt die *Mindestbreite*
   einer Spalte auf ein einziges Zeichen. Neben einer `.cell-grow`-Spalte, die
   sich den Rest nimmt, fiel jede nicht ausdruecklich schmal gestellte Spalte
   darauf zusammen — „parasiten · Dreh" stand als Buchstabensaeule da. Umbrechen
   duerfen nur die Zeichenketten, die wirklich kein Leerzeichen haben: Pfade,
   Kennungen, Adressen. Die stehen in `<code>`.

   `break-word` und nicht `anywhere`: Beide brechen eine ueberlange Zeichenkette
   um, aber nur `anywhere` zaehlt die Umbruchstellen in die *Mindestbreite* der
   Spalte ein — und macht sie damit ein Zeichen schmal. Genau daran zerfiel
   „Anlieferung" in der Zielspalte zu „Anlieferun/g". */
.table-wrap td code { overflow-wrap: break-word; }
.table-wrap th.cell-tight, .table-wrap td.cell-tight { width: 1%; white-space: nowrap; }
/* Zahlen stehen rechts. Untereinander gelesen wird eine Spalte nur, wenn Komma
   ueber Komma steht; `tabular-nums` allein richtet die Ziffern gleich breit aus,
   nicht die Zahlen aneinander. */
.table-wrap th.cell-numeric, .table-wrap td.cell-numeric { width: 1%; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
.table-wrap th.cell-grow, .table-wrap td.cell-grow { width: 100%; }
/* Der Grund eines Fehlschlags steht unter seiner Zeile, nicht dahinter, und in
   der Farbe des Fehlers. Als reines <small> haengt er hinter dem Pfad und liest
   sich wie dessen Fortsetzung. */
.table-wrap td .row-error { display: block; margin-top: 2px; color: var(--danger-color); }
/* Ausnahmen vom Kürzen: die Auswahlspalte — und Aktionszellen (#171). Eine
   Zelle mit max-width:0 meldet der Tabelle keine Mindestbreite; bei Text ist
   das die gewollte Ellipse, bei Formularfeldern ragte stattdessen das
   Unkürzbare über die Tabellenkante. */
.file-table td.files-select, .files-table td.files-select { max-width: none; overflow: visible; }
/* Die Aktionsspalte ist immer die letzte und immer so schmal wie ihr Inhalt. */
.table-wrap th.table-actions, .table-wrap td.table-actions { width: 1%; white-space: nowrap; text-align: right; }
/* Und ein Leerzustand ist Fließtext, keine Tabellenzeile (#171): In einer
   Zelle mit `white-space: nowrap` steht seine Erklärung auf einer einzigen
   Zeile, wird an der Tabellenkante abgeschnitten — und weil der Text dabei
   breiter ist als die Zelle, sitzt auch die Mitte nicht mehr in der Mitte. */
/* Ein Leerzustand fuellt die Zeile, statt sich die Restbreite mit einer
   Spaltenpriorisierung zu teilen, die es in dieser Zeile gar nicht gibt. */
.file-table td:has(.empty-state), .files-table td:has(.empty-state) { max-width: none; overflow: visible; white-space: normal; }
.table-wrap td:has(.empty-state) { width: auto; max-width: none; overflow: visible; white-space: normal; }
/* Bis PR 2 zu #406 stand hier `.viewer-file-name-row` daneben — die eigene
   Namenszeile der Viewer-Dateien, die mit deren Skript gefallen ist; seither
   schreibt der Baustein auf allen drei Listen `.file-name`. */
.file-name { display: inline-flex; max-width: 100%; align-items: center; gap: var(--space-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Die aktive Zeile der Tastaturbedienung muss man sehen, sonst führt sie ins Leere. */
tr.is-active > td { box-shadow: inset 0 0 0 1px var(--info); }
tr[aria-selected="true"] > td { background: var(--surface-selected); }
/* Aktionsleiste einer Auswahl. Sie steht immer da — in der Zeile, die links
   den Ordnerpfad und rechts die Spaltensteuerung trägt, also im Platz, der
   ohnehin frei ist. Vorher klappte sie bei der ersten Auswahl auf und schob
   die Liste unter dem Zeiger weg.

   Die Knöpfe sind Symbole, und nichts verschwindet: Was mit der aktuellen
   Auswahl nicht geht, bleibt stehen und nennt im `title` den Grund
   (public/file-actionbar.js schreibt ihn). Anzahl und Größe stehen daneben —
   sonst wäre „3 Objekte löschen" eine Zahl im Kopf. */
.files-actionbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); margin: 0; color: var(--text); font-size: var(--font-sm); }
.files-actionbar[hidden] { display: none; }
.files-actionbar-buttons { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
.files-actionbar-summary { color: var(--text-muted); font-size: var(--font-sm); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Ohne Auswahl bleibt die Angabe stehen, aber gedämpft: Die Zeile springt
   nicht, und „Keine Auswahl" ist eine Aussage, kein leerer Platz. */
.files-actionbar-summary.is-empty { color: var(--text-subtle); font-weight: var(--weight-regular); }
.files-action { display: inline-flex; width: var(--control-h-bar); min-width: var(--control-h-bar); height: var(--control-h-bar); min-height: var(--control-h-bar); align-items: center; justify-content: center; padding: 0; border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--text-muted); font-size: var(--font-md); line-height: var(--leading-none); cursor: pointer; }
.files-action:hover:not(:disabled) { border-color: var(--border-strong); background: var(--surface-hover); color: var(--text); }
.files-action.danger { border-color: transparent; background: transparent; color: var(--danger-color); }
.files-action.danger:hover:not(:disabled) { border-color: var(--danger-color); background: color-mix(in srgb, var(--danger-color) 12%, transparent); color: var(--danger-color); }
/* Gesperrt heißt sichtbar gesperrt: kein Fortschrittszeiger (die allgemeine
   Regel weiter unten meint wartende Knöpfe, nicht unpassende). */
.files-action:disabled { opacity: .38; cursor: not-allowed; }
/* Auf schmalen Anzeigen bleibt die Bedienfläche bei 44px — dieselbe Regel,
   die app.css für jeden Knopf führt. */
@media (pointer: coarse), (max-width: 760px) {
    .files-action { width: var(--control-h-touch); min-width: var(--control-h-touch); height: var(--control-h-touch); min-height: var(--control-h-touch); }
}
/* Neun Symbole zu 44px plus Abstände sind 412px — auf einem 360px-Telefon also
   zwei Reihen, und die zweite schob die Dateiliste nach unten aus dem Bild. Sie
   werden eine seitwärts scrollende Reihe: Alle neun bleiben erreichbar, keine
   Aktion verschwindet in einem Menü, das man erst finden muss. Was ausgewählt
   ist, steht darüber statt daneben — „3 Objekte · 1,2 GB" ist die Zahl, die in
   „3 Objekte löschen" fehlt, und sie darf nicht die sein, die wegscrollt.

   `width: 100%` und nicht bloß `min-width: 0`: Das Quermaß eines Flex-Kindes
   wird aus der Zeile abgeleitet, und die war hier bereits mit der
   Inhaltsbreite von 412px bemessen — `align-items: stretch` holte sie danach
   nicht zurück. Erst mit ausdrücklicher Breite bekommt `overflow-x` etwas zu
   scrollen (#234). */
@media (max-width: 760px) {
    .files-actionbar { flex-direction: column; align-items: stretch; gap: var(--space-2); min-width: 0; }
    .files-toolbar > .breadcrumbs { min-width: 0; }
    .files-actionbar-buttons {
        flex-wrap: nowrap; width: 100%; min-width: 0; max-width: 100%;
        overflow-x: auto; scrollbar-width: none;
    }
    .files-actionbar-buttons::-webkit-scrollbar { display: none; }
    .files-actionbar-buttons > * { flex: 0 0 auto; }
    .files-actionbar-summary { order: -1; width: 100%; max-width: 100%; white-space: normal; }
}
/* Neun Symbole zu 44px plus Abstände sind 412px — breiter als jedes Telefon
   hochkant (#234). Die Leiste stapelt deshalb, und die Knopfreihe wird eine
   *seitwärts scrollende* Reihe statt einer umbrechenden: Umgebrochen wären es
   drei Reihen, die je nach Auswahl ihre Höhe ändern, und die Liste darunter
   sprränge bei jedem Klick.

   Bewusste Lücke, notiert statt gebaut: Die bessere Antwort wäre eine
   Aktionsschublade, die bei Auswahl von unten einfährt. Sie braucht einen
   neuen Modus in file-actionbar.js — das ist mehr als eine Verengung und
   gehört in einen eigenen Schritt. */
@media (max-width: 760px) {
    .files-actionbar { align-items: stretch; flex-direction: column; gap: var(--space-2); }
    .files-actionbar-buttons {
        flex-wrap: nowrap;
        overflow-x: auto;
        /* Kein Schrumpfen: Ohne das teilten sich neun Knöpfe die Breite,
           statt zu scrollen — und aus 44px würden 38. */
        justify-content: flex-start;
        scrollbar-width: thin;
    }
    .files-actionbar-buttons > * { flex: 0 0 auto; }
}
.files-select { width: 34px; }
.files-select input { cursor: pointer; }

/* Die Spalte „Dauer": Nur Ordner tragen einen Wert, und jeder Messzustand hat
   eine eigene, einzeilige Gestalt — die Zeile springt beim Wechsel nicht.
   „Nie gemessen" ist eine leere Zelle, „0,00 min" ein heller Messwert: Ein
   Ordner ohne Videos ist ein fertiges Ergebnis, keine Lücke. „Eingereiht"
   (stehende Sanduhr) und „läuft" (drehender Ring) unterscheiden sich an Wort,
   Zeichen und Bewegung; bei prefers-reduced-motion bleibt der Viertelring
   stehen und ist von der Sanduhr trotzdem zu unterscheiden. Ein
   unvollständiger Wert trägt ein Warndreieck: Die Zahl ist dann eine
   Untergrenze, keine Summe — ohne das fiele „nicht lesbar" als stille Null in
   sie hinein.

   Geschrieben wird die Zelle ausschließlich in public/file-browser-columns.js;
   hier steht nur ihre Form. Die Rechtsbündigkeit steht zusätzlich in
   project-ui.css und workspace.css, weil die dortigen `td`-Regeln mit höherer
   Spezifität `text-align: left` setzen — dieselbe Doppelung, die .file-size
   schon führt. */
.file-duration { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.folder-duration { display: inline-flex; max-width: 100%; align-items: center; gap: var(--space-1); }
.folder-duration[title] { cursor: help; }
.folder-duration.is-pending, .folder-duration.is-running { color: var(--text-muted); font-size: var(--font-sm); }
.folder-duration.is-pending .icon { width: 12px; height: 12px; }
.folder-duration.is-partial .icon { width: 12px; height: 12px; color: var(--warning); }
.folder-duration.is-failed { color: var(--danger-color); font-size: var(--font-sm); }
.folder-duration.is-failed .icon { width: 12px; height: 12px; }
/* Wer beim Kürzen weichen darf — und wer nie.
   Die Spalte ist auf „3842,05 min" bemessen; „Wird gemessen … 34/120" ist
   länger, und in einer rechtsbündigen Zelle schneidet der Überlauf **links**
   ab, also genau dort, wo Sanduhr, Ring und Warndreieck stehen. Das Zeichen
   weicht deshalb nie; gekürzt wird allein die Beschriftung, mit
   Auslassungspunkten und dem vollen Wortlaut im Tooltip.

   `overflow: hidden` an der Beschriftung steht nicht der Optik wegen: Ein
   Flex-Element ohne es hat `min-width: auto` und schrumpft überhaupt nicht —
   dann wandert der Überlauf in die Zelle und schneidet links ab.

   Der frühere Vorbehalt neben der Zahl („· 2 nicht lesbar") ist entfallen: Er
   nahm ihr gemessen genau so viele Pixel weg, dass aus „671,60 min" ein
   „671,60 m…" wurde. Eine gekürzte Zahl ist eine falsche Zahl; der Vorbehalt
   steht jetzt im Warndreieck, im Tooltip und auf der Berichtsseite. */
.folder-duration > .icon, .folder-duration > .upload-ring { flex: 0 0 auto; }
.folder-duration-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Der Ring ist ein Bestandsbauteil (18px); in einer Tabellenzelle neben einer
   12px-Beschriftung wäre er der größte Gegenstand der Zeile. */
.folder-duration .upload-ring { width: 12px; height: 12px; }
.folder-duration .upload-ring::after { inset: 3px; }

/* Sortierbare Kopfzellen: ein Knopf, der wie eine Überschrift aussieht — die
   Richtung steht zusätzlich in `aria-sort`, nicht nur im Pfeil. */
th[data-sort], td[data-sort] { padding: 0; }
[data-sort] > button, button[data-sort] { display: inline-flex; width: 100%; min-height: 0; align-items: center; gap: var(--space-2); padding: inherit; border: 0; background: none; color: inherit; font: inherit; text-align: inherit; cursor: pointer; }
[data-sort] > button:hover, button[data-sort]:hover { color: var(--text); }
/* Der ruhende Pfeil in `--text-subtle`, der sortierende in `--text` — Farbe
   aus der Kontrasttabelle statt `opacity: .45`, das den Pfeil auf rund 2,5:1
   drückte (2026-09-13). */
[data-sort]::after, [data-sort] > button::after { color: var(--text-subtle); content: "↕"; font-size: var(--font-xs); }
[data-sort][aria-sort="ascending"]::after, [data-sort][aria-sort="ascending"] > button::after { content: "↑"; color: var(--text); }
[data-sort][aria-sort="descending"]::after, [data-sort][aria-sort="descending"] > button::after { content: "↓"; color: var(--text); }
[data-sort].is-sorted, [data-sort].is-sorted > button { color: var(--text); }

/* Der Rückgängig-Hinweis ist der einzige Toast, den man anfassen können muss —
   die Fläche selbst bleibt für Klicks durchlässig. */
/* Die Dialoge der Dateioperationen — Umbenennen, Ziel wählen, Konflikt klären.
   Sie leben hier und nicht in app.css, weil sie auf jeder Oberfläche mit
   Dateibrowser erscheinen, auch denen ohne das Admin-Stylesheet (Viewer).
   Die Schaltflächen tragen ihr Aussehen deshalb selbst, mit denselben Werten
   wie die Admin-Grundregeln — dort ändert sich damit nichts. */
.operation-dialog { min-width: min(480px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-2); color: var(--text); box-shadow: var(--shadow-3); }
.operation-dialog::backdrop { background: var(--backdrop); }
.operation-dialog-form { display: grid; gap: var(--space-5); padding: var(--space-5); }
.operation-dialog-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
/* Die Fußzeile teilte sich die Kopfzeilen-Regel und nahm `space-between` gleich
   darauf wieder zurück — die Ausrichtung stand zweimal da, einmal wirkungslos.
   Sie steht jetzt einmal, gleich richtig (#160). */
.operation-dialog-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-3); }
.operation-dialog h2 { margin: 0; font-size: var(--font-lg); }
.operation-dialog p { margin: 0; }
.operation-dialog label { display: grid; gap: var(--space-2); color: var(--text-muted); font-size: var(--font-md); font-weight: var(--weight-medium); }
/* 4 px senkrecht, nicht 8 (#397, Gruppe K): 13 px Schrift mal 1,45 sind 19,
   mit 8 px Polster und Rand 37 — eins über `--control-h`, und dann trüge die
   Polsterarithmetik das Feld, nicht das Token. Mit 4 trägt die Mindesthöhe
   (36), wie `.form-field > input` in ui-surfaces.css und das Feld in app.css
   seit Welle 3; der Browser zentriert den Text darin. */
.operation-dialog input:not([type="checkbox"]) { width: 100%; min-height: var(--control-h); padding: var(--space-1) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface-1); color: inherit; }
/* Auch hier ist der nackte Knopf die Sekundärform (app.css, 2026-09-10). Die
   Regel steht trotzdem eigens, weil diese Dialoge auf Oberflächen ohne
   app.css erscheinen (Viewer-Dateien). Eine Rückfrage zeigt beide Antworten
   gleich gewichtet; die zerstörende bleibt rot (`.danger` weiter unten). */
.operation-dialog-actions button { display: inline-flex; min-height: var(--control-h); align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text); font: inherit; font-size: var(--font-md); font-weight: var(--weight-medium); cursor: pointer; }
.operation-dialog-actions button:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.operation-dialog-actions button.primary { border-color: var(--brand); background: var(--brand); color: var(--on-brand); }
.operation-dialog-actions button.primary:hover { border-color: var(--brand-hover); background: var(--brand-hover); }
.operation-dialog .close { min-width: var(--control-h); min-height: var(--control-h); padding: 0; border: 0; background: transparent; color: inherit; font-size: var(--font-xl); cursor: pointer; }

/* Einheitliche Rückfrage auf derselben Hülle wie die Dateioperationsdialoge. */
.confirm-dialog { max-width: min(520px, calc(100vw - 32px)); }
/* Das Symbol steht nackt vor dem Titel, 20 px in der Tonfarbe — der
   40-px-Kreis darum ist am 2026-09-13 gefallen: ein Kreis, der nichts sagt,
   was das Symbol nicht sagt (docs/Oberflaeche/Anti-Muster.md, „Symbolkreis").
   Auf der ersten Zeile des Titels, nicht mittig zum Block: Bei zwei Zeilen
   Folge stünde es sonst neben dem Satz statt neben der Frage. */
.confirm-dialog-body { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-3); align-items: start; }
.confirm-dialog-mark { margin-top: 2px; color: var(--info); }
.confirm-dialog[data-tone="danger"] .confirm-dialog-mark { color: var(--danger-color); }
.confirm-dialog-text { display: grid; gap: var(--space-2); min-width: 0; }
.confirm-dialog-text h2 { line-height: var(--leading-tight); }
.confirm-dialog-text p { color: var(--text-muted); font-size: var(--font-md); line-height: var(--leading-base); }
.operation-dialog-actions button.danger { border-color: color-mix(in srgb, var(--danger-color) 48%, transparent); background: transparent; color: var(--danger-color); }
.operation-dialog-actions button.danger:hover { border-color: var(--danger-color); background: color-mix(in srgb, var(--danger-color) 12%, transparent); }
.confirm-dialog[open] { animation: confirm-dialog-in var(--motion-fast) ease-out; }
.confirm-dialog[open]::backdrop { animation: confirm-dialog-backdrop-in var(--motion-fast) ease-out; }
@keyframes confirm-dialog-in { from { opacity: 0; transform: translateY(8px) scale(.985); } }
@keyframes confirm-dialog-backdrop-in { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
    .confirm-dialog[open], .confirm-dialog[open]::backdrop { animation: none; }
}

/* Der Ordnerwähler: eine Liste statt eines Textfelds — was es nicht gibt,
   lässt sich nicht auswählen. */
.folder-picker-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; color: var(--text-muted); font-size: var(--font-sm); }
.folder-picker-crumbs button { min-height: 0; padding: 3px var(--space-1); border: 0; background: none; color: inherit; font: inherit; cursor: pointer; }
.folder-picker-crumbs button:hover { color: var(--text); }
.folder-picker-list { display: grid; gap: 2px; max-height: min(46vh, 320px); overflow: auto; padding: var(--space-1); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-recessed); }
.folder-picker-entry { display: block; width: 100%; padding: var(--space-2) var(--space-3); border: 0; border-radius: var(--radius-sm); background: none; color: var(--text); text-align: left; font: inherit; cursor: pointer; }
.folder-picker-entry:hover { background: var(--surface-hover); }
.folder-picker-empty { margin: 0; padding: var(--space-3) var(--space-3); color: var(--text-subtle); font-size: var(--font-sm); }
.folder-picker-skeleton { width: 60%; margin: var(--space-2) var(--space-3); }
.folder-picker-name { display: grid; gap: var(--space-1); font-size: var(--font-sm); }
.folder-picker-problem { margin: 0; color: var(--danger-color); font-size: var(--font-sm); }
.folder-picker-problem[hidden] { display: none; }
/* „Neuer Ordner" neben dem Pfad, die Eingabe darunter (folder-picker.js, 2026-10-03). */
.folder-picker-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.folder-picker-new { display: inline-flex; flex: none; align-items: center; gap: var(--space-1); }
.folder-picker-create { display: flex; gap: var(--space-2); }
.folder-picker-create[hidden] { display: none; }
.folder-picker-create input { flex: 1; min-width: 0; }

.undo-toast-region { display: grid; gap: var(--space-2); }
.undo-toast { display: grid; gap: var(--space-2); border-left-color: var(--info); pointer-events: auto; }
.undo-toast-message { font-weight: var(--weight-semibold); }
.undo-toast-error { color: var(--danger-color); font-size: var(--font-sm); font-weight: var(--weight-regular); }
.undo-toast-actions { display: flex; align-items: center; gap: var(--space-2); }
.undo-toast-action { display: inline-flex; min-height: var(--control-h-icon); align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: transparent; color: var(--text); font: inherit; font-size: var(--font-sm); font-weight: var(--weight-medium); text-decoration: none; cursor: pointer; }
.undo-toast-action:hover { border-color: var(--info); background: var(--surface-hover); }
.undo-toast-action[disabled] { cursor: progress; opacity: .6; }
.undo-toast-done { color: var(--text-muted); font-size: var(--font-sm); font-weight: var(--weight-regular); }

/* Die verbleibende Frist ist ein Ring, der voll läuft — keine Sekundenzahl.
   Als Zahl kostete sie zehn vollständige Neuaufbauten der Toast-Fläche je
   Hinweis, weil beide Oberflächen bei jeder Meldung `innerHTML` neu schreiben;
   hier läuft die Zeit in der CSS-Zeitachse ab, und die Anwendung meldet sich
   nur noch beim Anlegen, beim Klick und beim Ablauf.

   `--undo-elapsed` ist eine negative Verzögerung, gesetzt in public/undo-toast.js:
   Wird die Fläche doch neu geschrieben, weil ein zweiter Hinweis dazukommt,
   setzt der laufende Ring dort wieder ein, wo er stand. Ohne sie liefe er von
   vorn los und stünde bei drei Vierteln, wenn die Frist längst abgelaufen ist. */
.undo-toast-wheel { width: 15px; height: 15px; flex: none; }
.undo-toast-wheel circle { fill: none; stroke-width: 3; }
.undo-toast-wheel-track { stroke: color-mix(in srgb, var(--text-muted) 34%, transparent); }
.undo-toast-wheel-fill { stroke: var(--info); stroke-dasharray: 100; stroke-dashoffset: 100; transform: rotate(-90deg); transform-origin: 50% 50%; animation: undo-toast-wheel var(--undo-window, 10000ms) linear var(--undo-elapsed, 0ms) forwards; }
@keyframes undo-toast-wheel { to { stroke-dashoffset: 0; } }
/* Ohne Bewegung bleibt der Ring leer, statt still zu stehen: Die pauschale
   Rücknahme weiter unten in diesem Blatt
   kürzt bei prefers-reduced-motion jede Animation auf .01ms, und mit
   `forwards` stünde der Ring dann vom ersten Augenblick an voll — er meldete
   „abgelaufen", während die Rücknahme noch offen ist. Der leere Ring sagt
   weniger, aber nichts Falsches; der Knopf trägt die Auskunft. */
@media (prefers-reduced-motion: reduce) {
    .undo-toast-wheel-fill { animation: none; }
}

.global-request-loader {
    position: fixed;
    /* 120 als Zahl, mit Absicht: zwischen Speicherleiste (110) und Warnrahmen
       (`--z-alert-frame`, 130), und die Leiter hat dort keine Sprosse. */
    z-index: 120;
    top: 14px;
    left: 50%;
    display: inline-flex;
    min-height: var(--control-h);
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    box-shadow: var(--shadow-3);
    color: var(--text);
    font-size: var(--font-sm);
    font-weight: var(--weight-medium);
    pointer-events: none;
    transform: translateX(-50%);
}
.global-request-loader[hidden] { display: none; }
.global-request-loader-spinner {
    width: 14px;
    height: 14px;
    border: 2px solid color-mix(in srgb, var(--text-muted) 42%, transparent);
    border-top-color: var(--info);
    border-radius: var(--radius-round);
    animation: global-request-spin .75s linear infinite;
}

button, .button-link, summary, input[type="file"] + label, select, input, textarea,
a:not(.wordmark) {
    transition: transform var(--motion-fast) var(--motion-enter), background-color var(--motion-fast) var(--motion-enter), border-color var(--motion-fast) var(--motion-enter), box-shadow var(--motion-fast) var(--motion-enter), color var(--motion-fast) var(--motion-enter), opacity var(--motion-fast) var(--motion-enter);
}

button:not(.password-toggle):not(:disabled):hover, .button-link:hover, summary:hover, input[type="file"] + label:hover {
    box-shadow: var(--shadow-1);
}

button:not(.password-toggle):not(:disabled):active, .button-link:active, summary:active, input[type="file"] + label:active {
    transform: translateY(1px) scale(.985);
    box-shadow: none;
}

/* Gesperrt heißt „geht nicht", nicht „läuft": der Zeiger sagt es. Laden ist
   gesperrt plus `aria-busy` — dann gilt wieder der Fortschrittszeiger. */
button:disabled { cursor: not-allowed; opacity: .5; }
[aria-busy="true"], button:disabled[aria-busy="true"] { cursor: progress; }

/* Der Fokusring steht seit dem 2026-09-13 in ui-surfaces.css (Fundament,
   jede Schale lädt es) — Stock, Anlieferung „geschlossen" und Urkunde laden
   dieses Blatt nicht und blieben ohne Ring. */

details[open] > :not(summary) { animation: feedback-enter var(--motion-menu) var(--motion-enter); }

table tbody tr { transition: background-color var(--motion-fast) var(--motion-enter), box-shadow var(--motion-fast) var(--motion-enter); }
table tbody tr:hover { background: var(--surface-hover); box-shadow: inset 3px 0 0 var(--brand); }
table tbody tr:active { background: var(--surface-selected); }

.project-card, .asset, .files-mounts > a {
    transition: transform var(--motion-fast) var(--motion-enter), background-color var(--motion-fast) var(--motion-enter), border-color var(--motion-fast) var(--motion-enter), box-shadow var(--motion-fast) var(--motion-enter);
}
.project-card:hover, .asset:hover, .files-mounts > a:hover { border-color: var(--border-strong); }
.project-card:active, .asset:active, .files-mounts > a:active { transform: translateY(1px); }

/* Platzhalter für Inhalt, der noch lädt.
   Doppelung: Die drei Sonderformen stehen bewusst zweimal — hier das Flimmern,
   das alle Platzhalter gemeinsam haben, darunter je Form, was sie von einem
   einfachen Balken unterscheidet (Zeile, Kreis, Vorschaubild).
   Zusammengefaltet stünde der Verlauf viermal da (#160). */
.skeleton, .skeleton-text, .skeleton-circle, .skeleton-thumb {
    display: block;
    border: 0;
    border-radius: var(--radius-sm);
    background: linear-gradient(100deg, var(--surface-2) 32%, var(--surface-hover) 50%, var(--surface-2) 68%);
    background-size: 220% 100%;
    animation: skeleton-shimmer 1.5s ease-in-out infinite;
}
.skeleton-text { display: inline-block; height: .85em; width: 100%; min-width: 24px; vertical-align: middle; }
.skeleton-circle { border-radius: var(--radius-round); flex-shrink: 0; }
.skeleton-thumb { aspect-ratio: 16 / 9; border-radius: var(--radius-md); }
.skeleton-row, .skeleton-card { pointer-events: none; }

/* Tag badges, shared by the admin tag manager and every file browser. */
/* Die Töne des Blockzustands (`.state-error`, `.state-idle`) stehen seit #397
   bei `.empty-state` in `ui-surfaces.css`: Der Stock-Laden lädt dieses Blatt
   nicht, sein Fehlerblock war sonst grau. */
.state-row > td { padding: 0; }

/* <x-error-state>: a page-level failure sits above the surface it concerns
   instead of replacing it, so it is a banner rather than an empty-state block.
   Dieselbe Bauart wie `.ui-callout.is-warning` (ui-surfaces.css): Kante,
   Fläche 9 % und Rahmen 32 % aus `--warning` per `color-mix()` — bis zum
   2026-09-13 stand hier eine eigene Bernstein-Palette in vier Hexwerten, eine
   zweite Zustandspalette für einen Kasten (#360). Der Ton ist Gelb, nicht
   Rot: „Keine Speicherbereiche verfügbar" ist eine Einordnung, kein
   Datenverlust (docs/Oberflaeche/Farben-und-Thema.md). */
.state-banner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-4); padding: var(--space-3) var(--space-3); border: 1px solid color-mix(in srgb, var(--warning) 32%, transparent); border-left: 3px solid var(--warning); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--warning) 9%, transparent); color: var(--text-muted); }
.state-banner > div { display: flex; align-items: center; gap: var(--space-2); }
.state-banner > div > .icon { flex: 0 0 auto; color: var(--warning); }
.state-banner strong { color: var(--text); }
/* `--text-subtle` statt `opacity`: eine Farbe aus der Kontrasttabelle, keine,
   die niemand nachrechnet. */
.state-banner small { display: block; margin-top: 2px; color: var(--text-subtle); }
.state-banner form { margin: 0; }
.state-banner-actions { display: flex; flex: 0 0 auto; align-items: center; gap: var(--space-2); }
.state-banner button, .state-banner .button-link { white-space: nowrap; }
@media (max-width: 600px) {
    .state-banner { align-items: stretch; flex-direction: column; }
    .state-banner button, .state-banner .button-link { width: 100%; }
}

.color-field { display: grid; gap: var(--space-2); }
.color-presets { display: flex; flex-wrap: wrap; gap: var(--space-2); }
/* min-width/min-height ausdrücklich: die globalen Formularregeln in app.css
   setzen jedem <button> min-height 36px (mobil 44px) — height allein verliert
   gegen min-height, und aus dem Kreis wurde ein Oval (#100). */
.color-preset { width: 22px; min-width: 22px; height: 22px; min-height: 22px; padding: 0; border: 2px solid transparent; border-radius: var(--radius-round); box-shadow: inset 0 0 0 1px var(--border); cursor: pointer; transition: transform .12s ease, border-color .12s ease; }
.color-preset:hover { transform: scale(1.15); }
.color-preset:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.color-preset[aria-pressed="true"] { border-color: var(--text); }
.file-tags { display: inline-flex; align-items: center; gap: var(--space-1); margin-left: var(--space-2); vertical-align: middle; }
/* Outline statt Vollton: die Tag-Farbe kommt als `--tag-color` von außen und
   färbt nur Rand, Text und eine leichte Fläche — kein weißer Text mehr, der
   bei jeder Farbe gleich aussah und den Farbton selbst kaum zeigte. */
.tag-chip { display: inline-flex; align-items: center; padding: 2px var(--space-2); border: 1px solid var(--tag-color, var(--brand)); border-radius: var(--radius-pill); background: color-mix(in srgb, var(--tag-color, var(--brand)) 16%, transparent); color: var(--tag-color, var(--brand)); font-size: var(--font-xs); font-weight: var(--weight-medium); line-height: var(--leading-base); white-space: nowrap; }
/* `min-height: 0` und `padding: 0` an beiden Knöpfen der Markierungen, aus
   demselben Grund wie bei .color-preset und .icon-action (#100): Die
   globale Knopfregel in app.css setzt sonst 36 px Höhe und 12 px Polster
   durch — das ×-Kästchen wurde 36 px hoch, der Chip streckte sich auf 42,
   und jede Zeile mit einer Markierung war sechs Pixel höher als ihre
   Nachbarn (gemessen 2026-09-21, im Projektbereich wie im Adminbrowser). */
.tag-chip-remove { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; min-height: 0; margin-left: var(--space-1); margin-right: -3px; padding: 0; border: 0; border-radius: var(--radius-round); background: color-mix(in srgb, currentColor 22%, transparent); color: inherit; font-size: var(--font-xs); line-height: var(--leading-none); cursor: pointer; }
.tag-chip-remove:hover { background: color-mix(in srgb, currentColor 38%, transparent); }
/* Automatisch erkannt (#114): gestrichelt statt durchgezogen. Ein Vorschlag
   soll auf einen Blick von einer Entscheidung zu unterscheiden sein, ohne dafür
   eine zweite Farbe zu verbrauchen — die trägt schon die Tag-Gruppe. */
.tag-chip-auto { border-style: dashed; background: color-mix(in srgb, var(--tag-color, var(--brand)) 8%, transparent); }

/* `.row-actions` ist entfallen (#224): Eine Zeilenaktion ist seither entweder
   ein sekundaerer Textknopf oder ein „…"-Menue, nie eine Reihe aus
   Symbolknoepfen. `.icon-action` bleibt — es traegt weiterhin die Knoepfe im
   Dateibrowser und in den Formularen, wo ein Symbol allein steht und nicht
   neben einem zweiten. min-height/min-width ausdruecklich, aus demselben
   Grund wie bei .color-preset (#100): die globale Button-Regel in app.css
   setzt sonst 36px/44px durch. */
.icon-action { display: inline-flex; align-items: center; justify-content: center; width: var(--control-h-icon); min-width: var(--control-h-icon); height: var(--control-h-icon); min-height: var(--control-h-icon); padding: 0; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-muted); font-size: var(--font-md); cursor: pointer; }
.icon-action:hover { background: var(--surface-hover); color: var(--text); }
.icon-action.danger { color: var(--danger-color); }
.icon-action.danger:hover { background: color-mix(in srgb, var(--danger-color) 12%, transparent); color: var(--danger-color); }
.tag-picker { position: relative; display: inline-block; }
.tag-picker-add { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; min-height: 0; padding: 0; border: 1px dashed var(--border-strong); border-radius: var(--radius-round); background: none; color: var(--text-muted); font-size: var(--font-md); line-height: var(--leading-none); cursor: pointer; }
.tag-picker-add:hover { border-color: var(--brand); color: var(--brand); }
.tag-picker-menu { position: absolute; z-index: var(--z-menu); top: calc(100% + 4px); left: 0; display: grid; gap: 2px; min-width: 160px; max-height: min(320px, 60vh); padding: var(--space-2); overflow-y: auto; border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface-2); box-shadow: var(--shadow-2); }
/* `justify-content` und `min-height` ausdrücklich (2026-10-04): Der nackte
   `button` aus app.css zentriert und misst 36px, und im Projektbereich
   standen die Einträge deshalb mittig, in Abständen, die die Liste nach drei
   Tags rollen ließen — in den Viewer-Seiten ohne app.css nie. Am Telefon
   bleibt die tastbare Höhe (Regel unten). */
.tag-picker-menu button, .tag-picker-menu a { display: flex; min-height: var(--control-h-icon); align-items: center; justify-content: flex-start; gap: var(--space-2); padding: var(--space-1) var(--space-2); border: 0; border-radius: var(--radius-sm); background: none; color: var(--text); text-align: left; font: inherit; font-size: var(--font-sm); text-decoration: none; cursor: pointer; }
.tag-picker-menu button:hover { background: var(--surface-hover); }
.tag-picker-menu button[aria-checked="true"] { font-weight: var(--weight-semibold); }
.tag-picker-menu .tag-picker-dot { width: 9px; height: 9px; flex: 0 0 auto; border-radius: var(--radius-round); }
.tag-picker-menu .tag-picker-empty { padding: var(--space-2) var(--space-2); color: var(--text-subtle); font-size: var(--font-sm); }
/* Gruppenkopf wie in der Seitenleiste: 12/500, normale Schreibung. */
.tag-picker-menu .tag-picker-group { padding: var(--space-2) var(--space-2) 2px; color: var(--text-subtle); font-size: var(--font-sm); font-weight: var(--weight-medium); letter-spacing: 0; text-transform: none; }
/* „Neuen Tag anlegen …" und „Tags verwalten" (2026-10-04) kleben am unteren
   Rand des Wählers: Ab 320px rollt die Liste, und der Weg zu einem neuen Tag
   soll nicht hinter dem letzten alten liegen. Der Rand nimmt das Polster des
   Menüs zurück, sonst schiene die Liste unter der Leiste durch. */
.tag-picker-menu .tag-picker-actions { position: sticky; bottom: calc(-1 * var(--space-2)); display: grid; gap: 2px; margin: var(--space-1) calc(-1 * var(--space-2)) calc(-1 * var(--space-2)); padding: var(--space-1) var(--space-2) var(--space-2); border-top: 1px solid var(--border); background: var(--surface-2); }
.tag-picker-menu .tag-picker-actions a { color: var(--text-muted); }
.tag-picker-menu .tag-picker-actions a:hover { background: var(--surface-hover); color: var(--text); }
@media (pointer: coarse), (max-width: 760px) {
    .tag-picker-menu button, .tag-picker-menu a { min-height: var(--control-h-touch); }
}
/* Der Dialog „Neuen Tag anlegen": die Hülle der Dateioperationen, dazu die
   Wahl „Gilt für" als Zeilen mit Knopf vorn — `.operation-dialog label` ist
   ein Raster und stellte den Knopf sonst über den Text. */
.tag-create-dialog { max-width: min(480px, calc(100vw - 32px)); }
.tag-create-dialog .tag-create-file, .tag-create-dialog .tag-create-scope-line { color: var(--text-muted); font-size: var(--font-md); }
.tag-create-dialog .tag-create-scope-line strong { color: var(--text); font-weight: var(--weight-medium); }
.tag-create-scope { display: grid; gap: var(--space-2); min-width: 0; margin: 0; padding: 0; border: 0; }
.tag-create-scope legend { margin-bottom: var(--space-2); padding: 0; color: var(--text-muted); font-size: var(--font-md); font-weight: var(--weight-medium); }
.tag-create-dialog .tag-create-choice { display: flex; align-items: flex-start; gap: var(--space-2); color: var(--text); font-weight: var(--weight-regular); cursor: pointer; }
/* Das Feld der Dialoge (`.operation-dialog input`) und das aus app.css
   spannten den Knopf auf volle Breite und 36px Höhe — hier steht er klein. */
.tag-create-dialog .tag-create-choice input { flex: 0 0 auto; width: 16px; min-width: 16px; height: 16px; min-height: 16px; margin: 2px 0 0; padding: 0; accent-color: var(--brand); }
.tag-create-choice span { display: grid; gap: 2px; }
.tag-create-choice small { color: var(--text-subtle); font-size: var(--font-sm); }
.tag-create-duplicate { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); color: var(--text-muted); font-size: var(--font-md); }
.tag-create-duplicate[hidden], .tag-create-duplicate [hidden] { display: none; }
/* Measured and unmeasured progress have to look different, or the display is
   back to inventing numbers. A number means somebody counted; the running bar
   means the job is alive and nothing more is known. */
.background-job-progress { min-width: 42px; font-variant-numeric: tabular-nums; text-align: right; }
.background-job-progress.is-unknown { position: relative; overflow: hidden; height: 4px; min-width: 42px; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--text-muted) 28%, transparent); }
.background-job-progress.is-unknown::after { position: absolute; top: 0; bottom: 0; width: 45%; border-radius: var(--radius-pill); background: var(--brand); content: ""; animation: progress-indeterminate 1.25s ease-in-out infinite; }
/* Same rule for the operation trays, which all render the bar themselves.
   Der Projektbereich zeichnet seit dem 2026-09-13 ein `.ui-meter` und nur mit
   Messwert (#355) — `.workspace-operation-progress` ist gefallen. Seit dem
   2026-09-23 auch die Zeile der Anlieferung nach dem letzten Byte, solange
   der Server schreibt und prüft (`.delivery-row-bar`, delivery.css). */
.operation-progress.is-unknown, .delivery-row-bar.is-unknown { position: relative; overflow: hidden; }
.operation-progress.is-unknown::after, .delivery-row-bar.is-unknown::after {
    position: absolute; top: 0; bottom: 0; width: 40%; border-radius: inherit;
    background: var(--brand); content: "";
    animation: progress-indeterminate 1.25s ease-in-out infinite;
}
@keyframes progress-indeterminate { 0% { left: -45%; } 100% { left: 100%; } }
@media (prefers-reduced-motion: reduce) {
    /* No animation left to carry the meaning, so keep a static half-filled bar
       rather than something that reads as a stalled measurement. */
    .background-job-progress.is-unknown::after,
    .operation-progress.is-unknown::after,
    .delivery-row-bar.is-unknown::after { left: 0; width: 100%; opacity: .45; animation: none; }
}

/* Der Upload steht als Zeile in der Dateiliste, an der Stelle, an der die Datei
   gleich liegt. Geteilt von allen drei Dateibrowsern, deshalb aus denselben
   Merkmalen gebaut statt aus der Palette je Oberfläche. */
.upload-row > td { opacity: .85; }
.upload-row-name { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.upload-row-name .file-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload-row-status { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); color: var(--text-muted); font-size: var(--font-sm); font-variant-numeric: tabular-nums; }
/* `.upload-row-cancel` als 20-px-Kreis mit „×" ist gefallen (Welle 4): Der
   Knopf ist `.icon-action` mit dem Kreuz aus dem Bogen, wie in der Anlieferung. */
.upload-row .files-select input { opacity: .35; }
.upload-row.is-failed .upload-row-status { color: var(--danger-color); }

/* Der Ring an Stelle des Dateisymbols: ein Kegelverlauf, dessen Winkel an einer
   einzigen Variablen hängt — der Fortschritt lässt sich damit fortschreiben,
   ohne die Zeile neu zu bauen. Der Kern ist ein Loch, kein gefüllter Kreis:
   Sonst wäre bei 50 % nicht zu erkennen, ob es ein halber Ring oder ein halb
   gefüllter Punkt ist. */
.upload-ring {
    position: relative; flex: 0 0 auto; width: 18px; height: 18px; border-radius: var(--radius-round);
    background: conic-gradient(var(--brand) calc(var(--upload-progress, 0) * 1%), var(--border) 0);
}
.upload-ring::after {
    position: absolute; inset: 4px; border-radius: var(--radius-round);
    background: var(--surface-1); content: "";
}
/* Noch keine Messung (wartet) oder alle Bytes raus und der Server schreibt
   noch: ein drehender Bogen sagt „es arbeitet", wo ein stehender Ring wie ein
   hängender Transfer aussah. */
.upload-ring.is-spinning { background: conic-gradient(var(--brand) 25%, var(--border) 0); animation: upload-ring-spin 1s linear infinite; }
.upload-ring.is-failed { background: conic-gradient(var(--danger-color) 100%, var(--danger-color) 0); animation: none; }
@keyframes upload-ring-spin { to { transform: rotate(1turn); } }
@media (prefers-reduced-motion: reduce) {
    /* Ohne Drehung bleibt ein Viertelring stehen: erkennbar unfertig, aber
       nichts, was sich als Messung liest. */
    .upload-ring.is-spinning { animation: none; }
}
.upload-conflict-dialog p { margin: 0; font-size: var(--font-md); }
.upload-conflict-dialog .hint { color: var(--text-muted); font-size: var(--font-sm); }
.upload-conflict-dialog .check { display: flex; align-items: center; gap: var(--space-2); font-size: var(--font-sm); }

/* The strip below a paginated list. It is not decoration: without it a folder
   truncated at the page size looks exactly like a complete one. */
.list-footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-1) 2px; color: var(--text-muted); font-size: var(--font-sm); }
/* `.list-count` hatte hier ein zweites, wortgleiches `font-variant-numeric` —
   dieselbe Eigenschaft, derselbe Wert wie oben in der Zahlenspalten-Regel.
   Entfernt (#160). */
.list-footer button { font-size: var(--font-sm); }
.list-footer .list-select-all { margin-left: auto; }
.list-loading-row td { padding-top: var(--space-3); padding-bottom: var(--space-3); }

.skeleton-row td { vertical-align: middle; }
.skeleton-row .skeleton-text, .skeleton-card .skeleton-text { display: block; }

@keyframes feedback-enter { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
/* Bei reduzierter Bewegung steht der Ring still — sichtbar bleibt er trotzdem,
   sonst verschwände der einzige Hinweis, dass etwas läuft. */
@media (prefers-reduced-motion: reduce) {
    .global-request-loader-spinner { border-top-color: var(--info); animation: none; }
    .global-request-loader::after { content: ""; }
}

@keyframes global-request-spin { to { transform: rotate(360deg); } }
@keyframes skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -20% 0; } }

/* Die pauschale Rücknahme jeder Bewegung — die eine Fassung für beide Bereiche
   der Schale, denn dieses Blatt laden beide. workspace.css führte bis zum
   2026-09-11 dieselben vier Zeilen ein zweites Mal für den Projektbereich, und
   Kommentare in drei Blättern (der Ring `.undo-toast-wheel-fill` oben,
   unsaved-changes.css) nannten jene Kopie als Voraussetzung, als gälte hier
   keine. Gewollt ist, dass sie auch die Schublade der Seitenleiste
   (admin-layout.css, `transform .2s`) und die Übergänge ihres Rollbalkens
   (`.side-scrollbar-thumb`) stilllegt: Wer weniger Bewegung wünscht, bekommt
   sie nirgends. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
}

/* --- Fortschritt einer laufenden Auftragszeile (/admin/services) --------
   Ein Balken, waagerecht geteilt: oben der Auftrag im Ganzen, unten die
   gerade bearbeitete Datei. Die Teilung ist 2:1 statt 1:1 — die obere Spur
   beantwortet die Frage der Seite („wie weit?"), die untere ist der Puls
   („arbeitet er gerade?"); ihre Bewegung faellt auch schmal auf, Hoehe
   braucht sie nicht, und zwei gleich hohe Haelften fuehrten den Blick nicht.
   Eine haarfeine, durchsichtige Fuge trennt die Spuren; die Dateispur traegt
   denselben Farbton, nur verduennt — zwei Fragen, eine Farbe, es bleibt ein
   Balken und wird kein Doppeldiagramm.

   Fehlendes erscheint nie als 0 %: Eine Groesse ohne Messwert bekommt keine
   Spur, die verbleibende nimmt ueber `:only-child` die volle Hoehe. Eine
   leere, glatte Spur heisst damit wieder wirklich „0 %". „Laeuft, aber nicht
   zaehlbar" traegt Schraegstreifen statt einer Breite. */
/* Der Balken der Dienstzeile von /admin/services: Er steht im Zeilentext
   (`<x-row>`) unter Zustand und Lebenszeichen, also im Fluss statt in einer
   Flex-Zeile — die Breite kommt von der Zeile, der Abstand nach oben steht
   unten bei `.ui-row-text`. */
.job-progress { display: grid; min-width: 0; gap: var(--space-2); }
/* Radius 3px als Zahl, nicht aus der Skala: Der Balken ist 13px hoch, und
   die kleinste Stufe `--radius-sm` (6px) ist auf dieser Hoehe fast die
   Meter-Pille — Welle 3 hatte das fruehere 4px mechanisch dorthin gehoben,
   und ein Bogen von 6px griff am Rand bis in die untere, 4px hohe Spur
   hinein und nahm jeder Fuellung unter 3 % die ersten Pixel (#409). Mit 3px
   ist der Keil klein genug, dass ein 1-%-Stand in beiden Spuren steht; ganz
   weg ist er nicht, denn irgendeine Ecke hat der Behaelter. Die sitzt genau
   einmal, hier, mit `overflow: hidden`, und beschneidet beide Spuren und
   ihre Fuellung gleich — Spuren und `<i>` runden nicht selbst, und die
   Streifen des unbestimmten Zustands sind nur ein Hintergrund derselben
   Spur, keine zweite Form. 3px steht unter der 1–3-px-Regel des Waechters
   (SpacingTokenConsistencyTest); ein Wert ab 4 verlangte eine namentliche
   Ausnahme. Die Fuge ist das durchsichtige 1px-`gap` des Rasters, keine
   eigene Farbe. */
.job-progress-bar { display: grid; gap: 1px; overflow: hidden; border-radius: 3px; }
.job-progress-track { height: 8px; background: var(--surface-hover); }
.job-progress-track.is-file { height: 4px; }
/* Fehlt eine der beiden Groessen, nimmt die verbliebene Spur die volle Hoehe.
   Steht nach `.is-file`, damit sie deren 4px schlaegt. */
.job-progress-track:only-child { height: 13px; }

/* Die Uebergangsdauer ist der Takt der Abfrage (1,8 s) und die Kurve linear:
   So laeuft die Fuellung gleichmaessig von einem gemessenen Punkt zum
   naechsten und kommt genau dann an, wenn der neue Wert eintrifft. Kuerzer
   waere sie in der Restzeit stehengeblieben, `ease` haette sie ruckeln
   lassen. Erfunden wird dabei nichts — die Zahlen daneben springen weiter im
   Takt der Messung. */
.job-progress-track > i { display: block; width: 0; height: 100%; background: var(--shape-state, var(--brand)); transition: width 1800ms linear; }
.job-progress-track.is-file > i { background: color-mix(in srgb, var(--shape-state, var(--brand)) 55%, transparent); }

/* Unbestimmt: laeuft, aber nicht zaehlbar. Steht die Bewegung still
   (reduced motion), unterscheidet das Muster den Zustand weiterhin von
   „0 %" und von jeder echten Zahl. */
.job-progress-track.is-indeterminate {
    background-image: linear-gradient(45deg,
        color-mix(in srgb, var(--shape-state, var(--brand)) 34%, transparent) 25%,
        transparent 25% 50%,
        color-mix(in srgb, var(--shape-state, var(--brand)) 34%, transparent) 50% 75%,
        transparent 75%);
    background-size: 18px 18px;
    animation: job-progress-stripes 900ms linear infinite;
}
@keyframes job-progress-stripes { from { background-position: 0 0; } to { background-position: 18px 0; } }

/* Die Woerter zum Balken: links die Gesamtangabe, in der Mitte die Datei —
   die einzige Angabe, die lang werden darf —, rechts die Restzeit. In 4px
   Hoehe wohnt keine Beschriftung, deshalb darunter statt darin. */
/* `min-width: 0` ist hier die Fluchtluke: Als Rasterkind behaelt die
   Beschriftung sonst `min-width: auto`, und das ist die Breite des laengsten
   Dateipfads — der wegen `nowrap` (Zeile darunter) nie umbricht. Ohne die
   Luke bestimmt der Inhalt des Archivs die Breite der Seite. */
.job-progress-caption { display: flex; min-width: 0; align-items: baseline; flex-wrap: wrap; gap: 3px var(--space-4); margin: 0; color: var(--text-muted); font-size: var(--font-xs); line-height: var(--leading-base); }
.job-progress-caption > span { white-space: nowrap; font-variant-numeric: tabular-nums; }
.job-progress-file { flex: 1 1 120px; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--text-subtle); }
.job-progress-eta { margin-left: auto; }

@media (prefers-reduced-motion: reduce) {
    .job-progress-track > i { transition: none; }
    .job-progress-track.is-indeterminate { animation: none; }
}

/* Der Abstand nach oben, den der Balken im Zeilentext der Dienstzeile braucht. */
.ui-row-text > [data-queue-progress] { display: block; margin-top: var(--space-2); }

/* --- Auftragsprotokoll (Konsolenfenster) ------------------------------------
   Was der Auftrag ueber sich selbst meldet — eine Zeile je Datei, dazu
   Anfang, Ende und jeder Abbruch mit Grund. Nicht der rohe Ausgang des
   Containers: Den koennte die Anwendung nur mit eingehaengtem Docker-Socket
   lesen, und der ist Root auf dem Host. */
.job-console { width: min(920px, 92vw); max-height: 80vh; padding: 0; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-1); color: var(--text); }
.job-console::backdrop { background: var(--backdrop); }
.job-console-head { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-4) var(--space-4); border-bottom: 1px solid var(--border); }
.job-console-head strong { min-width: 0; overflow: hidden; font-size: var(--font-base); text-overflow: ellipsis; white-space: nowrap; }
.job-console-follow { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; color: var(--text-muted); font-size: var(--font-sm); white-space: nowrap; }
.job-console-body { max-height: 62vh; margin: 0; padding: var(--space-4) var(--space-4); overflow: auto; background: var(--canvas); color: var(--text-muted); font-family: var(--font-mono); font-size: var(--font-sm); line-height: var(--leading-base); white-space: pre-wrap; overflow-wrap: anywhere; }
.job-console-open { display: inline-grid; width: var(--control-h); min-width: var(--control-h); height: var(--control-h); place-items: center; padding: 0; }
