/* ============================================================================
   components.css — die Primitive.

   STEP_00_FUNDAMENT.md, Aufgabe E · Regelwerk: docs/redesign/STIL.md

   Bauweise
   --------
   Selektoren sind so global wie möglich: Elementselektoren (`button`, `input`,
   `table`) plus die Zustands- und Variantenhaken, die im Bestand schon
   existieren (`.is-open`, `--primary`, `[data-state]`). Damit sind die 134
   Knöpfe und die 55 Overlays durch die Bauweise erfasst und müssen nicht
   einzeln gesucht werden.

   `theme.css` ist unbeschichtet (ohne `@layer`) und gewinnt deshalb im
   Kaskadenvergleich gegen ALLES hier drin — unabhängig von der Ladereihenfolge.
   Das ist Absicht: Schritt 0 darf nichts kaputt machen. Diese Datei ist die
   Grundlage, auf die die Gruppen 1–4 zurückfallen, wenn `theme.css` Fläche für
   Fläche abgebaut wird. Wer die reine Wirkung sehen will: `/dev/components`
   lädt `theme.css` nicht.

   Acht Zustände je Primitiv
   -------------------------
   default · hover · focus-visible · active · disabled · loading · error · success

   Die Haken dafür sind einheitlich und überall gleich:
     disabled  :disabled · [disabled] · [aria-disabled="true"] · .is-disabled
     loading   [data-loading] · [aria-busy="true"] · .is-loading
     error     [data-state="error"] · [aria-invalid="true"] · .is-error · .has-error
     success   [data-state="success"] · .is-success

   Bewegung (STIL.md §6)
   ---------------------
   Nur `transform` und `opacity`. Kein `transition: all`. Jede Dauer und jede
   Kurve aus einem Token. Kein `ease-in`. Erscheinen aus `scale(0.97)` +
   `opacity: 0`. Hover nur hinter `@media (hover: hover) and (pointer: fine)`.
   ========================================================================= */

@layer components {

    /* ====================================================================
       1 · Zustandsvertrag — gilt für jedes Primitiv gleich
       ==================================================================== */

    /* Abgeschaltet: eine Stufe Deckkraft, keine Farbe. Ein grauer Knopf, der
       grau bleibt, ist ehrlicher als einer, der die Farbe wechselt. */
    :where(button, input, select, textarea, [role="button"], [role="menuitem"], [role="option"], [role="tab"]):is(
        :disabled,
        [disabled],
        [aria-disabled="true"],
        .is-disabled
    ) {
        cursor: not-allowed;
        opacity: 0.42;
        pointer-events: none;
    }

    /* Ladend: die Fläche bleibt stehen, nur der Inhalt tritt zurück. Kein
       Umspringen der Breite — deshalb bekommt der Knopf den Spinner als
       Pseudoelement über den Inhalt gelegt, nicht statt seiner. */
    :where(button, [role="button"], .btn):is(
        [data-loading],
        [aria-busy="true"],
        .is-loading
    ) {
        cursor: progress;
        pointer-events: none;
    }

    /* ====================================================================
       2 · Ladeanzeige — Spinner, Skelett, Fortschritt
       Nur `transform` und `opacity` in den Keyframes. Kein `width`/`height`.
       ==================================================================== */

    @keyframes ui-spin {
        to { transform: rotate(360deg); }
    }

    @keyframes ui-shimmer {
        from { transform: translateX(-100%); }
        to   { transform: translateX(100%); }
    }

    @keyframes ui-pulse {
        0%, 100% { opacity: 1; }
        50%      { opacity: 0.45; }
    }

    .ui-spinner,
    .hotbar-thinking-spinner {
        display: inline-block;
        flex: none;
        inline-size: var(--icon-md);
        block-size: var(--icon-md);
        border: 1.5px solid var(--line-strong);
        border-block-start-color: var(--ink-secondary);
        border-radius: 50%;
        animation: ui-spin 640ms linear infinite;
    }

    .ui-spinner--sm { inline-size: var(--icon-sm); block-size: var(--icon-sm); }
    .ui-spinner--lg { inline-size: var(--icon-xl); block-size: var(--icon-xl); }

    /* Skelett: die Fläche steht, nur ein Lichtstreifen wandert darüber. */
    .ui-skeleton {
        position: relative;
        display: block;
        overflow: hidden;
        /* Ein durchscheinender Film statt einer festen Fläche. `--surface-raised`
           und `--surface-overlay` tragen denselben Wert (STIL.md §2) — ein
           Skelett aus `--surface-raised` wäre im Menü und im Dialog
           unsichtbar. */
        background: color-mix(in oklab, var(--ink-primary) 8%, transparent);
        border-radius: var(--radius-chip);
        block-size: 1em;
        color: transparent;
        user-select: none;
    }

    .ui-skeleton::after {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(
            90deg,
            transparent,
            color-mix(in oklab, var(--ink-primary) 6%, transparent),
            transparent
        );
        animation: ui-shimmer 1200ms var(--ease-standard) infinite;
    }

    /* Unbestimmter Fortschrittsbalken. Der Balken bewegt sich per transform,
       seine Breite ist fest — Layouteigenschaften werden nicht animiert. */
    .ui-progress {
        position: relative;
        overflow: hidden;
        block-size: 2px;
        background: var(--line-subtle);
        border-radius: var(--radius-chip);
    }

    .ui-progress::after {
        content: "";
        position: absolute;
        inset-block: 0;
        inset-inline-start: 0;
        inline-size: 40%;
        background: var(--ink-secondary);
        animation: ui-shimmer 1400ms var(--ease-standard) infinite;
    }

    /* Bestimmter Fortschritt: `--ui-progress: 0..1` von aussen setzen. */
    .ui-progress[style*="--ui-progress"]::after {
        inline-size: 100%;
        transform: scaleX(var(--ui-progress, 0));
        transform-origin: left center;
        animation: none;
        transition: transform var(--dur-base) var(--ease-out);
    }

    /* Reduzierte Bewegung: die Anzeige bleibt sichtbar, sie bewegt sich nur
       nicht mehr. Ein stehender Ring plus `cursor: progress` sagt weiterhin
       „ich arbeite" — base.css schaltet die Keyframes global ab. */
    @media (prefers-reduced-motion: reduce) {
        .ui-spinner,
        .hotbar-thinking-spinner {
            border-block-start-color: var(--ink-primary);
            opacity: 0.7;
        }

        .ui-skeleton::after,
        .ui-progress::after {
            background: color-mix(in oklab, var(--ink-primary) 6%, transparent);
        }
    }
}

@layer components {

    /* ====================================================================
       3 · Knopf — primär / ghost / gefährlich / nur-Icon
       Deckt die 134 `<button>`-Vorkommen über den Elementselektor ab.

       Die Grundform IST der Ghost-Knopf: keine Füllung, keine Linie, die
       Fläche erscheint erst beim Überfahren. STIL.md R3 — die Fläche ordnet,
       nicht die Linie.

       MATERIAL UND GEOMETRIE SIND GETRENNT.
       Das Material — Farbe, Schrift, Radius, Bewegung — liegt auf dem
       Elementselektor und erwischt damit alle 134 Knöpfe. Die Geometrie —
       Höhe, Polsterung, Anordnung — liegt NUR auf den Primitiv-Haken
       (`.btn`, `[data-variant]`).

       Grund: STEP_00_FUNDAMENT.md §1.3 — in Schritt 0 ändert sich kein
       Layout. `theme.css` deklariert für viele Bestandsknöpfe gar keine
       Höhe; ein globales `min-block-size: var(--row-h)` hätte nichts zu
       überschreiben und würde die Kanalliste und die Knopfleisten
       auseinanderziehen. Gemessen: 5 bis 13 Prozent Bildunterschied,
       reiner Höhenversatz. Die Geometrie zieht in den Gruppen 1–4 nach,
       Fläche für Fläche, zusammen mit dem Abbau von `theme.css`.
       ==================================================================== */

    :where(button, [role="button"], .btn, input[type="submit"], input[type="button"]) {
        border: 0;
        border-radius: var(--radius-control);
        background: transparent;
        color: var(--ink-secondary);
        font-family: var(--font-ui);
        font-size: var(--text-sm);
        font-weight: var(--font-weight-medium);
        line-height: 1.2;
        text-align: center;
        white-space: nowrap;
        -webkit-user-select: none;
        user-select: none;
        transition:
            transform var(--dur-fast) var(--ease-out),
            opacity var(--dur-instant) var(--ease-standard),
            background-color var(--dur-instant) var(--ease-standard),
            color var(--dur-instant) var(--ease-standard);
    }

    /* Geometrie — nur für die Primitive, nicht für den Bestand. */
    .btn,
    button[data-variant],
    [role="button"][data-variant] {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--space-2);
        min-block-size: var(--row-h);
        padding-inline: var(--space-3);
    }

    /* Knopfdruck — STIL.md §6. Gilt für jede drückbare Fläche. */
    :where(button, [role="button"], .btn, input[type="submit"], input[type="button"]):active:not(
        :disabled, [disabled], [aria-disabled="true"], .is-disabled
    ) {
        transform: scale(0.97);
    }

    /* --- Ghost (Grundform) ------------------------------------------- */

    @media (hover: hover) and (pointer: fine) {
        :where(button, [role="button"], .btn):hover:not(
            :disabled, [disabled], [aria-disabled="true"], .is-disabled
        ) {
            background: var(--surface-hover);
            color: var(--ink-primary);
        }
    }

    /* --- Primär ------------------------------------------------------
       Grauer Akzent auf dunkler Schrift. STIL.md R2: nie grün, nie farbig.
       Haken: die vorhandenen BEM-Modifikatoren des Bestands
       (`--primary`, `--confirm`, `--start`) plus `[data-variant]`. */

    button[data-variant="primary"],
    button.is-primary,
    button[class*="--primary"],
    button[class*="--confirm"],
    button[class*="--start"],
    [role="button"][data-variant="primary"],
    .btn[data-variant="primary"],
    input[type="submit"] {
        background: var(--accent);
        color: var(--accent-ink);
        font-weight: var(--font-weight-semibold);
    }

    @media (hover: hover) and (pointer: fine) {
        button[data-variant="primary"]:hover:not(:disabled, [disabled], .is-disabled),
        button.is-primary:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--primary"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--confirm"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--start"]:hover:not(:disabled, [disabled], .is-disabled),
        .btn[data-variant="primary"]:hover:not(:disabled, [disabled], .is-disabled),
        input[type="submit"]:hover:not(:disabled, [disabled], .is-disabled) {
            background: var(--accent-hover);
            color: var(--accent-ink);
        }
    }

    /* --- Zweitrangig -------------------------------------------------
       Für „Abbrechen" neben einem Primärknopf: eine Flächenstufe, keine
       Linie. */

    button[data-variant="secondary"],
    button.is-secondary,
    button[class*="--secondary"],
    button[class*="--cancel"],
    button[class*="-cancel"],
    button[class*="--soft"] {
        background: var(--surface-raised);
        color: var(--ink-primary);
    }

    @media (hover: hover) and (pointer: fine) {
        button[data-variant="secondary"]:hover:not(:disabled, [disabled], .is-disabled),
        button.is-secondary:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--secondary"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--cancel"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="-cancel"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--soft"]:hover:not(:disabled, [disabled], .is-disabled) {
            background: var(--surface-hover);
        }
    }

    /* --- Gefährlich --------------------------------------------------
       STIL.md R2 verbietet Rot als Knopffüllung ausdrücklich. Der
       gefährliche Knopf trägt das Signal deshalb in der SCHRIFT, die
       Fläche kommt erst beim Überfahren — und auch dann nur als
       12-Prozent-Tönung. */

    button[data-variant="danger"],
    button.is-danger,
    button[class*="--danger"],
    button[class*="--delete"],
    button[class*="-delete"],
    button[class*="--stop"],
    button[class*="--destructive"] {
        background: transparent;
        color: var(--signal-down);
    }

    @media (hover: hover) and (pointer: fine) {
        button[data-variant="danger"]:hover:not(:disabled, [disabled], .is-disabled),
        button.is-danger:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--danger"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--delete"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="-delete"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--stop"]:hover:not(:disabled, [disabled], .is-disabled),
        button[class*="--destructive"]:hover:not(:disabled, [disabled], .is-disabled) {
            background: var(--signal-down-surface);
            color: var(--signal-down);
        }
    }

    /* --- Nur Icon ----------------------------------------------------
       Quadratisch, ohne waagerechte Polsterung. `:has()` erwischt jeden
       Knopf, der nur ein Icon-Kind trägt — das sind im Bestand die
       Schliessen-, Menü- und Werkzeugknöpfe. */

    /* Erkannt wird NUR über einen ausdrücklichen Haken. Die naheliegende
       Abkürzung `button:has(> i:only-child)` ist falsch: `:only-child`
       zählt Elemente, nicht Textknoten — sie erwischt damit auch jeden
       Knopf aus Icon PLUS Beschriftung und quetscht ihn auf 40 px. Einen
       CSS-Test auf „enthält keinen Text" gibt es nicht. */

    button[data-variant="icon"],
    button.is-icon-only,
    [role="button"][data-variant="icon"] {
        inline-size: var(--row-h);
        min-inline-size: var(--row-h);
        padding-inline: 0;
        border-radius: var(--radius-control);
    }

    /* Ein Icon-Knopf ohne Text braucht eine Beschriftung — STIL.md §5.
       Sichtbare Erinnerung nur in der Entwicklungsansicht, siehe
       templates/dev/components.html. */

    /* --- Ladend ------------------------------------------------------
       Der Inhalt tritt zurück, der Ring legt sich darüber. Die Breite des
       Knopfes bleibt stehen — kein Umspringen des Layouts. */

    :where(button, [role="button"], .btn):is([data-loading], [aria-busy="true"], .is-loading) {
        position: relative;
        color: transparent;
    }

    :where(button, [role="button"], .btn):is([data-loading], [aria-busy="true"], .is-loading) > * {
        opacity: 0;
        transition: opacity var(--dur-fast) var(--ease-out);
    }

    :where(button, [role="button"], .btn):is([data-loading], [aria-busy="true"], .is-loading)::after {
        content: "";
        position: absolute;
        inset-block-start: 50%;
        inset-inline-start: 50%;
        inline-size: var(--icon-md);
        block-size: var(--icon-md);
        margin-block-start: calc(var(--icon-md) / -2);
        margin-inline-start: calc(var(--icon-md) / -2);
        border: 1.5px solid var(--line-strong);
        border-block-start-color: var(--ink-secondary);
        border-radius: 50%;
        animation: ui-spin 640ms linear infinite;
    }

    /* Auf dem Primärknopf steht der Ring auf hellem Grund. */
    button[data-variant="primary"]:is([data-loading], [aria-busy="true"], .is-loading)::after,
    button[class*="--primary"]:is([data-loading], [aria-busy="true"], .is-loading)::after {
        border-color: color-mix(in oklab, var(--accent-ink) 25%, transparent);
        border-block-start-color: var(--accent-ink);
    }

    /* --- Fehler / Erfolg --------------------------------------------
       Rückmeldung nach dem Klick: Schriftfarbe plus 12-Prozent-Tönung.
       Beides sind Zustandssignale — R2 erlaubt hier Farbe.

       Spezifität: der Zustand muss die VARIANTE schlagen. Ein primärer
       Knopf, der fehlschlägt, darf nicht weiter weiss leuchten. Deshalb
       tragen diese Selektoren dasselbe Gewicht wie `button[data-variant]`
       und stehen danach — nicht `:where()`, das wäre gewichtslos. */

    button:is([data-state="error"], .is-error, .has-error),
    [role="button"]:is([data-state="error"], .is-error, .has-error),
    .btn:is([data-state="error"], .is-error, .has-error) {
        background: var(--signal-down-surface);
        color: var(--signal-down);
    }

    button:is([data-state="success"], .is-success),
    [role="button"]:is([data-state="success"], .is-success),
    .btn:is([data-state="success"], .is-success) {
        background: var(--signal-up-surface);
        color: var(--signal-up);
    }

    /* --- Bestandshaken ----------------------------------------------
       Diese beiden Klassen im Inventar (Gruppe 0) sind Knöpfe in
       `api_settings.js`, die per JS erzeugt werden. */

    .api-secret-save {
        background: var(--accent);
        color: var(--accent-ink);
    }

    .api-secret-delete {
        background: transparent;
        color: var(--signal-down);
    }

    @media (hover: hover) and (pointer: fine) {
        .api-secret-save:hover { background: var(--accent-hover); }
        .api-secret-delete:hover { background: var(--signal-down-surface); }
    }

    @media (prefers-reduced-motion: reduce) {
        :where(button, [role="button"], .btn, input[type="submit"]):active {
            transform: none;
            opacity: 0.8;
        }
    }
}

@layer components {

    /* ====================================================================
       4 · Eingabefeld, Textfeld, Auswahlfeld
       Deckt die 39 `input`/`textarea`/`select`-Vorkommen über
       Elementselektoren ab.

       Das Feld ist eine FLÄCHE (`--surface-raised`), keine Linie —
       STIL.md R3. Die Linie liegt nur als Kante darauf, damit das Feld auf
       `--surface-raised`-Grund nicht verschwindet.
       ==================================================================== */

    :where(
        input:not([type="button"], [type="submit"], [type="reset"], [type="checkbox"], [type="radio"], [type="range"], [type="file"]),
        textarea,
        select
    ) {
        border: 1px solid var(--line);
        border-radius: var(--radius-control);
        background: var(--surface-raised);
        color: var(--ink-primary);
        font-family: var(--font-ui);
        font-size: var(--text-sm);
        line-height: 1.4;
        transition:
            background-color var(--dur-instant) var(--ease-standard),
            border-color var(--dur-instant) var(--ease-standard),
            opacity var(--dur-instant) var(--ease-standard);
    }

    :where(textarea) {
        line-height: var(--line-height-prose);
    }

    /* Geometrie der Feld-Primitive. Der Bestand behaelt seine Masse aus
       `theme.css`, bis die jeweilige Gruppe sie ueberholt.

       Ankreuzfeld und Auswahlknopf sind ausgenommen: sie sind 16x16 und
       haben keinen Innenabstand — `--row-h` und `--space-3` machten aus
       ihnen sonst 26x40-Flaechen (gemessen). Ihre Masse steht in 4b. */
    .ui-field:not([type="checkbox"], [type="radio"]),
    input[data-ui="field"]:not([type="checkbox"], [type="radio"]),
    textarea[data-ui="field"],
    select[data-ui="field"] {
        min-block-size: var(--row-h);
        padding-block: var(--space-2);
        padding-inline: var(--space-3);
    }

    textarea[data-ui="field"] {
        min-block-size: calc(var(--row-h) * 2);
    }

    /* Zahlen und Zeitwerte stehen in der Datenschrift — STIL.md §4. */
    :where(input[type="number"], input[type="time"], input[type="date"], input[type="datetime-local"]) {
        font-family: var(--font-data);
        font-variant-numeric: tabular-nums;
    }

    /* Auswahlfeld: eigener Pfeil, damit er die Strichstärke der Icons trägt.
       Der Pfeil ist ein Maskenbild — keine Hintergrundfarbe, keine Grafik. */
    :where(select) {
        appearance: none;
        padding-inline-end: var(--space-6);
        background-image: linear-gradient(
            to bottom,
            transparent 0,
            transparent 100%
        );
        background-repeat: no-repeat;
        cursor: pointer;
    }

    :where(select):not([multiple], [size]) {
        background-position: right var(--space-3) center;
    }

    /* --- Überfahren -------------------------------------------------- */

    @media (hover: hover) and (pointer: fine) {
        :where(input:not([type="button"], [type="submit"], [type="reset"]), textarea, select):hover:not(
            :disabled, [disabled], [readonly], .is-disabled
        ) {
            border-color: var(--line-strong);
        }
    }

    /* --- Fokus -------------------------------------------------------
       Der Ring kommt aus base.css (`:focus-visible`, 2 px `--line-focus`).
       Hier kommt nur die Kante dazu, damit auch ein Mausfokus im Feld
       sichtbar ist. `outline: none` steht nirgends. */

    :where(input, textarea, select):focus {
        border-color: var(--line-strong);
        background: var(--surface-hover);
    }

    /* --- Gedrückt / aktiv --------------------------------------------
       Ein Feld hat keinen Druckzustand mit Skalierung — es würde beim
       Tippen zappeln. Stattdessen eine Stufe heller. */

    :where(input, textarea, select):active:not(:disabled, [disabled], .is-disabled) {
        background: var(--surface-hover);
    }

    /* --- Abgeschaltet / schreibgeschützt ----------------------------- */

    :where(input, textarea, select):is([readonly], [aria-readonly="true"]) {
        background: var(--surface-panel);
        color: var(--ink-secondary);
        cursor: default;
    }

    /* --- Ladend ------------------------------------------------------
       Ein Feld, das auf eine Antwort wartet, wird nicht zum Spinner. Es
       wird gesperrt und tritt zurück. */

    :where(input, textarea, select):is([data-loading], [aria-busy="true"], .is-loading) {
        cursor: progress;
        opacity: 0.6;
        pointer-events: none;
    }

    /* --- Fehler / Erfolg --------------------------------------------
       R2: Rot und Grün sind hier Zustandssignal, das ist erlaubt. Die
       Fläche bleibt neutral, nur die Kante trägt das Signal — sonst
       leuchtet ein halb ausgefülltes Formular wie ein Weihnachtsbaum. */

    :where(input, textarea, select):is([aria-invalid="true"], [data-state="error"], .is-error, .has-error) {
        border-color: var(--signal-down-line);
        background: var(--signal-down-surface);
    }

    :where(input, textarea, select):is([data-state="success"], .is-success) {
        border-color: var(--signal-up-line);
    }

    /* Begleittext unter dem Feld. */
    .field-hint {
        display: block;
        margin-block-start: var(--space-1);
        color: var(--ink-muted);
        font-size: var(--text-xs);
        line-height: 1.45;
    }

    .field-hint[data-state="error"],
    .field-hint.is-error { color: var(--signal-down); }

    .field-hint[data-state="success"],
    .field-hint.is-success { color: var(--signal-up); }

    /* ====================================================================
       4b · Ankreuzfeld, Auswahlknopf, Umschalter — STIL.md §5.2

       Vollständig selbst gebaut. `accent-color` ist nicht genug: der
       Browser bringt eigene Radien, eigene Fokusdarstellung und unter
       Windows eine eigene Haken-Zeichnung mit. Deshalb `appearance: none`
       und alles darüber aus Tokens.

       Haken und Punkt sind SVG nach §5.1 — viewBox 0 0 24 24, `fill:
       none`, Strichstärke 1,5, runde Enden. Sie liegen als `mask-image`
       auf `::after`; die Farbe kommt damit aus `--accent-ink` und nicht
       aus der Grafik. Ein fester Farbwert steht in keinem der beiden.

       Klickfläche: das Feld sieht 16x16 aus, `::before` legt darüber
       unsichtbar `inset: -4px` — 24x24 nach §5.2, ohne die Zeile
       umzubrechen.

       WARUM NUR AUF `[data-ui="field"]` GEOMETRIE, ABER AUF ALLEN DAS
       AUSSEHEN: im Bestand liegt jedes echte Ankreuzfeld unsichtbar
       (`opacity: 0`, `position: absolute`) unter einer eigenen Karte —
       Sprachauswahl, Themenauswahl, Versandart, Alarm-Umschalter. Die
       Deckkraft kommt aus `theme.css` und gewinnt; das Aussehen hier
       trifft sie also nicht. Die Geometrie bliebe gefährlich, weil ein
       zeilenfüllendes <input> seine Klickfläche verlöre — deshalb bleibt
       sie am Haken.
       ==================================================================== */

    :where(input[type="checkbox"], input[type="radio"]) {
        position: relative;
        flex: none;
        /* `display: grid` ohne `place-content` — die Spur füllt die Fläche.
           Mit `place-content: center` fällt sie auf ihren Inhalt zusammen,
           und ein `::after` mit `100%` hat dann nichts, worauf es sich
           beziehen kann: gemessen 0x0, der Haken blieb unsichtbar. */
        display: grid;
        inline-size: var(--icon-md);
        block-size: var(--icon-md);
        margin: 0;
        border: 1px solid var(--line-strong);
        background: var(--surface-raised);
        appearance: none;
        cursor: pointer;
        transition:
            background-color var(--dur-fast) var(--ease-standard),
            border-color var(--dur-fast) var(--ease-standard),
            transform var(--dur-fast) var(--ease-standard);
    }

    :where(input[type="checkbox"]) { border-radius: 4px; }
    :where(input[type="radio"]) { border-radius: 999px; }

    /* Klickfläche 24x24 — unsichtbar, ohne Einfluss auf den Fluss. */
    :where(input[type="checkbox"], input[type="radio"])::before {
        content: "";
        position: absolute;
        inset: -4px;
    }

    /* Haken und Punkt. Erscheinen aus `scale(0.6)` — nie aus `scale(0)`
       (§6), und nur `transform` und `opacity` bewegen sich. */
    :where(input[type="checkbox"], input[type="radio"])::after {
        content: "";
        inline-size: 100%;
        block-size: 100%;
        background: var(--accent-ink);
        opacity: 0;
        transform: scale(0.6);
        transition:
            opacity var(--dur-fast) var(--ease-out),
            transform var(--dur-fast) var(--ease-out);
        -webkit-mask-repeat: no-repeat;
        mask-repeat: no-repeat;
        -webkit-mask-size: 100% 100%;
        mask-size: 100% 100%;
    }

    :where(input[type="checkbox"])::after {
        -webkit-mask-image: var(--mark-check);
        mask-image: var(--mark-check);
    }

    :where(input[type="radio"])::after {
        -webkit-mask-image: var(--mark-dot);
        mask-image: var(--mark-dot);
    }

    :where(input[type="checkbox"]):indeterminate::after {
        -webkit-mask-image: var(--mark-dash);
        mask-image: var(--mark-dash);
    }

    /* --- gewählt ------------------------------------------------------ */

    :where(input[type="checkbox"], input[type="radio"]):is(:checked, :indeterminate) {
        border-color: var(--accent);
        background: var(--accent);
    }

    :where(input[type="checkbox"], input[type="radio"]):is(:checked, :indeterminate)::after {
        opacity: 1;
        transform: scale(1);
    }

    /* --- Überfahren --------------------------------------------------- */

    @media (hover: hover) and (pointer: fine) {
        :where(input[type="checkbox"], input[type="radio"]):hover:not(
            :checked, :indeterminate, :disabled, [disabled], .is-disabled
        ) {
            border-color: var(--ink-secondary);
            background: var(--surface-hover);
        }

        :where(input[type="checkbox"], input[type="radio"]):is(:checked, :indeterminate):hover:not(
            :disabled, [disabled], .is-disabled
        ) {
            border-color: var(--accent-hover);
            background: var(--accent-hover);
        }
    }

    /* --- Gedrückt ----------------------------------------------------- */

    :where(input[type="checkbox"], input[type="radio"]):active:not(
        :disabled, [disabled], .is-disabled
    ) {
        transform: scale(0.92);
    }

    /* --- Ladend ------------------------------------------------------- */

    :where(input[type="checkbox"], input[type="radio"]):is(
        [data-loading], [aria-busy="true"], .is-loading
    ) {
        cursor: progress;
        opacity: 0.6;
    }

    /* --- Fehler / Erfolg ---------------------------------------------
       Nur die Kante trägt das Signal, die Fläche bleibt neutral — wie
       beim Eingabefeld. Ein gewähltes Feld behält seine Akzentfläche;
       sonst stünde der Haken auf Rot. */

    :where(input[type="checkbox"], input[type="radio"]):is(
        [aria-invalid="true"], [data-state="error"], .is-error, .has-error
    ) {
        border-color: var(--signal-down);
    }

    :where(input[type="checkbox"], input[type="radio"]):is(
        [data-state="success"], .is-success
    ) {
        border-color: var(--signal-up);
    }

    /* --- Umschalter ---------------------------------------------------
       32x18 mit Knauf 14 px (§5.2). Kein zusätzliches Markup: ein
       `<input type="checkbox" role="switch">` genügt, der Knauf ist
       `::after`. Der Knauf ist eine Fläche, kein Icon — deshalb ohne
       Maske. */

    :where(input[type="checkbox"][role="switch"]) {
        inline-size: 32px;
        block-size: 18px;
        border-radius: 999px;
        place-content: center start;
        padding: 1px;
    }

    :where(input[type="checkbox"][role="switch"])::after {
        inline-size: 14px;
        block-size: 14px;
        border-radius: 999px;
        background: var(--ink-secondary);
        opacity: 1;
        transform: translateX(0);
        transition: transform var(--dur-fast) var(--ease-out),
            background-color var(--dur-fast) var(--ease-standard);
        -webkit-mask-image: none;
        mask-image: none;
    }

    :where(input[type="checkbox"][role="switch"]):checked::after {
        background: var(--accent-ink);
        transform: translateX(14px);
    }

    /* Der Umschalter zappelt beim Drücken nicht — er schiebt. */
    :where(input[type="checkbox"][role="switch"]):active:not(
        :disabled, [disabled], .is-disabled
    ) {
        transform: none;
    }

    :where(input[type="range"]) {
        accent-color: var(--accent);
        cursor: pointer;
    }

    /* --- Beschriftung -------------------------------------------------
       `.label` steht im Inventar (Gruppe 0) und wird in stats.html als
       Kopfzeile einer Kennzahl benutzt. Gleiche Rolle wie `<label>`. */

    /* Kein `display` auf `<label>` — ein Kästchen mit Beschriftung steht
       in einer Zeile, `display: block` bräche sie um. */
    :where(label),
    .label {
        color: var(--ink-secondary);
        font-family: var(--font-ui);
        font-size: var(--text-xs);
        font-weight: var(--font-weight-regular);
        line-height: 1.4;
    }
}

@layer components {

    /* ====================================================================
       5 · Menü, Popover, Kontextmenü, eigene Dropdowns
       Teil der 55 Overlay-Klassen.

       Sicherheitsentscheidung: die Menüs bekommen KEINEN versteckten
       Grundzustand (`opacity: 0`). Ein Menü, das der Bestand über eine
       andere Klasse einblendet, würde sonst unsichtbar bleiben. Das
       Erscheinen läuft stattdessen über `@starting-style` — das greift
       genau dann, wenn das Element auftaucht, und niemals sonst.
       ==================================================================== */

    /* --- Auslöser ----------------------------------------------------
       Der Dropdown-Auslöser ist ein Knopf mit Feldcharakter: Fläche wie
       ein Eingabefeld, Verhalten wie ein Knopf. */

    .ui-select,
    [class*="dropdown-trigger"],
    [class*="date-trigger"],
    [class*="date-select"] {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        border-radius: var(--radius-control);
        background: var(--surface-raised);
        color: var(--ink-primary);
        font-size: var(--text-sm);
        cursor: pointer;
        transition:
            background-color var(--dur-instant) var(--ease-standard),
            transform var(--dur-fast) var(--ease-out);
    }

    .ui-select {
        min-block-size: var(--row-h);
        padding-inline: var(--space-3);
    }

    @media (hover: hover) and (pointer: fine) {
        .ui-select:hover,
        [class*="dropdown-trigger"]:hover,
        [class*="date-trigger"]:hover {
            background: var(--surface-hover);
        }
    }

    .ui-select:active,
    [class*="dropdown-trigger"]:active {
        transform: scale(0.97);
    }

    /* Der Pfeil dreht sich beim Öffnen — 180 Grad, `transform`, sonst nichts. */
    .ui-select__caret,
    [class*="dropdown-caret"],
    [class*="date-caret"] {
        flex: none;
        color: var(--ink-muted);
        transition: transform var(--dur-fast) var(--ease-out);
    }

    :is(.ui-select, [class*="dropdown"], [class*="date-select"]):is(.is-open, .show, .visible, [aria-expanded="true"], [data-state="open"])
        :is(.ui-select__caret, [class*="dropdown-caret"], [class*="date-caret"]) {
        transform: rotate(180deg);
    }

    /* --- Fläche ------------------------------------------------------ */

    .ui-menu,
    [role="menu"],
    [role="listbox"],
    [class*="dropdown-menu"],
    [class*="menu-panel"],
    .user-menu,
    .chat-item-menu,
    .liquid-dropdown,
    .journal-meta-select-menu {
        z-index: var(--z-dropdown);
        border-radius: var(--radius-panel);
        background: var(--surface-overlay);
        color: var(--ink-primary);
        font-family: var(--font-ui);
        font-size: var(--text-sm);
        /* Ein Menü schwebt — das ist die einzige Stelle, an der ein Schatten
           erlaubt ist. STIL.md §7 verbietet Schatten auf RUHENDEN Flächen. */
        box-shadow: 0 8px 28px color-mix(in oklab, var(--surface-canvas) 72%, transparent);
        /* Wächst aus dem Auslöser. Wer die Kante kennt, setzt
           `--transform-origin` beim Öffnen. */
        transform-origin: var(--transform-origin, top center);
        transition:
            opacity var(--dur-base) var(--ease-out),
            transform var(--dur-base) var(--ease-out),
            display var(--dur-base) var(--ease-out) allow-discrete;
    }

    .ui-menu {
        min-inline-size: 10rem;
        padding: var(--space-1);
    }

    @starting-style {
        .ui-menu,
        [role="menu"],
        [role="listbox"],
        [class*="dropdown-menu"],
        [class*="menu-panel"],
        .user-menu,
        .chat-item-menu,
        .liquid-dropdown,
        .journal-meta-select-menu {
            opacity: 0;
            transform: scale(0.97);
        }
    }

    /* Schliessen: der Bestand setzt `.is-closing`, bevor er das Element
       entfernt. Rückwärts derselbe Weg. */
    :is(.ui-menu, [role="menu"], [role="listbox"], [class*="dropdown-menu"], [class*="menu-panel"], .user-menu, .chat-item-menu).is-closing {
        opacity: 0;
        transform: scale(0.97);
    }

    /* --- Einträge ---------------------------------------------------- */

    .ui-menu-item,
    [role="menuitem"],
    [role="option"],
    [class*="dropdown-item"],
    .extras-menu-item {
        display: flex;
        align-items: center;
        /* Ein Menüeintrag ist linksbündig. Ohne diese Zeile gewinnt das
           `justify-content: center` aus dem Knopf-Grundstil, weil die
           meisten Einträge `<button>` sind. */
        justify-content: flex-start;
        gap: var(--space-2);
        inline-size: 100%;
        border-radius: var(--radius-chip);
        background: transparent;
        color: var(--ink-secondary);
        font-size: var(--text-sm);
        font-weight: var(--font-weight-regular);
        text-align: start;
        cursor: pointer;
        transition:
            background-color var(--dur-instant) var(--ease-standard),
            color var(--dur-instant) var(--ease-standard),
            transform var(--dur-fast) var(--ease-out);
    }

    @media (hover: hover) and (pointer: fine) {
        :is(.ui-menu-item, [role="menuitem"], [role="option"], [class*="dropdown-item"], .extras-menu-item):hover:not(
            [aria-disabled="true"], .is-disabled, :disabled
        ) {
            background: var(--surface-hover);
            color: var(--ink-primary);
        }
    }

    :is(.ui-menu-item, [role="menuitem"], [role="option"], [class*="dropdown-item"]):active:not([aria-disabled="true"], .is-disabled) {
        transform: scale(0.97);
    }

    /* Gewählt / aktiv — Fläche, keine Farbe. STIL.md R2: der aktive
       Eintrag ist NIE grün. */
    :is(.ui-menu-item, [role="menuitem"], [role="option"], [class*="dropdown-item"], .extras-menu-item):is(
        .is-selected, .is-active, .active, [aria-selected="true"], [aria-current="true"], [data-state="checked"]
    ) {
        background: var(--surface-selected);
        color: var(--ink-primary);
        font-weight: var(--font-weight-medium);
    }

    /* Ladend: der ganze Eintragsblock wird zum Skelett, nicht der Eintrag. */
    :is(.ui-menu, [role="menu"], [role="listbox"]):is([data-loading], [aria-busy="true"], .is-loading) {
        cursor: progress;
    }

    /* Zustandssignale im Menü. */
    :is(.ui-menu-item, [role="menuitem"], [role="option"]):is([data-state="error"], .is-error) {
        color: var(--signal-down);
    }

    :is(.ui-menu-item, [role="menuitem"], [role="option"]):is([data-state="success"], .is-success) {
        color: var(--signal-up);
    }

    /* --- Abschnittsmarke und Hinweis im Menü -------------------------
       STIL.md §7 verbietet gesperrte Grossbuchstaben über Abschnitten.
       Deshalb: normale Schreibweise, kleiner Grad, gedämpft. */

    /* Geometrie nur fuer die Primitive. */
    .ui-menu-item,
    .ui-menu [role="menuitem"],
    .ui-menu [role="option"] {
        min-block-size: var(--row-h);
        padding-inline: var(--space-3);
    }

    .ui-menu-label,
    [class*="menu-section-label"],
    [class*="dropdown-label"] {
        padding-block: var(--space-2) var(--space-1);
        padding-inline: var(--space-3);
        color: var(--ink-muted);
        font-size: var(--text-xs);
        font-weight: var(--font-weight-regular);
        letter-spacing: normal;
        text-transform: none;
    }

    .extras-menu-hint {
        padding-inline: var(--space-3);
        color: var(--ink-muted);
        font-size: var(--text-xs);
    }

    .extras-menu-text {
        flex: 1 1 auto;
        min-inline-size: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    @media (prefers-reduced-motion: reduce) {
        :is(.ui-select, [class*="dropdown-trigger"], .ui-menu-item, [role="menuitem"], [role="option"]):active {
            transform: none;
        }

        :is(.ui-menu, [role="menu"], [role="listbox"], [class*="dropdown-menu"], [class*="menu-panel"]).is-closing {
            transform: none;
        }
    }
}

@layer components {

    /* ====================================================================
       6 · Dialog und Hintergrundverdunkelung
       ==================================================================== */

    /* Die Verdunkelung ist bewusst namentlich aufgezählt statt über
       `[class*="overlay"]` erfasst: `grid-overlay`, `noise-overlay` und
       `chart-overlay` sind Diagrammschichten, keine Dialoghintergründe.
       Ein Selektor, der sie mitnimmt, legt einen schwarzen Schleier über
       den Scanner. */

    dialog::backdrop,
    .ui-backdrop,
    .auth-modal-overlay,
    .journal-modal-overlay,
    .ui-confirm-overlay,
    .ui-prompt-overlay,
    .settings-overlay,
    .settings-shell-overlay,
    .scanner-confirm-overlay,
    .image-gallery-overlay,
    .inbox-overlay {
        z-index: var(--z-modal-backdrop);
        background: color-mix(in oklab, var(--surface-canvas) 78%, transparent);
        transition: opacity var(--dur-slow) var(--ease-out);
    }

    @starting-style {
        dialog::backdrop,
        .ui-backdrop,
        .auth-modal-overlay,
        .journal-modal-overlay,
        .ui-confirm-overlay,
        .ui-prompt-overlay,
        .settings-overlay,
        .settings-shell-overlay,
        .scanner-confirm-overlay,
        .image-gallery-overlay,
        .inbox-overlay {
            opacity: 0;
        }
    }

    /* Der Dialog selbst. `transform-origin` bleibt hier bei Mitte — ein
       Dialog hängt an keinem Auslöser, er erscheint mittig. */

    dialog,
    .ui-dialog,
    [role="dialog"],
    [role="alertdialog"],
    .auth-modal,
    .journal-modal,
    .ui-confirm-modal,
    .ui-prompt-modal,
    .chart-prompt-modal,
    .chart-trade-modal {
        z-index: var(--z-modal);
        border: 0;
        border-radius: var(--radius-panel);
        background: var(--surface-overlay);
        color: var(--ink-primary);
        font-family: var(--font-ui);
        font-size: var(--text-sm);
        box-shadow: 0 18px 56px color-mix(in oklab, var(--surface-canvas) 76%, transparent);
        transform-origin: center;
        transition:
            opacity var(--dur-slow) var(--ease-out),
            transform var(--dur-slow) var(--ease-out),
            display var(--dur-slow) var(--ease-out) allow-discrete,
            overlay var(--dur-slow) var(--ease-out) allow-discrete;
    }

    /* Der Posteingang ist der Grund hinter seinem Panel, kein Dialog — er
       traegt aber `role="dialog"` und bekam damit aus der Regel darueber
       die DECKENDE Dialog-Flaeche (`--surface-overlay`): hinter ihm war
       nichts mehr zu sehen. Hier bekommt er die Flaeche einer
       Ueberlagerung zurueck — leicht abgedunkelt und minimal
       verschwommen, damit der Hintergrund sichtbar bleibt. Die Regel steht
       bewusst nach der Dialog-Regel: gleiche Spezifitaet, spaetere
       Position — ohne `!important` und ohne neue Schicht. */
    .inbox-overlay {
        background: color-mix(in oklab, var(--surface-canvas) 62%, transparent);
        backdrop-filter: blur(4px);
        -webkit-backdrop-filter: blur(4px);
    }

    /* Geschlossene Ueberlagerungen (opacity 0) behielten ihren Blur und damit
       je eine Vollbild-Compositing-Ebene samt Blur-Puffer — bei 1440p/4K
       Hunderte MB Grafikspeicher fuer nichts Sichtbares. Blur nur, solange
       sie offen sind. */
    .inbox-overlay:not(.inbox-overlay--open),
    .ui-prompt-overlay:not(.show),
    .ui-confirm-overlay:not(.show),
    .auth-modal-overlay:not(.show) {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }

    .ui-dialog {
        max-inline-size: min(92vw, 34rem);
        padding: var(--space-5);
    }

    @starting-style {
        dialog[open],
        .ui-dialog,
        [role="dialog"],
        [role="alertdialog"],
        .auth-modal,
        .journal-modal,
        .ui-confirm-modal,
        .ui-prompt-modal,
        .chart-prompt-modal,
        .chart-trade-modal {
            opacity: 0;
            transform: scale(0.97);
        }
    }

    :is(dialog, .ui-dialog, [role="dialog"], .auth-modal, .journal-modal, .ui-confirm-modal, .ui-prompt-modal).is-closing {
        opacity: 0;
        transform: scale(0.97);
    }

    /* Dialogteile — Kopf, Text, Fussleiste. Kein Rahmen, nur Abstand.
       STIL.md R3: keine verschachtelten Karten im Dialog. */

    .ui-dialog__title,
    .auth-modal-title,
    .journal-modal-title {
        margin-block-end: var(--space-2);
        color: var(--ink-primary);
        font-size: var(--text-lg);
        font-weight: var(--font-weight-medium);
        line-height: 1.3;
    }

    .ui-dialog__body,
    .journal-modal-text {
        color: var(--ink-secondary);
        font-size: var(--text-sm);
        line-height: var(--line-height-prose);
    }

    .ui-dialog__actions,
    .journal-modal-actions {
        display: flex;
        justify-content: flex-end;
        gap: var(--space-2);
        margin-block-start: var(--space-5);
    }

    /* Ladend / Fehler / Erfolg im Dialog. */

    :is(dialog, .ui-dialog, [role="dialog"]):is([data-loading], [aria-busy="true"], .is-loading) {
        cursor: progress;
    }

    :is(dialog, .ui-dialog, [role="dialog"]) :is([data-state="error"], .is-error).ui-dialog__body {
        color: var(--signal-down);
    }

    :is(dialog, .ui-dialog, [role="dialog"]) :is([data-state="success"], .is-success).ui-dialog__body {
        color: var(--signal-up);
    }

    @media (prefers-reduced-motion: reduce) {
        dialog,
        .ui-dialog,
        [role="dialog"],
        [role="alertdialog"] {
            transform: none;
        }
    }

    /* ====================================================================
       7 · Tooltip
       Kurz, klein, ohne Verzögerung beim zweiten Mal — `[data-instant]`
       schaltet die Bewegung ab, damit eine Werkzeugleiste beim Wandern
       nicht bei jedem Knopf neu aufblendet.
       ==================================================================== */

    .ui-tooltip,
    [role="tooltip"] {
        z-index: var(--z-tooltip);
        border-radius: var(--radius-chip);
        background: var(--surface-overlay);
        color: var(--ink-primary);
        font-family: var(--font-ui);
        font-size: var(--text-xs);
        font-weight: var(--font-weight-regular);
        line-height: 1.4;
        text-align: start;
        box-shadow: 0 4px 16px color-mix(in oklab, var(--surface-canvas) 70%, transparent);
        transform-origin: var(--transform-origin, top center);
        transition:
            opacity var(--dur-fast) var(--ease-out),
            transform var(--dur-fast) var(--ease-out),
            display var(--dur-fast) var(--ease-out) allow-discrete;
    }

    .ui-tooltip {
        max-inline-size: 20rem;
        padding-block: var(--space-1);
        padding-inline: var(--space-2);
    }

    @starting-style {
        .ui-tooltip,
        [role="tooltip"] {
            opacity: 0;
            transform: scale(0.97);
        }
    }

    :is(.ui-tooltip, [role="tooltip"])[data-instant] {
        transition: opacity var(--dur-instant) var(--ease-out);
    }

    :is(.ui-tooltip, [role="tooltip"]).is-closing {
        opacity: 0;
        transform: scale(0.97);
    }

    @media (prefers-reduced-motion: reduce) {
        .ui-tooltip,
        [role="tooltip"] {
            transform: none;
        }
    }

    /* ====================================================================
       8 · Toast / Benachrichtigung
       Betritt und verlässt den Schirm auf demselben Weg — von unten.
       Übergang statt Keyframe, damit schnell nacheinander erzeugte Toasts
       sich nicht gegenseitig zurücksetzen.
       ==================================================================== */

    .ui-toast,
    [class*="toast"],
    .background-job-bubble {
        z-index: var(--z-toast);
        display: flex;
        align-items: center;
        gap: var(--space-3);
        border-radius: var(--radius-panel);
        background: var(--surface-overlay);
        color: var(--ink-primary);
        font-family: var(--font-ui);
        font-size: var(--text-sm);
        box-shadow: 0 10px 32px color-mix(in oklab, var(--surface-canvas) 74%, transparent);
        transition:
            opacity var(--dur-base) var(--ease-out),
            transform var(--dur-base) var(--ease-out);
    }

    .ui-toast {
        max-inline-size: 26rem;
        min-block-size: var(--row-h);
        padding-block: var(--space-2);
        padding-inline: var(--space-4);
    }

    @starting-style {
        .ui-toast,
        [class*="toast"],
        .background-job-bubble {
            opacity: 0;
            transform: translateY(100%) scale(0.97);
        }
    }

    :is(.ui-toast, [class*="toast"], .background-job-bubble).is-closing {
        opacity: 0;
        transform: translateY(100%) scale(0.97);
    }

    /* Der Zustand steht in der Schrift, nicht in der Füllung — R2. */

    :is(.ui-toast, [class*="toast"], .background-job-bubble):is([data-state="error"], .is-error) {
        color: var(--signal-down);
    }

    :is(.ui-toast, [class*="toast"], .background-job-bubble):is([data-state="success"], .is-success) {
        color: var(--signal-up);
    }

    :is(.ui-toast, [class*="toast"], .background-job-bubble):is([data-state="warn"], .is-warning) {
        color: var(--signal-warn);
    }

    :is(.ui-toast, [class*="toast"], .background-job-bubble):is([data-loading], [aria-busy="true"], .is-loading) {
        color: var(--ink-secondary);
    }

    /* Der laufende Hintergrundauftrag: ein Punkt in der Leiste. */
    .background-job-indicator {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        inline-size: var(--icon-sm);
        block-size: var(--icon-sm);
        border-radius: 50%;
        background: var(--ink-muted);
        animation: ui-pulse 1600ms var(--ease-standard) infinite;
    }

    .background-job-indicator:is([data-state="error"], .is-error) {
        background: var(--signal-down);
        animation: none;
    }

    .background-job-indicator:is([data-state="success"], .is-success) {
        background: var(--signal-up);
        animation: none;
    }

    @media (prefers-reduced-motion: reduce) {
        .ui-toast,
        [class*="toast"],
        .background-job-bubble {
            transform: none;
        }

        .background-job-indicator {
            opacity: 0.75;
        }
    }
}

@layer components {

    /* ====================================================================
       9 · Chip / Tag / Statusabzeichen
       Eine Fläche, keine Linie, kein Pillenradius — `--radius-chip`
       kommt aus dem Dichtemodus.
       ==================================================================== */

    .ui-chip,
    [class*="-chip"],
    [class*="-badge"],
    [class*="-tag"] {
        display: inline-flex;
        align-items: center;
        gap: var(--space-1);
        border-radius: var(--radius-chip);
        background: var(--accent-subtle);
        color: var(--ink-secondary);
        font-family: var(--font-ui);
        font-size: var(--text-xs);
        font-weight: var(--font-weight-regular);
        line-height: 1.4;
        white-space: nowrap;
        transition:
            background-color var(--dur-instant) var(--ease-standard),
            color var(--dur-instant) var(--ease-standard),
            transform var(--dur-fast) var(--ease-out);
    }

    .ui-chip {
        padding-block: 2px;
        padding-inline: var(--space-2);
    }

    /* Ein Chip mit Zahl trägt die Datenschrift — STIL.md §4. */
    :is(.ui-chip, [class*="-badge"])[data-numeric] {
        font-family: var(--font-data);
        font-variant-numeric: tabular-nums;
    }

    /* Nur anklickbare Chips reagieren. Ein Statusabzeichen ist kein Knopf. */
    @media (hover: hover) and (pointer: fine) {
        :is(.ui-chip, [class*="-chip"], [class*="-tag"]):is(button, [role="button"], [tabindex]):hover:not(
            .is-disabled, [aria-disabled="true"]
        ) {
            background: var(--surface-hover);
            color: var(--ink-primary);
        }
    }

    :is(.ui-chip, [class*="-chip"], [class*="-tag"]):is(button, [role="button"]):active {
        transform: scale(0.97);
    }

    :is(.ui-chip, [class*="-chip"], [class*="-tag"], [class*="-badge"]):is(
        .is-selected, .is-active, .active, [aria-pressed="true"]
    ) {
        background: var(--surface-selected);
        color: var(--ink-primary);
    }

    :is(.ui-chip, [class*="-chip"], [class*="-tag"], [class*="-badge"]):is([data-loading], [aria-busy="true"], .is-loading) {
        color: var(--ink-muted);
        cursor: progress;
    }

    /* Signalchips. Fläche 12 %, Schrift voll — die einzige erlaubte
       Farbfüllung, weil sie Finanz- bzw. Zustandsbedeutung trägt. */

    :is(.ui-chip, [class*="-chip"], [class*="-tag"], [class*="-badge"]):is([data-state="success"], .is-success, .is-up) {
        background: var(--signal-up-surface);
        color: var(--signal-up);
    }

    :is(.ui-chip, [class*="-chip"], [class*="-tag"], [class*="-badge"]):is([data-state="error"], .is-error, .is-down) {
        background: var(--signal-down-surface);
        color: var(--signal-down);
    }

    :is(.ui-chip, [class*="-chip"], [class*="-tag"], [class*="-badge"]):is([data-state="warn"], .is-warning) {
        background: var(--signal-warn-surface);
        color: var(--signal-warn);
    }

    /* ====================================================================
       10 · Panel und Abschnittskopf
       `menu-panel` ist ausgenommen — das ist ein Menü (Abschnitt 5), kein
       Panel.
       ==================================================================== */

    .ui-panel,
    .settings-panel,
    .scanner-panel,
    .dp-panel,
    .inbox-panel,
    .user-panel,
    .chart-alert-panel,
    .chart-indicator-panel,
    .chart-manager-panel {
        border-radius: var(--radius-panel);
        background: var(--surface-panel);
        color: var(--ink-primary);
        font-family: var(--font-ui);
        /* Ruhende Fläche: kein Schatten, kein Rahmen — STIL.md §7 und R3. */
        box-shadow: none;
    }

    /* Ein Panel IN einem Panel wird nicht zur zweiten Karte, es wird zur
       nächsten Helligkeitsstufe. STIL.md R3: keine verschachtelten Karten. */
    :is(.ui-panel, .settings-panel, .scanner-panel, .dp-panel) :is(.ui-panel, .dp-panel) {
        background: var(--surface-raised);
    }

    .ui-panel__head,
    .scanner-panel-head,
    .dp-panel-head,
    .settings-panel-head,
    .chart-alert-panel__header,
    .chart-indicator-panel__header,
    .chart-manager-panel__header {
        /* Der Kopf ist die eine Stelle, an der eine Linie erlaubt ist —
           R3: „wo eine Fläche die Ordnung nicht trägt". */
        border-block-end: 1px solid var(--line-subtle);
        color: var(--ink-primary);
    }

    /* `display: flex` NUR auf dem Primitiv. Auf einem Bestandskopf macht
       es aus einem Blockcontainer einen Flexcontainer — Überschrift und
       Untertitel schrumpfen dann auf Inhaltsbreite. Gemessen an
       `.settings-panel-head`: H2 von 802 auf 134 px. */
    .ui-panel__head {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        min-block-size: var(--row-h);
        padding-inline: var(--space-4);
    }

    .ui-panel__title,
    .scanner-panel-title,
    .dp-title,
    .radar-title,
    .da-title {
        flex: 1 1 auto;
        min-inline-size: 0;
        overflow: hidden;
        color: var(--ink-primary);
        font-size: var(--text-lg);
        font-weight: var(--font-weight-medium);
        letter-spacing: normal;
        text-overflow: ellipsis;
        text-transform: none;
        white-space: nowrap;
    }

    .ui-panel__body,
    .dp-panel-body {
        padding: var(--space-4);
    }

    .ui-panel__actions {
        display: flex;
        align-items: center;
        gap: var(--space-1);
        margin-inline-start: auto;
    }

    /* Im Bestand nur die Ausrichtung der Kinder, keine Anordnung. */
    .scanner-panel-actions,
    .dp-header-actions,
    .radar-header-actions,
    .da-header-actions,
    .chat-history-actions {
        gap: var(--space-1);
    }

    :is(.ui-panel, .settings-panel, .scanner-panel):is([data-loading], [aria-busy="true"], .is-loading) {
        cursor: progress;
    }

    /* ====================================================================
       11 · Tabelle
       Kopf, Zeile, Überfahren, Zahlenspalte rechtsbündig mit tabular-nums.
       ==================================================================== */

    table,
    .data-table {
        inline-size: 100%;
        border-collapse: collapse;
        font-family: var(--font-ui);
        font-size: var(--text-sm);
    }

    :is(table, .data-table) thead th {
        position: sticky;
        inset-block-start: 0;
        z-index: var(--z-sticky);
        /* Der Kopf steht auf einer eigenen Stufe UND trägt eine Linie —
           eine Tabelle scrollt, die Fläche allein hält die Ordnung nicht. */
        background: var(--surface-raised);
        border-block-end: 1px solid var(--line);
        color: var(--ink-secondary);
        font-size: var(--text-xs);
        font-weight: var(--font-weight-medium);
        letter-spacing: normal;
        text-align: start;
        text-transform: none;
        white-space: nowrap;
    }

    :is(table, .data-table) tbody td {
        border-block-end: 1px solid var(--line-subtle);
        color: var(--ink-primary);
        vertical-align: middle;
    }

    :is(table, .data-table) tbody tr {
        transition: background-color var(--dur-instant) var(--ease-standard);
    }

    /* Geometrie nur, wo die Tabelle ein Primitiv ist. `theme.css` bemasst
       `.data-table` im Bestand bis heute selbst. */
    .ui-table :is(th, td),
    table[data-ui="table"] :is(th, td) {
        block-size: var(--row-h);
        padding-block: var(--space-2);
        padding-inline: var(--space-3);
    }

    @media (hover: hover) and (pointer: fine) {
        :is(table, .data-table) tbody tr:hover {
            background: var(--surface-hover);
        }
    }

    :is(table, .data-table) tbody tr:is(.is-selected, .is-active, .active, [aria-selected="true"]) {
        background: var(--surface-selected);
    }

    :is(table, .data-table) tbody tr:is([data-state="error"], .is-error) td {
        color: var(--signal-down);
    }

    :is(table, .data-table) tbody tr:is([data-state="success"], .is-success) td {
        color: var(--signal-up);
    }

    :is(table, .data-table):is([data-loading], [aria-busy="true"], .is-loading) tbody {
        opacity: 0.5;
    }

    /* Zahlenspalten: rechtsbündig, Datenschrift, gleiche Ziffernbreite.
       `.font-mono` ist der Bestandshaken aus `stats.html`. */

    :is(table, .data-table) :is(th, td):is(.font-mono, .num, [data-numeric]),
    :is(table, .data-table) :is(th, td):has(> .font-mono),
    .font-mono {
        font-family: var(--font-data);
        font-variant-numeric: tabular-nums;
    }

    :is(table, .data-table) :is(th, td):is(.font-mono, .num, [data-numeric]) {
        text-align: end;
    }

    /* Zeile ohne Entscheidung — Journal und V2-Datenmodus. Kein Signal,
       nur gedämpft. */
    .v2-row-undecided td,
    .v2-row-undecided {
        color: var(--ink-muted);
    }
}

@layer components {

    /* ====================================================================
       12 · Scrollbar
       Vorher grün getönt. Jetzt neutral: die Bahn ist unsichtbar, der
       Griff ist eine Linie. STIL.md R2 — ein Scrollbalken hat keine
       Finanzbedeutung, also keine Farbe.
       ==================================================================== */

    * {
        scrollbar-width: thin;
        scrollbar-color: var(--line-strong) transparent;
    }

    ::-webkit-scrollbar {
        inline-size: 8px;
        block-size: 8px;
    }

    ::-webkit-scrollbar-track {
        background: transparent;
    }

    ::-webkit-scrollbar-thumb {
        border-radius: var(--radius-chip);
        background: var(--line);
    }

    ::-webkit-scrollbar-corner {
        background: transparent;
    }

    @media (hover: hover) and (pointer: fine) {
        ::-webkit-scrollbar-thumb:hover {
            background: var(--line-strong);
        }
    }

    /* ====================================================================
       13 · Trennlinie
       ==================================================================== */

    hr,
    .ui-divider,
    [class*="-divider"],
    .nav-divider,
    .server-divider {
        margin: 0;
        border: 0;
        border-block-start: 1px solid var(--line-subtle);
        block-size: 0;
    }

    /* Senkrecht, z. B. in einer Werkzeugleiste. */
    .ui-divider[data-orientation="vertical"],
    .chart-toolbar__divider {
        align-self: stretch;
        inline-size: 0;
        border-block-start: 0;
        border-inline-start: 1px solid var(--line-subtle);
    }

    /* ====================================================================
       14 · Leerzustand, Fehlerzustand, Erfolgszustand
       ==================================================================== */

    .empty-state,
    .journal-empty-state,
    .chat-history-empty {
        color: var(--ink-secondary);
        text-align: center;
    }

    /* Geometrie nur fuer das Primitiv. */
    .ui-empty {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--space-2);
        padding-block: var(--space-7);
        padding-inline: var(--space-5);
    }

    .empty-state .title,
    .empty-state-title,
    .journal-empty-text {
        color: var(--ink-primary);
        font-size: var(--text-sm);
        font-weight: var(--font-weight-medium);
    }

    .empty-state .desc,
    .empty-state-desc {
        max-inline-size: 42ch;
        color: var(--ink-muted);
        font-size: var(--text-xs);
        line-height: 1.5;
    }

    .empty-state .ui-icon,
    .journal-empty-icon {
        color: var(--ink-muted);
        font-size: var(--icon-xl);
    }

    /* Fehler- und Erfolgsblock. Fläche 12 %, Schrift voll, keine Linie
       links — STIL.md §7 verbietet Farbstreifen breiter als 1 px. */

    .ui-state,
    .ui-state--error,
    .ui-state--success,
    .ui-state--warning {
        display: flex;
        align-items: flex-start;
        gap: var(--space-2);
        padding: var(--space-3);
        border-radius: var(--radius-control);
        background: var(--surface-raised);
        color: var(--ink-secondary);
        font-size: var(--text-sm);
        line-height: 1.5;
    }

    .ui-state--error {
        background: var(--signal-down-surface);
        color: var(--signal-down);
    }

    .ui-state--success {
        background: var(--signal-up-surface);
        color: var(--signal-up);
    }

    .ui-state--warning {
        background: var(--signal-warn-surface);
        color: var(--signal-warn);
    }

    /* ====================================================================
       15 · Kennzahlenkarte und Kennzahlengitter
       Flach, ohne Rahmen, ohne Schatten. Die Karte ist eine Stufe heller
       als ihr Grund — mehr braucht sie nicht.
       ==================================================================== */

    /* Geometrie nur fuer das Primitiv — `.stats-grid` und `.stat-card`
       werden im Bestand von `theme.css` bemasst. */
    .ui-stats-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
        gap: var(--space-3);
    }

    .ui-stats-grid > .stat-card {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        padding: var(--space-4);
    }

    .stat-card {
        border-radius: var(--radius-panel);
        background: var(--surface-raised);
        transition: background-color var(--dur-instant) var(--ease-standard);
    }

    @media (hover: hover) and (pointer: fine) {
        .stat-card:hover {
            background: var(--surface-hover);
        }
    }

    .stat-card .label {
        color: var(--ink-secondary);
        font-size: var(--text-xs);
        font-weight: var(--font-weight-regular);
        letter-spacing: normal;
        text-transform: none;
    }

    /* Der Wert steht in der Datenschrift und trägt NIE den Akzent —
       STIL.md R2: „der Akzent liegt niemals auf einer Zahl". */
    .stat-card .value,
    .value {
        color: var(--ink-primary);
        font-family: var(--font-data);
        font-size: var(--text-xl);
        font-weight: var(--font-weight-medium);
        font-variant-numeric: tabular-nums;
        line-height: 1.2;
    }

    .stat-card .desc {
        color: var(--ink-muted);
        font-size: var(--text-xs);
    }

    /* ====================================================================
       16 · Finanz- und Zustandsklassen des Bestands
       Die einzigen Stellen, an denen Grün, Rot und Gelb vorkommen dürfen.
       ==================================================================== */

    .green,
    .is-up,
    [data-signal="up"] { color: var(--signal-up); }

    .red,
    .is-down,
    [data-signal="down"] { color: var(--signal-down); }

    .warning,
    .chart-tool--warning,
    .chart-status-bar__warning,
    .chart-trade-notice--warning,
    [data-signal="warn"] { color: var(--signal-warn); }

    .chart-status-bar__ok { color: var(--signal-up); }

    /* Der Scanner meldet einen ausgelösten Alarm. Zustandssignal, erlaubt. */
    .alert,
    .triggered {
        color: var(--signal-warn);
    }

    /* Ein blockierter Handelshinweis ist eine Sperre, kein Verlust —
       deshalb gedämpft und nicht rot. */
    .chart-trade-hint--blocked {
        color: var(--ink-muted);
    }

    /* Kursaufblitzen bei neuem Wert. Nur `color`, keine Bewegung — eine
       Zahl, die sich bewegt, ist unlesbar. */
    .flash-up {
        color: var(--signal-up);
        transition: color var(--dur-slow) var(--ease-out);
    }

    .flash-down {
        color: var(--signal-down);
        transition: color var(--dur-slow) var(--ease-out);
    }

    /* ====================================================================
       17 · Listeneinträge der Shell — Kanal, Chatverlauf, Einstellungen
       Gleiche Bauweise wie ein Menüeintrag: Fläche, keine Linie, nie grün.
       ==================================================================== */

    .channel-item,
    .chat-item,
    .chat-history-item,
    .settings-nav-item,
    .settings-lang-option,
    .chart-indicator-item,
    .chart-linewidth-option {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        gap: var(--space-2);
        border-radius: var(--radius-control);
        color: var(--ink-secondary);
        font-size: var(--text-sm);
        cursor: pointer;
        transition:
            background-color var(--dur-instant) var(--ease-standard),
            color var(--dur-instant) var(--ease-standard);
    }

    @media (hover: hover) and (pointer: fine) {
        :is(.channel-item, .chat-item, .chat-history-item, .settings-nav-item, .settings-lang-option, .chart-indicator-item, .chart-linewidth-option):hover {
            background: var(--surface-hover);
            color: var(--ink-primary);
        }
    }

    :is(.channel-item, .chat-item, .chat-history-item, .settings-nav-item, .settings-lang-option, .chart-indicator-item, .chart-linewidth-option):is(
        .active, .is-active, .is-selected, [aria-current="page"], [aria-selected="true"]
    ) {
        background: var(--surface-selected);
        color: var(--ink-primary);
    }

    /* Segmentknopf und Verschiebeknopf im Diagramm-Werkzeug. */
    .chart-seg-btn,
    .chart-tool,
    .chart-indicator-item__move-btn {
        min-block-size: var(--row-h);
        border-radius: var(--radius-control);
        color: var(--ink-secondary);
    }

    .chart-seg-btn:is(.active, .is-active, [aria-pressed="true"]),
    .chart-tool:is(.active, .is-active, [aria-pressed="true"]) {
        background: var(--surface-selected);
        color: var(--ink-primary);
    }

    /* Farbfeld im Diagramm-Farbwähler. Die Farbe kommt per `--swatch` von
       aussen — hier steht nur die Form. */
    .chart-color-swatch {
        inline-size: var(--icon-xl);
        block-size: var(--icon-xl);
        border-radius: var(--radius-chip);
        background: var(--swatch, var(--surface-raised));
        cursor: pointer;
        transition: transform var(--dur-fast) var(--ease-out);
    }

    .chart-color-swatch:is(.is-selected, .active, [aria-pressed="true"]) {
        outline: 2px solid var(--line-focus);
        outline-offset: 2px;
    }

    .chart-color-swatch:active { transform: scale(0.97); }

    .chart-color-picker__section-label {
        color: var(--ink-muted);
        font-size: var(--text-xs);
        letter-spacing: normal;
        text-transform: none;
    }

    /* ====================================================================
       18 · Nachricht und Fliesstext
       Prosa nie in Mono, Zeile auf Lesebreite — STIL.md §4.
       ==================================================================== */

    .chat-message,
    .v2-prose,
    .v2-prose-body {
        color: var(--ink-primary);
        font-family: var(--font-ui);
        font-size: var(--text-base);
        font-weight: var(--font-weight-regular);
        line-height: var(--line-height-prose);
    }

    /* Die Lesebreite liegt NUR auf dem Primitiv. `.v2-prose` im Bestand
       bekommt sie in Gruppe 2 — STEP_00_FUNDAMENT.md §5 führt „Lesespalte
       von 900 px auf Lesebreite" ausdrücklich dort. Hier gesetzt hätte sie
       den Datenmodus von 852 auf 700 px verengt. */
    .ui-prose {
        max-inline-size: var(--measure-prose);
    }

    .chat-message:is([data-state="error"], .is-error) {
        color: var(--signal-down);
    }

    .chat-message:is([data-loading], [aria-busy="true"], .is-loading) {
        color: var(--ink-secondary);
    }

    /* Diagramm im Datenmodus: erbt die Linienfarben aus den Tokens. */
    .v2-chart-svg {
        color: var(--ink-secondary);
        overflow: visible;
    }

    /* ====================================================================
       19 · Icons
       Ein System: Phosphor regular. Strichstärke 1,5 bei 16 px kommt aus
       der Schriftart selbst; hier stehen nur Grösse und Ausrichtung.
       ==================================================================== */

    .ui-icon,
    [class^="ph-"],
    [class*=" ph-"] {
        flex: none;
        font-size: var(--icon-md);
        line-height: 1;
        vertical-align: middle;
    }

    .lucide-sm,
    .ui-icon--sm { font-size: var(--icon-sm); }

    .lucide-lg,
    .ui-icon--lg { font-size: var(--icon-lg); }

    .ui-icon--xl { font-size: var(--icon-xl); }

    /* ====================================================================
       20 · Sicht- und Zustandsklassen des Bestands
       Diese Namen werden von 281 `classList`-Stellen geschaltet. Sie
       bekommen hier eine Bedeutung, damit die Gruppen 1–4 sich darauf
       verlassen können — umbenannt wird nichts.
       ==================================================================== */

    /* Ein- und Ausblenden von Flächen. Die Bewegung ist ein Überblenden
       plus eine halbe Stufe Massstab — nie aus `scale(0)`. */
    .inbox-pane--entering,
    .inbox-pane--visible,
    .inbox-pane--exiting,
    .inbox-pane--exiting-left,
    .inbox-pane--hidden {
        transition:
            opacity var(--dur-base) var(--ease-out),
            transform var(--dur-base) var(--ease-out);
    }

    .inbox-pane--entering { opacity: 0; transform: translateX(8%); }
    .inbox-pane--visible  { opacity: 1; transform: none; }
    .inbox-pane--exiting  { opacity: 0; transform: translateX(8%); }
    .inbox-pane--exiting-left { opacity: 0; transform: translateX(-8%); }
    .inbox-pane--hidden   { opacity: 0; pointer-events: none; }

    .inbox-overlay--open { opacity: 1; }

    .inbox-back-btn--hidden {
        opacity: 0;
        pointer-events: none;
    }

    /* Sichtbarkeitsschalter. Sie setzen NUR Sichtbarkeit, keine Farbe und
       keine Fläche — sonst überschreiben sie das Aussehen des Elements,
       auf dem sie liegen. */
    .show,
    .visible,
    .is-visible { opacity: 1; }

    @media (prefers-reduced-motion: reduce) {
        .inbox-pane--entering,
        .inbox-pane--exiting,
        .inbox-pane--exiting-left {
            transform: none;
        }
    }
}

@layer components {

    /* ====================================================================
       Zuletzt: das `hidden`-Attribut wieder wirksam machen.

       Der Browser setzt fuer `[hidden]` ein `display: none` — aber nur in
       seiner eigenen Herkunftsebene. Jede Regel aus diesem Stylesheet
       schlaegt das. Gemessen an `#inbox-badge`: die Abzeichen-Regel oben
       (`[class*="-badge"] { display: inline-flex }`) hat das Attribut
       ausgehebelt, das leere Abzeichen stand als roter Punkt an der
       Glocke, obwohl nichts ungelesen war.

       Diese Regel steht bewusst am ENDE der Schicht: gleiche
       Spezifitaet wie die Primitive darueber, spaetere Position — also
       gewinnt sie gegen jede von ihnen, ohne `!important`.

       `hidden="until-found"` ist ausgenommen: dieser Wert soll gefunden
       und aufgeklappt werden koennen, er ist kein Verstecken. */

    [hidden]:not([hidden="until-found"]) {
        display: none;
    }
}

/* ══════════════════════════════════════════════════════════════════════
   ICONS — der eigene Satz aus templates/partials/icons.svg
   ══════════════════════════════════════════════════════════════════════
   Ein Icon ist ein <svg class="icon"><use href="#i-..."></use></svg>.
   Groesse kommt aus einem Rollen-Token, Farbe aus `currentColor` — ein
   Icon hat nie eine eigene Farbe, es erbt die der Schrift daneben.

   Die Strichstaerke steht am aeusseren <svg>, nicht an den Pfaden:
   `stroke-width` wird vererbt, und so laesst sich der ganze Satz an einer
   Stelle aendern statt an achtzig.  STIL.md §5.1 */
@layer components {
    .icon {
        display: inline-block;
        flex: none;
        inline-size: var(--icon-nav);
        block-size: var(--icon-nav);
        stroke-width: var(--icon-stroke);
        vertical-align: -0.18em;
        /* Ein Icon hat nie eine eigene Farbe. `color: inherit` ist hier kein
           Selbstlaeufer: die Altschicht faerbt Icons pauschal grau, und ohne
           diese Zeile schlaege das durch, sobald ein Motiv in einem
           eingefaerbten Bereich steht. */
        color: inherit;
    }

    /* Zeitstempel, Hinweise, Kleinkram im Fliesstext. */
    .icon--inline { inline-size: var(--icon-inline); block-size: var(--icon-inline); }

    /* Nur wo etwas wirklich gross ist — leere Zustaende, Dialogkoepfe. */
    .icon--feature { inline-size: var(--icon-feature); block-size: var(--icon-feature); }

    /* Kleine Motive brauchen etwas mehr Strich, sonst verschwinden sie;
       grosse etwas weniger, sonst wirken sie fett. Der Unterschied ist
       bewusst klein — es bleibt ein Satz, keine zwei. */
    .icon--inline  { stroke-width: calc(var(--icon-stroke) + 0.25); }
    .icon--feature { stroke-width: calc(var(--icon-stroke) - 0.15); }
}

/* ── Statuspunkt der AI-Modelle ──────────────────────────────────────
   Zustandssignal fuer Smart Routing: rot offline, orange langsam,
   gruen laeuft. Die drei Signalfarben sind hier ausdruecklich Zustand
   und keine Zier (R2). */
@layer components {
    .model-dot {
        inline-size: 8px;
        block-size: 8px;
        border-radius: 50%;
        background: var(--ink-muted);
        flex: none;
    }

    .model-dot--ok { background: var(--signal-up); }
    .model-dot--slow { background: var(--signal-warn); }
    .model-dot--down { background: var(--signal-down); }
    .model-dot--unknown { background: var(--ink-muted); }
}
