/* GridSense Mesh - Admin/Monitoring. Technisch, minimal, kein Framework. */

:root {
    color-scheme: light dark;
    --bg: #f5f6f7;
    --surface: #ffffff;
    --border: #d9dde1;
    --text: #1b1f23;
    --muted: #6a7078;
    --accent: #2b6cb0;
    --fresh: #2f9e5e;
    --stale: #b7860b;
    --old: #9aa1a9;
    --danger: #c0392b;
    --restore: #7c3aed;
    --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #14171a;
        --surface: #1b1f23;
        --border: #2c3237;
        --text: #e6e9ec;
        --muted: #8b939c;
        --accent: #5b9bd5;
        --fresh: #4fbf7a;
        --stale: #d4a72c;
        --old: #6c737b;
        --danger: #e0685c;
        --restore: #a78bfa;
    }
}

* { box-sizing: border-box; }

/* Ohne das gewinnt jedes Inline-style="display:..." (oder jede spaetere
   Klassenregel mit derselben Spezifitaet wie die Browser-Standardregel
   fuer [hidden]) gegen ein hidden-Attribut, und ein per JS ausgeblendetes
   Element bleibt trotzdem sichtbar. Betraf bisher u.a. device.php
   (Alarm-Schnellaktionen/Geraeteaktionen/Hop-Limit-/WLAN-Intervall-Zeile,
   alle mit "hidden style="display:flex""), dort auf jeder Detailseite
   unabhaengig von Geraeteklasse und Zustand sichtbar. */
[hidden] { display: none !important; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.admin-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
    padding: 0.9rem 1.5rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.admin-header-title {
    font-weight: 600;
    font-size: 1.05rem;
}

.admin-header-sub {
    font-weight: 400;
    color: var(--muted);
    margin-left: 0.4rem;
}

.admin-nav a {
    display: inline-block;
    margin-left: 1.25rem;
    padding: 0.3rem 0;
    color: var(--muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
}

.admin-nav a:hover {
    color: var(--text);
}

.admin-nav a.active {
    color: var(--text);
    border-bottom-color: var(--accent);
    font-weight: 600;
}

.admin-main {
    max-width: 1200px;
    margin: 0 auto;
    padding: 1.5rem;
}

h1 {
    font-size: 1.15rem;
    margin: 0 0 0.9rem;
}

table {
    width: 100%;
    border-collapse: collapse;
    background: var(--surface);
    border: 1px solid var(--border);
    font-size: 0.85rem;
}

th, td {
    padding: 0.5rem 0.65rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

th {
    color: var(--muted);
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    background: var(--bg);
}

tr:last-child td { border-bottom: none; }

/* overflow-wrap:anywhere zusätzlich zu white-space:normal, weil die Payload-
   JSON oft ein einziges langes Token ohne Leerzeichen ist (z.B. Hex-Werte);
   ohne das bricht der Browser dort gar nicht um, siehe device-layout oben. */
td.wrap, th.wrap { white-space: normal; overflow-wrap: anywhere; }

.mono { font-family: var(--mono); }

.muted { color: var(--muted); }

.badge {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.5;
}

.badge-enabled { background: color-mix(in srgb, var(--fresh) 18%, transparent); color: var(--fresh); }
.badge-disabled { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); }
.badge-accepted { background: color-mix(in srgb, var(--fresh) 18%, transparent); color: var(--fresh); }
.badge-rejected { background: color-mix(in srgb, var(--old) 25%, transparent); color: var(--muted); }
.badge-muted { background: color-mix(in srgb, var(--stale) 18%, transparent); color: var(--stale); }

/* Schütz-Zustand (echte, hardwarebestätigte OPTO4-Rückmeldung, siehe
   docs/decisions/schuetz-rueckmeldung.md): dieselben drei Zustände wie in
   der iOS-App, AN (grün), AUS (grau) und unbekannt (nur Rahmen, noch keine
   Rückmeldung bekommen). AUS bewusst grau und nicht rot wie ein Alarm: ein
   offenes Schütz ist ein normaler Betriebszustand, kein Fehler. Gemeinsam
   genutzt von beiden Dashboards, den Geräte-Detailseiten und den
   Schalt-Kacheln weiter unten. */
.badge-schuetz-on { background: color-mix(in srgb, var(--fresh) 18%, transparent); color: var(--fresh); }
.badge-schuetz-off { background: color-mix(in srgb, var(--old) 25%, transparent); color: var(--muted); }
.badge-schuetz-unknown { border: 1px dashed var(--border); color: var(--muted); }

/* Schlafbereitschaft einer Node (siehe gridsense-mesh
   docs/decisions/deep-sleep-verhalten.md). Zwei bewusst verschieden
   gewichtete Zustände: "scharf" ist eine WARNUNG, die Node sieht normal aus,
   verschweigt aber den nächsten echten Stromausfall, und bekommt deshalb den
   Warnton. "schläft" ist nur die Erklärung für ein Offline und bleibt grau
   wie ein ausgeschaltetes Schütz. Gemeinsam genutzt von /admin und /m. */
.badge-sleep-armed { background: color-mix(in srgb, var(--stale) 18%, transparent); color: var(--stale); }
.badge-sleep-asleep { background: color-mix(in srgb, var(--old) 25%, transparent); color: var(--muted); }

.recency-fresh .recency-dot { background: var(--fresh); }
.recency-stale .recency-dot { background: var(--stale); }
.recency-old .recency-dot { background: var(--old); }
.recency-never .recency-dot { background: var(--border); }
/* Schlafend: offline, aber gewollt. Eigener Punkt statt des Offline-Graus,
   sonst sieht die Node in der Liste aus wie eine ausgefallene. */
.recency-sleeping .recency-dot { background: var(--accent); }

.recency-dot {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    margin-right: 0.4rem;
}

.recency-old, .recency-never, .recency-sleeping { color: var(--muted); }

/* Zustand-Spalte der Geraete-Uebersicht (siehe admin/devices.js, renderRow):
   scharf/letzter Status/zuletzt gesehen untereinander in einer Zelle statt
   drei eigene Spalten. */
.state-cell {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
    white-space: normal;
}

.recency-line {
    font-size: 0.78rem;
}

/* Kebab-Menue fuer die selteneren Zeilen-Aktionen (Bearbeiten/Freigeben) in
   der Geraete-Uebersicht, damit nicht drei Buttons pro Zeile stehen (siehe
   admin/devices.js). */
.row-menu {
    position: relative;
    display: inline-block;
}

.row-menu-btn {
    padding: 0.25rem 0.5rem;
    line-height: 1;
}

.row-menu-list {
    position: absolute;
    right: 0;
    top: calc(100% + 0.25rem);
    z-index: 10;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 8.5rem;
    padding: 0.3rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
}

.row-menu-list button {
    display: block;
    width: 100%;
    padding: 0.35rem 0.5rem;
    border: none;
    border-radius: 4px;
    background: none;
    color: var(--text);
    font-size: 0.78rem;
    text-align: left;
    cursor: pointer;
}

.row-menu-list button:hover { background: var(--bg); }

.row-menu-list button.row-menu-danger { color: var(--danger); }

.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: 0.9rem;
    margin-bottom: 1rem;
    padding: 0.85rem 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}

.filter-bar label {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.78rem;
    color: var(--muted);
}

.filter-bar select {
    padding: 0.3rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg);
    color: var(--text);
    font-size: 0.85rem;
}

.filter-bar button {
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--accent);
    border-radius: 4px;
    background: var(--accent);
    color: #fff;
    font-size: 0.85rem;
    cursor: pointer;
}

.filter-bar label.filter-bar-checkbox-label {
    flex-direction: row;
    align-items: center;
    gap: 0.4rem;
}

.filter-bar a.reset {
    font-size: 0.8rem;
    color: var(--muted);
    align-self: center;
}

.pagination {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1rem;
    font-size: 0.85rem;
    color: var(--muted);
}

.pagination a {
    color: var(--accent);
    text-decoration: none;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 4px;
}

.pagination a.disabled {
    color: var(--muted);
    pointer-events: none;
    opacity: 0.5;
}

.count-hint {
    font-size: 0.8rem;
    color: var(--muted);
    margin-bottom: 0.75rem;
}

.count-hint.error-text {
    color: var(--danger);
}

.actions-cell {
    white-space: nowrap;
}

.btn {
    display: inline-block;
    padding: 0.25rem 0.65rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--surface);
    color: var(--text);
    font-size: 0.78rem;
    cursor: pointer;
}

.btn:hover {
    border-color: var(--accent);
}

.btn-sm {
    font-size: 0.75rem;
}

.btn-primary {
    border-color: var(--accent);
    background: var(--accent);
    color: #fff;
}

.btn-danger {
    border-color: var(--danger);
    background: var(--danger);
    color: #fff;
}

.row-edit-input,
.row-edit-select {
    padding: 0.25rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg);
    color: var(--text);
    font-size: 0.85rem;
    max-width: 12rem;
}

/* Oeffentliches Dashboard + Geraete-Detailseite (public/dashboard.php,
   public/device.php): gleiche Bausteine wie der Admin-Bereich oben, plus
   ein paar zusaetzliche fuer Panels/Kennzahlen/Chart, die der Admin-Bereich
   bisher nicht braucht. */

.dashboard-title-link {
    color: inherit;
    text-decoration: none;
}

.stat-row {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

.stat-tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.65rem 1rem;
    min-width: 110px;
}

.stat-tile .stat-value {
    font-size: 1.35rem;
    font-weight: 700;
    line-height: 1.3;
}

.stat-tile .stat-value.danger { color: var(--danger); }

.stat-tile .stat-label {
    font-size: 0.72rem;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.section-heading {
    font-size: 0.95rem;
    font-weight: 600;
    margin: 1.6rem 0 0.6rem;
}

.section-heading:first-of-type { margin-top: 0; }

table.clickable tbody tr { cursor: pointer; }
table.clickable tbody tr:hover { background: var(--bg); }

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 1rem 1.1rem;
    margin-bottom: 1rem;
}

.panel h2 {
    font-size: 0.95rem;
    margin: 0 0 0.2rem;
}

.panel .subline {
    font-size: 0.78rem;
    color: var(--muted);
    margin-bottom: 0.75rem;
}

/* Willkommens-Banner auf dem Admin-Dashboard (public/admin/index.php,
   dashboard.js), siehe docs/decisions/hello-onboarding-und-aktivierung.md
   in gridsense-docs. Grüner Akzentrand statt --danger: das ist ein
   Hinweis, keine Störung. */

.welcome-banner {
    border-left: 4px solid var(--fresh);
}

.welcome-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.welcome-list li a {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
    padding: 0.5rem 0.6rem;
    border-radius: 4px;
    background: var(--bg);
    text-decoration: none;
    color: inherit;
}

.welcome-list li a:hover {
    background: color-mix(in srgb, var(--fresh) 12%, var(--bg));
}

.welcome-list .welcome-device {
    font-weight: 600;
}

.welcome-list .welcome-meta {
    font-size: 0.8rem;
    color: var(--muted);
}

/* Firmware-Verwaltung (public/admin/firmware.php, firmware.js), siehe
   docs/decisions/wlan-ota-firmware-update.md in gridsense-docs. Rot statt
   grün: das ist keine Info wie das Willkommens-Banner, sondern eine
   ausdrückliche Warnung vor einer Aktion, die ein Feldgerät ohne Kabel
   unerreichbar machen kann. */

.warning-banner {
    border-left: 4px solid var(--danger);
}

.firmware-upload-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
}

.firmware-upload-form label {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.85rem;
}

.firmware-upload-form input,
.firmware-upload-form select {
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg);
    color: inherit;
    font: inherit;
}

/* Login-Seite (public/admin/login.php, siehe docs/decisions/benutzer-auth.md). */

.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
}

.login-panel {
    width: 100%;
    max-width: 320px;
}

.login-panel h1 {
    font-size: 1.05rem;
    margin: 0 0 0.9rem;
}

.login-field {
    display: block;
    font-size: 0.8rem;
    color: var(--muted);
    margin-bottom: 0.8rem;
}

.login-input {
    display: block;
    width: 100%;
    max-width: none;
    margin-top: 0.3rem;
}

.login-panel .btn {
    width: 100%;
}

.device-layout {
    display: grid;
    /* minmax(0, 1fr) statt 1fr: sonst erzwingt der Inhalt (z.B. die lange,
       leerzeichenlose Payload-JSON in der Events-Tabelle) eine größere
       Spurbreite als der Viewport und die ganze Seite wird breiter als
       der Bildschirm, siehe docs/decisions/mobile-web-uebergang.md,
       Nebenbefund "Payload-Zeile sprengte die Node-Detailseite". */
    grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
    gap: 1rem;
    align-items: start;
}

@media (max-width: 860px) {
    .device-layout { grid-template-columns: 1fr; }
}

.kv-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0.65rem 1rem;
}

.kv-grid .k {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--muted);
}

.kv-grid .v {
    font-size: 0.88rem;
    margin-top: 0.1rem;
}

/* Abschnittsueberschriften in der Node-Detail-Kachel (siehe admin/device.php):
   trennt Stammdaten/Verbindung/Mesh-Steuerung/Alarm optisch, ohne dass jede
   ihr eigenes Panel braucht. */
.kv-section-heading {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--muted);
    margin: 0.9rem 0 0.5rem;
    padding-top: 0.7rem;
    border-top: 1px solid var(--border);
}

.kv-section-heading.kv-section-first {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}

/* Seltener gebrauchte Abschnitte (WLAN-Details, USV) als <details> zugeklappt,
   damit die Kachel auf den ersten Blick nur Stammdaten/Verbindung/Alarm
   zeigt. Gleiche Optik wie .kv-section-heading, nur klickbar. */
.kv-details {
    margin: 0.9rem 0 0;
    padding-top: 0.7rem;
    border-top: 1px solid var(--border);
}

.kv-details summary {
    cursor: pointer;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--muted);
    list-style: none;
}

.kv-details summary::-webkit-details-marker { display: none; }
.kv-details summary::before { content: '▸ '; }
.kv-details[open] summary::before { content: '▾ '; }

.kv-details .kv-grid {
    margin-top: 0.6rem;
}

.raw-payload {
    font-family: var(--mono);
    font-size: 0.78rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 0.6rem;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 220px;
    overflow: auto;
}

/* Verlauf/Aktions-Log in der Geraete-Detail-Kachel (siehe admin/device.php,
   device.js loadAuditLog()) - kompakte Liste, aehnlich .raw-payload oben:
   begrenzte Hoehe mit eigenem Scrollbalken, damit ein langer Verlauf nicht
   die ganze Kachel in die Laenge zieht. */
.device-log-list {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 220px;
    overflow-y: auto;
    font-size: 0.82rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.device-log-list li {
    line-height: 1.4;
}

.chart-wrap {
    position: relative;
    height: 220px;
}

.chart-controls {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
    align-items: center;
    font-size: 0.78rem;
    color: var(--muted);
}

.rx-strip {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    align-items: flex-end;
    min-height: 32px;
    margin-top: 0.5rem;
}

.rx-block {
    width: 14px;
    height: 28px;
    border-radius: 2px;
    flex-shrink: 0;
}

.rx-legend {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    font-size: 0.72rem;
    color: var(--muted);
    margin-top: 0.5rem;
}

.rx-legend .dot {
    display: inline-block;
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 2px;
    margin-right: 0.3rem;
    vertical-align: middle;
}

/* Karten-Grid fuers oeffentliche Dashboard (public/dashboard.php): vom
   Gen-1-Dashboard uebernommenes Layout (Karten statt Tabellenzeilen, siehe
   docs/decisions/oeffentliches-dashboard.md), aber mit den Gen-2-Farben/
   -Bausteinen von oben statt Gen-1s eigenem dunklem Farbschema. */

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.grid-card {
    background: var(--surface);
    border: 2px solid var(--border);
    border-radius: 8px;
    padding: 0.75rem 0.85rem;
    cursor: pointer;
    /* Flex-Spalte statt reinem Block-Fluss: braucht public/wifi-dashboard.js'
       .card-pill-row (siehe dort, margin-top: auto), damit die Pill IMMER
       denselben Abstand zur unteren Kante hat, egal wie viel Name/Meta-Text
       darüber steht - vorher bestimmte die Textmenge darüber die
       Pill-Position. dashboard.js' Karten (Pill oben neben dem Namen in
       .row1, kein .card-pill-row) sehen dabei unveraendert aus, Flex-Column
       mit stretch entspricht optisch demselben Block-Fluss wie vorher. */
    display: flex;
    flex-direction: column;
    transition: transform 0.1s ease, border-color 0.1s ease;
}

.grid-card:hover { transform: translateY(-1px); }

.grid-card.status-ok { border-color: var(--fresh); }
.grid-card.status-alarm { border-color: var(--danger); }
.grid-card.status-restore { border-color: var(--restore); }
.grid-card.status-offline { border-color: var(--border); filter: saturate(0.6); opacity: 0.75; }
/* WLAN-Dashboard (siehe public/wifi-dashboard.php): "schwach, aber noch
   verbunden" braucht eine eigene, dritte Farbe zwischen status-ok (grün)
   und status-alarm (rot) - dieselbe Amber-Variable wie badge-muted/
   badge-sleep-armed weiter unten. */
.grid-card.status-wifi-degraded { border-color: var(--stale); }

.grid-card .row1 {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 0.5rem;
}

.grid-card .name {
    font-weight: 700;
    font-size: 0.9rem;
}

.grid-card .id {
    margin-top: 0.1rem;
    font-size: 0.72rem;
    color: var(--muted);
}

.grid-card .pill {
    font-size: 0.68rem;
    font-weight: 600;
    padding: 0.15rem 0.5rem;
    border-radius: 999px;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    white-space: nowrap;
}

.grid-card .card-meta {
    margin-top: 0.6rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.75rem;
    color: var(--muted);
}

.grid-card .card-site {
    margin-top: 0.15rem;
}

.grid-card .card-schuetz {
    margin-top: 0.55rem;
}

/* WLAN-Dashboard (siehe public/wifi-dashboard.js, renderCard()): die
   Verbunden/Getrennt/Schwach-Pill steht hier NICHT wie bei dashboard.js
   oben rechts neben dem Namen, sondern als eigene Zeile unten mittig -
   "verbunden, schwach" ist zu breit, um sich mit einem langen Node-Namen
   dieselbe Flex-Row1-Zeile zu teilen, ohne aus der Karte zu laufen.
   margin-top: auto statt eines festen Werts (Chris' Rückmeldung
   2026-08-23, "das jeder Karte an einer anderen Höhe sitzt"): schiebt die
   Pill in der Flex-Spalte (.grid-card oben) auf den gesamten verbleibenden
   Platz nach unten, dadurch hat sie IMMER denselben Abstand zur unteren
   Kartenkante (= .grid-cards padding-bottom), unabhängig davon, wie viele
   Zeilen der Name/die Meta-Zeilen darüber brauchen. */
.grid-card .card-pill-row {
    margin-top: auto;
    padding-top: 0.6rem;
    display: flex;
    justify-content: center;
}

.bars8 {
    display: grid;
    grid-auto-flow: column;
    gap: 2px;
    align-items: end;
}

.bars8 span {
    width: 3px;
    border-radius: 1px;
    background: var(--border);
}

.bars8 span:nth-child(1) { height: 4px; }
.bars8 span:nth-child(2) { height: 5px; }
.bars8 span:nth-child(3) { height: 6px; }
.bars8 span:nth-child(4) { height: 7px; }
.bars8 span:nth-child(5) { height: 8px; }
.bars8 span:nth-child(6) { height: 9px; }
.bars8 span:nth-child(7) { height: 10px; }
.bars8 span:nth-child(8) { height: 11px; }

.bars8.q1 span:nth-child(-n+1),
.bars8.q2 span:nth-child(-n+2),
.bars8.q3 span:nth-child(-n+3),
.bars8.q4 span:nth-child(-n+4),
.bars8.q5 span:nth-child(-n+5),
.bars8.q6 span:nth-child(-n+6),
.bars8.q7 span:nth-child(-n+7),
.bars8.q8 span:nth-child(-n+8) {
    background: var(--accent);
}

/* Site-Auswahl (public/index.php): neutrale Kacheln im selben Grid wie
   .card-grid oben, aber ohne Status-Farbmodifikatoren - eine Site hat
   selbst keinen Alarm-/Offline-Zustand. */

.site-card {
    background: var(--surface);
    border: 2px solid var(--border);
    border-radius: 8px;
    padding: 1rem 1.1rem;
    cursor: pointer;
    transition: transform 0.1s ease, border-color 0.1s ease;
}

.site-card:hover { transform: translateY(-1px); border-color: var(--accent); }

.site-card .site-name { font-weight: 700; font-size: 1rem; }

.site-card .site-count {
    margin-top: 0.3rem;
    font-size: 0.8rem;
    color: var(--muted);
}

/* Gruppenchat pro Site (public/admin/chat.php/js), siehe
   docs/decisions/site-chat.md. Einfache Bubble-Liste, kein Rich-Text. */
.chat-messages {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    max-height: 60vh;
    overflow-y: auto;
    padding: 0.85rem 1rem;
    margin-bottom: 0.75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}

/* Sprechblase, ausgerichtet per align-self innerhalb der flex-column
   .chat-messages: fremde Nachrichten links (Default), eigene rechts
   (.chat-message-own), gleiche Konvention wie .m-msg/.m-msg-own im
   Mobil-Bereich (public/m/m.css) - is_own kommt seit Chat-API v2 direkt
   von der API statt ueber einen Namensvergleich. */
.chat-message {
    max-width: 78%;
    align-self: flex-start;
    padding: 0.5rem 0.7rem;
    border-radius: 8px;
    background: var(--bg);
    border: 1px solid var(--border);
}

.chat-message-own {
    align-self: flex-end;
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}

.chat-message-meta {
    display: block;
    font-size: 0.72rem;
    color: var(--muted);
    margin-bottom: 0.15rem;
}

.chat-author-user { font-weight: 700; color: var(--text); }
.chat-author-device { font-weight: 700; color: var(--accent); }

.chat-message-body {
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 0.9rem;
}

/* Tagestrenner zwischen zwei Kalendertagen, gleiche Konvention wie in der
   App (ChatDaySeparator in gridsense-app-ios ChatView.swift). Zentriert
   ueber die volle Breite der flex-column .chat-messages. */
.chat-day {
    align-self: center;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--muted);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 0.15rem 0.7rem;
    margin: 0.2rem 0;
}

/* Byte-Zaehler unterm Eingabefeld gegen ChatLimits::BODY_MAX_BYTES (siehe
   chat.js/chat.php), gleiche Schwellen wie ChatLimits.remainingBytes() in
   der iOS-App: ab 20 Byte Rest orange, darueber (negativer Rest) rot. */
.chat-counter {
    text-align: right;
}

.chat-counter-warn { color: var(--stale); }
.chat-counter-over { color: var(--danger); font-weight: 600; }

.chat-compose {
    display: flex;
    gap: 0.6rem;
    align-items: flex-end;
}

.chat-compose textarea {
    flex: 1;
    min-height: 2.4rem;
    max-height: 8rem;
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg);
    color: var(--text);
    font-family: inherit;
    font-size: 0.9rem;
    resize: vertical;
}

.chat-compose button {
    padding: 0.5rem 1.1rem;
    border: 1px solid var(--accent);
    border-radius: 4px;
    background: var(--accent);
    color: #fff;
    font-size: 0.85rem;
    cursor: pointer;
}

/* Schalt-Kacheln (public/admin/schuetz.php): Layout vom Gen-1-WebApp-Bildschirm
   "Switch-Steuerung" übernommen (Kachelraster mit AN/AUS je Node, darüber
   Gruppenkarten in voller Breite), aber mit den Gen-2-Farben/-Bausteinen von
   oben statt Gen-1s eigenem dunklem Farbschema, genau wie beim Karten-Grid des
   öffentlichen Dashboards. Kachelrahmen grün/rot je Schütz-Zustand: hier ist
   Rot für AUS eindeutig, weil auf dieser Seite kein Alarm-Zustand mitgefärbt
   wird (deshalb tragen die Badges selbst weiter das neutrale Grau, siehe
   .badge-schuetz-off oben). */

.switch-tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.switch-tile {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    min-height: 9rem;
    padding: 0.75rem 0.85rem;
    background: var(--surface);
    border: 2px solid var(--border);
    border-radius: 8px;
}

.switch-tile.state-on { border-color: var(--fresh); }
.switch-tile.state-off { border-color: var(--danger); }
.switch-tile.state-unknown { border-style: dashed; }

/* Deep-Link von der Geräte-Detailseite (?source_id=, siehe schuetz.js):
   hebt genau die gemeinte Kachel hervor, ohne die Zustandsfarbe des Rahmens
   zu überschreiben. */
.switch-tile.is-highlighted {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}

.switch-tile .tile-name {
    font-weight: 700;
    font-size: 0.9rem;
}

.switch-tile .tile-id {
    margin-top: 0.1rem;
    font-size: 0.72rem;
    color: var(--muted);
}

.switch-badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
}

.switch-tile .tile-meta {
    font-size: 0.75rem;
    color: var(--muted);
}

.switch-tile .tile-buttons {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.4rem;
    margin-top: auto;
}

.btn-switch-on,
.btn-switch-off {
    padding: 0.55rem 0.4rem;
    border-radius: 4px;
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--text);
    cursor: pointer;
}

.btn-switch-on {
    border: 1px solid color-mix(in srgb, var(--fresh) 55%, transparent);
    background: color-mix(in srgb, var(--fresh) 14%, transparent);
}

.btn-switch-off {
    border: 1px solid color-mix(in srgb, var(--danger) 55%, transparent);
    background: color-mix(in srgb, var(--danger) 12%, transparent);
}

.btn-switch-on:hover { background: color-mix(in srgb, var(--fresh) 26%, transparent); }
.btn-switch-off:hover { background: color-mix(in srgb, var(--danger) 24%, transparent); }

/* Gruppenkarte: volle Breite, weil sie eine aufklappbare Mitgliederliste
   trägt (Gen-1-Aufteilung, dort ebenfalls Kacheln für Nodes und breite
   Karten für Gruppen). */
.switch-group-card {
    background: var(--surface);
    border: 2px solid var(--border);
    border-radius: 8px;
    margin-bottom: 0.75rem;
}

.switch-group-card.state-on { border-color: var(--fresh); }
.switch-group-card.state-off { border-color: var(--danger); }

.switch-group-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.75rem 0.9rem;
}

.switch-group-name {
    font-weight: 700;
    font-size: 0.95rem;
}

.switch-group-meta {
    margin-top: 0.15rem;
    font-size: 0.75rem;
    color: var(--muted);
}

.switch-group-buttons {
    display: flex;
    gap: 0.4rem;
}

.switch-group-buttons button {
    min-width: 6.5rem;
}

.star-btn {
    padding: 0 0.2rem;
    border: none;
    background: none;
    color: var(--muted);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.star-btn.is-favorite { color: var(--stale); }

.switch-group-card details > summary {
    padding: 0.5rem 0.9rem;
    border-top: 1px solid var(--border);
    font-size: 0.78rem;
    color: var(--muted);
    cursor: pointer;
}

.switch-member-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    padding: 0.55rem 0.9rem;
    border-top: 1px solid var(--border);
}

.switch-member-name {
    font-weight: 600;
    font-size: 0.85rem;
}

.switch-group-edit {
    padding: 0.75rem 0.9rem;
    border-top: 1px solid var(--border);
    background: var(--bg);
}

.switch-checkbox-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 0.35rem;
    margin: 0.4rem 0 0.6rem;
}

.switch-checkbox-item {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 4px;
    font-size: 0.82rem;
}

.switch-inline-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.6rem;
}

details.switch-new-group {
    margin-bottom: 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}

details.switch-new-group > summary {
    padding: 0.6rem 0.9rem;
    font-size: 0.82rem;
    color: var(--muted);
    cursor: pointer;
}

.switch-new-group-body {
    padding: 0.75rem 0.9rem;
    border-top: 1px solid var(--border);
}

/* ── Geländeplan (site-map.js, docs/decisions/site-karte.md) ── */
.site-map-wrap {
    position: relative;
    width: 100%;
    background: repeating-conic-gradient(var(--bg) 0% 25%, var(--surface) 0% 50%) 50% / 16px 16px;
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
}
/* Positionierung/Zoom kommt per Inline-Style von site-map.js (layout()):
   left/top/width/height bilden immer die kontenierte Bildfläche exakt ab,
   transform:scale() zoomt im Crop-Modus zentriert hinein. <img> UND <svg>
   liegen beide HIER drin und teilen sich dadurch zwangsläufig dieselbe
   Transformation, siehe Nachtrag-Kommentar in site-map.js. */
.site-map-content {
    position: absolute;
    transform-origin: 0 0;
}
.site-map-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    user-select: none;
    pointer-events: none;
}
.site-map-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}
.site-map-empty {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    font-size: 0.85rem;
    text-align: center;
    padding: 1rem;
}

.site-map-marker-shape { stroke: var(--surface); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.site-map-marker-shape.status-ok      { fill: var(--fresh); }
.site-map-marker-shape.status-alarm   { fill: var(--danger); }
.site-map-marker-shape.status-restore { fill: var(--restore); }
.site-map-marker-shape.status-offline { fill: var(--old); }
.site-map-marker-shape.status-unknown,
.site-map-marker-shape:not([class*="status-"]) { fill: var(--accent); }
/* Hervorhebung "das ist das Gerät dieser Seite" (site-map.js, mount()-Option
   "highlight"): wiederverwendet --stale statt einer eigenen neuen Farbe,
   dasselbe Amber wie bei "Schlaf scharf"/"gemutet"/schwachem WLAN anderswo
   im Dashboard - unterscheidet sich klar von Grün (ok), Rot (Alarm) und
   Lila (gerade wiederhergestellt), ohne eine vierte, nirgendwo sonst
   benutzte Markenfarbe einzuführen. */
.site-map-marker-shape.status-highlight { fill: var(--stale); }
.site-map-alarm-ring {
    fill: none;
    stroke: var(--danger);
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
    opacity: 0.75;
}
.site-map-highlight-ring {
    fill: none;
    stroke: var(--stale);
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
    opacity: 0.75;
}
/* font-size kommt gezielt PRO Marker als Inline-Attribut von site-map.js
   (render()): die viewBox liegt jetzt in echten, unskalierten CSS-Pixeln
   der Content-Box (siehe dortiger Kommentar), ein fester CSS-Wert hier
   würde sich im gezoomten Crop-Modus mit hochskalieren, `vector-effect:
   non-scaling-stroke` gibt es für font-size nicht (nur für stroke). */
.site-map-marker-label {
    fill: var(--text);
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3px;
    vector-effect: non-scaling-stroke;
    pointer-events: none;
}
.site-map-marker { cursor: pointer; }
.site-map-marker-editable { cursor: grab; }
.site-map-marker-editable:active { cursor: grabbing; }

/* 40%-Ausschnitt auf der Node-Detailseite/Mobil-Gerätekarte: fest niedrigere
   Höhe als die volle Editor-/Live-Karte, damit er wirklich als kompakter
   Ausschnitt wirkt und nicht wie eine zweite Vollkarte. */
.site-map-wrap.site-map-crop { height: 220px; }
/* Nur Rückfall, solange noch kein Plan geladen ist ("lädt..."/"kein Plan
   konfiguriert"): sobald ein Bild da ist, setzt sizeContainer() in
   site-map.js eine Inline-Höhe nach dem Bild-Seitenverhältnis bei voller
   Container-Breite (immer maximale Breite statt einer festen Höhe, die
   einen breiten Plan winzig machte, siehe dortiger Kommentar). */
.site-map-wrap.site-map-full { height: 420px; }
@media (max-width: 900px) {
    .site-map-wrap.site-map-full { height: 320px; }
}

.site-map-toolbar {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 0.6rem;
}
.site-map-toolbar .active-tool { border-color: var(--accent) !important; color: var(--accent) !important; }
.site-map-flex-spacer { flex: 1; }

/* Geräte-Chip-Reihe (Nachtrag 2026-08-17): ersetzt die vorherige schmale
   Seitenspalten-Liste. Umbricht wie normaler Text, deshalb immer ohne
   Scrollbalken sichtbar, egal wie viele Geräte eine Site hat. */
.site-map-devices-panel { margin-bottom: 1rem; }
.site-map-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem;
}
.site-map-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.55rem 0.3rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    font-size: 0.82rem;
    font-family: inherit;
    cursor: pointer;
    line-height: 1.3;
}
.site-map-chip:hover { border-color: var(--accent); }
.site-map-chip.is-armed { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.site-map-chip.is-placed { border-color: color-mix(in srgb, var(--fresh) 45%, var(--border)); }
.site-map-placed-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--old); }
.site-map-placed-dot.yes { background: var(--fresh); }
.site-map-chip-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    color: var(--muted);
    font-size: 0.75rem;
    line-height: 1;
}
.site-map-chip-remove:hover { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }

.site-map-upload-label input[type="file"] {
    display: block;
    font-size: 0.78rem;
    max-width: 260px;
}
