/* base.css — Farbmarken, Grundtypografie und das Seitenskelett.
 *
 * Das CSS ist nach Bereichen aufgeteilt statt in einer wachsenden app.css;
 * diese Datei traegt nur, was jede Seite braucht. Karten, Kacheln, Tabellen,
 * Formularzeilen und Diagramme bekommen eigene Dateien, sobald es sie gibt.
 *
 * Namensraum: Variablen beginnen mit --app- , Klassen mit .app- .
 * (Bewusst mit Leerzeichen geschrieben: die Zeichenfolge Stern-Schrägstrich
 * schlösse diesen Kommentar mitten im Satz, und der komplette folgende
 * :root-Block fiele lautlos aus — die Datei bliebe dabei gültig.)
 *
 * Pfade in url() sind relativ zu DIESER Datei, also zu assets/css/ —
 * Schriften liegen unter ../fonts/ , nicht unter assets/fonts/ .
 */

:root {
    /* Temperatur-Farbskala. Gilt projektweit identisch und wird sowohl
     * serverseitig als auch im JavaScript benutzt; das JS liest die Werte
     * ueber getComputedStyle aus genau diesen Variablen, damit sie am
     * Hell/Dunkel-Umschalter teilnehmen. Eine neue Farbe gehoert deshalb
     * immer auch hierher. */
    --app-frost: #7a4fd0;   /* unter 5 °C */
    --app-cold:  #4aa8ff;   /* 5 bis 16 °C */
    --app-good:  #3ecf8e;   /* 16 bis 23 °C */
    --app-warm:  #ffb454;   /* 23 bis 27 °C */
    --app-hot:   #ff6b6b;   /* ab 27 °C */

    /* Luftfeuchte: die drei mittleren Baender nutzen die Temperaturfarben
     * wieder, nur die Extreme sind eigene Toene. */
    --app-hum:        #4aa8ff;
    --app-hum-vdry:   #e08e4a;
    --app-hum-vhumid: #2f6fd0;

    /* Luftqualitaet, angelehnt an die offizielle AQI-Farbfamilie. */
    --app-aqi-yellow: #ffd54a;
    --app-aqi-purple: #9b5de5;
    --app-aqi-maroon: #8c2f39;

    /* CO2 als Diagrammlinie (NEUBAU_PLAN 6.3): derselbe Ton wie
     * --app-aqi-purple, eigener Name, weil die Bedeutung hier eine andere ist
     * (Innenraum-Messwert statt Aussen-Index) — dieselbe Wiederverwendung wie
     * bei der Luftfeuchte-Skala (CLAUDE.md 10.3). */
    --app-co2: #9b5de5;

    /* Batteriehinweis an der Messpunkt-Kurzkarte. Der einzige neue Farbton
     * dieser Reihe, und zwar aus einem messbaren Grund: der Hinweis kann mit
     * **jedem** der drei anderen zusammen auf einer Karte stehen (er sagt
     * etwas ueber das Geraet, nicht ueber die Luft), waehrend jene sich
     * teilweise ausschliessen. CO2-Violett, Frost-Violett und Feuchte-Blau
     * sind alle kalt — ein Ocker liegt als einziger Ton ausserhalb dieser
     * Familie und ist auch bei 0,72 rem Schriftgroesse noch als anderer
     * Hinweis zu erkennen. Nachgemessen gegen den weissen Text der Plakette:
     * 5,0:1, damit im Band der drei vorhandenen (4,1 bis 5,5:1). Ein
     * dunkleres Rot haette 8:1 gegen den Text, faellt dafuer aber auf der
     * dunklen Karte zusammen (2,1:1 statt 3,5:1) — die Plakette waere dort
     * kaum noch als Flaeche zu sehen. */
    /* Die Mondscheibe (Spezifikation 9k) — gebraucht von der Kachel „Mond" auf
     * dem Dashboard (`weather.css`) und von der Seite „Sonne & Mond"
     * (`himmel.css`). Sie stehen deshalb hier und nicht in einer der beiden:
     * dieselbe Scheibe muss an beiden Stellen gleich aussehen, und zwei
     * Definitionen derselben Farbe driften auseinander.
     *
     * **Kein reines Weiss fuer die beleuchtete Seite.** Auf hellem Grund
     * verschwaende sie, und der Mond ist auch am Himmel nie weiss. */
    --app-moon-lit:  #f4e3b8;
    --app-moon-dark: #cbd3e2;

    --app-battery: #a85c0e;

    /* Durchschnittslinie im Langzeitverlauf. Bewusst NICHT theme-abhaengig:
     * derselbe Grauton bleibt auf hellem und dunklem Kartengrund lesbar. */
    --app-avgline: #8b93a1;

    /* Wetter-Icon-Farbe: fest und temperaturunabhaengig (CLAUDE.md 10.7),
     * damit sie nicht mit der Temperaturskala vermischt wird — die belegt
     * bereits Rot, Orange, Gelb und Blau (Frost/Kalt/Gut/Warm/Heiss). Terrakotta
     * und Pflaume liegen ausserhalb dieser Skala und ausserhalb von Blau, das im
     * Rest der Oberflaeche fuer Akzent, Kaelte und Luftfeuchte steht. Hell
     * braucht einen dunkleren, kraeftigeren Ton als Dunkel — Weiss waere auf der
     * hellen Karte unsichtbar. */
    --app-wx-day:   #d97757;
    --app-wx-night: #6b5a7a;

    /* Flaechen und Text.
     *
     * **Der Grund fuer den Blaustich im hellen Schema.** Bis 0.7.0 war der
     * Grund ein neutrales Grau (#eef1f6) und der Text ein Fast-Schwarz
     * (#16191f). Beides fuer sich ist korrekt, zusammen ergeben sie die
     * Anmutung einer noch nicht gestalteten Seite: der Unterschied zwischen
     * Grund und Karte ist dann reine **Helligkeit**, und Helligkeit allein
     * liest sich als „Weiss auf Grau" statt als Gestaltung. Der Grund traegt
     * deshalb jetzt denselben Blauton wie der Akzent, nur stark verduennt,
     * und der Text ist ein tiefes Marineblau statt Schwarz — die Seite
     * besteht damit aus **einer** Farbfamilie statt aus Grau plus einem
     * blauen Akzent, der nirgends sonst vorkommt.
     *
     * Die Karte bleibt reines Weiss. Sie ist der einzige Ton ohne Stich, und
     * genau daran erkennt man sie als Flaeche ueber dem Grund. */
    --app-bg:        #eaf0fb;
    --app-surface:   #ffffff;
    /* Die zweite Flaeche: Kartenkopf, Rail, alles, was auf einer Karte liegt
     * und sich trotzdem absetzen soll. Ohne sie bleibt nur „Rahmen ziehen",
     * und drei ineinander liegende Rahmen sehen nach Tabelle aus. */
    --app-surface-2: #f2f6fd;
    --app-border:    #dbe4f4;
    --app-text:      #1a2744;
    --app-text-mute: #5d6b95;
    --app-accent:    #2f6fd0;

    /* Die getoente Akzentflaeche: Symbolplaettchen im Kartenkopf, weiche
     * Hinterlegungen. Als eigener Wert und nicht als `color-mix` an jeder
     * Fundstelle, weil der dunkle Modus hier nicht dieselbe Rechnung braucht
     * — dort traegt dieselbe Rolle eine halbdurchsichtige Aufhellung. */
    --app-accent-soft: #e4edfc;

    /* Tiefe statt Linien. Zwei Lagen: eine harte Kante von einem Pixel und ein
     * weit gestreuter, sehr schwacher Schlagschatten — so heben sich Karten
     * vom Grund ab, ohne dass irgendwo ein grauer Kasten steht.
     *
     * Der Schatten traegt denselben Blauton wie der Grund. Ein neutralgrauer
     * Schatten auf einem getoenten Grund sieht schmutzig aus — er legt Grau
     * ueber Blau, und das ist die Farbe, die entsteht, wenn man zwei
     * Entscheidungen nicht zusammen getroffen hat. */
    --app-shadow:    0 1px 2px rgba(26, 39, 68, .04), 0 14px 32px -18px rgba(26, 39, 68, .30);
    --app-shadow-sm: 0 1px 2px rgba(26, 39, 68, .05);
    /* Die Kopfzeile liegt ueber dem scrollenden Inhalt und laesst ihn
     * durchscheinen. */
    --app-veil:      rgba(255, 255, 255, .72);

    /* Der Lichtschein am oberen Seitenrand — die einzige Flaeche der ganzen
     * Anwendung, die keine Auskunft traegt. Sie steht hier, weil ein
     * Wetter-Dashboard unter einem Himmel sitzt und nicht auf einem Blatt
     * Papier: ohne sie endet der Grund oben an der Kopfzeile, als waere die
     * Seite abgeschnitten. Sie scrollt bewusst mit weg (kein
     * `background-attachment: fixed`), damit sie oben zum Seitenanfang
     * gehoert und nicht als Schleier ueber allem liegt. */
    --app-glow: radial-gradient(130% 78% at 50% -18%, rgba(47, 111, 208, .16), transparent 64%);

    /* Der Winkel im Auswahlfeld. Die Farbe ist die von `--app-text-mute`,
     * hier aber URL-kodiert eingebacken — mehr dazu bei `.app-field select`.
     * Wer den gedaempften Ton aendert, aendert diese Zeile mit. */
    --app-select-arrow: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2016%2016%27%20fill%3D%27none%27%20stroke%3D%27%235d6b95%27%20stroke-width%3D%271.8%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%27M4%206.5%208%2010.5%2012%206.5%27%2F%3E%3C%2Fsvg%3E");

    color-scheme: light;

    /* Zwei Radien, und der groessere ist neu: Karten und Kacheln tragen ihn,
     * Knoepfe und Felder den kleinen. Vorher lagen beide auf 14px, und eine
     * Kachel sah damit aus wie ein grosses Eingabefeld. */
    --app-radius-lg: 20px;
    --app-radius:    14px;
    --app-radius-sm: 9px;
    --app-gap:       1.25rem;

    /* Masse des Rahmens (CLAUDE.md 9c). Als Variablen, weil drei Stellen sie
     * brauchen: die Kopfzeile ihre Hoehe, die Leiste ihren Abstand von oben,
     * und die ausgefahrene Leiste auf dem Telefon ihre Breite. */
    --app-topbar-h: 3.5rem;
    --app-nav-w:    15rem;
    --app-nav-rail: 4.25rem;

    --app-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
}

/* --- Farbschema: drei Zustaende, nicht zwei -------------------------------
 *
 * „automatisch" ist der Regelfall und der einzige, in dem `data-theme` fehlt:
 * dann entscheidet das Geraet. Sobald jemand den Schalter in der Kopfzeile
 * benutzt, steht der Wert in der Sitzung (`lib/ansicht.php`) und ist schon beim
 * Rendern des `<html>`-Tags da — die Seite kommt fertig im richtigen Schema an,
 * statt nach dem Laden umzuspringen.
 *
 * Daraus folgt die Form: die dunklen Werte stehen **zweimal**, einmal hinter
 * der `@media`-Regel (mit `:not([data-theme="hell"])`, damit ein ausdrueckliches
 * Hell auf einem dunklen Geraet gewinnt) und einmal hinter dem Attribut. Das
 * laesst sich in CSS nicht zusammenfassen, und zwei Bloecke, die
 * auseinanderlaufen koennen, sind genau die Sorte Fehler, die dieses Projekt
 * mechanisch prueft: `tests/test_ansicht.php` haelt beide Bloecke gegeneinander
 * — in **jeder** Stylesheet-Datei, denn eine Datei, die nur die `@media`-Haelfte
 * mitbringt, ergaebe eine halb umgeschaltete Seite. */

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="hell"]) {
        --app-bg:        #0e1117;
        --app-surface:   #171b22;
        --app-surface-2: #1e232c;
        --app-border:    #2a303a;
        --app-moon-lit:  #f2dfae;
        --app-moon-dark: #2a303c;
        --app-text:      #e8eaee;
        --app-text-mute: #98a2b3;
        --app-accent:    #6ea8ff;

        --app-accent-soft: rgba(110, 168, 255, .15);

        --app-shadow:    0 1px 2px rgba(0, 0, 0, .3), 0 12px 28px -14px rgba(0, 0, 0, .7);
        --app-shadow-sm: 0 1px 2px rgba(0, 0, 0, .35);
        --app-veil:      rgba(23, 27, 34, .74);
        --app-glow:      radial-gradient(130% 78% at 50% -18%, rgba(110, 168, 255, .10), transparent 64%);
        --app-select-arrow: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2016%2016%27%20fill%3D%27none%27%20stroke%3D%27%2398a2b3%27%20stroke-width%3D%271.8%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%27M4%206.5%208%2010.5%2012%206.5%27%2F%3E%3C%2Fsvg%3E");

        color-scheme: dark;

        --app-wx-day:   #ffffff;
        --app-wx-night: #7c8fc9;
    }
}

:root[data-theme="dunkel"] {
    --app-bg:        #0e1117;
    --app-surface:   #171b22;
    --app-surface-2: #1e232c;
    --app-border:    #2a303a;
    --app-moon-lit:  #f2dfae;
    --app-moon-dark: #2a303c;
    --app-text:      #e8eaee;
    --app-text-mute: #98a2b3;
    --app-accent:    #6ea8ff;

    --app-accent-soft: rgba(110, 168, 255, .15);

    --app-shadow:    0 1px 2px rgba(0, 0, 0, .3), 0 12px 28px -14px rgba(0, 0, 0, .7);
    --app-shadow-sm: 0 1px 2px rgba(0, 0, 0, .35);
    --app-veil:      rgba(23, 27, 34, .74);
    --app-glow:      radial-gradient(130% 78% at 50% -18%, rgba(110, 168, 255, .10), transparent 64%);
    --app-select-arrow: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2016%2016%27%20fill%3D%27none%27%20stroke%3D%27%2398a2b3%27%20stroke-width%3D%271.8%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%27M4%206.5%208%2010.5%2012%206.5%27%2F%3E%3C%2Fsvg%3E");

    color-scheme: dark;

    --app-wx-day:   #ffffff;
    --app-wx-night: #7c8fc9;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    margin: 0;
    min-height: 100dvh;
    font: 16px/1.55 var(--app-font);
    /* Grundton, darueber der Lichtschein. `no-repeat` und eine feste Hoehe:
     * der Schein gehoert zum Seitenanfang und soll sich nicht ueber eine
     * lange Seite wiederholen. */
    background: var(--app-glow) no-repeat top center / 100% 46rem, var(--app-bg);
    color: var(--app-text);
}

h1 {
    margin: 0 0 .5rem;
    font-size: 1.6rem;
    line-height: 1.2;
}

p {
    margin: 0 0 .75rem;
}

a {
    color: var(--app-accent);
}

/* --- Nur fuer Bildschirmleser --------------------------------------------
 * Die Knoepfe der Kopfzeile tragen nur ein Symbol. `display:none` waere hier
 * falsch: der Text soll vorgelesen werden, nur nicht zu sehen sein. */

.app-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* --- Seitenskelett (CLAUDE.md 9c) -----------------------------------------
 *
 * Drei Baenke: die Kopfzeile ueber die volle Breite, darunter Leiste links und
 * Inhalt rechts. Die dritte *Spalte* — Messpunkt-Kurzkarten — ist kein
 * Rahmenteil, sondern das Klassenpaar .app-columns weiter unten, das eine Seite
 * bei Bedarf innerhalb von .app-main selbst aufspannt. */

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
}

/* --- Kopfzeile ------------------------------------------------------------ */

.app-topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: .5rem;
    height: var(--app-topbar-h);
    padding: 0 .9rem;
    background: var(--app-veil);
    /* Der Inhalt scrollt darunter durch und bleibt als weiche Ahnung sichtbar.
     * Wo es das nicht gibt, traegt `--app-veil` genug Deckkraft, dass die Zeile
     * trotzdem lesbar bleibt — deshalb .72 und nicht .5.
     *
     * `saturate` neben `blur`: ein reiner Weichzeichner zieht die Farbe unter
     * der Scheibe aus, und die Kopfzeile sitzt genau ueber dem Lichtschein.
     * Ohne die Saettigung waere sie der einzige entfaerbte Streifen der
     * Seite. */
    backdrop-filter: blur(14px) saturate(1.4);
    border-bottom: 1px solid var(--app-border);
}

.app-brand {
    font-size: 1.02rem;
    font-weight: 650;
    letter-spacing: -.01em;
    color: var(--app-text);
    text-decoration: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Die Version hinter der Marke. `flex-shrink: 0`, weil die Marke daneben
 * schrumpfen darf und diese Zahl nicht: eine abgeschnittene Versionsnummer
 * („0.5…") ist schlechter als keine.
 *
 * Die Ausblendgrenze ist **nachgemessen, nicht gegriffen**: auf der
 * iPhone-Referenzbreite (393px) passt das Abzeichen angemeldet neben Marke,
 * Kontonamen und Abmelden-Knopf, ohne dass etwas gekuerzt wird — bei 360px
 * kuerzt sich die Marke. Die Grenze liegt deshalb dazwischen und nicht bei den
 * 26rem, die ich zuerst geschaetzt hatte: die haetten das Abzeichen
 * ausgerechnet auf dem Telefon verschwinden lassen, fuer das es passt. */
.app-brand-version {
    flex-shrink: 0;
    padding: .1rem .4rem;
    border: 1px solid var(--app-border);
    border-radius: var(--app-radius-sm);
    background: var(--app-surface-2);
    color: var(--app-text-mute);
    font-size: .7rem;
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
    white-space: nowrap;
}

@media (max-width: 23rem) {
    .app-brand-version { display: none; }
}

/* Schiebt alles Weitere nach rechts — ohne feste Breite fuer die Marke, die
 * damit auf einem schmalen Telefon so viel bekommt, wie uebrig ist. */
.app-topbar-actions {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin-left: auto;
}

.app-topbar-user {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin: 0;
}

/* Hier oben ist Platz: der Name steht neben der Marke und nicht in einer
 * 4,5rem breiten Spalte, in der „Patrick" als „Patr…" endete. Die Grenze
 * bleibt trotzdem — ein sehr langer Anzeigename soll die Zeile nicht
 * auseinanderdruecken. */
.app-topbar-name {
    max-width: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--app-text-mute);
    font-size: .9rem;
}

.app-topbar-login {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .35rem .8rem;
    border-radius: 999px;
    background: var(--app-accent);
    color: #fff;
    font-size: .9rem;
    text-decoration: none;
}

/* Der wiederkehrende quadratische Knopf: Symbol, keine Beschriftung. */
.app-iconbutton {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 0;
    border-radius: var(--app-radius-sm);
    background: transparent;
    color: var(--app-text-mute);
    font: inherit;
    cursor: pointer;
}

/* Trefferflaeche auf die HIG-Mindestgroesse (44px) erweitern, ohne den
 * sichtbaren Knopf zu vergroessern. Ein tatsaechlich groesserer Knopf kuerzt
 * in der Kopfzeile die Marke — bei 393px angemeldet fehlten 3 Knoepfe x 8px
 * = 24px (nachgemessen, Sichtung 2026-08-24), genau die Breite, fuer die
 * `.app-brand-version` oben als „passt ohne Kuerzung" belegt ist. Die
 * unsichtbare Flaeche liegt ausserhalb des Layoutflusses (`position:
 * absolute`), zaehlt also nicht zur Breite, die Flex den Geschwistern laesst. */
.app-iconbutton::after {
    content: "";
    position: absolute;
    inset: -.25rem;
}

.app-iconbutton:hover {
    background: var(--app-surface-2);
    color: var(--app-text);
}

/* Der Farbschalter: drei Knoepfe, von denen immer genau einer sichtbar ist —
 * einer je Zustand (automatisch, hell, dunkel).
 *
 * **Der sichtbare zeigt den aktuellen Zustand und schaltet zyklisch weiter.**
 * Bis 0.6.0 waren es zwei, und der sichtbare trug den Zielwert; das brauchte
 * die `@media`-Regel als Entscheider, weil der Server bei „automatisch" nicht
 * weiss, was das Geraet anzeigt — und es machte „automatisch" nach dem ersten
 * Klick unerreichbar. Jetzt entscheidet allein, ob und welches `data-theme`
 * am `<html>`-Element steht, also ein Wert, den der Server selbst gesetzt hat.
 * Die Systemabfrage entfaellt damit an dieser Stelle vollstaendig. */

.app-theme-btn {
    display: none;
}

:root:not([data-theme]) .app-theme-btn--auto,
:root[data-theme="hell"] .app-theme-btn--hell,
:root[data-theme="dunkel"] .app-theme-btn--dunkel {
    display: inline-flex;
}

/* --- Leiste und Inhalt ---------------------------------------------------- */

.app-body {
    display: flex;
    align-items: stretch;
    flex: 1;
    min-height: 0;
}

.app-nav {
    position: sticky;
    top: var(--app-topbar-h);
    align-self: flex-start;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    width: var(--app-nav-w);
    height: calc(100dvh - var(--app-topbar-h));
    /* Der letzte Punkt (Einstellungen, `.app-nav-list--fuss`) sitzt sonst
     * auf dem iPhone genau unter der Home-Indicator-Leiste — `env()` ist 0
     * auf jedem Geraet ohne solchen Rand, also keine Aenderung anderswo. */
    padding: .9rem .6rem calc(.9rem + env(safe-area-inset-bottom));
    /* Dieselbe Scheibe wie die Kopfzeile — beide gehoeren zum Rahmen und
     * sollen sich vom Inhalt gleich absetzen. Der Lichtschein oben scheint
     * dadurch auch hinter der Leiste durch, statt an ihrer Kante zu enden.
     *
     * In der Schublade auf dem Telefon liegt sie ueber dem Inhalt; dort ist
     * `.72` zu wenig, weshalb der Umbruch bei 50rem sie wieder deckend
     * setzt. */
    background: var(--app-veil);
    backdrop-filter: blur(14px) saturate(1.4);
    border-right: 1px solid var(--app-border);
    overflow-x: hidden;
    overflow-y: auto;
    transition: width .18s ease;
}

.app-nav-list {
    display: flex;
    flex-direction: column;
    gap: .15rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Die Einstellungen ans untere Ende, unabhaengig davon, wie viele Punkte
 * darueber stehen (CLAUDE.md 9c: `fuss` in `layout_nav_items()`). */
.app-nav-list--fuss {
    margin-top: auto;
    padding-top: .6rem;
    border-top: 1px solid var(--app-border);
}

.app-nav-item {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: var(--app-nav-just, flex-start);
    gap: .7rem;
    padding: .58rem .7rem;
    /* 44px Trefferflaeche (HIG) — der Inhalt (20px-Symbol + Zeile) bleibt
     * durch `align-items: center` in der Mitte, auch wenn er selbst
     * schmaler ist. */
    min-height: 2.75rem;
    border-radius: var(--app-radius);
    color: var(--app-text-mute);
    text-decoration: none;
    white-space: nowrap;
    transition: background-color .15s ease, color .15s ease;
}

.app-nav-item span {
    display: var(--app-nav-label, inline);
}

.app-nav-item:hover {
    background: var(--app-surface-2);
    color: var(--app-text);
}

/* Der aktive Punkt ist **gefuellt**, nicht getoent.
 *
 * Bis 0.7.0 war er eine 12%-Toenung mit farbigem Text und einem 3px-Balken
 * links; der Balken musste dabei sein, weil die Toenung im eingefahrenen
 * Zustand — wo nur das Symbol bleibt — zu schwach war, um „hier bist du" zu
 * sagen. Eine gefuellte Flaeche sagt es von selbst, auf jeder Breite, und der
 * Balken wird damit zum zweiten Zeichen fuer dieselbe Auskunft. Also weg
 * damit. */
.app-nav-item.is-active {
    background: var(--app-accent);
    color: #fff;
    font-weight: 550;
    box-shadow: 0 6px 16px -8px var(--app-accent);
}

.app-nav-item.is-active:hover {
    background: var(--app-accent);
    color: #fff;
}

/*
 * Drei Breitenbereiche, und sie ueberschneiden sich bewusst nicht — sonst
 * kaeme es auf die Reihenfolge im Stylesheet an, und das ist die Sorte
 * Abhaengigkeit, die man beim naechsten Einfuegen nicht mehr sieht:
 *
 *   bis 50rem      die Leiste liegt **ueber** dem Inhalt und ist zu, bis
 *                  jemand sie oeffnet (unten)
 *   50 bis 68,75rem   in der Spur, eingefahren — es sei denn, jemand faehrt
 *                  sie ausdruecklich aus
 *   ab 68,75rem    in der Spur, ausgefahren — es sei denn, jemand faehrt sie
 *                  ausdruecklich ein
 *
 * Eingefahren heisst: Symbolbreite, keine Beschriftung, zentriert. Die drei
 * Werte stehen als Variablen am `.app-shell`, weil `display` und
 * `justify-content` sich nicht anders aus einer Bedingung heraus setzen
 * lassen — und weil dieselben drei Zeilen sonst zweimal als Regelblock
 * daestuenden.
 */
@media (min-width: 50.0625rem) and (max-width: 68.75rem) {
    .app-shell:not([data-nav="auf"]) {
        --app-nav-w:     var(--app-nav-rail);
        --app-nav-label: none;
        --app-nav-just:  center;
    }
}

@media (min-width: 50.0625rem) {
    .app-shell[data-nav="zu"] {
        --app-nav-w:     var(--app-nav-rail);
        --app-nav-label: none;
        --app-nav-just:  center;
    }
}

/*
 * Auf dem Telefon nimmt eine Leiste in der Spur zu viel weg — 4,25rem sind auf
 * 393px ein Zehntel der Breite, dauerhaft und fuer drei Symbole. Sie liegt
 * deshalb **ueber** dem Inhalt und ist zu, bis jemand sie oeffnet; der Knopf
 * dafuer steht ohnehin schon oben links.
 */
/*
 * Welcher der beiden Menue-Knoepfe sichtbar ist, entscheidet **nicht** die
 * Frage „Schublade oder Spur" (50rem), sondern die Frage „steht die Leiste
 * ohne Zutun offen oder zu" — und die kippt bei 68,75rem. Das ist der
 * Unterschied, der beim ersten Anlauf danebenlag: auf dem iPad liegt die
 * Leiste in der Spur, ist aber trotzdem eingefahren, und der „breite" Knopf
 * bot dort das Einfahren einer bereits eingefahrenen Leiste an. Ein Tippen
 * ohne jede sichtbare Wirkung — im Testlauf sieht die Zeile richtig aus,
 * gefunden hat es erst die Sichtung.
 */
@media (max-width: 68.75rem) {
    .app-topbar-burger--breit {
        display: none;
    }
}

@media (min-width: 68.8125rem) {
    .app-topbar-burger--schmal {
        display: none;
    }
}

@media (max-width: 50rem) {
    .app-nav {
        position: fixed;
        left: 0;
        top: var(--app-topbar-h);
        z-index: 20;
        transform: translateX(-100%);
        /* Ueber dem Inhalt statt darin: hier muss sie decken. Eine
         * halbdurchsichtige Schublade laesst die Karten darunter durch die
         * Beschriftung scheinen. */
        background: var(--app-surface);
        box-shadow: var(--app-shadow);
        transition: transform .2s ease;
    }

    .app-shell[data-nav="auf"] .app-nav {
        transform: none;
    }

    /* Die Flaeche daneben. Sie ist ein Knopf (CLAUDE.md 9c) und traegt deshalb
     * die ueblichen Ruecksetzungen. */
    .app-nav-backdrop {
        display: none;
        position: fixed;
        inset: var(--app-topbar-h) 0 0 0;
        z-index: 10;
        padding: 0;
        border: 0;
        background: rgba(8, 11, 16, .45);
        cursor: pointer;
    }

    .app-shell[data-nav="auf"] .app-nav-backdrop {
        display: block;
    }
}

/* Ueber 50rem liegt die Leiste in der Spur — dort gibt es nichts zu
 * verdecken und damit auch nichts wegzutippen. */
@media (min-width: 50.0625rem) {
    .app-nav-backdrop {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .app-nav,
    .app-nav-item,
    .app-card--point {
        transition: none;
    }
}

.app-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    /* Ohne dies sprengt ein breiter Inhalt (z. B. ein Diagramm) die Spalte,
     * statt sich ihr zu fuegen — derselbe Grund wie bei .app-chart. */
    min-width: 0;
}

.app-main {
    flex: 1;
    width: 100%;
    padding: var(--app-gap);
}

/* --- Tastaturbedienung ---------------------------------------------------
 * Ein sichtbarer Fokusring an allem, was man anfassen kann. `:focus-visible`
 * statt `:focus`: nach einem Mausklick soll er nicht stehen bleiben. */

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
    outline: 2px solid var(--app-accent);
    outline-offset: 2px;
    border-radius: var(--app-radius-sm);
}

/* --- Zwei-Spalten-Bereich innerhalb von .app-main --------------------------
 * Das wiederverwendbare Vokabular fuer die dritte Spalte (Messpunkt-
 * Kurzkarten rechts, NEUBAU_PLAN 6.1). Nur Seiten, die es brauchen, oeffnen
 * es — die Anmeldeseite etwa bleibt einspaltig. */

.app-columns {
    display: flex;
    align-items: flex-start;
    gap: var(--app-gap);
}

.app-col-main {
    flex: 1;
    min-width: 0;
}

.app-col-aside {
    flex: 0 0 20rem;
    display: flex;
    flex-direction: column;
    gap: .75rem;
}

/* Unter ~800px wandert die rechte Spalte unter den Inhalt (NEUBAU_PLAN 6.1).
 * Die Quellreihenfolge im HTML (main vor aside) sorgt dafuer von selbst. */
@media (max-width: 50rem) {
    .app-columns {
        flex-direction: column;
    }

    .app-col-aside {
        flex-basis: auto;
        width: 100%;
    }
}

/* --- Icons (lib/icons.php, NEUBAU_PLAN 6.6) -------------------------------
 * Font Awesome Free, eingebettet als SVG-Pfad statt als Webfont — die Regel
 * dazu steht im Dateikopf von lib/icons.php. `vertical-align` verhindert den
 * kleinen Versatz, den ein Inline-SVG sonst neben Text hat (es sitzt sonst
 * auf der Grundlinie statt zentriert). */

.app-icon {
    display: inline-block;
    vertical-align: -0.15em;
    flex-shrink: 0;
}

/* --- Karte ---------------------------------------------------------------- */

.app-card {
    max-width: 42rem;
    padding: 1.5rem;
    background: var(--app-surface);
    border: 1px solid var(--app-border);
    border-radius: var(--app-radius-lg);
    /* Der Schatten ist das, was eine Karte zur Karte macht. Nur ein Rahmen
     * ergibt einen Kasten auf einer Flaeche, und mehrere davon nebeneinander
     * ergeben ein Formular. */
    box-shadow: var(--app-shadow);
}

/* Ein Panel im Dashboard (Diagramm, Prognose): dieselbe Karte, aber ohne die
 * Lesbarkeitsgrenze — sie soll die Hauptspalte ausfuellen, nicht wie ein
 * Formular in der Mitte des Bildschirms stehen. */
.app-card--wide {
    max-width: none;
}

/* Messpunkt-Kurzkarte (NEUBAU_PLAN 6.1), rechte Spalte: dieselbe Karte, enger
 * und anklickbar — sie waehlt den im Hauptbereich gezeigten Messpunkt.
 *
 * `position: relative` traegt den Klappknopf: er liegt als Geschwister ueber
 * dem Auswahlknopf statt in ihm, weil ein `<button>` im `<button>` ungueltig
 * ist (Begruendung im Kopf von buildCard(), assets/js/dashboard.js). */
.app-card--point {
    position: relative;
    padding: .85rem 1rem;
    box-shadow: var(--app-shadow-sm);
    transition: border-color .15s ease, box-shadow .15s ease;
}

.app-card--point:hover {
    border-color: color-mix(in srgb, var(--app-accent) 40%, var(--app-border));
    box-shadow: var(--app-shadow);
}

/* Die gewaehlte Karte traegt denselben Akzentrahmen wie zuvor, dazu einen
 * zweiten Ring nach aussen: ein einzelner 1px-Rahmen in Akzentfarbe ist auf
 * einer Spalte aus fuenf gleich aussehenden Karten leicht zu uebersehen. */
.app-card--point.is-active {
    border-color: var(--app-accent);
    box-shadow: 0 0 0 1px var(--app-accent), var(--app-shadow-sm);
}

.app-card--message {
    margin: 2rem auto;
    text-align: center;
}

.app-muted {
    color: var(--app-text-mute);
}

.app-errorcode {
    margin: 0;
    font-size: 3rem;
    font-weight: 700;
    line-height: 1;
    opacity: .2;
}

.app-button {
    display: inline-block;
    margin-top: .5rem;
    padding: .5rem 1.1rem;
    border: 0;
    border-radius: 999px;
    background: var(--app-accent);
    color: #fff;
    font: inherit;
    text-decoration: none;
    cursor: pointer;
}

/* Zurueckhaltende Variante fuer Nebenwege — etwa das Abmelden in der
 * Kopfzeile, das neben den Navigationspillen nicht um Aufmerksamkeit
 * konkurrieren soll. */
.app-button-quiet {
    margin-top: 0;
    padding: .35rem .8rem;
    /* `var(--app-text-mute)` statt `--app-border`: Letzteres ist auf die
     * Flaechentrennung von Karten ausgelegt (rund 1,3:1 im Dunkelmodus) und
     * war dort als Knopfrand kaum wahrnehmbar — und identisch mit dem Rand
     * der inaktiven `.app-settings-tab`, was den Knopf mit einem Reiter
     * verwechselbar machte. Die Textfarbe traegt beides zugleich: mehr
     * Kontrast (sie muss ohnehin lesbar sein) und eine eigene Kontur. */
    border: 1px solid var(--app-text-mute);
    background: transparent;
    color: var(--app-text-mute);
}

.app-button-quiet:hover {
    background: color-mix(in srgb, var(--app-accent) 14%, transparent);
    color: var(--app-accent);
}

/* --- Anmeldung und Formulare --------------------------------------------- */

/* Die Anmeldemaske traegt wenig und soll nicht ueber die halbe Breite eines
 * Monitors laufen. */
.app-card-narrow {
    max-width: 24rem;
    margin: 3rem auto;
}

.app-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin-top: 1.25rem;
}

/*
 * Der Knopf haelt sich an seine Beschriftung.
 *
 * `.app-form` ist eine Spalte, und ein Flex-Kind zieht sich darin von selbst
 * ueber die volle Breite. In der Anmeldemaske faellt das nicht auf — sie ist
 * 24rem breit, und dort ist ein durchgehender Knopf sogar das Uebliche. Auf
 * den Einstellungsseiten steht dasselbe Formular in einer Karte ueber die
 * ganze Fensterbreite, und aus „Speichern" wurde ein 1500px breiter blauer
 * Balken.
 *
 * Deshalb hier `flex-start` und in der schmalen Karte ausdruecklich zurueck
 * auf die volle Breite — die Ausnahme steht bei dem Fall, der sie braucht,
 * statt als Vorgabe fuer alle.
 */
.app-form > .app-button {
    align-self: flex-start;
    padding-inline: 1.6rem;
}

.app-card-narrow .app-form > .app-button {
    align-self: stretch;
}

.app-field {
    display: flex;
    flex-direction: column;
    gap: .35rem;
}

.app-field > span {
    color: var(--app-text-mute);
    font-size: .9rem;
}

/*
 * Eingabefeld und Auswahlfeld sehen gleich aus — das ist der Punkt der
 * gemeinsamen Regel und nicht Sparsamkeit. Bis 0.7.0 trug nur `input` eine
 * Gestaltung; ein `select` daneben blieb das rohe Bedienelement des Browsers,
 * mit eckigen Ecken und eigenem Grau. In *Mein Konto* standen beide
 * unmittelbar nebeneinander („Anzeigename" und „Sprache der Oberflaeche"),
 * und die Zeile sah aus wie zur Haelfte fertig geworden.
 */
.app-field input,
.app-field select {
    padding: .55rem .75rem;
    border: 1px solid var(--app-border);
    border-radius: var(--app-radius);
    background: var(--app-bg);
    color: var(--app-text);
    font: inherit;
    /* Kein Feld sprengt sein Kaestchen. Ein `select` ist von sich aus so
     * breit wie seine laengste Option, und das ist eine Laenge, die aus den
     * Daten kommt und nicht aus dem Entwurf. */
    max-width: 100%;
}

/*
 * Der eigene Pfeil. `appearance: none` nimmt dem Feld den des Browsers — ohne
 * Ersatz staende dann gar keiner mehr da, und das Feld waere von einem
 * Textfeld nicht mehr zu unterscheiden.
 *
 * Das Zeichen ist ein `data:`-SVG und keine Datei: die CSP erlaubt
 * `img-src 'self' data:` (lib/bootstrap.php), und ein einzelner Winkel ist
 * keinen zusaetzlichen Abruf wert.
 *
 * Es steht als Variable und nicht hier in der Regel, weil die Farbe **im SVG**
 * steht und nicht davor: `currentColor` gilt in einem als Bild geladenen SVG
 * nicht. Der dunkle Modus braucht deshalb eine eigene Fassung, und die kann
 * nur ueber die Variable kommen.
 */
.app-field select {
    appearance: none;
    padding-right: 2rem;
    background-image: var(--app-select-arrow);
    background-repeat: no-repeat;
    background-position: right .7rem center;
    background-size: 1rem;
    /* Abgeschnitten mit Auslassungspunkten statt hart abgeschnitten: ohne die
     * Zeile endet eine zu lange Option mitten im Wort und sieht aus wie ein
     * Datenfehler. */
    text-overflow: ellipsis;
}

.app-field input:focus-visible,
.app-field select:focus-visible {
    outline: 2px solid var(--app-accent);
    outline-offset: 1px;
}

.app-error {
    margin: 1rem 0 0;
    padding: .6rem .85rem;
    border-radius: var(--app-radius);
    background: color-mix(in srgb, var(--app-hot) 16%, transparent);
    color: var(--app-text);
}

.app-notice {
    margin: 1rem 0 0;
    padding: .6rem .85rem;
    border-radius: var(--app-radius);
    background: color-mix(in srgb, var(--app-good) 16%, transparent);
    color: var(--app-text);
}

/* Der zweite Anmeldeweg, abgesetzt vom Formular. */
.app-alt {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--app-border);
}

/* --- Listen mit Bedienelementen ------------------------------------------ */

.app-list {
    margin: 1.5rem 0 0;
    padding: 0;
    list-style: none;
}

/* „Noch nichts da" steht sonst direkt am Knopf darueber und liest sich wie
 * dessen Beschriftung. */
.app-empty {
    margin-top: 1.5rem;
}

.app-list-item + .app-list-item {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--app-border);
}

/* Eingabefeld und Knoepfe nebeneinander, aber umbruchfaehig: auf einem
 * schmalen Telefon steht sonst der letzte Knopf ausserhalb der Karte. */
.app-inline-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
}

.app-inline-form input[type="text"] {
    flex: 1 1 12rem;
    min-width: 0;
    padding: .45rem .7rem;
    border: 1px solid var(--app-border);
    border-radius: var(--app-radius);
    background: var(--app-bg);
    color: var(--app-text);
    font: inherit;
}

/* Der Geraetename der Push-Liste ist reiner Text, kein Eingabefeld (es gibt
 * kein Umbenennen) — braucht aber dieselbe Flex-Breite wie das Passkey-Label
 * daneben, sonst rutscht er auf eine eigene Zeile und der Knopf darunter
 * (Sichtung 2026-08-25: 21,8 px Versatz, weil der Name ausserhalb des
 * `.app-inline-form` stand). */
.app-list-item-name {
    flex: 1 1 12rem;
}

.app-list-meta {
    margin: .35rem 0 0;
    font-size: .85rem;
}

/* --- Datenliste (Bezeichnung / Wert) -------------------------------------- */

.app-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: .35rem 1.25rem;
    margin: 1.25rem 0 0;
}

.app-facts dt {
    color: var(--app-text-mute);
}

.app-facts dd {
    margin: 0;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 30rem) {
    .app-facts {
        grid-template-columns: 1fr;
        gap: 0 0;
    }

    .app-facts dd {
        margin-bottom: .6rem;
    }
}

/* --- Dashboard (NEUBAU_PLAN 6.1, 6.3) --------------------------------------
 * Aufgebaut von assets/js/dashboard.js aus /daten/aktuell und /daten/verlauf.
 * Serverseitig steht hier nur das Geruest (pages/start.php); Inhalt und
 * Aktualisierung sind Sache des Skripts — derselbe Schnitt wie bei der
 * Altfassung (CLAUDE.md 7: Wetter serverseitig, Raeume clientseitig) und aus
 * demselben Grund: ein 60-Sekunden-Poll darf keinen vollen Seiten-Reload
 * ausloesen. */

/* Der Auswahlknopf deckt die ganze Karte ab — die Karte **ist** der
 * Umschalter (CLAUDE.md 9d.1). Er traegt deshalb die Knopf-Ruecksetzungen,
 * die vor dem Klappknopf an der Karte selbst standen. */
.app-point-card-select {
    display: block;
    width: 100%;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

/* Rechts bleibt Platz fuer den Klappknopf: ohne ihn schoebe sich der
 * Messwert unter das Dreieck. */
.app-point-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .5rem;
    padding-right: 1.7rem;
}

/* Das Dreieck ist klein, die Trefferflaeche darf es nicht sein: der Knopf ist
 * 2,25rem gross und zentriert das Zeichen darin, statt sich auf dessen Groesse
 * zu legen — auf dem Telefon waere er sonst kaum zu treffen. */
.app-point-card-toggle {
    position: absolute;
    top: .3rem;
    right: .3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--app-radius);
    color: var(--app-text-mute);
    font: inherit;
    line-height: 1;
    cursor: pointer;
}

.app-point-card-toggle:hover {
    color: var(--app-text);
}

/* Zugeklappt bleiben Name, Messwert und die Schwellwert-Hinweise stehen —
 * verborgen werden nur die Meta-Zeile und die Sparkline. Das Diagramm misst
 * dabei die Groesse 0 und zeichnet nicht; der ResizeObserver in chart.js
 * meldet sich von selbst wieder, sobald die Karte aufgeht (CLAUDE.md 9a.1). */
.app-card--point.is-collapsed .app-point-card-body {
    display: none;
}

.app-point-card-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.app-point-card-value {
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
}

.app-point-card-meta {
    margin: .15rem 0 .5rem;
    color: var(--app-text-mute);
    font-size: .8rem;
}

/* --- Auswertungen (NEUBAU_PLAN 11) ----------------------------------------
 * Schwellwert-Hinweise an der Kurzkarte. Bewusst nur farbliche Markierung —
 * einen Benachrichtigungsweg gibt es ausdruecklich nicht. Die drei Toene sind
 * vorhandene Farbrollen und keine neuen: CO2 traegt seine eigene Farbe aus dem
 * Serien-Umschalter, „dauerhaft feucht" die Feuchtefarbe, Frost den
 * Frost-Ton der Temperaturskala (CLAUDE.md 10.1/10.3). */

.app-point-card-flags {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    /* Sie stehen jetzt direkt unter der Kopfzeile und brauchen deshalb einen
     * eigenen Abstand nach oben — vor der Klappmechanik lag die Meta-Zeile
     * dazwischen und hat ihn mitgebracht. */
    margin: .35rem 0 .5rem;
}

.app-point-card-flags:empty {
    margin: 0;
}

.app-flag {
    padding: .1rem .45rem;
    border-radius: 999px;
    font-size: .72rem;
    line-height: 1.5;
    /* Der Text sitzt auf einer kraeftigen Farbe — deshalb eine eigene
     * Vordergrundfarbe statt `currentColor`, sonst waere er im hellen Thema
     * dunkel auf dunkel. */
    color: #fff;
    background: var(--flag-color);
}

/* Frost traegt den Frost-Ton der Temperaturskala und liegt damit dicht am
 * Violett von CO2 — das sieht nach einem Versehen aus und ist keins. Die
 * beiden koennen **nie zusammen** auf einer Karte stehen: Frost gibt es nur am
 * Aussenmesspunkt (lib/climate.php), und ein Aussensensor hat keinen
 * CO2-Fuehler. Zusammen auftreten koennen dagegen Frost und „dauerhaft
 * feucht" — eine klamme Frostnacht draussen —, und genau dieses Paar ist mit
 * Violett gegen Blau deutlich getrennt. Frost stattdessen auf --app-cold zu
 * legen wuerde es neben die Feuchtefarbe schieben und damit das eine Paar
 * angleichen, auf das es ankommt. */
.app-flag--co2      { --flag-color: var(--app-co2); }
.app-flag--humidity { --flag-color: var(--app-hum-vhumid); }
.app-flag--frost    { --flag-color: var(--app-frost); }
.app-flag--battery  { --flag-color: var(--app-battery); }

/* Taupunkt, absolute Feuchte und Lueftungsempfehlung im Hauptbereich. */

.app-climate {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem 1rem;
    margin-top: .75rem;
}

.app-climate-advice {
    padding: .25rem .7rem;
    border: 1px solid var(--advice-color);
    border-left-width: 4px;
    border-radius: var(--app-radius);
    color: var(--app-text);
    font-size: .85rem;
}

/* „Trocknet" ist die gute Nachricht, „bringt Feuchte herein" die abratende,
 * „kaum" die neutrale — dieselben drei Rollen wie ueberall sonst. */
.app-climate-advice--dry   { --advice-color: var(--app-good); }
.app-climate-advice--humid { --advice-color: var(--app-warm); }
.app-climate-advice--equal { --advice-color: var(--app-border); }

.app-climate-values {
    display: flex;
    flex-wrap: wrap;
    gap: .15rem .75rem;
    color: var(--app-text-mute);
    font-size: .8rem;
    font-variant-numeric: tabular-nums;
}

/* Die Kennzeichnung „Berechnet" bzw. „Gemessen" (Ehrlichkeitsregel,
 * CLAUDE.md 1) steht als Bezeichner vor den Werten und nicht als Fussnote
 * darunter — eine Fussnote liest niemand, und die Unterscheidung
 * gemessen/berechnet ist der Punkt.
 *
 * **Eine Regel fuer beide Faelle, mit Absicht.** Die Herkunft eines Wertes
 * soll ueberall gleich aussehen; zwei getrennte Regeln liefen beim naechsten
 * Feinschliff auseinander, und dann sieht „Gemessen" auf der Startseite
 * anders aus als „Berechnet" im Raumverlauf — als waeren es zwei
 * verschiedene Arten von Hinweis. */
.app-climate-values b,
.app-wx-current-meta b {
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    font-size: .7rem;
    opacity: .75;
}

/* --- Zeitraum-Pillen, wiederverwendbar fuer jede Gruppe umschaltbarer
 * Knoepfe (Kurzzeitverlauf-Fenster, spaeter der Langzeitverlauf-Zoom). */
.app-pills {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.app-pills button {
    padding: .3rem .75rem;
    border: 1px solid var(--app-border);
    border-radius: 999px;
    background: transparent;
    color: var(--app-text-mute);
    font: inherit;
    font-size: .85rem;
    cursor: pointer;
}

.app-pills button[aria-pressed="true"] {
    background: var(--app-accent);
    border-color: var(--app-accent);
    color: #fff;
}

.app-dashboard-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: 1rem;
}

/* Mehrere Messpunkte im selben Raum (NEUBAU_PLAN 6.1): je Graph ein Grid-Feld
 * statt gedehnter Karten — dieselbe Regel wie bei .app-grid-metrics. Ein
 * einzelner Messpunkt fuellt die volle Breite, weil auto-fit die uebrigen,
 * ungenutzten Spuren kollabiert; damit sieht der Ein-Messpunkt-Fall aus wie
 * zuvor, ohne eigenen Zweig im Markup. */
.app-dashboard-points {
    display: grid;
    /* min(22rem, 100%) statt reinem 22rem: eine Spur, die ohne das nie unter
     * 352px faellt, sprengt auf einem 393px-Telefon die Seite waagerecht —
     * derselbe Fehler, den .app-chart mit min-width:0 fuer den Fall des
     * einzelnen Diagramms schon vermeidet, hier aber am Grid-Track selbst.
     * 22rem statt der besser lesbaren 26rem, damit auf der MacBook-Referenzbreite
     * (1512px, abzueglich Navigation, Kurzkarten-Spalte und Kartenpolsterung
     * bleiben rund 860px) tatsaechlich zwei Spalten nebeneinander stehen. */
    grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
    gap: 1.75rem 2rem;
}

/* Grid-Kinder sind ohne dies auto-breit wie ihr Inhalt (derselbe Grund wie bei
 * .app-col-main/.app-chart) — auf einem 393px-Telefon reisst sonst das
 * Diagramm die ganze Seite waagerecht auf, statt sich der Spalte zu fuegen. */
.app-dashboard-block {
    min-width: 0;
}

.app-dashboard-block-title {
    margin: 0 0 .5rem;
    font-size: 1rem;
}

.app-dashboard-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 1.25rem;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--app-border);
    font-size: .9rem;
}

.app-dashboard-stats b {
    display: block;
    color: var(--app-text-mute);
    font-size: .78rem;
    font-weight: 400;
}

/* Serien-Umschalter (NEUBAU_PLAN 6.3): die Legende ist zugleich der
 * Umschalter, kein zusaetzliches Bedienelement.
 *
 * Die drei Zeilen `margin`/`padding`/`list-style` sind der vollstaendige
 * Listen-Reset, den `.app-pills` von Anfang an hat und der hier fehlte — mit
 * zwei sichtbaren Folgen: der Umschalter stand um die 40px Browser-Vorgabe
 * eingerueckt neben allem anderen in seiner Karte, und in der
 * Vergleichsansicht trug jede Messpunkt-Zeile einen Aufzaehlungspunkt. Der
 * Punkt kommt daher, dass ein `<li>` als Flex-Kind seinen `display:
 * list-item` behaelt; dass er am Dashboard fehlte, liegt allein daran, dass
 * dessen Legende ihre Knoepfe ohne `<li>` einhaengt. */
.app-chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
    margin: 0 0 .6rem;
    padding: 0;
    list-style: none;
}

/* **Das `display: flex` oben schlaegt das `[hidden]` des Browser-Stylesheets.**
 * Dessen Regel ist nur `[hidden] { display: none }` und damit gleich
 * spezifisch, aber sie steht frueher — jede eigene `display`-Angabe an einem
 * Element gewinnt und macht `hidden` wirkungslos. Das Element bleibt dann im
 * Fluss stehen, mitsamt seinen Eintraegen, und niemand sieht dem `hidden` im
 * Markup an, dass es nichts tut.
 *
 * Aufgefallen an einer Legende, die aus dem Kachel-Dialog stehen blieb, als
 * danach ein Tagesdialog geoeffnet wurde (CLAUDE.md 9i.14): 30 px Luft unter
 * der Ueberschrift, aus dem Nichts. Es ist dieselbe Falle wie beim Fadenkreuz
 * in `chart.js`, wo `hidden` an einem SVG-Element gar keine Regel findet
 * (CLAUDE.md 9a.4a) — nur hier andersherum begruendet. */
.app-chart-legend[hidden] {
    display: none;
}

.app-chart-legend button {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .25rem .6rem;
    border: 1px solid var(--app-border);
    border-radius: 999px;
    background: transparent;
    color: var(--app-text-mute);
    font: inherit;
    font-size: .82rem;
    cursor: pointer;
}

.app-chart-legend button[aria-pressed="false"] {
    text-decoration: line-through;
    opacity: .6;
}

.app-chart-legend-dot {
    width: .6rem;
    height: .6rem;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--dot-color, currentColor);
}

/* Blaettern im Kurzzeitverlauf (NEUBAU_PLAN 6.6a) — „◀ Datum ▶" ueber der
 * Gruppe, das Fenster ist global. Dasselbe Knopfvokabular kehrt je
 * Langzeitverlauf-Block wieder (dort mit eigener Blaetter-Position). */
.app-dashboard-pager {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: .85rem;
    color: var(--app-text-mute);
}

.app-dashboard-pager button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.8rem;
    height: 1.8rem;
    padding: 0;
    border: 1px solid var(--app-border);
    border-radius: 999px;
    background: transparent;
    color: var(--app-text);
    font: inherit;
    cursor: pointer;
}

.app-dashboard-pager button:disabled {
    opacity: .35;
    cursor: default;
}

.app-dashboard-pager [data-pager-label],
.app-dashboard-pager [data-label] {
    min-width: 9rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.app-dashboard-long-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin: 2rem 0 1rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--app-border);
}

.app-dashboard-long-head h3 {
    margin: 0;
    font-size: 1rem;
}

/* Der Pager je Langzeitverlauf-Block sitzt unter dem Messpunktnamen statt
 * daneben — der Block ist schmaler als die ganze Kartenbreite (NEUBAU_PLAN
 * 6.1: mehrere Spalten auf breiten Monitoren), ein Nebeneinander wie beim
 * globalen Pager oben liefe dort schnell um. */
.app-dashboard-block .app-dashboard-pager {
    justify-content: center;
    margin-bottom: .5rem;
}

/* --- Verwaltung (NEUBAU_PLAN 6.5) ----------------------------------------- */

/* Die fuenf Einstellungsbereiche. Dieselbe Pillenoptik wie `.app-pills`, aber
 * als `<a>` statt `<button>` — es sind fuenf eigene Routen, kein
 * clientseitiger Umschalter (`lib/layout.php` `layout_settings_tabs()`). */
.app-settings-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
    margin: 0 0 1.5rem;
}

.app-settings-tab {
    padding: .35rem .85rem;
    border: 1px solid var(--app-border);
    border-radius: 999px;
    color: var(--app-text-mute);
    font-size: .9rem;
    text-decoration: none;
}

.app-settings-tab.is-active {
    background: var(--app-accent);
    border-color: var(--app-accent);
    color: #fff;
}

.app-settings-section + .app-settings-section {
    margin-top: 2rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--app-border);
}

/* `einstellungen_benutzer.php` hat vor der ersten Sektion ein eigenstaendiges
 * `<form>` (Tabelle mit Sammel-Speichern) statt eines Absatzes — dessen
 * `.app-button` traegt keinen unteren Abstand, und ohne diese Regel klebte
 * die folgende Ueberschrift direkt am Knopf. */
form + .app-settings-section {
    margin-top: 2rem;
}

.app-settings-section h2 {
    margin: 0 0 .25rem;
    font-size: 1.15rem;
}

/* Push-Mitteilungen (Paket 6c) haengen als eigener Absatz unterhalb der
 * Meldematrix — keine eigene `.app-settings-section`, weil das eine zweite
 * Ueberschriftenebene und `border-top` waere, wo tatsaechlich nur „noch ein
 * Absatz zur Benachrichtigung" gemeint ist. `<h3>` statt `<h2>` traegt das
 * bereits in der Textstruktur. */
.app-settings-subsection {
    margin-top: 1.75rem;
    padding-top: 1.25rem;
    border-top: 1px dashed var(--app-border);
}

.app-settings-subsection h3 {
    margin: 0 0 .25rem;
    font-size: 1rem;
}

/* Tabellen (Benutzer-, Geraete-, Reparatur- und Cron-Listen). Eigener
 * Scroll-Container statt eines schrumpfenden Layouts: auf einem schmalen
 * Telefon soll die Tabelle seitlich scrollen, nicht die ganze Seite
 * (CLAUDE.md 10.8 gilt sinngemaess auch hier, nicht nur fuer Diagramme). */
.app-table-wrap {
    margin-top: 1.25rem;
    overflow-x: auto;
}

.app-table {
    width: 100%;
    border-collapse: collapse;
    font-size: .92rem;
}

.app-table th,
.app-table td {
    padding: .5rem .6rem;
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--app-border);
}

.app-table th {
    color: var(--app-text-mute);
    font-weight: 600;
}

.app-table td.app-table-wrap-cell {
    white-space: normal;
}

.app-table input[type="text"],
.app-table input[type="number"],
.app-table select {
    width: 100%;
    min-width: 7rem;
    padding: .35rem .5rem;
    border: 1px solid var(--app-border);
    border-radius: calc(var(--app-radius) / 2);
    background: var(--app-bg);
    color: var(--app-text);
    font: inherit;
}

.app-table input[type="number"] {
    min-width: 4.5rem;
}

.app-table input[type="checkbox"] {
    width: 1.1rem;
    height: 1.1rem;
}

/* Die Stufenspalte der Meldematrix. Ohne die Begrenzung laeuft der
 * Auswahlkasten auf einem breiten Fenster ueber die halbe Karte — sichtbar
 * nur im Bild, in keiner Zahl. Auf schmalen Geraeten bleibt `width: 100%` aus
 * der Regel darueber wirksam, weil die Begrenzung ein Hoechstwert ist. */
.app-notify-level select {
    max-width: 16rem;
}

/* Zwei Kanaele je Zelle, untereinander statt in einer vierten Tabellenspalte
 * (Spezifikation 6c.8, Falle 7 — eine zusaetzliche Spalte passte bei 303 px
 * Platz nicht mehr neben die bestehenden drei). Jede Zeile traegt ihre
 * Kanalbezeichnung selbst, damit „Mail" und „Push" auch beim seitlichen
 * Wischen sichtbar bleiben und nicht nur im (weiterhin einspaltigen)
 * Tabellenkopf stehen. */
.app-notify-channel {
    display: flex;
    align-items: center;
    gap: .4rem;
}

.app-notify-channel + .app-notify-channel {
    margin-top: .35rem;
}

.app-notify-channel-label {
    flex: 0 0 auto;
    min-width: 3rem;
    color: var(--app-text-mute);
    font-size: .78rem;
}

.app-notify-level .app-notify-channel select {
    flex: 1 1 auto;
    min-width: 0;
}

.app-notify-quiet .app-notify-channel {
    justify-content: flex-start;
}

/* Feste statt inhaltsabhaengige Spaltenbreiten (`table-layout: auto` waere
 * die Vorgabe). Zwei Gruende, gefunden bei der Sichtung 2026-08-24:
 *
 * 1. Die Hauptmatrix und die eingeklappte Pollengruppe sind zwei getrennte
 *    `<table>`-Elemente (ein `<details>` kann keine `<tr>`-Kinder tragen).
 *    Mit `auto` bestimmt die laengste Stufen-Option jeder Tabelle fuer sich
 *    ihre Spaltenbreite — und weil beide Tabellen unterschiedlichen Inhalt
 *    tragen, liefen ihre Spaltengrenzen auseinander (bis zu 170 px Versatz
 *    bei 1512 px).
 * 2. Ein natives `<select>` beansprucht unter `auto` oft die Breite seiner
 *    laengsten `<option>` als Mindestinhalt, auch mit `width: 100%` und
 *    einem eigenen `max-width` auf dem Element selbst — das trieb die
 *    Hauptmatrix auf 311 px in einer 303-px-Karte (8 px Ueberlauf, nur per
 *    Wisch erreichbar, vom Viewport-Ueberlaufcheck nicht erfasst, weil der
 *    nur den Dokumentrand prueft, nicht den Karteninnenrand).
 *
 * Feste Prozente auf beiden Tabellen heilen beides ab 25 rem Breite: die
 * Spalten fluchten immer. Darunter uebernimmt der Media Query weiter unten
 * feste `rem`-Breiten statt Prozenten — der Grund steht dort. */
.app-notify-matrix {
    table-layout: fixed;
}

.app-notify-matrix th:first-child,
.app-notify-matrix td:first-child {
    width: 33%;
}

.app-notify-matrix th:nth-child(2),
.app-notify-matrix td:nth-child(2) {
    width: 41%;
}

.app-notify-matrix th:last-child,
.app-notify-matrix td:last-child {
    width: 26%;
}

/* Die eingeklappte Pollengruppe (Ausbauplan 2, 6b.1): acht Zeilen, die in den
 * meisten Haushalten dauerhaft auf „aus" stehen, deshalb per <details> aus dem
 * Weg — ohne JavaScript, die CSP erlaubt kein `unsafe-inline`. */
.app-notify-pollen {
    margin-top: 1rem;
}

.app-notify-pollen summary {
    cursor: pointer;
    font-weight: 600;
}

.app-notify-pollen .app-table-wrap {
    margin-top: .75rem;
}

/*
 * Auf einem Telefon zaehlt jedes Pixel: drei Spalten mal 1,2 rem Innenabstand
 * sind allein 58 px. Gemessen bei 393 px Fensterbreite (303 px Platz in der
 * Karte): mit dem regulaeren Abstand ist die Tabelle 323 px breit.
 *
 * **Prozentbreiten sind hier die falsche Grundlage — feste `rem`-Breiten
 * verdraengen sie deshalb** (Sichtung 2026-08-24, dritter Durchgang). Der
 * Grund: die laengste vorkommende Stufe „lange ausgefallen" (Paket 6a,
 * 17 Zeichen) und das laengste einzelne Wort eines Meldungsnamens
 * („Eingefrorener", 13 Zeichen) brauchen zusammen mit dem Kopf „Ruhezeit"
 * mehr Platz, als 303 px bei lesbarer Schrift hergeben — keine Kombination
 * aus Prozentanteilen loest das auf, ohne irgendwo zu kappen. Ein
 * Prozentversuch (33/41/26) schloss zwar den Karten-Ueberlauf, kappte dafuer
 * „lange ausgefallen" um vier Zeichen **ohne Wisch-Ausweg** (der Wrap selbst
 * war ja nicht mehr breiter als die Karte) und liess „Wetterwarnung" unter
 * den Nachbar-Auswahlkasten laufen. Das ist schlechter als ein Ueberlauf, der
 * sich wegwischen laesst.
 *
 * Feste `rem`-Breiten stellen die urspruengliche Absicht von
 * `.app-table-wrap` wieder her (Kommentar oben: seitliches Wischen statt
 * schrumpfendem Layout) — aber **zielgenau**: die Tabelle wird nur dann
 * breiter als ihre Karte, wenn eine Zeile es wirklich braucht, und alle
 * Spalten bleiben dabei zwischen Hauptmatrix und Pollentabelle deckungsgleich,
 * weil beide dieselben `rem`-Werte tragen.
 *
 * **`width: auto` reicht dafuer nicht — Chrome skaliert die festen
 * Spaltenbreiten sonst proportional auf 100 % herunter statt die Tabelle
 * wachsen zu lassen** (Sichtung 2026-08-24, vierter Durchgang: 7.5/9.5/5 rem
 * wurden zu gemessen 104/125/73 px, macht „lange ausgefallen" wieder
 * unerreichbar UND liess Haupt- und Pollentabelle erneut auseinanderlaufen,
 * weil jede fuer sich nach ihrem eigenen Inhalt herunterskaliert). Die feste
 * Summe der drei Spalten (7.5 + 9.5 + 5 rem) als eigene Breite umgeht das:
 * gegengeprueft, liefert exakt 120/152/80 px in **beiden** Tabellen. */
@media (max-width: 25rem) {
    .app-notify-matrix th,
    .app-notify-matrix td {
        padding-left: .35rem;
        padding-right: .35rem;
    }

    .app-notify-matrix {
        width: 22rem;
        min-width: 22rem;
    }

    .app-notify-matrix th:first-child,
    .app-notify-matrix td:first-child {
        width: 7.5rem;
    }

    .app-notify-matrix th:nth-child(2),
    .app-notify-matrix td:nth-child(2) {
        width: 9.5rem;
    }

    .app-notify-matrix th:last-child,
    .app-notify-matrix td:last-child {
        width: 5rem;
    }

    /* Kleinere Schrift und Raum vor dem Pfeil halten den angezeigten Wert
     * lesbar statt am Pfeil abgeschnitten (Sichtung 2026-08-24: „auffällig
     * klein" stiess sonst an den Pfeil). */
    .app-notify-matrix select {
        padding-right: 1rem;
        font-size: .85rem;
    }

    /*
     * Seit Paket 6c teilt sich die 9.5-rem-Spalte zwei Kanaele. Nebeneinander
     * (Kanalbeschriftung + Auswahlkasten in einer Zeile) blieben dem `select`
     * nur rund 62 px — „lange ausgefallen" braucht 134 px und wurde mitten im
     * Wort abgeschnitten (Sichtung 2026-08-25). Gestapelt statt nebeneinander
     * gibt der Beschriftung keine eigene Spaltenbreite mehr weg: das `select`
     * bekommt praktisch die volle Zellbreite. Die Kanalbeschriftung bleibt als
     * kleine Zeile darueber stehen, nicht als `aria-label` versteckt — beide
     * Kanaele sollen auch beim Ueberfliegen auseinanderzuhalten sein.
     */
    .app-notify-channel {
        flex-direction: column;
        align-items: stretch;
        gap: .1rem;
    }

    .app-notify-channel + .app-notify-channel {
        margin-top: .5rem;
    }

    .app-notify-channel-label {
        min-width: 0;
    }
}

/* Eine Formularzeile ohne eigene Liste — Standort, oeffentlicher Messpunkt,
 * Sprache der Instanz: mehrere `.app-field` nebeneinander statt
 * untereinander, weil sie inhaltlich zusammengehoeren. */
.app-field-row {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.app-field-row .app-field {
    flex: 1 1 12rem;
    /*
     * **`min-width: 0` hebt die Vorgabe `auto` auf.** Ein Flex-Element ist
     * sonst nie schmaler als sein Inhalt — und der Inhalt ist hier ein
     * `select`, das so breit wird wie seine laengste Option.
     *
     * Sichtbar wurde das erst mit den DWD-Pollenregionen: „Baden-Wuerttemberg
     * — Hohenlohe/mittlerer Neckar/Oberschwaben" macht das Feld 530 px breit
     * und schob die Seite auf dem iPhone um **182 px** ueber den Rand
     * (gemessen 2026-08-23). Bis dahin standen in dieser Zeile nur
     * Textfelder, deren Eigenbreite klein ist; die Regel war die ganze Zeit
     * zu schwach, nur fiel es an nichts auf.
     */
    min-width: 0;
}

.app-hint {
    margin: .35rem 0 0;
    color: var(--app-text-mute);
    font-size: .85rem;
}

/* Ein Anbieter-Zugang (Einstellungen, „Raeume und Geraete"). Je Zugang ein
 * eigenes Formular, deshalb ein sichtbar abgesetzter Block: ohne Rahmen
 * verschwimmen zwei untereinanderstehende Zugaenge zu einem einzigen langen
 * Formular, und der Speichern-Knopf sieht aus, als gaelte er fuer beide. */
.app-provider-account {
    margin-top: 1rem;
    padding: 1rem;
    border: 1px solid var(--app-border);
    border-radius: var(--app-radius);
}

.app-provider-state {
    margin: .25rem 0 0;
    color: var(--app-text-mute);
    font-size: .85rem;
}

/* Mehrere Knoepfe nebeneinander, umbruchfaehig — dieselbe Ueberlegung wie bei
 * `.app-inline-form`: auf einem schmalen Telefon steht der letzte Knopf sonst
 * ausserhalb der Karte. */
.app-button-row {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}

/* Die beiden Knoepfe der Pruefung verdaechtiger Messwerte (Spezifikation
 * 8b.3a) tragen `app-button-quiet` wie der Speichern-Knopf der
 * Reparaturmaske — anders als der sind sie aber eine **Entscheidung zwischen
 * zwei Wegen**. Der folgenreichere traegt deshalb zusaetzlich die Warnfarbe:
 * „unbrauchbar" macht aus einer Zeile eine offene Reparatur und wirft das
 * Tagesaggregat weg. */
.app-verify button[value="bad"] {
    color: var(--app-battery);
}

.app-verify button[value="bad"]:hover {
    background: color-mix(in srgb, var(--app-battery) 14%, transparent);
    color: var(--app-battery);
}

/* Ein Hinweis, der ueber einem eigenen Block steht und nicht als Nachsatz an
 * der Zeile darueber haengt — er braucht deshalb unten Luft. */
.app-hint--block {
    margin-bottom: .9rem;
}
