/* =====================================================================
   EquiFeed — stylesheet
   Token-based: all colors via CSS variables so dark mode is a single
   prefers-color-scheme block at the bottom.
   ===================================================================== */

:root {
  --primary: #2563eb;
  --primary-dark: #1d4ed8;
  --success: #16a34a;
  --danger: #dc2626;
  --warning: #f59e0b;
  /* --- Skalenfarben für Akku und Temperatur (Nutzer-Vorgabe 2026-08-03) -------
     Sie stehen bewusst NEBEN --success/--warning/--danger: Jene bedeuten
     „Erfolg/Warnung/Fehler", diese sind Stufen einer Messskala. Wer die
     Zustandsfarben umfärbt, soll damit nicht die Skala verschieben.
     Dieselben Werte liegen in iOS (SkalenFarben.swift) und Android
     (Colors.kt) — wer hier etwas ändert, muss beide nachziehen. */
  --skala-gruen-dunkel: #15803d;
  --skala-gruen:        #16a34a;
  --skala-gruen-hell:   #4ade4a;
  --skala-gruengelb:    #a3d219;
  --skala-gelb:         #eab308;
  --skala-orange:       #f59e0b;
  --skala-rot:          #dc2626;
  /* Kaltes Ende des Lichtspektrums — nur für die Temperatur. */
  --skala-violett:      #8b5cf6;
  --skala-blau:         #2563eb;
  --skala-cyan:         #06b6d4;

  --bg: #f1f5f9;
  --card: #ffffff;
  --card-elevated: #ffffff;
  --text: #1e293b;
  --text-muted: #64748b;
  --border: #e2e8f0;
  --border-strong: #cbd5e1;

  /* Scrollbalken (siehe globale Scrollbar-Regeln) — hell */
  --scrollbar-thumb: #c1ccd9;
  --scrollbar-thumb-hover: #94a3b8;
  --scrollbar-track: transparent;

  --status-online-bg:  #dcfce7;
  --status-online-fg:  #15803d;
  --status-offline-bg: #fee2e2;
  --status-offline-fg: #b91c1c;

  --alert-error-bg: #fee2e2;
  --alert-error-fg: #b91c1c;
  --alert-error-border: #fca5a5;
  --alert-success-bg: #dcfce7;
  --alert-success-fg: #15803d;
  --alert-success-border: #86efac;
  --alert-info-bg: #dbeafe;
  --alert-info-fg: #1d4ed8;
  --alert-info-border: #93c5fd;

  /* Empfangsstufen im Einrichtungsguide (0..4). Eigene Farben statt der
     Warn-/Fehlerfarben: Schwacher Empfang ist kein Fehler, sondern ein Messwert —
     rot ist er erst, wenn wirklich nichts mehr da ist. */
  --signal-4: #15803d;
  --signal-3: #4d7c0f;
  --signal-2: #a16207;
  --signal-1: #c2410c;
  --signal-0: #b91c1c;

  --badge-admin-bg: #ede9fe;
  --badge-admin-fg: #7c3aed;
  --badge-user-bg:  #e0f2fe;
  --badge-user-fg:  #0284c7;

  --shadow-sm: 0 1px 3px rgba(0,0,0,0.04);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
  --shadow-lg: 0 4px 24px rgba(0,0,0,0.08);

  --radius: 12px;
  /* Einheitliches Easing fuer alle Transitions (sanfter als Default). */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Scrollbalken an Light/Dark koppeln. Ohne dies zeigt der Browser im Dark Mode
   den hellen OS-Default — auffaellig hell. Firefox: scrollbar-color; Chromium/
   WebKit: ::-webkit-scrollbar. Die bewusst versteckte .nav-tabs-Scrollbar hat
   eine eigene, speziellere Regel und bleibt unberuehrt. */
html {
  scrollbar-width: thin;                                       /* Firefox */
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: 8px;
  border: 2px solid transparent;       /* schmaler, eingerueckter Daumen */
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
  background-clip: padding-box;
}
::-webkit-scrollbar-corner { background: transparent; }

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  /* Remove the gray flash iOS Safari shows on tap */
  -webkit-tap-highlight-color: transparent;
}

html {
  /* Prevent iOS landscape font scaling */
  -webkit-text-size-adjust: 100%;
  /* Remove the 300ms click delay globally */
  touch-action: manipulation;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  min-height: 100dvh; /* iOS: dynamische Viewport-Höhe -> kein Sprung beim Ein-/Ausblenden der URL-Leiste */
  /* Better elastic-scroll feel on iOS; prevents the body scroll-chain glitch */
  overscroll-behavior-y: contain;
  /* Safe-Area (iOS-PWA, viewport-fit=cover) — bewusst OHNE oberen Inset:
     `position: sticky` misst `top` gegen den Scrollport, nicht gegen die
     Padding-Kante eines Vorfahren. Der oben klebende .app-header waere daher
     ungescrollt korrekt gesessen, gescrollt aber unter die Statusleiste/Dynamic
     Island gefahren. Regel deshalb: den oberen Inset traegt jedes Element, das
     oben klebt, selbst (.app-header, .offline-banner, #login-page); seitlich und
     unten klebt nichts im Fluss, dort bleibt der Inset am body. Bildschirmfeste
     Elemente (position:fixed) sehen dieses Padding ohnehin nicht — sie bringen
     ihre Insets seit jeher selbst mit. */
  padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* All form fields stay at >=16px on touch so iOS Safari won't auto-zoom on focus */
input, select, textarea, button {
  font-family: inherit;
}
input[type="text"],
input[type="password"],
input[type="number"],
input[type="time"],
input[type="url"],
input[type="tel"],
input[type="email"],
select,
textarea {
  font-size: 16px;
}

.hidden { display: none !important; }

/* Das `hidden`-ATTRIBUT global durchsetzen.
 *
 * Der Browser bringt dafuer nur `[hidden] { display: none }` mit — eine
 * UA-Regel, und die verliert gegen JEDE Autoren-Regel mit `display`. Ein
 * Element mit `class="btn"` (`display: inline-flex`) blieb also sichtbar,
 * obwohl `hidden` gesetzt war.
 *
 * Diese Falle wurde in dieser Datei bereits VIERMAL einzeln geflickt
 * (.settings-menu/.settings-page, .settings-action-row .btn,
 * .settings-dirty-hint, .unsaved-hint). Beim vierten Mal war der Schaden
 * bereits im Feld: Die Aktionsleiste stand auf JEDER Unterseite, und
 * "Speichern" schrieb auf der Seite "Geraeteuser" die Geraetekonfiguration
 * statt den Besitzer (1.64.182, am Geraet gemeldet).
 *
 * Statt beim fuenften Mal wieder lokal zu flicken: hier EINMAL fuer alle.
 * Wer ein Element mit `hidden` versteckt, meint das auch — eine Ausnahme
 * gaebe es nur, wenn `hidden` als reine Semantik ohne Anzeigewirkung genutzt
 * wuerde, und dafuer ist `aria-hidden` da. */
[hidden] { display: none !important; }
.muted { color: var(--text-muted); font-size: 0.85rem; }
.muted-sm { font-size: 0.85rem; color: var(--text-muted); }
.muted-block { color: var(--text-muted); padding: 0.5rem; font-size: 0.9rem; }

/* ===== Login ===== */
#login-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh; /* iOS: s.o. — Login-Screen folgt der sichtbaren Viewporthöhe */
  /* Oberer Inset jetzt hier statt am body (s. dort): die Login-Karte wird in
     diesem Kasten zentriert und darf bei kleiner Viewporthöhe nicht unter die
     Statusleiste rutschen. Unten reicht 1rem — den Home-Indicator-Abstand
     liefert bereits das padding-bottom des body. */
  padding: calc(1rem + env(safe-area-inset-top, 0px)) 1rem 1rem;
  /* Dezenter Markentönungs-Schimmer (Blau) oben — wirkt in Light & Dark. */
  background:
    radial-gradient(1100px 520px at 50% -12%, rgba(37, 99, 235, 0.12), transparent 60%),
    var(--bg);
}

.login-card {
  background: var(--card);
  padding: 2.5rem;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--border);
}

.login-card h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
.login-card p  { color: var(--text-muted); margin-bottom: 1.5rem; font-size: 0.9rem; }

/* EquiFeed wordmark + horse-head logo. Inlined SVG with fill="currentColor",
   so it follows the active theme (light = dark ink, dark = light ink). */
.login-logo {
  display: block;
  width: 240px;
  max-width: 85%;
  height: auto;
  margin: 0 auto 1rem;
}
/* Schwarzes Line-Art-Logo -> im Dark-Mode invertieren (wird sauber weiß). */
:root[data-theme="dark"] .login-logo { filter: invert(1); }

/* Visually hidden but available to assistive tech (screen readers) */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ===== Forms ===== */
.form-group { margin-bottom: 1rem; }

.form-group label,
.form-group .form-label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0.35rem;
  color: var(--text);
}
/* (Fund #7) `.form-label` ist die Gruppen-ÜBERSCHRIFT für Felder, deren Beschriftung
   ein Info-Symbol enthält. Bewusst kein <label>: der `tip` liegt darin und ist
   fokussierbar — mit `for=` würde ein Klick aufs „i" die Checkbox umschalten. Die
   Zuordnung macht stattdessen `aria-labelledby` am Feld, auf den reinen Textspan;
   sonst zöge die Namensberechnung den kompletten Tooltip in den Feldnamen. */

.form-label-block {
  font-size: 0.85rem;
  font-weight: 600;
  display: block;
  margin-bottom: 0.35rem;
}

.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="email"],
.form-group input[type="search"],
.form-group input[type="number"],
.form-group input[type="time"],
.form-group input[type="url"],
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 0.6rem 0.75rem;
  border: 1.5px solid var(--border);
  border-radius: 8px;
  /* 16px: verhindert iOS-Safari Auto-Zoom beim Fokus (siehe globale Regel oben) */
  font-size: 16px;
  transition: border-color 0.2s;
  background: var(--card);
  color: var(--text);
}

/* Passwort-Feld mit Anzeigen/Verbergen-Schalter (Augen-Button) */
.password-field { position: relative; }
/* Platz rechts für den Button schaffen (überschreibt das padding-Shorthand) */
.password-field input { padding-right: 2.75rem !important; }
.password-toggle {
  position: absolute;
  top: 0; bottom: 0; right: 0.3rem;
  margin: auto 0;
  width: 2.1rem; height: 2.1rem;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted);
  border-radius: 8px;
  transition: color 0.2s var(--ease);
}
.password-toggle:hover { color: var(--primary); }
.password-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.password-toggle svg { width: 1.2rem; height: 1.2rem; }

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--primary);
}

.form-grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

.checkbox-group {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 1rem 0;
}

.checkbox-group input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--primary);
}

.checkbox-group label {
  font-size: 0.85rem;
  color: var(--text-muted);
  cursor: pointer;
}

/* ===== Buttons ===== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem 1.2rem;
  min-height: 40px;
  border: none;
  border-radius: 8px;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s var(--ease);
  color: #fff;
  /* No tap delay, no double-tap zoom on iOS */
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
}

.btn-primary { background: var(--primary); }
.btn-primary:hover { background: var(--primary-dark); }
.btn-success { background: var(--success); }
.btn-danger  { background: var(--danger); }
.btn-warning { background: var(--warning); }
.btn-outline {
  background: transparent;
  border: 1.5px solid var(--border-strong);
  color: var(--text);
}
.btn-outline:hover { border-color: var(--primary); color: var(--primary); }
.btn-sm { padding: 0.4rem 0.85rem; font-size: 0.8rem; min-height: 34px; }
.btn-block { width: 100%; }

/* Touch-Geraete: groessere Tap-Flaechen (Apple HIG ~44px). Greift nur bei
   coarse Pointern (Finger), Desktop-Maus bleibt kompakt. */
@media (pointer: coarse) {
  .btn-sm { min-height: 44px; }   /* Apple-HIG-Mindestgröße für Touch */
  /* Toggle-Trefferflaeche unsichtbar vergroessern, ohne die Optik (52x30) zu
     aendern: das transparente <input> ragt oben/unten ueber den Slider hinaus. */
  .toggle input { top: -8px; bottom: -8px; height: auto; }

  /* ===== (Fund #1) Touch-Ziele auf 44 px =====
     Dies war bis 1.64.120 die EINZIGE Touch-Regel im ganzen Stylesheet — sie deckte
     nur `.btn-sm` und den Toggle ab. Alles andere blieb bei 14–34 px.
     Zwei Techniken, je nach Element:
       (a) min-height/min-width, wo das Layout Wachstum verträgt,
       (b) ein unsichtbares ::before, wo der Knopf klein AUSSEHEN muss.
     Der Fund sagt es selbst: „Die sichtbaren Symbole dürfen kleiner bleiben." */

  /* (a) Layout verträgt echtes Wachstum */
  .user-info-btn { min-height: 44px; }
  /* Der Timer-Dialog ist hier bewusst AUSGENOMMEN (Stand 1.64.123): `.tmr-action`,
     `.tmr-boxbtn` und `.tmr-remove` waren bis 1.64.122 ebenfalls auf 44 px gezogen.
     Das ließ die Timerzeile von 56 auf 62 px wachsen — bei einem langen Zeitplan
     dehnte das den Dialog spürbar, und die Ansicht wurde auf dem Telefon insgesamt
     unruhiger als der Gewinn an Trefferfläche wert war. Die Knöpfe sind damit wieder
     unter 44 px; das ist eine bewusste Abwägung, kein Versehen. */
  .settings-back,
  .theme-toggle,
  .install-hint-x { min-height: 44px; min-width: 44px; }

  /* (b) Optik bleibt, nur die Trefferfläche wächst.
     Bewusst KEIN visueller Effekt (kein background/border) — das ::before ist reine
     Fläche. Kein z-index: die Nachbarn in derselben Zeile sollen ihre eigenen
     Flächen behalten, statt von einem darüberliegenden Rechteck verdeckt zu werden. */
  .info-tip::before,
  .password-toggle::before,
  .cam-row-more::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }
  .cam-row-more { position: relative; }
  /* .info-tip und .password-toggle sind bereits positioniert (relative/absolute). */

  /* Textlinks (Login/Footer): Fläche über Padding, Optik über die negative Marge
     halten — sonst rutschte das Layout auseinander. */
  .footer-link {
    display: inline-block;
    padding: 0.7rem 0.4rem;
    margin: -0.7rem -0.4rem;
  }
  /* „Angemeldet bleiben“: die Checkbox selbst ist 18×18 und damit das kleinste Ziel
     im Login (im Review nicht genannt). Das Label ist bereits per `for` verknüpft —
     ein Tipp darauf schaltet also mit. Die Fläche deshalb über das LABEL vergrößern
     statt die Checkbox aufzublasen: die Optik bleibt, das Ziel wird 44 px hoch. */
  .checkbox-group { min-height: 44px; }
  .checkbox-group label {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ===== Header ===== */
.app-header {
  background: var(--card);
  border-bottom: 1px solid var(--border);
  /* Der obere Safe-Area-Inset gehoert HIERHER, nicht an den body: als sticky-
     Element klebt der Header an der Scrollport-Kante (y=0) und waere gescrollt
     unter die Statusleiste/Dynamic Island gefahren. Als Innenabstand faehrt der
     Inset mit — die Kopfzeile bleibt in jeder Scrollposition frei, und die
     Headerflaeche (im Glas-Design der Blur) reicht sauber bis unter die Uhr.
     Die Hoehe waechst um denselben Betrag, damit der Inhalt weiterhin in 64 px
     zentriert liegt und #toast-stack (top: 64px + Inset) weiter buendig unter
     der Headerkante sitzt. */
  padding: env(safe-area-inset-top, 0px) 1.5rem 0;
  height: calc(64px + env(safe-area-inset-top, 0px));
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: sticky;
  top: 0;
  z-index: 100;
}

.app-header h1 {
  font-size: 1.1rem;
  font-weight: 400;            /* nicht der fette h1-Default */
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
/* Schriftzug passend zum filigranen Logo: leicht und mit luftiger Laufweite. */
.brand-name {
  font-weight: 300;
  letter-spacing: 0.06em;
  font-size: 1.2rem;           /* Light wirkt optisch kleiner -> etwas größer */
  color: var(--text);
}
/* Logo-Marke (Pferdekopf) links im Header. Schwarzes Line-Art mit Alpha ->
   im Dark-Mode invertiert (Schwarz wird sauber Weiß), Transparenz bleibt. */
.brand-mark {
  height: 52px;
  width: auto;
  display: block;
}
:root[data-theme="dark"] .brand-mark { filter: invert(1); }

.header-right { display: flex; align-items: center; gap: 1rem; }

/* Warnhinweis im Header: Neuregistrierungen sind gesperrt (nur für Admins
   eingeblendet). Dezent pulsierend, damit es auffällt, aber nicht stört. */
.reg-warning {
  position: absolute;
  left: 50%;
  /* 50 % bezieht sich auf die PADDING-Box des Headers — die enthaelt seit der
     Safe-Area-Umstellung oben den Inset. Ohne die halbe Korrektur saesse das
     Zeichen in der iOS-PWA rund 30 px zu tief. So trifft es die Mitte der
     64-px-Inhaltszeile, wo auch Logo und Konto-Knopf liegen. */
  top: calc(50% + env(safe-area-inset-top, 0px) / 2);
  transform: translate(-50%, -50%);
  color: var(--warning, #d97706);
  font-size: 4rem;
  line-height: 1;
  cursor: default;
  pointer-events: auto;
  animation: reg-warning-pulse 2s ease-in-out infinite;
}
@keyframes reg-warning-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) { .reg-warning { animation: none; } }

.mqtt-indicator {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--danger);
  transition: background 0.2s;
}
.mqtt-indicator.connected { background: var(--success); }
.mqtt-status-line { display: inline-flex; align-items: center; gap: 0.4rem; }
.theme-options { display: flex; gap: 0.5rem; }
.theme-options .btn { flex: 1; }
.account-menu { display: flex; flex-direction: column; gap: 0.6rem; }

/* Konto-Dropdown (Desktop) — unter dem Konto-Button, position: fixed */
.account-dropdown {
  position: fixed;
  z-index: 300;
  min-width: 200px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-lg);
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.14s var(--ease), transform 0.14s var(--ease);
}
.account-dropdown.open { opacity: 1; transform: none; }
.account-dropdown .dropdown-head {
  padding: 0.4rem 0.7rem 0.5rem;
  font-size: 0.9rem;
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0.2rem;
}
.account-dropdown .dropdown-item {
  appearance: none;
  background: none;
  border: none;
  text-align: left;
  padding: 0.55rem 0.7rem;
  border-radius: 7px;
  font-size: 0.9rem;
  font-family: inherit;
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s var(--ease);
}
.account-dropdown .dropdown-item:hover,
.account-dropdown .dropdown-item:focus-visible { background: var(--bg); outline: none; }

.user-info { font-size: 0.85rem; color: var(--text-muted); }
/* Benutzername = Einstieg ins Konto-Menü -> als Button erkennbar (Rahmen). */
.user-info-btn {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  border: 1.5px solid var(--border-strong);
  border-radius: 8px;
  transition: border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.user-info-btn .user-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Chevron signalisiert das aufklappende Konto-Menü */
.user-info-btn::after {
  content: '';
  flex: none;
  width: 7px; height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  opacity: 0.7;
}
.user-info-btn:hover { color: var(--primary); border-color: var(--primary); }
.user-info-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ===== Navigation tabs ===== */
.nav-tabs {
  display: flex;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  padding: 0 1.5rem;
  overflow-x: auto;
}

.nav-tab {
  padding: 0.85rem 1.25rem;
  min-height: 44px;
  border: none;
  background: none;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: all 0.2s;
  white-space: nowrap;
  touch-action: manipulation;
}
.nav-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.nav-tab:hover { color: var(--text); }

/* ===== Main content ===== */
.main-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1.5rem;
}

/* ===== Cards ===== */
.card {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--border);
  padding: 1.5rem;
  margin-bottom: 1rem;
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1rem;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.card-header h2 { font-size: 1.1rem; }
.card-header h3 { font-size: 1rem; }

/* ===== Device cards ===== */
.device-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 1rem;
  /* Jede Kachel nur so hoch wie ihr Inhalt: sonst streckt Grid-Standard
     (align-items:stretch) die kürzeren Kacheln auf die Höhe der höchsten in
     derselben Zeile (z.B. wenn eine Kachel Schnellsteuerung/Nächste Timer zeigt). */
  align-items: start;
}

.device-card {
  background: var(--card);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  overflow: hidden;
  transition: box-shadow 0.2s var(--ease), transform 0.2s var(--ease), background-color 0.3s var(--ease), border-color 0.3s var(--ease);
}
/* Kein farbiger Rand-Indikator mehr. Stattdessen: offline (KEIN „Wartet") ->
   Kachel dezent rot einfärben. color-mix mischt die Akzentfarbe leicht in den
   Karten-Hintergrund (funktioniert in Hell/Dunkel). „Wartet" (neu, noch nie
   verbunden) ist kein Fehler -> bleibt neutral. */
.device-card--offline:not(.device-card--waiting) {
  background: color-mix(in srgb, var(--danger) 8%, var(--card));
  border-color: color-mix(in srgb, var(--danger) 25%, var(--border));
}
.device-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }

/* ===== Live-Feedback: weiche Übergänge bei WS-Updates =====
   (Der frühere Flash bei Live-Updates — value-flash/card-flash — wurde in
   1.64.29 auf Nutzerwunsch wieder entfernt; Werte wechseln jetzt nur weich.) */
.status-badge { transition: background-color 0.35s var(--ease), color 0.35s var(--ease); }
.status-value { transition: color 0.3s var(--ease); }

/* Haptischeres Tippen: Buttons sinken beim Drücken leicht ein (Release
   animiert über die vorhandene .btn-Transition zurück). */
.btn:active { transform: translateY(1px) scale(0.985); }

@media (prefers-reduced-motion: reduce) {
  .btn:active { transform: none; }
  .status-badge, .status-value { transition: none; }
  /* Die Steuerknöpfe heben/stauchen sich beim Bedienen — hier abschalten. Der
     Pending-Spinner bleibt: er ist keine Zierde, sondern die einzige Rückmeldung,
     dass der Befehl unterwegs ist. */
  .ctl-btn:hover, .ctl-btn:active { transform: none; }
  .ctl-btn { transition: none; }
}

.device-card-header {
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.device-card-header-right {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.device-name { font-weight: 600; font-size: 1rem; }
.device-owner { font-size: 0.75rem; color: var(--text-muted); display: block; margin-top: 0.15rem; }

/* Info-modal definition list */
.info-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.65rem 1rem;
  margin: 0;
  align-items: baseline;
}
.info-grid dt {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  font-weight: 600;
  margin: 0;
}
.info-grid dd {
  margin: 0;
  font-size: 0.95rem;
  color: var(--text);
  word-break: break-all;
}
.info-grid .mono {
  font-family: monospace;
  font-size: 0.9rem;
}
.info-signal {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
@media (max-width: 480px) {
  .info-grid { grid-template-columns: 1fr; gap: 0.2rem 0 ; }
  .info-grid dd { margin-bottom: 0.5rem; }
}

/* Modul-Selbsttest der Firmware (Info-Karte): Checkpoint je Sensor
   (INA260 / DS18B20 / DS3231) — erkannt / nicht verfügbar. */
.info-modules-head {
  grid-column: 1 / -1;
  margin-top: 0.35rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--border);
  font-size: 0.72rem !important;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  font-weight: 700;
}
.info-modules-head + .info-modules-head { display: none; }  /* leere zweite Zelle */
.mod-check { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: 600; }
.mod-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; border-radius: 50%;
  font-size: 0.8rem; line-height: 1; color: #fff; flex: 0 0 auto;
}
.mod-ok      { color: var(--success); }
.mod-ok      .mod-ico { background: var(--success); }
.mod-fail    { color: var(--danger); }
.mod-fail    .mod-ico { background: var(--danger); }
.mod-warn    { color: var(--warning); }
.mod-warn    .mod-ico { background: var(--warning); }
.mod-unknown { color: var(--text-muted); }
.mod-unknown .mod-ico { background: var(--text-muted); }

.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 20px;
  font-size: 0.75rem;
  font-weight: 600;
}
.status-online  { background: var(--status-online-bg);  color: var(--status-online-fg); }
.status-offline { background: var(--status-offline-bg); color: var(--status-offline-fg); }
/* Während eines laufenden Firmware-Downloads WECHSELT der Status-Badge selbst auf
   diesen Zustand (kein separater Badge). --warning ist in beiden Themes amber, der
   halbtransparente Hintergrund liegt über der Kartenfarbe → theme-bewusst ohne
   prefers-color-scheme (das würde gegen die manuelle Hell/Dunkel-Wahl arbeiten). */
.status-updating { background: rgba(245, 158, 11, 0.16); color: var(--warning); white-space: nowrap; }
/* "Wartet": frisch registriertes Gerät, das sich noch nie verbunden hat */
.status-waiting { background: var(--badge-user-bg); color: var(--badge-user-fg); }
/* "Nicht freigegeben": Gerät ist nicht in der Admin-Allowlist -> gesperrt */
.status-locked { background: rgba(239, 68, 68, 0.16); color: var(--danger, #ef4444); white-space: nowrap; }
.lock-icon { flex: 0 0 auto; font-size: 1.4rem; line-height: 1; }
.device-card--locked { opacity: 0.92; }

.device-card-body { padding: 1rem 1.25rem; }

/* Platzhalter-Karte "Warte auf Gerätedaten" (Gerät noch nie verbunden) */
.device-waiting-body { display: flex; align-items: center; gap: 0.9rem; }
.waiting-title { font-weight: 600; margin-bottom: 0.2rem; }
.waiting-dot {
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--primary);
  animation: waiting-pulse 1.6s ease-in-out infinite;
}
@keyframes waiting-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50%      { opacity: 1;    transform: scale(1.1); }
}
@media (prefers-reduced-motion: reduce) {
  .waiting-dot { animation: none; }
}

.status-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.status-item { display: flex; flex-direction: column; gap: 0.15rem; }
.status-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  font-weight: 600;
}
.status-value { font-size: 0.9rem; font-weight: 500; font-variant-numeric: tabular-nums; }

.device-card-actions {
  padding: 0.75rem 1.25rem;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Desktop: alle vier Aktions-Buttons in EINER Zeile (Einstellungen neben Log).
   Sie wachsen gleichmäßig auf die Kartenbreite und werden etwas kompakter, damit
   auch „Einstellungen" mit hineinpasst. Auf dem Smartphone (max-width:640px)
   bleibt das gestapelte Umbruch-Layout unverändert. */
@media (min-width: 641px) {
  .device-card-actions .btn {
    flex: 1 1 auto;
    white-space: nowrap;
    font-size: 0.8rem;
    padding-left: 0.6rem;
    padding-right: 0.6rem;
  }
}

/* Contact dots */
.contact-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  vertical-align: middle;
  margin-right: 4px;
}
.contact-open   { background: var(--success); }
.contact-closed { background: var(--danger); }

/* Battery icon + label */
.battery-cell {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  vertical-align: middle;
}
.battery-icon { display: block; }
.battery-text { font-weight: 600; }

/* Climate (DHT22) icon + label — same shape as battery for consistency */
.climate-cell {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  vertical-align: middle;
}
.climate-icon { display: block; flex-shrink: 0; }
.climate-text { font-weight: 600; white-space: nowrap; }

/* ===== Timer table ===== */
.timer-table {
  width: 100%;
  border-collapse: collapse;
}

.timer-table th,
.timer-table td {
  padding: 0.6rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}

.timer-table th {
  font-weight: 600;
  color: var(--text-muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.timer-table input[type="time"],
.timer-table input[type="number"] {
  padding: 0.3rem 0.5rem;
  border: 1.5px solid var(--border);
  border-radius: 6px;
  /* 16px: verhindert iOS-Safari Auto-Zoom beim Fokus (Timer- & Akkutyp-Editor) */
  font-size: 16px;
  background: var(--card);
  color: var(--text);
}


/* ===== Timer (Einzel-Aktions-Liste, BLE-Stil) ===== */
/* Box-Umschalter (segmentiert) */
.tmr-boxsel {
  display: flex;
  gap: 0;
  background: var(--border);
  border-radius: 10px;
  padding: 3px;
  margin: 0.5rem 0;
}
.tmr-boxbtn {
  flex: 1;
  border: none;
  background: transparent;
  padding: 0.5rem;
  border-radius: 8px;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--text-muted);
  cursor: pointer;
  touch-action: manipulation;
}
.tmr-boxbtn.active {
  background: var(--card);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.tmr-status { min-height: 1.2rem; }

/* Wochentags-Übersicht mit Punkten */
.tmr-weekstrip {
  display: flex;
  justify-content: space-between;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 0.5rem 0.25rem;
  margin: 0.35rem 0;
}
.tmr-wd {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.7rem;
  color: var(--text-muted);
}
.tmr-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border); }
.tmr-dot.on { background: var(--success); }
.tmr-dot.prep { background: var(--warning); }

/* Timer-Liste + Zeilen */
.tmr-list { display: flex; flex-direction: column; gap: 0.5rem; }
.tmr-empty { text-align: center; padding: 1rem 0.5rem; font-style: italic; }
.tmr-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 0.5rem 0.6rem;
}
.tmr-day,
.tmr-time {
  box-sizing: border-box;
  height: 38px;                 /* beide exakt gleich hoch */
  border: 1.5px solid var(--border);
  border-radius: 8px;
  background: var(--card);
  color: var(--text);
  padding: 0 0.5rem;
}
/* Feste Breite: die offene Liste zeigt die vollen Namen, das geschlossene Feld
   bleibt schmal (Kurzform) — so verrutscht die Zeile nie, egal welcher Text
   gerade im Feld steht. */
.tmr-day  {
  font-size: 15px;
  font-weight: 600;
  width: 4rem;
  flex: 0 0 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 16px: verhindert iOS-Safari Auto-Zoom beim Fokus.
   KEINE feste Breite: iOS rendert input[type=time] kompakt (nur „12:00", zentriert)
   und soll seine schmale intrinsische Breite behalten. Nur nach OBEN deckeln —
   Samsungs Variante (eigener Chevron) bläht sich sonst ueber den freien Platz auf.
   max-width greift nur dort, wo die intrinsische Breite zu gross ist (Android), auf
   iOS bleibt das Feld unveraendert schmal. */
.tmr-time { font-size: 16px; flex: 0 0 auto; width: auto; max-width: 6rem; }
.tmr-action {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  /* zwischen Uhrzeit und Schalter zentrieren: gleiche auto-Marge wie der Toggle
     -> der freie Platz verteilt sich gleichmäßig auf beide Seiten der Aktion */
  margin-left: auto;
  border: 1.5px solid var(--border);
  background: var(--card);
  border-radius: 999px;
  padding: 0.3rem 0.6rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  touch-action: manipulation;
}
.tmr-action--open  { color: var(--success); border-color: color-mix(in srgb, var(--success) 45%, var(--border)); }
.tmr-action--close { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }
.tmr-action[disabled] { opacity: 0.6; cursor: default; }
/* Aktion NUR per farbigem Pfeil visualisieren (grün ↑ = öffnen, rot ↓ = schließen):
   der Text „Öffnen/Schließen" machte die Zeile unter Android zu breit (passte nicht mehr).
   Label immer ausblenden — die Farbe + Pfeilrichtung tragen die Bedeutung, der Screenreader
   nutzt weiter das aria-label des Buttons. */
.tmr-act-label { display: none; }
.tmr-arrow { font-weight: 800; font-size: 1.15rem; line-height: 1; }
/* Toggle: bewährte Standard-Optik (.toggle 52×30) übernehmen — nur das Layout
   anpassen (rechtsbündig). Eigene Verkleinerung sah im „An“-Zustand unsauber aus. */
.tmr-toggle { margin-left: auto; flex-shrink: 0; }
.tmr-remove {
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.25rem 0.4rem;
  border-radius: 6px;
  flex-shrink: 0;
  touch-action: manipulation;
}
.tmr-remove:hover { color: var(--danger); }
.tmr-add { margin-top: 0.75rem; }

/* Sehr schmale Screens: Zeilen-Abstand leicht reduzieren (der Aktionstext ist
   ohnehin generell ausgeblendet). */
@media (max-width: 380px) {
  .tmr-row { gap: 0.4rem; }
}

/* ===== Toggle switch ===== */
.toggle {
  position: relative;
  display: inline-block;
  width: 52px;
  height: 30px;
  flex-shrink: 0;
  cursor: pointer;
  touch-action: manipulation;
}

.toggle input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
  z-index: 2;
}

.toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--danger);
  border-radius: 30px;
  transition: background 0.2s;
  pointer-events: none;
}

.toggle-slider::before {
  content: '';
  position: absolute;
  width: 24px;
  height: 24px;
  left: 3px;
  top: 3px;
  background: white;
  border-radius: 50%;
  transition: transform 0.2s;
  box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}

.toggle input:checked + .toggle-slider { background: var(--success); }
.toggle input:checked + .toggle-slider::before { transform: translateX(22px); }

.toggle-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}
.toggle-label { font-size: 0.85rem; font-weight: 600; }

/* ===== Settings sections ===== */
.settings-section {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
}
.settings-section h3 { margin-bottom: 0.5rem; font-size: 1rem; }
.settings-section.danger-zone h3 { color: var(--danger); }

/* ===== Battery type cards (admin) — collapsible ===== */
.battery-type-card {
  margin-bottom: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card);
  overflow: hidden;
}

/* Hide native triangle marker, use our own chevron */
.battery-type-card > summary {
  list-style: none;
  cursor: pointer;
  padding: 0.65rem 0.85rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  user-select: none;
  -webkit-user-select: none;
}
.battery-type-card > summary::-webkit-details-marker { display: none; }
.battery-type-card > summary::marker { content: ''; }

.battery-type-summary-main {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-width: 0;
  flex: 1;
}

/* Chevron — points right when collapsed, down when open */
.battery-type-chevron {
  display: inline-block;
  width: 8px;
  height: 8px;
  flex-shrink: 0;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg);
  transition: transform 0.18s ease;
  margin-right: 0.15rem;
  margin-top: -2px;
}
.battery-type-card[open] .battery-type-chevron {
  transform: rotate(45deg);
  margin-top: -4px;
}

.battery-type-actions {
  display: flex;
  gap: 0.4rem;
  flex-shrink: 0;
  flex-wrap: wrap;
}

/* Schmale Screens: Name/Anzahl und Buttons quetschen sich sonst in EINE Zeile
   (der Name bricht mitten im Wort um). Stattdessen: Titelzeile voll breit,
   die Aktions-Buttons rutschen rechtsbündig in eine eigene Zeile darunter. */
@media (max-width: 640px) {
  .battery-type-card > summary { flex-wrap: wrap; }
  .battery-type-summary-main { flex: 1 1 100%; white-space: nowrap; }
  .battery-type-actions { width: 100%; justify-content: flex-end; }
}

/* Voltage table inside an open card — pad to align under the summary */
.battery-type-table {
  font-size: 0.8rem;
  width: calc(100% - 1.7rem) !important;
  margin: 0 0.85rem 0.85rem;
  border-top: 1px solid var(--border);
}

/* ===== Modal ===== */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 1rem;
  backdrop-filter: blur(2px);
}

.modal {
  background: var(--card);
  border-radius: var(--radius);
  padding: 2rem;
  width: 100%;
  max-width: 480px;
  max-height: 90vh;
  overflow-y: auto;
  /* Horizontal NICHT scrollen: absolut positionierte, versteckte Tooltips
     (.info-tip::after, visibility:hidden -> bleiben im Layout) ragten sonst über
     den rechten Rand hinaus und erzeugten eine unschöne horizontale Scrollleiste.
     Inner-Scroller (z.B. Tabellen mit eigenem overflow-x:auto) bleiben unberührt. */
  overflow-x: hidden;
  /* Verhindert "Scroll-Bleed": am Scroll-Ende des Modals scrollt nicht die
     Seite dahinter mit (klassischer iOS-Effekt). */
  overscroll-behavior: contain;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
}

/* Body-Scroll sperren, solange ein Modal offen ist (von modal.js gesetzt).
   overflow:hidden statt position:fixed -> kein iOS-"springt-nach-oben"-Bug. */
body.modal-open { overflow: hidden; }

.modal h2 { margin-bottom: 1.25rem; }
.modal h3 { font-size: 1rem; }

.modal-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
  margin-top: 1.5rem;
  flex-wrap: wrap;
}

/* Kontoeinstellungen: Formular-Aktionen (Passwort/E-Mail ändern) einheitlich —
   gleiche Größe, gleiche Optik, mittig. */
.acc-action-row {
  display: flex;
  justify-content: center;
  margin-top: 1.5rem;
}
.acc-action-btn {
  min-width: 220px;
}

/* ===== Alerts (legacy inline) ===== */
.alert {
  padding: 0.75rem 1rem;
  border-radius: 8px;
  font-size: 0.85rem;
  margin-bottom: 1rem;
}
.alert-error {
  background: var(--alert-error-bg);
  color: var(--alert-error-fg);
  border: 1px solid var(--alert-error-border);
}
/* Empfangsbalken im Einrichtungsguide: Farbe UND Balkenzahl sagen dasselbe, damit die
   Anzeige auch bei Farbenblindheit lesbar bleibt. */
.empfang { font-weight: 600; letter-spacing: .1em; }
.empfang-4 { color: var(--signal-4); }
.empfang-3 { color: var(--signal-3); }
.empfang-2 { color: var(--signal-2); }
.empfang-1 { color: var(--signal-1); }
.empfang-0 { color: var(--signal-0); }

/* Sachhinweis, kein Fehler — z. B. „Felder aus dem Anbieternamen der SIM vorbelegt". */
.alert-info {
  background: var(--alert-info-bg);
  color: var(--alert-info-fg);
  border: 1px solid var(--alert-info-border);
}

/* `.btn-pending` und `.btn-current` sind mit 1.64.129 entfallen: die Steuerknöpfe
   tragen seit 1.64.126 eigene Klassen (`.ctl-btn.is-pending` / `.is-current`), die
   Regeln hatten danach keinen Nutzer mehr im ganzen Repo.
   `@keyframes btn-spin` bleibt — es lebt: `.ctl-btn.is-pending::after` und
   `.btn-loading::after` nutzen es weiter. */
@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

/* "Pulsing dot" used in pending state labels */
.dot-pulse {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--primary);
  margin-left: 0.2rem;
  animation: dot-pulse 1.1s ease-in-out infinite;
  vertical-align: middle;
}
.state-pending {
  color: var(--primary);
  font-weight: 600;
  font-size: 0.85rem;
}
@keyframes dot-pulse {
  0%, 100% { opacity: 0.4; transform: scale(0.85); }
  50%      { opacity: 1;   transform: scale(1.2); }
}

/* (Fund #16) Kurzes Aufblitzen der Karte, zu der eine angetippte Push-Meldung führt.
   Ohne das landet man zwar an der richtigen Stelle, sieht aber nicht, WELCHE Karte
   gemeint war — bei mehreren Geräten untereinander ist das der halbe Nutzen. */
.device-card.device-card--flash {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
}
/* BEWUSST keine `animation` und kein `box-shadow`:
   - `animation` verliert still gegen `aurora.css:73`
     (`:root[data-design-variant="aurora"] .device-card` hat hoehere Spezifitaet und
     setzt dort die Eintritts-Animation `aurora-rise`) — die Hervorhebung waere im
     Aurora-Design unsichtbar gewesen, obwohl die Klasse korrekt am Element haengt.
   - `box-shadow` nutzen die Design-Themes ebenfalls fuer die Kartenkontur.
   `outline` ist an dieser Stelle unbestritten, wirkt in allen Designs und braucht
   keine Sonderbehandlung fuer `prefers-reduced-motion` (es bewegt sich nichts). */

/* Statuszeile, die den Verlauf öffnet (Fund #8).
   Vorher war nur der WERT klickbar (~20 px hoch) und rein optisch von den nicht
   klickbaren Kontakt-Zeilen ununterscheidbar: kein Pfeil, kein Icon, keine Farbe —
   nur `cursor:pointer` (auf Touch bedeutungslos) und ein Hover-Hintergrund. Jetzt
   trägt die ganze Zeile die Aktion, mit sichtbarem Hinweis-Pfeil. */
.status-item-clickable {
  cursor: pointer;
  border-radius: 6px;
  padding: 2px 4px;
  margin: -2px -4px;
  min-height: 44px;          /* Apple HIG — die Zeile ist jetzt das Touch-Ziel */
  transition: background 0.15s;
}
.status-item-clickable:hover,
.status-item-clickable:active {
  background: var(--bg);
}
/* Tastaturfokus muss sichtbar sein — die Zeile ist ab jetzt fokussierbar. */
.status-item-clickable:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
/* .status-item ist ein COLUMN-Flex (Label über Wert) -> Wert und Pfeil brauchen
   eine eigene Zeile, sonst rutschte der Pfeil unter den Wert. */
.status-value-row { display: flex; align-items: center; }
/* Der Pfeil ist reine Affordance (aria-hidden) und darf den Wert nicht verdrängen. */
.status-item-hint {
  margin-left: 0.35rem;
  color: var(--text-muted);
  font-size: 1rem;
  line-height: 1;
  flex: none;
}

/* Device event log */
.device-events-body {
  max-height: 60vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 -0.25rem;
  padding: 0 0.25rem;
}
.event-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.event-row {
  display: grid;
  grid-template-columns: 22px 1fr auto;
  align-items: center;
  gap: 0.35rem 0.6rem;
  padding: 0.55rem 0.4rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
  transition: background 0.4s;
}
/* On narrow phones, drop the timestamp to its own line under the text */
@media (max-width: 480px) {
  .event-row {
    grid-template-columns: 22px 1fr;
  }
  .event-row .event-time {
    grid-column: 2;
  }
}
.event-row:last-child { border-bottom: none; }
.event-icon {
  width: 22px;
  text-align: center;
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1;
}
.event-icon.evt-online  { color: var(--success); }
.event-icon.evt-offline { color: var(--danger); }
.event-icon.evt-open    { color: var(--success); }
.event-icon.evt-close   { color: var(--danger); }
.event-icon.evt-timer   { color: var(--primary); }
.event-text { color: var(--text); }
.event-time {
  font-size: 0.78rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Highlight rows just pushed via WS */
.event-row-new {
  background: color-mix(in srgb, var(--primary) 15%, transparent);
}

/* Battery history chart */
.battery-history-range {
  display: flex;
  gap: 0.4rem;
  margin-bottom: 0.85rem;
  flex-wrap: wrap;
}
.battery-history-body {
  min-height: 240px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.battery-history-loading {
  text-align: center;
  padding: 2rem 1rem;
}
.battery-chart {
  width: 100%;
  height: auto;
  display: block;
}
.battery-history-current {
  margin-top: 0.5rem;
  font-size: 0.9rem;
  color: var(--text);
}

/* Messwert-Anzeige am Zeiger (Akku-/Klima-Verlauf, chart-hover.js) --------- */
.chart-hover-wrap {
  position: relative;          /* Bezugsrahmen fuer den absolut gesetzten Tooltip */
  touch-action: pan-y;         /* vertikales Scrollen im Modal bleibt moeglich */
}
.chart-hover-marker {
  opacity: 0;
  pointer-events: none;        /* Markierung darf die Zeiger-Events nicht abfangen */
  transition: opacity 0.08s ease-out;
}
.chart-hover-marker.is-visible { opacity: 1; }
.chart-hover-line {
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.7;
}
.chart-hover-dot {
  stroke: var(--surface);
  stroke-width: 1.5;
}
.chart-hover-tip {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 5;
  display: none;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--border-strong);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.18);
  font-size: 0.78rem;
  line-height: 1.25;
  white-space: nowrap;
  pointer-events: none;        /* nie selbst Ziel des Zeigers -> kein Flackern */
}
.chart-hover-tip.is-visible { display: block; }
.chart-hover-val { display: block; font-weight: 600; }
.chart-hover-ts  { display: block; color: var(--text-muted); font-size: 0.72rem; }

/* Steuerung modal — boxes side by side, buttons stacked underneath */
.control-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-top: 1rem;
}
/* Nur eine Box (boxes=1): einzelne Karte zentriert statt linksbuendig in der
   halben Breite (die zweite Grid-Spalte entfaellt). */
.control-grid--single {
  grid-template-columns: minmax(0, 320px);
  justify-content: center;
}

.control-box {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card);
}

.control-box-title {
  font-size: 0.95rem;
  font-weight: 700;
  margin-bottom: 0.15rem;
}

.control-box-state {
  font-size: 0.8rem;
  margin-bottom: 0.85rem;
}

/* Steuerknöpfe: drei gleich große Symbol-Quadrate NEBENEINANDER (Mockup 1.64.126).
   Vorher standen sie untereinander und trugen die Wörter „Öffnen/Stop/Schließen".
   `1fr 1fr 1fr` statt flex: die drei bleiben exakt gleich breit, egal wie schmal die
   Kachel wird — bei zwei Boxen nebeneinander auf einem 375-px-Telefon ist das der
   Normalfall, nicht der Sonderfall. */
/* flex statt grid: die drei Knöpfe sollen sich LINKS gruppieren (wie im Mockup) und
   nicht über die volle Breite auseinandergezogen werden. Ein 3-Spalten-Raster
   verteilte die Restbreite als Spalten-Schlupf — die Knöpfe standen dann 30 px
   auseinander statt der gemeinten 9,6.
   `flex: 1 1 0` + `min-width: 0` an `.ctl-btn`: gleich breit, dürfen schrumpfen,
   gedeckelt per max-width. Ohne `min-width: 0` wäre die Basis min-content (44 px aus
   min-height + aspect-ratio) und die Reihe liefe aus ihrer Box heraus, statt sich zu
   teilen. */
.control-box-buttons {
  display: flex;
  justify-content: center;
  gap: 0.6rem;
}

.ctl-btn {
  /* Gleich breit, schrumpffähig, quadratisch, gedeckelt bei 52 px. Die Höhe folgt der
     Breite über `aspect-ratio`; `justify-content: center` an der Reihe zentriert den
     Rest-Platz.
     BEWUSST OHNE `min-height: 44px`: das Touchziel stand hier bis 1.64.127 drin und
     zwang die Knöpfe auf mindestens 44 px — nebeneinander lief die Reihe damit aus
     ihrer Box heraus, statt sich zu teilen. Ohne die Untergrenze schrumpfen sie auf
     das, was da ist (auf der Kachel ~37 px, im Dialog volle 52). Das ist eine bewusste
     Abwägung gegen die 44-px-Empfehlung zugunsten der Nebeneinander-Ansicht. */
  flex: 1 1 0;
  min-width: 0;
  max-width: 52px;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  /* Radius in PROZENT, nicht in Pixeln: die Knöpfe sind je nach Platz 36–52 px groß.
     Ein fester Radius (15px) ergab bei 36 px 42 % der Breite — 50 % wäre ein Kreis,
     die Quadrate sahen also rund aus. 28 % hält die Mockup-Proportion bei jeder Größe. */
  border-radius: 28%;
  cursor: pointer;
  position: relative;
  /* Leichter Glanz von oben — das ist im Mockup das, was die Knöpfe „gedrückt
     werden wollen" lässt. Auf der Farbe darunter, nicht statt ihr. */
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0) 55%);
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
}
.ctl-btn svg {
  width: 46%;
  height: 46%;
  max-width: 30px;
  max-height: 30px;
}
.ctl-btn:hover  { filter: brightness(1.06); transform: translateY(-1px); }
.ctl-btn:active { transform: scale(0.94); }
.ctl-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }

.ctl-btn--open {
  background-color: var(--success);
  color: #fff;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--success) 40%, transparent);
}
.ctl-btn--close {
  background-color: var(--danger);
  color: #fff;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--danger) 40%, transparent);
}
/* Stop ist bewusst NEUTRAL statt farbig: Öffnen und Schließen sind die Aktionen mit
   Folgen, Stop bricht nur ab. Eine dritte Signalfarbe nähme den beiden die Wirkung. */
.ctl-btn--stop {
  background-color: color-mix(in srgb, var(--text) 10%, var(--card));
  color: var(--text);
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.10);
}

/* Aktueller Zustand: Ring um den Knopf, der ihn NICHT verschiebt (box-shadow statt
   border). Ersetzt `.btn-current`, das an `.btn` hing. */
.ctl-btn.is-current {
  box-shadow: 0 0 0 2px var(--card), 0 0 0 4px var(--primary);
}

/* Befehl unterwegs: Symbol ausblenden, Spinner in die MITTE. `.btn-pending` hängte
   den Spinner per margin-left NEBEN die Beschriftung — auf einem Knopf ohne Text
   hätte er das Symbol zur Seite geschoben. */
.ctl-btn.is-pending { cursor: progress; }
.ctl-btn.is-pending svg { visibility: hidden; }
.ctl-btn.is-pending::after {
  content: '';
  position: absolute;
  width: 40%;
  height: 40%;
  max-width: 22px;
  max-height: 22px;
  border: 2.5px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.7s linear infinite;
}

/* Box 1 + Box 2 stehen ab Tablet-Breite nebeneinander (2 Spalten); auf dem Telefon
   untereinander — s. Mobile-Media-Block weiter unten samt Begründung (drei
   Symbol-Knöpfe brauchen die volle Breite, um 44 px zu erreichen). */

/* ===== Empty state ===== */
.empty-state {
  text-align: center;
  padding: 3rem 1rem;
  color: var(--text-muted);
}
.empty-state h3 { font-size: 1.1rem; margin-bottom: 0.5rem; color: var(--text); }
.empty-state p  { font-size: 0.9rem; margin-bottom: 1.5rem; }

/* App-Footer (ganz unten): Impressum für alle, SW-Version nur für Admins */
.app-footer {
  margin-top: 2.5rem;
  /* Kein eigener Home-Indicator-Abstand mehr: der Footer ist der letzte Knoten im
     Fluss, darunter liegt bereits das padding-bottom des body (#12 — der Abstand
     wurde sonst doppelt gezaehlt). */
  padding: 1rem 1.5rem;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem 1rem;
  flex-wrap: wrap;
  font-size: 0.75rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Als Link gestylter Button (Footer / Login-Seite) */
.footer-link {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.footer-link:hover { color: var(--primary); }
.footer-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px; }
.login-legal {
  margin-top: 1.25rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* Impressum-Modal */
.legal h3 { margin: 1.1rem 0 0.3rem; font-size: 0.95rem; }
.legal p  { margin: 0 0 0.4rem; line-height: 1.45; }

/* Warnung: Geräte-Uhr nicht synchron (Info-Modal) */
.clock-warn { color: var(--danger); font-weight: 600; }

/* Push-Benachrichtigungs-Schalter im Geräte-Einstellungen-Modal */
.notif-prefs { display: flex; flex-direction: column; gap: 0.45rem; margin-top: 0.5rem; }
.notif-pref { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; cursor: pointer; color: var(--text); }
.notif-pref-label { flex: 1; }

/* Warnung ohne den nötigen Sensor: INA260 bzw. Temperatursensor sind optionale
   Komponenten. Fehlen sie, kann die Warnung nie ausgelöst werden — Zeile ausgegraut,
   Schalter gesperrt, mit Begründung darunter. Bewusst nur gedämpft und nicht
   ausgeblendet: der Nutzer soll sehen, DASS es die Warnung gibt und WARUM sie hier
   nicht greift. */
.notif-pref-na { opacity: 0.55; cursor: default; }
.notif-pref-na-hint {
  display: block;
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--text-muted);
}

/* Verzögerungs-Auswahl für die Offline-Meldung (unter dem Online-Toggle,
   eingerückt auf Höhe des Toggle-Labels) */
.notif-pref-delay {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.1rem 0 0.25rem 2.9rem;
  font-size: 0.85rem;
  color: var(--text-muted);
  flex-wrap: wrap;
}
.notif-pref-delay span { display: inline-flex; align-items: center; }
.notif-pref-delay select {
  padding: 0.3rem 0.5rem;
  border: 1.5px solid var(--border);
  border-radius: 8px;
  background: var(--card);
  color: var(--text);
  font-size: 16px;          /* iOS: kein Auto-Zoom beim Fokus */
}

/* kleine Variante des Schiebeschalters */
.toggle.toggle-sm { width: 40px; height: 22px; }
.toggle.toggle-sm .toggle-slider { border-radius: 22px; }
.toggle.toggle-sm .toggle-slider::before { width: 16px; height: 16px; left: 3px; top: 3px; }
.toggle.toggle-sm input:checked + .toggle-slider::before { transform: translateX(18px); }

/* Aufklappbarer Einstellungs-Abschnitt (<details>) */
.settings-collapse > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
}
.settings-collapse > summary::-webkit-details-marker { display: none; }
.settings-collapse > summary::after {
  content: '';
  flex: none;
  width: 8px; height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.2s var(--ease);
  opacity: 0.55;
}
.settings-collapse[open] > summary::after { transform: rotate(-135deg); }
.settings-collapse .collapse-body { margin-top: 1rem; }
.settings-collapse .collapse-body h3:first-child { margin-top: 0; }
.collapse-subhead { margin-top: 1.5rem; }

/* ===== Geräteeinstellungen: Master-Detail-Navigation (Unterseiten) ===== */
.settings-menu {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1rem;
}
.settings-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.9rem 1rem;
  font: inherit;
  font-weight: 600;
  color: var(--text);
  text-align: left;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease);
}
.settings-menu-item:hover { border-color: var(--primary); }
.settings-menu-item:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.settings-menu-chev { flex: none; color: var(--text-muted); font-size: 1.3em; line-height: 1; }
/* Symbol + Text als eine Einheit links, damit `justify-content: space-between` am
   Knopf weiterhin nur zwei Kinder gegeneinander schiebt (Label | Chevron).
   `min-width: 0` + Umbruchschutz: lange Bezeichnungen sollen den Chevron nicht
   aus der Zeile druecken. */
.settings-menu-label {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
}
/* Gedaempft wie das Chevron: das Symbol ordnet ein, es ist nicht die Aussage —
   die traegt der Text. `flex: none` haelt es bei langen Labels auf 20px. */
.settings-menu-icon { flex: none; color: var(--text-muted); }
.settings-menu-item:hover .settings-menu-icon { color: var(--primary); }
.settings-page-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.25rem 0 1rem;
}
.settings-page-title { margin: 0; font-size: 1.1rem; }
.settings-back {
  flex: none;
  background: none;
  border: none;
  color: var(--primary);
  font-size: 1.6rem;
  line-height: 1;
  padding: 0.1rem 0.5rem 0.1rem 0;
  cursor: pointer;
}
.settings-back:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 6px; }
.settings-page .collapse-body { margin-top: 0.5rem; }
.settings-page .collapse-body h3:first-child { margin-top: 0; }

/* ===== Funksensoren: Karten wie in der iOS-App ============================ */
/* Aufbau bewusst gleich: Übersicht mit zwei Einträgen, dahinter je eine Seite aus
   abgesetzten Karten (Messwert / Verlauf / Gerät / Entfernen). Wer beide Oberflächen
   benutzt, soll dasselbe Bild vorfinden. */
.radio-card {
  margin: 0 0 1rem;
  padding: 1rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
}
.radio-card > *:first-child { margin-top: 0; }
.radio-card > *:last-child { margin-bottom: 0; }
.radio-card-title {
  margin: 0 0 0.6rem;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.radio-card .btn { margin-top: 0.2rem; }
.radio-card select,
.radio-card input[type="text"] { width: 100%; }

/* Der Messwert ist die Antwort auf die Frage, mit der man die Seite aufruft —
   entsprechend gross. Ziffern in fester Breite, damit die Zahl beim Aktualisieren
   nicht zappelt. */
.radio-reading {
  margin: 0 0 0.15rem;
  font-size: 1.5rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.radio-age { margin: 0 0 0.6rem; font-size: 0.82rem; color: var(--text-muted); }
.radio-error { margin: 0.6rem 0 0.2rem; font-size: 0.82rem; }
.radio-error span { color: var(--text-muted); }
.radio-warn { color: var(--warning); }

.radio-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}
.radio-row:last-of-type { border-bottom: none; }
.radio-row > span:last-child,
.radio-row label { color: var(--text-muted); }
.radio-row select { width: auto; max-width: 60%; }

/* Zeile der Übersicht + Modellauswahl + Suchtreffer: eine Schaltfläche über die ganze
   Breite, Symbol links, Chevron rechts (Gegenstück zu RadioSensorEntry). */
.radio-entry,
.radio-hit,
.radio-link {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  width: 100%;
  margin-bottom: 0.6rem;
  padding: 0.85rem 1rem;
  font: inherit;
  color: var(--text);
  text-align: left;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  transition: border-color 0.15s var(--ease);
}
.radio-link { justify-content: space-between; margin-bottom: 0.4rem; }
.radio-hit { justify-content: space-between; }
.radio-entry:hover:not(:disabled),
.radio-hit:hover:not(:disabled),
.radio-link:hover { border-color: var(--primary); }
.radio-entry:focus-visible,
.radio-hit:focus-visible,
.radio-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.radio-entry:disabled,
.radio-hit:disabled { opacity: 0.55; cursor: not-allowed; }
/* In der Karte sitzen die Zeilen schon in einem Rahmen — dort ohne zweiten. */
.radio-card .radio-entry,
.radio-card .radio-hit,
.radio-card .radio-link { background: none; }
.radio-entry-icon { flex: none; color: var(--text-muted); }
.radio-entry:hover:not(:disabled) .radio-entry-icon { color: var(--primary); }
.radio-entry-text {
  display: flex;
  flex-direction: column;
  gap: 0.12rem;
  min-width: 0;
  flex: 1;
}
.radio-entry-title { font-weight: 600; }
.radio-entry-sub { font-size: 0.82rem; color: var(--text-muted); }

/* Laufende Funkarbeit: eine Uhr, die sich sichtbar bewegt. Eine Minute ohne jede
   Rückmeldung hält jeder für einen Fehler — die Box kann aber nicht schneller sein
   als der Sensor, der nur etwa einmal pro Minute sendet. */
.radio-countdown {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.6rem 0 0.2rem;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.radio-spinner {
  flex: none;
  width: 14px; height: 14px;
  border: 2px solid var(--border);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: radio-spin 1s linear infinite;
}
@keyframes radio-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .radio-spinner { animation: none; } }

.radio-offline {
  margin: 0 0 0.9rem;
  padding: 0.7rem 0.9rem;
  font-size: 0.85rem;
  color: var(--warning);
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  border-radius: 12px;
}
/* Ungültige Adresse: sichtbar, bevor die Box sie ablehnt. */
.input-invalid { border-color: var(--danger) !important; }

/* ===== Interface-Kachel: Schnellsteuerung inline + Nächste Timer ===== */
/* Schnellsteuerung auf der Kachel: eigener Block mit Trennstrich + Überschrift
   „Steuerung" (analog zum „Nächste Timer"-Block). */
.tile-controls-block {
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}
.tile-controls-block .tile-controls { margin-top: 0; }
.tile-controls { margin-top: 0.75rem; }

/* Schnellsteuerung auf der Kachel: Box 1 und Box 2 nebeneinander (zwei Spalten aus
   `.control-grid`) — ausdrücklicher Wunsch, mit einem gemessenen Preis:
   `.device-grid` rastert die Kacheln mit `minmax(340px, 1fr)`, eine Kachel ist also
   IMMER ~341 px schmal, egal wie groß der Bildschirm ist. Eine halbe Kachel hat damit
   ~115-125 px Innenbreite; drei Knöpfe nebeneinander werden darin ~37 px groß — UNTER
   dem 44-px-Touchziel. Das ist der Grund, warum sie in 1.64.126 untereinander standen.
   Getragen wird das von `.ctl-btn { flex: 1 1 0; min-width: 0 }`: die Knöpfe schrumpfen
   selbsttätig auf den verfügbaren Platz, statt aus der Box zu laufen. Im Dialog und bei
   nur EINER Box greift stattdessen der Deckel (max-width) -> dort volle 52 px.
   Wer das zurückdreht: hier `grid-template-columns: minmax(0, 1fr)` setzen (0,2,0
   schlägt `.control-grid--single`) und `min-height: 44px` an `.ctl-btn` zurückholen. */
.tile-next-timers {
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}
.tile-controls-head,
.tile-next-timers-head {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 0.35rem;
}
.tile-next-timers ul { list-style: none; margin: 0; padding: 0; }
.tile-next-timers li {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0;
  font-size: 0.85rem;
}
.tile-next-timers .nt-when { font-weight: 600; white-space: nowrap; }
.tile-next-timers .nt-box  { text-align: center; color: var(--text-muted); }
.tile-next-timers .nt-act  { text-align: right; font-size: 1.15rem; font-weight: 700; line-height: 1; }
.tile-next-timers .nt-act.nt-open  { color: var(--success); }
.tile-next-timers .nt-act.nt-close { color: var(--danger); }
.tile-next-timers li.nt-empty { display: block; color: var(--text-muted); font-style: italic; }

/* Info-Icon mit Tooltip (Hover am Desktop, Antippen/Fokus am Smartphone) */
.info-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 16px;
  height: 16px;
  margin-left: 0.35rem;
  border: 1.5px solid var(--text-muted);
  border-radius: 50%;
  color: var(--text-muted);
  font: italic 700 11px/1 Georgia, "Times New Roman", serif;
  cursor: help;
  user-select: none;
  vertical-align: middle;
  position: relative;
}
.info-tip:hover, .info-tip:focus-visible { border-color: var(--primary); color: var(--primary); outline: none; }
.info-tip::after {
  content: attr(data-tip);
  position: absolute;
  left: 0;
  top: calc(100% + 6px);
  z-index: 60;
  width: max-content;
  max-width: min(260px, calc(100vw - 1.5rem));
  white-space: normal;
  text-align: left;
  background: var(--text);
  color: var(--card);
  font: 400 0.78rem/1.35 system-ui, sans-serif;
  padding: 0.45rem 0.6rem;
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-2px);
  transition: opacity 0.15s var(--ease), transform 0.15s var(--ease);
  pointer-events: none;
}
.info-tip:hover::after,
.info-tip:focus::after,
.info-tip:focus-visible::after { opacity: 1; visibility: visible; transform: none; }
/* In der Danger-Zone steht das Info-Icon weit rechts in der Zeile -> Tooltip
   nach LINKS öffnen, damit er bei aktivem overflow-x:hidden des Modals nicht
   am rechten Rand abgeschnitten wird. */
.danger-zone .info-tip::after { left: auto; right: 0; }

/* Feste Aktionsleiste am Ende des (langen) Geräte-Einstellungen-Sheets (Fund #5).
   Vorher stand hier NUR „Schließen“ sticky, während „Konfiguration speichern“ ans
   Ende einer bildschirmfüllenden Seite gescrollt werden musste: der dauerhaft
   sichtbare Knopf war ausgerechnet der verwerfende. Jetzt liegen beide zusammen,
   kontextabhängig (Menü: Schließen · Unterseite: ‹ Zurück [+ Speichern]). */
.settings-action-bar {
  position: sticky;
  bottom: 0;
  margin-top: 1.25rem;
  padding-top: 0.75rem;
  padding-bottom: calc(0.4rem + env(safe-area-inset-bottom));
  background: var(--card);
  border-top: 1px solid var(--border);
}
.settings-action-row { display: flex; gap: 0.6rem; }
/* Beide Knöpfe teilen sich die Breite; allein stehend füllt einer sie ganz aus. */
.settings-action-row .btn { flex: 1; min-height: 44px; }
.settings-dirty-hint {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  /* Bewusst NICHT var(--warning) (#f59e0b): auf dem hellen --card liegt das bei
     ~2:1 Kontrast und ist als Text kaum lesbar. Amber-700 erreicht dort AA.
     Auf dunklem Grund ist es umgekehrt — dort trägt --warning. */
  color: #b45309;
}
:root[data-theme="dark"] .settings-dirty-hint { color: var(--warning); }

/* PWA-Installations-Hinweis (untere Leiste, verwerfbar) */
.install-hint {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 150;                 /* über Inhalt, unter Modal-Overlay (200) */
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.7rem 1rem calc(0.7rem + env(safe-area-inset-bottom));
  background: var(--card);
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 16px rgba(0,0,0,0.12);
  font-size: 0.85rem;
  color: var(--text);
}
.install-hint-text { flex: 1; }
.install-hint .btn { flex: none; }
.install-hint-x {
  flex: none;
  background: none; border: none; cursor: pointer;
  width: 2rem; height: 2rem; border-radius: 8px;
  font-size: 1.4rem; line-height: 1; color: var(--text-muted);
}
.install-hint-x:hover { color: var(--text); }

/* Der Hinweis ist position:fixed und damit aus dem Fluss — ohne Ausgleich liegt er
   ueber dem Seitenende (App-Footer, letzte Aktionszeile der Geraetekarte). Die Hoehe
   liefert install-hint.js gemessen, weil der Text je nach Geraetebreite ein- oder
   zweizeilig umbricht; ein fester Wert waere in einem der beiden Faelle falsch.
   Der Messwert enthaelt die Safe-Area der Leiste bereits und ERSETZT das
   padding-bottom aus dem body-Kurzschreibweise-padding komplett — addieren wuerde
   den Inset doppelt zaehlen. Der Fallback greift nur, falls die Messung ausfaellt. */
body.has-install-hint { padding-bottom: var(--install-hint-h, 100px); }

/* ===== Section title ===== */
.section-title {
  font-size: 1.25rem;
  font-weight: 700;
  margin-bottom: 1rem;
}

/* ===== Admin user table ===== */
.user-table {
  width: 100%;
  border-collapse: collapse;
}
.user-table th,
.user-table td {
  padding: 0.6rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.user-table th {
  font-weight: 600;
  color: var(--text-muted);
  font-size: 0.75rem;
  text-transform: uppercase;
}

/* Horizontaler Scroll als Absicherung, falls die Tabelle breiter als die
   (overflow:hidden) Card wird — verhindert abgeschnittene Aktions-Buttons. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* Auf schmalen Phones die "Erstellt"-Spalte (3.) der BENUTZER-Tabelle ausblenden,
   damit Name, Rolle und die Aktions-Buttons ohne Quetschen nebeneinander passen.
   WICHTIG: nur .users-table — andere .user-table (z.B. Firmware-Liste) haben in
   Spalte 3 die Aktions-Buttons und dürfen sie NICHT ausblenden. */
@media (max-width: 480px) {
  .users-table th:nth-child(3),
  .users-table td:nth-child(3) { display: none; }
  .user-table th, .user-table td { padding: 0.5rem 0.5rem; }
}

.badge {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 12px;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
}
.badge-admin { background: var(--badge-admin-bg); color: var(--badge-admin-fg); }
.badge-user  { background: var(--badge-user-bg);  color: var(--badge-user-fg);  }

.tab-panel { display: none; }
.tab-panel.active { display: block; }

/* ===== Toasts ===== */
/* Gemeinsamer Stack für beide Toast-Live-Regions (polite + assertive): nur ER
   ist fixed positioniert; die Regions fließen darin untereinander, statt sich
   an identischer Position zu überlappen (toast.js). */
#toast-stack {
  position: fixed;
  top: calc(64px + env(safe-area-inset-top, 0));
  right: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 9999;
  pointer-events: none;
}

.toast-container {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  pointer-events: none;
}
.toast-container:empty { display: none; }

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.7rem 1rem;
  border-radius: 10px;
  font-size: 0.9rem;
  font-weight: 500;
  box-shadow: var(--shadow-md);
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--text);
  min-width: 220px;
  max-width: 360px;
  cursor: pointer;
  opacity: 0;
  transform: translateX(20px);
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
}
.toast-icon {
  flex-shrink: 0;
  font-size: 1rem;
  font-weight: 700;
  line-height: 1;
}
.toast.toast-in { opacity: 1; transform: translateX(0); }
.toast.toast-out { opacity: 0; transform: translateX(20px); }

.toast-success {
  background: var(--alert-success-bg);
  color: var(--alert-success-fg);
  border-color: var(--alert-success-border);
}
.toast-error {
  background: var(--alert-error-bg);
  color: var(--alert-error-fg);
  border-color: var(--alert-error-border);
}
.toast-info {
  /* Uses default card colors */
}

/* ===== Offline banner ===== */
.offline-banner {
  position: sticky;
  top: 0;
  z-index: 150;
  background: var(--warning);
  color: #1f1300;
  text-align: center;
  font-size: 0.85rem;
  font-weight: 600;
  /* Das Banner ist der erste Knoten im body (offline.js: body.prepend) und klebt
     oben — wie beim .app-header traegt es den oberen Inset deshalb selbst. */
  padding: calc(0.5rem + env(safe-area-inset-top, 0px)) 1rem 0.5rem;
  border-bottom: 1px solid rgba(0,0,0,0.1);
}
/* (Fund #9) Banner und Header kleben BEIDE bei top:0, liegen aber in getrennten
   Teilbäumen (offline.js hängt den Banner an den body, der Header steckt in
   #app-page). Ungescrollt stehen sie untereinander; gescrollt rasten beide bei 0 ein
   und der Banner (z-index 150) schneidet die obere Hälfte des Headers (z-index 100)
   ab. Der Header muss deshalb um die Bannerhöhe tiefer einrasten.

   `--banner-h` misst offline.js nach, statt es hier zu raten: der Bannertext bricht
   auf schmalen Handys zweizeilig um (~33 px → ~50 px). Die bisherige Annahme
   `96px = 64 + 32` lag schon einzeilig 1 px daneben.

   WICHTIG (Zusammenspiel mit #13): bei sichtbarem Banner trägt der BANNER den oberen
   Safe-Area-Inset, und `--banner-h` enthält ihn damit bereits. Der Header muss seinen
   eigenen Inset dann abwerfen — sonst stünde er doppelt und es bliebe ein toter
   Streifen unter dem Banner. Auf dem Desktop (env = 0) wäre das unsichtbar. */
.has-banner .app-header {
  top: var(--banner-h, 33px);
  padding-top: 0;
  height: 64px;
}
/* Toasts bündig unter der Headerkante. Kein env() mehr: der Inset steckt in --banner-h. */
.has-banner #toast-stack { top: calc(var(--banner-h, 33px) + 64px); }

/* ===== Responsive (phones / iPhone) ===== */
@media (max-width: 640px) {
  .main-content { padding: 0.85rem; }
  .device-grid { grid-template-columns: 1fr; gap: 0.85rem; }
  .status-grid { grid-template-columns: 1fr 1fr; gap: 0.65rem 0.85rem; }
  .app-header { padding: 0 0.85rem; }
  .nav-tabs {
    padding: 0 0.85rem;
    /* Smoother momentum scrolling for the tab strip */
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .nav-tabs::-webkit-scrollbar { display: none; }
  .nav-tab { padding: 0.85rem 1rem; }

  .timer-table { font-size: 0.85rem; }
  .timer-table th, .timer-table td { padding: 0.45rem 0.4rem; }
  .form-grid-2 { grid-template-columns: 1fr; }

  /* Toasts span full width on phones */
  #toast-stack { right: 0.5rem; left: 0.5rem; }
  .toast { max-width: none; }

  /* Cards & login don't need huge padding on phone */
  .card { padding: 1rem; }
  .login-card { padding: 1.75rem 1.25rem; }

  /* Device card actions sit on their own row, each button stretches */
  .device-card-actions {
    /* Bewusst ohne Safe-Area-Inset (#12): die Leiste sitzt in JEDER Geraetekarte,
       nicht nur in der letzten — der Home-Indicator-Abstand landete so unter jeder
       einzelnen Karte. Den Abstand am Seitenende liefert das padding-bottom des
       body. */
    padding: 0.65rem 0.85rem;
  }
  .device-card-actions .btn { flex: 1 1 30%; }

  /* --------- Bottom-sheet modal ---------
     Slides up from the bottom of the screen with a finger-friendly drag handle.
     Keeps the safe-area bottom inset so the last button never sits under the
     home indicator on iPhones without a home button. */
  .modal-overlay {
    align-items: flex-end;
    padding: 0;
  }
  .modal {
    max-width: 100%;
    width: 100%;
    /* (Fund #6) Dreistufig, jede Stufe ueberschreibt die vorige, wo unterstuetzt:
         1. 92vh            — Fallback fuer alte Browser
         2. 92dvh           — folgt der ein-/ausfahrenden URL-Leiste
         3. --vvh           — die TATSAECHLICH sichtbare Hoehe (visualViewport, modal.js).
       Nur Stufe 3 beruecksichtigt die Bildschirmtastatur auf iOS: dort schrumpft weder
       vh noch dvh, wenn sie aufgeht — das Sheet (und mit ihm die Aktionsleiste) lag
       sonst dahinter. */
    max-height: 92vh;
    max-height: 92dvh;
    max-height: calc(var(--vvh, 92dvh) - 2rem);
    border-radius: 18px 18px 0 0;
    padding: 1rem 1.1rem;
    padding-top: 1.5rem;
    padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0));
    /* Visual drag-handle hint at the top of the sheet */
    background-image:
      linear-gradient(var(--border-strong), var(--border-strong));
    background-position: 50% 0.5rem;
    background-size: 40px 4px;
    background-repeat: no-repeat;
    animation: sheet-up 0.22s ease-out;
  }
  .modal h2 { margin-bottom: 1rem; font-size: 1.1rem; }
  .modal-actions .btn { flex: 1 1 45%; }

  /* Steuerung: Box 1 und Box 2 bleiben auf dem Telefon NEBENEINANDER (seit 1.64.128
     wieder). Jedes Pixel, das die Abstände hier hergeben, geht direkt in die
     Knopfgröße — bei ~115 px Innenbreite je Box zählt das:
       Knopfgröße = (Innenbreite − 2 × Knopf-Abstand) / 3
     Deshalb enger als am Desktop. Kleiner als hier geht nur noch über die Knöpfe
     selbst, und die sind mit ~37 px bereits unter dem 44-px-Touchziel. */
  .control-grid { gap: 0.5rem; }
  .control-box { padding: 0.85rem 0.55rem; }
  .control-box-buttons { gap: 0.3rem; }
  /* Kein eigener Größen-Deckel mehr fürs Telefon: die Knöpfe sind generell auf 52 px
     gedeckelt und schrumpfen auf der Kachel ohnehin unter diesen Wert. Der frühere
     Deckel (64) machte sie hier GRÖSSER als am Desktop — das Gegenteil seiner Absicht. */

  /* Bigger primary action buttons on touch */
  .login-card .btn-block { min-height: 48px; font-size: 1rem; }

}

@keyframes sheet-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* Very narrow phones (iPhone SE width ~375) */
@media (max-width: 380px) {
  .header-right { gap: 0.5rem; }
  /* Benutzername bleibt sichtbar (Einstieg ins Konto), wird aber gekürzt,
     damit "Abmelden" nicht aus dem Header gedrückt wird. */
  .user-info-btn .user-name { max-width: 90px; }
}

/* =====================================================================
   DARK MODE
   Applied when <html data-theme="dark"> is set. The actual theme is
   resolved in JS (modules/theme.js) — "system" mode reads
   prefers-color-scheme and sets the attribute accordingly, so we only
   need one selector here.
   ===================================================================== */
/* Native Widgets (Time-Picker, Checkboxen, Scrollbalken, Autofill) folgen dem
   ERZWUNGENEN App-Theme, nicht dem OS: 'light dark' auf den Formularfeldern
   ließ den Browser nach OS-Einstellung wählen — dunkle native Controls auf
   heller App (und umgekehrt). color-scheme vererbt sich von hier auf alles. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

:root[data-theme="dark"] {
  --bg: #0f172a;
  --card: #1e293b;
  --card-elevated: #273449;
  --text: #f1f5f9;
  --text-muted: #94a3b8;
  --border: #334155;
  --border-strong: #475569;

  /* Scrollbalken — dunkel (gedämpft, nicht das helle OS-Default) */
  --scrollbar-thumb: #475569;
  --scrollbar-thumb-hover: #64748b;
  --scrollbar-track: transparent;

  --status-online-bg:  #14532d;
  --status-online-fg:  #86efac;
  --status-offline-bg: #7f1d1d;
  --status-offline-fg: #fecaca;

  --alert-error-bg: #7f1d1d;
  --alert-error-fg: #fecaca;
  --alert-error-border: #b91c1c;
  --alert-success-bg: #14532d;
  --alert-success-fg: #86efac;
  --alert-success-border: #166534;
  --alert-info-bg: #1e3a5f;
  --alert-info-fg: #bfdbfe;
  --alert-info-border: #1d4ed8;
  --signal-4: #86efac;
  --signal-3: #bef264;
  --signal-2: #fde047;
  --signal-1: #fdba74;
  --signal-0: #fca5a5;

  --badge-admin-bg: #4c1d95;
  --badge-admin-fg: #ddd6fe;
  --badge-user-bg:  #075985;
  --badge-user-fg:  #bae6fd;

  --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.45);
  --shadow-lg: 0 4px 24px rgba(0,0,0,0.5);
}

/* Dark-Mode-Politur: Karten auf die etwas hellere "elevated"-Fläche heben,
   damit sie sich klarer vom dunklen Hintergrund abheben (mehr Tiefe). */
:root[data-theme="dark"] .device-card,
:root[data-theme="dark"] .card,
:root[data-theme="dark"] .login-card { background: var(--card-elevated); }

:root[data-theme="dark"] .modal-overlay { background: rgba(0,0,0,0.6); }
:root[data-theme="dark"] .toggle-slider::before { background: #f1f5f9; }

/* ===== Theme toggle button ===== */
.theme-toggle {
  background: transparent;
  border: 1.5px solid var(--border-strong);
  color: var(--text);
  width: 40px;
  height: 40px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: border-color 0.2s, color 0.2s, background 0.2s;
  touch-action: manipulation;
}
.theme-toggle:hover {
  border-color: var(--primary);
  color: var(--primary);
}
.theme-toggle svg {
  width: 18px;
  height: 18px;
  display: block;
}

/* Floating theme toggle for the login screen (header isn't visible there).
   The app.js init shows/hides this depending on which page is active so it
   doesn't double-stack with the header copy. */
.theme-toggle-floating {
  position: fixed;
  top: calc(1rem + env(safe-area-inset-top, 0));
  right: calc(1rem + env(safe-area-inset-right, 0));
  background: var(--card);
  box-shadow: var(--shadow-sm);
  z-index: 150;
}

/* --- Log: zusaetzliche Event-Typen (Befehl/Neustart/OTA) --- */
.event-icon.evt-command    { color: var(--primary); }
.event-icon.evt-restart    { color: var(--text-muted); }
.event-icon.evt-ota        { color: var(--primary); }
.event-icon.evt-ota-failed { color: var(--danger); }
/* Uebersprungener Timer (Kontakt offen): gedaempft darstellen */
.event-row-skipped .event-text { color: var(--text-muted); font-style: italic; }
.event-row-skipped .event-icon { color: var(--text-muted); }
.event-icon.evt-firmware   { color: var(--primary); }

/* "Update verfügbar"-Pfeil auf der Geräte-Karte (orange, dezenter Puls) */
.update-badge {
  color: #f97316;            /* orange */
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  cursor: help;
  animation: update-pulse 2s ease-in-out infinite;
}
@keyframes update-pulse {
  0%, 100% { opacity: 1;   transform: translateY(0); }
  50%      { opacity: 0.6; transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .update-badge { animation: none; }
}
.update-badge-clickable { cursor: pointer; border-radius: 50%; transition: transform 0.12s; }
.update-badge-clickable:hover { transform: scale(1.18); }
.update-badge-clickable:focus-visible { outline: 2px solid #f97316; outline-offset: 2px; }

/* Aktuelle Zeitanzeige oben im Timer-Modal */
.timer-now {
  font-weight: 600;
  color: var(--text);
  margin-bottom: 0.35rem;
  font-variant-numeric: tabular-nums;
}

/* Update-Pfeil: größere Touch-Fläche (Icon bleibt 22px, Padding erweitert die
   klickbare Zone ~auf 42px; negativer Margin verhindert Layout-Verschiebung) */
.update-badge-clickable { padding: 10px; margin: -10px; }

/* File-Input (Firmware-Upload) darf die Karte auf schmalen Phones nicht sprengen */
#firmware-file { max-width: 100%; }

/* Event-Log: Einstellung geändert / Log geleert */
.event-icon.evt-settings { color: var(--primary); }
.event-icon.evt-log      { color: var(--text-muted); }

/* ===========================================================================
   Verbesserungen: Confirm-Dialog, Button-Loading, Skeleton, Last-Seen,
   Unsaved-Hinweis  (UI-Feinschliff)
   =========================================================================== */

/* --- Bestätigungsdialog (ui.js confirmDialog) — liegt ÜBER dem Haupt-Modal --- */
.confirm-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.45);
  display: flex; align-items: center; justify-content: center;
  z-index: 300;                /* > .modal-overlay (200) */
  padding: 1rem;
  backdrop-filter: blur(2px);
  opacity: 0; transition: opacity 0.16s ease;
}
.confirm-overlay.confirm-in  { opacity: 1; }
.confirm-overlay.confirm-out { opacity: 0; }
.confirm-box {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 1.25rem 1.25rem 1rem;
  width: 100%; max-width: 400px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.25);
  transform: translateY(8px) scale(0.98);
  transition: transform 0.16s ease;
}
.confirm-overlay.confirm-in .confirm-box { transform: none; }
.confirm-title { font-size: 1.05rem; margin: 0 0 0.5rem; }
.confirm-msg   { color: var(--text-muted); font-size: 0.92rem; line-height: 1.45; margin: 0 0 1.1rem; white-space: pre-line; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 0.6rem; }
.confirm-actions .btn { min-height: 40px; }

/* --- Button-Ladezustand (ui.js runWithLoading) --- */
.btn-loading { position: relative; color: transparent !important; pointer-events: none; }
.btn-loading::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  /* currentColor ist transparent (s.o.) -> Spinner über border-Trick sichtbar machen */
  color: #fff;
  animation: btn-spin 0.6s linear infinite;
}
.btn-outline.btn-loading::after { color: var(--text); }

/* --- Skeleton-Ladekarten (devices.js renderSkeletons) --- */
.skeleton-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 1rem;
  min-height: 168px;
}
.skeleton {
  background: linear-gradient(90deg, var(--border) 25%, var(--border-strong) 37%, var(--border) 63%);
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.4s ease infinite;
  border-radius: 6px;
}
.skeleton-line { height: 12px; margin-bottom: 0.6rem; }
@keyframes skeleton-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
  .btn-loading::after { animation-duration: 1.2s; }
  .device-card:hover { transform: none; }
}

/* --- „zuletzt gesehen“ auf der Geräte-Karte (offline) --- */
.device-last-seen {
  display: block;
  font-size: 0.72rem;
  color: var(--text-muted);
  margin-top: 0.25rem;
  white-space: nowrap;
}

/* --- „● ungespeichert“-Hinweis (Timer-Modal) --- */
.unsaved-hint {
  color: var(--warning);
  font-size: 0.8rem;
  align-self: center;
  margin-right: auto;   /* schiebt die Buttons nach rechts */
}

/* --- API-Zugriff: read-only Geräte-Tokens (Geräteeinstellungen) --- */
.api-token-list { display: flex; flex-direction: column; gap: 0.5rem; }
.api-token-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0.7rem;
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.api-token-meta { min-width: 0; }
.api-token-name { font-weight: 600; }
.api-token-reveal {
  margin-top: 0.75rem;
  padding: 0.7rem;
  background: var(--alert-success-bg);
  border: 1px solid var(--alert-success-border);
  border-radius: var(--radius);
}
.api-token-value-row { display: flex; align-items: center; gap: 0.5rem; }
.api-token-value {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-all;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.4rem 0.5rem;
}

/* ==========================================================================
   Kamera-Archiv (Foto-Dialog): Fortschrittsbalken, Tages-Liste,
   Platzhalter-Zeile beim Upload, Wisch-Löschen mit „Rückgängig".
   ========================================================================== */
.cam-progress {
  display: none;
  margin-bottom: 0.85rem;
  padding: 0.6rem 0.75rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.cam-progress.is-active { display: block; }
.cam-progress-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text);
}
.cam-progress-pct { color: var(--primary); font-variant-numeric: tabular-nums; }
.cam-progress-track {
  height: 6px;
  border-radius: 3px;
  background: var(--border);
  margin-top: 0.5rem;
  overflow: hidden;
}
.cam-progress-fill {
  height: 100%;
  width: 0%;
  border-radius: 3px;
  background: linear-gradient(90deg, #3b82f6, var(--primary));
  transition: width 0.35s ease;
}
/* Unbestimmte Phase (Aufnahme): der Balken pulst statt zu füllen. */
.cam-progress.is-indeterminate .cam-progress-fill {
  width: 35%;
  transition: none;
}
@media (prefers-reduced-motion: no-preference) {
  .cam-progress.is-indeterminate .cam-progress-fill {
    animation: cam-indet 1.2s ease-in-out infinite;
  }
  @keyframes cam-indet {
    0%   { margin-left: -35%; }
    100% { margin-left: 100%; }
  }
}

.cam-day-label {
  margin: 1rem 0 0.4rem;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.cam-day-label:first-child { margin-top: 0; }

.cam-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.cam-row {
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
}
.cam-row:last-child { border-bottom: none; }

.cam-row-inner {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.6rem;
  background: var(--card);
  position: relative;
  z-index: 1;
  transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
  touch-action: pan-y;
}
.cam-row.is-open .cam-row-inner { transform: translateX(-88px); }
.cam-row-thumb {
  width: 62px;
  height: 46px;
  flex: none;
  border-radius: 8px;
  object-fit: cover;
  background: var(--bg);
  cursor: zoom-in;
  border: 0;
  padding: 0;
  display: block;
}
.cam-row-meta { min-width: 0; flex: 1; }
.cam-row-time {
  display: block;
  font-size: 0.9rem;
  font-weight: 650;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.cam-row-sub {
  display: block;
  font-size: 0.76rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.cam-row-more {
  margin-left: auto;
  flex: none;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text-muted);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
}
.cam-row-more:hover { background: var(--bg); color: var(--text); }
.cam-row-more:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Rote Lösch-Fläche: standardmäßig rechts AUS dem Bild geschoben (nicht nur vom
   Zeilen-Hintergrund überdeckt) — sonst scheint sie bei halbtransparenten
   Themes wie „Glass" durch jede geschlossene Zeile durch. Nur bei .is-open
   fährt sie herein. */
.cam-row-del {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 88px;
  border: 0;
  background: var(--danger);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 700;
  cursor: pointer;
  z-index: 2;
  transform: translateX(100%);
  transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
.cam-row.is-open .cam-row-del { transform: translateX(0); }
.cam-row-del:focus-visible { outline: 2px solid #fff; outline-offset: -4px; }

/* Undo-Leiste am unteren Rand des Dialogs. */
.cam-undo {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.85rem;
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius);
  background: #101418;
  color: #e8ecf2;
  font-size: 0.82rem;
  font-weight: 500;
}
.cam-undo.is-active { display: flex; }
.cam-undo button {
  border: 0;
  background: none;
  color: #7fb0ff;
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
  padding: 0.15rem 0.25rem;
}
.cam-undo button:focus-visible { outline: 2px solid #7fb0ff; outline-offset: 2px; border-radius: 4px; }

/* --- Serverlog-Tab (Admin) ------------------------------------------------ */
.serverlog-header { flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.serverlog-controls { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.serverlog-auto { display: flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; color: var(--text-muted); white-space: nowrap; }
.serverlog-select,
.serverlog-filter {
  padding: 0.35rem 0.6rem; font-size: 0.85rem;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.serverlog-filter { min-width: 160px; }
.serverlog-meta {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem; color: var(--text-muted); margin: 0.25rem 0 0.75rem;
}
.serverlog-output {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem; line-height: 1.5;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.75rem 1rem; margin: 0;
  max-height: 70vh; overflow: auto;
  white-space: pre-wrap; word-break: break-word;
  overscroll-behavior: contain;
}
.serverlog-output .log-err  { color: var(--danger); }
.serverlog-output .log-warn { color: var(--warning); }
.serverlog-output .serverlog-empty { color: var(--text-muted); font-style: italic; }
@media (max-width: 640px) {
  .serverlog-filter { min-width: 0; flex: 1 1 100%; }
  .serverlog-output { font-size: 0.72rem; max-height: 60vh; }
}

/* --- Bluetooth-Einrichtung im Guide (devices-setup-ble.js) ------------------------- */
/* Die Zeilen sind bewusst FLAECHENLOS: dieselbe Entscheidung wie in den Apps — die
   Auswahl traegt der Ring links, nicht eine eingefaerbte Zeile. */
.ble-einstieg {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.9rem 1rem;
  background: var(--surface, transparent);
}
.ble-netzliste { margin: 0.6rem 0 0; }
.ble-netz {
  display: flex; align-items: center; gap: 0.7rem;
  width: 100%; min-height: 46px;
  padding: 0.35rem 0.25rem;
  background: none; border: 0; border-top: 1px solid var(--border);
  color: var(--text); font: inherit; text-align: left; cursor: pointer;
}
.ble-netzliste .ble-netz:first-child { border-top: 0; }
.ble-netz:hover { color: var(--primary); }
.ble-netz:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: 8px; }
.ble-netz .ble-ring { flex: 0 0 auto; width: 1.1rem; color: var(--text-muted); font-size: 1.1rem; line-height: 1; }
.ble-netz.ist-gewaehlt .ble-ring { color: var(--primary); }
.ble-netz.ist-gewaehlt .ble-ssid { font-weight: 600; }
.ble-netz .ble-ssid { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ble-netz .ble-schloss { flex: 0 0 auto; font-size: 0.75rem; }
.ble-netz .ble-balken { flex: 0 0 auto; color: var(--text-muted); font-size: 0.8rem; letter-spacing: 0.05em; }
.btn-link {
  background: none; border: 0; padding: 0.4rem 0.25rem;
  color: var(--primary); font: inherit; cursor: pointer;
}
.btn-link:hover { text-decoration: underline; }
.btn-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px; }
