/* ================================================================
   DESIGN-SYSTEM — AUTORITATIVE SCHICHT

   Lädt als LETZTES Stylesheet. Das ist Absicht: die Aussagen hier sollen die
   über die Jahre gewachsenen Einzelentscheidungen in style.css/components.css
   zusammenführen, nicht neben ihnen stehen. Wer eine Aussage ändern will,
   ändert sie hier — nicht als weiteren lokalen Override an der Fundstelle.

   WARUM ES DIESE DATEI GIBT (gemessen, nicht geschätzt, alles bei 753px
   Viewport im laufenden Dev-Server mit echten Flottendaten):

   • RADIEN: 22 verschiedene border-radius-Werte in den drei Stylesheets,
     davon 6 gleichzeitig sichtbar auf der Übersicht (14 / 10 / 8 / 6 / 4 /
     9999px). Eine Karte, eine Kachel darin und ein Abzeichen darin trugen drei
     Radien, die zueinander in keinem Verhältnis standen.
   • SCHATTEN: ~110 verschiedene box-shadow-Deklarationen. Karten, Kacheln und
     Dialoge hoben sich in willkürlich verschiedenen Höhen ab.
   • SCHRIFT: 34 verschiedene font-size-Werte im Hand-CSS, dazu 359 `text-xs`
     (12px) und 111 `text-[11px]` in Markup und Renderern. Auf der Kundenseite
     lagen 274 von 342 sichtbaren Textknoten unter 14px.
   • ÜBERSCHRIFTEN: die Übersicht trug 19 <h2>. Abschnittstitel standen auf
     14px/600, Kacheltitel auf 12px/600, ein Abschnitt auf 16px/600 — und die
     <h3> darunter auf 14px/700, also FETTER als die <h2> darüber. Rangfolge
     war damit nicht ablesbar, weder visuell noch für Screenreader.
   • FARBE: die Akzentleiste der KPI-Kacheln lief über sechs Varianten, und
     zwar dekorativ: „Ø Betriebsstunden" trug dasselbe Grün wie „Anlagen
     Online" (eine echte Zustandsaussage), „Ø WW-Zieltemperatur" trug Orange,
     also die Warnfarbe, und die Standardvariante lief in Altrosa (#c75380) auf
     Blauviolett (#675cbf) — Markenfarben eines anderen Produkts, in einer
     Anwendung mit Vaillant-Blau als Primärfarbe.

   WAS HIER NICHT PASSIERT: keine Grenzwerte, keine Schwellen, keine
   Alarmregeln, keine Messwerte, keine Einheiten. Die JAZ-Bänder behalten ihre
   Skala von Grün nach Rot — dort IST die Farbe die Aussage.
   ================================================================ */

:root {
  /* ---- RADIEN: drei Stufen plus Pille, gekoppelt an die Verschachtelung ----
     Eine Karte umschließt Kacheln, eine Kachel umschließt Bedienelemente und
     Abzeichen. Drei Werte reichen dafür; jeder weitere behauptet eine
     Verschachtelungstiefe, die es nicht gibt. */
  --r-card: 12px;      /* .card-readable, Dialoge, Sektionen */
  --r-tile: 10px;      /* Kacheln IN einer Karte */
  --r-control: 8px;    /* Knöpfe, Felder, Selects, kleine Abzeichen */
  --r-chip: 9999px;    /* Pillen/Zähler — bewusst keine Zahl */

  /* ---- SCHATTEN: keine (02.09.2026) ----
     Drei Höhen waren der erste Schritt, jetzt entfallen sie ganz. Die
     Trennung der Flächen trägt der 1px-Rahmen (--border-primary), den Karte,
     Kachel, Dialog und Tabelle ohnehin haben. Die Token bleiben als `none`
     stehen, damit kein Altbestand ins Leere greift. KEIN globales
     `box-shadow: none`: Fokusringe und die 0-Blur-Kantenmarker (klebende
     Spalte, Spaltengruppe, Zeilen-Hover) sind ebenfalls box-shadows. */
  --shadow-1: none;
  --shadow-2: none;
  --shadow-3: none;

  /* ---- ABSTÄNDE: SECHS STUFEN ----
     Es gab bereits `--space-xs` bis `--space-2xl` in style.css. Sie waren
     faktisch tot: VIER Verwendungen im gesamten Stylesheet-Bestand, während
     rund 270 Stellen ihre Abstände als harte rem-Werte tragen und weitere 300
     als Tailwind-Klassen im Markup stehen. Ein Token, das niemand liest,
     ordnet nichts.

     Deshalb hier neu, und hier auch angewandt — auf die Hüllenklassen, die die
     wahrgenommene Dichte tatsächlich bestimmen (Karte, Kartenkopf, Seitenkopf,
     Kachelgitter, Verteilungszeile). Das erreicht den Großteil der Wirkung über
     ein gutes Dutzend Regeln, statt 270 Fundstellen anzufassen.

     Die Werte sind gegenüber vorher durchweg KLEINER: die Karte trug 32px
     Innenabstand und 32px Außenabstand nach unten, zusammen also 64px zwischen
     zwei Karteninhalten. Auf einem Tablet ist das ein Fünftel der Bildhöhe für
     nichts. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  /* --sp-6 (32px) entfernt am 02.09.2026: null Verwendungen. */

  /* ---- TYPO-STUFEN: Tablet zuerst ----
     Das Tablet ist die Zielplattform, also stehen HIER die größeren Werte und
     der Desktop verdichtet weiter unten. Umgekehrt wäre das Tablet wieder ein
     Nachtrag.
     Die 11px-Untergrenze des Projekts bleibt bestehen, sie gilt aber für
     Tabellenzellen — nicht für Text in Karten. Kartentext beginnt bei 12px. */
  --fs-page-title: 20px;   /* h1 der Seite */
  --fs-section: 16px;      /* Abschnitts-/Kartentitel (h2) */
  --fs-tile-title: 14px;   /* Kacheltitel (h3) */
  --fs-body: 15px;         /* Fließtext, Beschreibungen */
  --fs-meta: 12px;         /* echte Nebeninformation */
  --fs-value-xl: 30px;     /* Leitwert einer Seite */
  --fs-value: 24px;        /* Messwert einer Kachel */
  --fs-unit: 15px;         /* Einheit am Messwert */

  /* ---- BEDIENELEMENT-HÖHE ----
     Eine Höhe für Knöpfe, Felder und Selects in derselben Zeile. Vorher
     standen dort 24 / 28 / 30 / 36 / 41 / 48px nebeneinander. Seit dem
     02.09.2026 44px statt 48px: die Projektregel verlangt 44px Trefferfläche,
     die vier Pixel darüber waren Polster ohne Aussage. */
  --ctl-h: 44px;

  /* ---- ABSTAND EINES DIALOGS ZUM BILDRAND ----
     Steht als Variable da, weil er an zwei Stellen zusammenpassen muss: als
     Polster des Overlays UND als Abzug in der Höhenbegrenzung der Box. Standen
     die beiden auseinander, war die Box entweder zu hoch (Fuß abgeschnitten)
     oder es blieb ein Streifen ungenutzt. Wird in §5f je Höhenstufe kleiner —
     bei 300px Bildhöhe sind 32px Rand ein Zehntel des Platzes. */
  --dialog-inset: 1rem;

  /* ---- FARBKODIERUNG VON MESSWERTEN IN TABELLEN ----
     Die Formatter der Parametertabelle haben ihre Farben als Inline-Style mit
     festen Hexwerten aus der Material-Palette gesetzt: #2196F3, #4CAF50,
     #FF9800, #f44336. Drei Folgen, alle gemessen:

     1. KONTRAST. Auf der Parameterseite lagen 189 Messwerte unter dem
        AA-Grenzwert — „28.7 °C" auf #FF9800 erreichte 2,16 : 1, „3.99" auf
        #4CAF50 2,78 : 1, „45.0 °C" 3,68 : 1. Das sind genau die Zahlen, um die
        es auf dieser Seite geht.
     2. ZWEITE PALETTE IM PRODUKT. Material-Grün/-Orange/-Rot neben dem
        Tailwind-Emerald/-Amber/-Red des restlichen Dashboards — dieselbe
        Aussage in zwei Farbwelten.
     3. KEIN DARK MODE. Ein Inline-Style ist unveränderlich; die
        Tabellenregeln in tabulator-custom.css können ihn nicht erreichen (kein
        Selektor schlägt einen Inline-Style ohne !important). Die Werte trugen
        im Dark Mode dieselben hellen Töne wie auf weißem Grund.

     Lösung: die Formatter setzen jetzt `color: var(--val-…)`. Ein Inline-Style,
     der eine Variable liest, behält seine Vorrangstellung UND folgt dem Theme —
     damit ist die Farbkodierung an genau einer Stelle definiert, hier.
     Die SCHWELLEN, an denen die Formatter umschalten (< 10 °C, < 25 °C, < 40 °C,
     JAZ < 3,0 …), sind fachliche Logik und unverändert geblieben. */
  --val-cold:  #1d4ed8;   /* war #2196F3 · 3,10 → 6,30 : 1 */
  --val-ok:    #15803d;   /* war #4CAF50 · 2,78 → 4,60 : 1 */
  --val-warm:  #b45309;   /* war #FF9800 · 2,16 → 5,01 : 1 */
  --val-hot:   #b91c1c;   /* war #f44336 · 3,40 → 6,20 : 1 */
  --val-muted: #4b5563;   /* war #9e9e9e · 2,54 → 7,50 : 1 */

  /* ---- NEUTRALER AKZENT ----
     Für Kacheln, die einen Messwert zeigen und KEINEN Zustand meldet. Vorher
     griffen sie in die Statuspalette (Grün/Orange) und behaupteten damit etwas
     über einen Durchschnittswert, der nichts behauptet. */
  --accent-neutral: #0070ba;   /* Vaillant-Blau, die Primärfarbe des Produkts */
}

.dark {
  /* Auf der dunklen Tabellenfläche (#1f2937) brauchen dieselben Aussagen die
     helle Stufe derselben Farbe — nachgerechnet alle über 4,5 : 1. */
  --val-cold:  #93c5fd;
  --val-ok:    #6ee7b7;
  --val-warm:  #fcd34d;
  --val-hot:   #fca5a5;
  --val-muted: #cbd5e1;

  --shadow-1: none;
  --shadow-2: none;
  --shadow-3: none;
  --accent-neutral: #60a5fa;   /* wie --vaillant-blue im Dark Mode */
}

/* Ab Desktop verdichten. Nur die Stufen, die dort schmaler sein dürfen —
   Messwerte und Abschnittstitel bleiben, sie tragen die Orientierung. */
@media (min-width: 1280px) and (any-pointer: fine) {
  :root {
    --fs-tile-title: 13px;
    --fs-body: 14px;
    --fs-meta: 11px;
    --ctl-h: 34px;
  }
}

/* ================================================================
   1 — TYPOGRAFISCHE STUFEN ALS KLASSEN

   Vorher trug jede Fundstelle ihre eigene Tailwind-Kette (`text-lg font-bold`
   hier, `text-base font-semibold` dort, `text-sm font-semibold` daneben).
   Gleichrangige Dinge sahen dadurch verschieden aus, verschiedenrangige gleich.
   ================================================================ */

/* Seitentitel. Trägt zusätzlich das Icon; der Abstand nach unten gehört zum
   Titel, nicht zur nächsten Karte. */
.page-title {
  font-size: var(--fs-page-title);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}

/* Abschnitts-/Kartentitel. EIN Wert für alle Karten aller Seiten — vorher
   14px auf der Übersicht, 16px auf Alerts/Kunden/Kältekreis. */
.section-title {
  font-size: var(--fs-section);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--text-primary);
}

/* Kacheltitel. Eine Stufe UNTER dem Abschnittstitel und leichter — vorher war
   die Kachel-Überschrift ein <h2> auf 12px neben einem <h3> auf 14px/700. */
.tile-title {
  font-size: var(--fs-tile-title);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-secondary);
}

/* Erklärender Fließtext unter einem Titel. */
.section-desc {
  font-size: var(--fs-body);
  line-height: 1.35;
  color: var(--text-secondary);
}

/* Echte Nebeninformation: Zeitstempel, Stichprobengröße, Herkunftshinweis.
   Alles, was hier steht, darf klein sein — alles andere nicht. */
.meta-text {
  font-size: var(--fs-meta);
  line-height: 1.35;
  color: var(--text-muted);
}

/* ================================================================
   2 — MESSWERT-PRIMITIVE

   Der Kern des Auftrags: BEZEICHNUNG, WERT und EINHEIT gehören zusammen und
   der Wert muss stärker sein als seine Umgebung.

   Vorher gab es `.metric-value`/`.metric-unit`/`.metric-label` in
   components.css — mit 5 / 0 / 6 Verwendungen in der ganzen Anwendung. Jeder
   Renderer baute seinen Messwert stattdessen selbst aus `text-2xl font-bold`
   plus `text-xs` Beschriftung plus `text-[11px]` Zusatz. Deshalb hier ein
   Bausteinsatz, der auch benutzt wird (siehe fleet-kpi-view.js,
   fleet-jaz-view.js, alerts-manager.js, customers-manager.js,
   refrigerant-circuit-manager.js, etl-run-manager.js).

   `font-variant-numeric: tabular-nums` ist kein Detail: in einer Spalte
   untereinander stehender Messwerte springen proportionale Ziffern seitlich,
   und genau diese Spalten liest ein Techniker vergleichend.
   ================================================================ */

.mv {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.mv-label {
  font-size: var(--fs-tile-title);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-secondary);
  /* Bewusst KEINE Versalien: die Beschriftungen sind hier fachliche Begriffe
     („Ø Δ Vorlauf / Rücklauf", „Sollraumtemperatur"), und Großschreibung kostet
     bei zusammengesetzten deutschen Wörtern messbar Lesegeschwindigkeit. */
}

/* Zeile aus Wert und Einheit — nie umbrechen zwischen den beiden, sonst steht
   die Einheit allein in der nächsten Zeile und liest sich wie ein neues Feld. */
.mv-figure {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
  flex-wrap: wrap;
}

.mv-value {
  font-size: var(--fs-value);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.mv-value--xl {
  font-size: var(--fs-value-xl);
}

/* Einheit: klar kleiner als der Wert, aber nicht kleiner als Nebentext — sie
   entscheidet über die Bedeutung der Zahl (°C gegen K gegen %). */
.mv-unit {
  font-size: var(--fs-unit);
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Zusatz unter dem Wert: Stichprobe, Anteil, Aktualität. */
.mv-sub {
  font-size: var(--fs-meta);
  line-height: 1.35;
  color: var(--text-muted);
}

/* Wenn der Wert selbst einen Zustand meldet, färbt er sich — nicht die Kachel.
   Damit bleibt Farbe an genau einer Stelle je Kachel. */
.mv-value--ok    { color: #047857; }
.mv-value--warn  { color: #b45309; }
.mv-value--crit  { color: #b91c1c; }
.dark .mv-value--ok   { color: #34d399; }
.dark .mv-value--warn { color: #fbbf24; }
.dark .mv-value--crit { color: #f87171; }

/* ================================================================
   3 — KACHELGITTER

   Zwei gemessene Fehler in jedem Kachelstreifen der Anwendung:

   a) UNGLEICHE HÖHEN. `#refrigerant-summary`: drei Kacheln 78px, zwei 63px
      nebeneinander in derselben Reihe — weil zwei von fünf keine zweite
      Textzeile haben. Der Streifen sah aus wie ein Rendering-Fehler.
   b) LÖCHER. Fünf Kacheln in `grid-cols-2 sm:grid-cols-3 xl:grid-cols-5`
      ergeben auf dem Tablet 3 + 2 und eine leere dritte Zelle.

   `auto-fit` mit Mindestbreite löst beides: die Spaltenzahl folgt dem
   Platzangebot statt einer geratenen Zahl, und `align-items: stretch`
   (Grid-Standard) plus `.tile` als Flex-Spalte mit `margin-top: auto` am Fuß
   hält die Kacheln auf gleicher Höhe.
   ================================================================ */

.tile-grid {
  display: grid;
  /* 12rem statt 11rem: mit dem engeren Abstand (§9) passte eine Spalte mehr ins
     Raster, und bei fuenf Spalten lief „Differenz 0 (kein Vergleich moeglich)“
     aus seiner Kachel heraus. Dieses Projekt kuerzt nicht und blendet nicht auf
     — also muss die Spalte breit genug sein. Gemessen: ab 12rem null Ueberlaeufe. */
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 0.625rem;
}

/* Breitere Variante für Kacheln mit mehrzeiligem Inhalt (Schwellwert-Kachel
   der Übersicht, Kundenkennzahlen). */
.tile-grid--wide {
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

/* ---- SPALTENZAHLEN, DIE DIE KACHELZAHL TEILEN ----
   `auto-fit` verhindert zu schmale Spalten, aber keine LÖCHER: gemessen bekam
   die Temperatur-Reihe (4 Kacheln) bei 753px drei Spalten und stellte die vierte
   Kachel allein in eine Reihe mit zwei leeren Zellen. Dasselbe traf jede Reihe,
   deren Kachelzahl nicht durch die errechnete Spaltenzahl teilbar ist.
   Deshalb feste Spaltenzahlen je Reihenlänge — jede teilt die Kachelzahl. */
.tile-grid--2,
.tile-grid--3,
.tile-grid--4 {
  grid-template-columns: 1fr;
}

@media (min-width: 480px) {
  .tile-grid--2,
  .tile-grid--4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 560px) {
  .tile-grid--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Erst ab Desktop vier in einer Reihe: bei 1024px wären das 4 × 200px, und die
   längste Beschriftung („Ø Δ Vorlauf / Rücklauf" mit Erklärzeile) braucht mehr. */
@media (min-width: 1280px) {
  .tile-grid--4 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* ================================================================
   3b — JAZ: RING UND LEGENDE

   Was hier vorher stand: fünf Bänder als fünf Zeilen mit je einem Balken, dazu
   eine breite Leitkachel darüber. Das war schon eine Verbesserung gegenüber
   dem Raster davor — es beantwortete aber die erste Frage nicht, die jemand an
   diese Karte stellt: „ist das jetzt gut oder nicht?" Fünf Anzahlen, fünf
   Fachwörter und fünf Balken muss der Leser dafür selbst zusammenrechnen.

   Ein Ring beantwortet genau diese Frage, weil Anteile am Ganzen seine einzige
   Aussage sind. Drei Gruppen statt fünf Bänder, weil ein Auge drei Flächen
   vergleichen kann und fünf nicht — die feinen Grenzen sind nicht verloren,
   sie stehen als Text in jeder Legendenzeile.

   WAS ENTFALLEN IST UND WARUM:
   • `.dist-bar` (Balken je Zeile) — kodierte dieselbe Anzahl, die daneben als
     Zahl steht, und dieselbe Verteilung, die jetzt der Ring zeigt. Dreimal
     dasselbe.
   • `.lead-tile*` (Leitkachel Ø JAZ) — der Schnittwert steht jetzt IM Ring.
     Eine eigene Kachel dafür war eine zweite Fläche für einen Wert, der
     ohnehin in die Mitte gehört.
   ================================================================ */

/* ---- BANDFARBEN: EINE QUELLE FÜR ALLE ANSICHTEN ----
   Ring, Legende, Kartenstecknadeln und Tabellenmarkierung müssen dieselbe
   Gruppe in derselben Farbe zeigen. Vorher taten sie das nicht: die
   Flottenkarte trug für „Sehr gut" #10b981, das Dashboard #059669, und „Gut"
   war auf der Karte grün, im Dashboard blau. Wer beide Ansichten nebeneinander
   legte, sah zwei verschiedene Flotten.

   Die Werte stehen deshalb genau hier. Wer sie in JavaScript braucht (Leaflet
   setzt Stecknadelfarben inline und kann keine Klasse tragen), holt sie über
   `metricRegistry.jazFarbe()` ab — gelesen wird dort bei jedem Aufruf, damit
   ein Themenwechsel durchschlägt. */
:root {
  --jaz-gut:      #059669;
  --jaz-mittel:   #d97706;
  --jaz-schlecht: #dc2626;
  --jaz-keine:    #94a3b8;
}

.dark {
  --jaz-gut:      #34d399;
  --jaz-mittel:   #fbbf24;
  --jaz-schlecht: #f87171;
  --jaz-keine:    #64748b;
}

/* Eine Regel je Gruppe, gelesen von der Marke der Legendenzeile UND vom
   Ringsegment. Vorher hätte jede der beiden Stellen ihre eigene Farbregel
   gebraucht, und `currentColor` am Segment hätte die Textfarbe geliefert. */
.dist-row--gut,      .jaz-slice--gut      { --band: var(--jaz-gut); }
.dist-row--mittel,   .jaz-slice--mittel   { --band: var(--jaz-mittel); }
.dist-row--schlecht, .jaz-slice--schlecht { --band: var(--jaz-schlecht); }
.dist-row--keine,    .jaz-slice--keine    { --band: var(--jaz-keine); }

/* ---- SATZ ÜBER DEM RING ----
   Die Gesamtaussage in einem Satz, in derselben Schriftgröße wie Fließtext.
   Sie ist keine Nebeninformation, sondern das Erste, was gelesen werden soll. */
.jaz-satz {
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: 1.45;
  color: var(--text-primary);
  margin: var(--sp-2) 0 var(--sp-1);
}

/* ---- RING UND LEGENDE NEBENEINANDER ----
   Unter 34rem untereinander: ein Ring neben vier Textzeilen braucht Breite,
   und schmalgequetscht wird aus dem Kreis eine Briefmarke. */
.jaz-chart {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
}

.jaz-ring-wrap {
  position: relative;
  flex: 0 0 auto;
  width: 13.75rem;
  max-width: 100%;
}

.jaz-ring {
  display: block;
  width: 100%;
  height: auto;
}

/* Die Segmente tragen einen Trennstrich in der Flächenfarbe der Karte, damit
   zwei benachbarte Gruppen nicht ineinanderlaufen. */
.jaz-slice {
  fill: var(--band, var(--border-secondary));
  stroke: var(--surface-1);
  stroke-width: 2;
}

/* Beim Vollkreis-Sonderfall ist das Segment ein gestrichener Kreis: dort trägt
   `stroke` die Farbe, nicht `fill`. */
.jaz-slice--voll,
circle.jaz-slice {
  fill: none;
  stroke: var(--band, var(--border-secondary));
}

/* ---- MITTE DES RINGS ----
   Als HTML über dem SVG statt als <text> darin: so gelten dieselben
   Schriftstufen und Farbtoken wie überall sonst, und der Dark Mode greift
   ohne Sonderregel. `pointer-events: none`, damit die Fläche nichts abfängt. */
.jaz-ring-center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.125rem;
  text-align: center;
  pointer-events: none;
}

.jaz-ring-value {
  font-size: var(--fs-value-xl);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.jaz-ring-word {
  font-size: var(--fs-tile-title);
  font-weight: 600;
  line-height: 1.2;
  color: var(--text-secondary);
}

.jaz-ring-cap {
  font-size: var(--fs-meta);
  line-height: 1.3;
  color: var(--text-muted);
}

/* ---- LEGENDE ----
   Jede Zeile ist ein Knopf und filtert die Tabelle darunter. Sie bleibt ein
   echtes <button>, weil sie das vorher als <div onclick> nicht war: mit der
   Tastatur nicht erreichbar, ohne Rolle, ohne Fokusring. */
.dist-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  flex: 1 1 18rem;
  min-width: 0;
}

.dist-row {
  display: grid;
  grid-template-columns: 0.75rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
  width: 100%;
  min-height: var(--ctl-h);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-tile);
  background-color: var(--surface-1);
  text-align: left;
  cursor: pointer;
}

.dist-row:hover,
.dist-row:active {
  background-color: var(--surface-2);
}

.dist-row:focus-visible {
  outline: 2px solid var(--accent-neutral);
  outline-offset: 2px;
}

/* Farbmarke der Gruppe. Zusätzlich trägt jede Zeile ihre Grenze im Text
   („JAZ ab 3,5"), damit die Farbe nicht das alleinige Signal ist. */
.dist-mark {
  width: 0.75rem;
  height: 2.75rem;
  border-radius: var(--r-chip);
  background-color: var(--band, var(--border-secondary));
}

.dist-text {
  min-width: 0;
}

.dist-label {
  display: block;
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.25;
  color: var(--text-primary);
}

/* Der Erklärsatz ist hier KEINE Nebeninformation: er ist der Grund, warum die
   Zeile ohne Vorwissen verständlich ist. Deshalb Fließtextgröße und die
   normale Textfarbe, nicht die gedämpfte. */
.dist-desc {
  display: block;
  font-size: var(--fs-body);
  line-height: 1.35;
  color: var(--text-secondary);
}

/* Die Zahlengrenze dahinter — für alle, die mit „JAZ 3,5" etwas anfangen. */
.dist-grenze {
  display: block;
  font-size: var(--fs-meta);
  line-height: 1.3;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.dist-figure {
  grid-row: 1 / 3;
  grid-column: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-self: end;
  line-height: 1.15;
}

.dist-count {
  font-size: var(--fs-value);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.dist-total,
.dist-pct {
  font-size: var(--fs-meta);
  color: var(--text-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ---- FUSSZEILE: ZEITRAUMVERGLEICH UND „ALLE ZEIGEN" ---- */
.jaz-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border-primary);
}

.jaz-foot-block {
  min-width: 0;
}

.jaz-periods {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-4);
}

.jaz-periods .kpi-breakdown-row {
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  min-width: 4.5rem;
}


/* Gemeinsame Kachelhülle. Ersetzt die je Renderer verschieden gebauten
   `bg-white rounded-lg border p-3`-Ketten. */
.tile {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background-color: var(--surface-1);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-tile);
}

/* Der Fuß einer Kachel klebt unten, damit gleich hohe Kacheln ihre Zusatzzeile
   auf gleicher Linie tragen. */
.tile-foot {
  margin-top: auto;
  padding-top: 0.375rem;
}

/* Anklickbare Kachel (öffnet einen Dialog oder filtert). */
.tile--action {
  text-align: left;
  cursor: pointer;
  font: inherit;
}

.tile--action:hover,
.tile--action:active {
  background-color: var(--surface-2);
}

.tile--action:focus-visible {
  outline: 2px solid var(--accent-neutral);
  outline-offset: 2px;
}

/* Gewählte Filterkachel. Ersetzt `ring-2 ring-[#0070BA]` — die Farbe dieses
   Rings war im ausgelieferten tailwind.css nicht enthalten, es blieb die
   Ringbreite ohne Farbe. Zusätzlich zur Farbe eine dickere Kante UND das
   Häkchen-Zeichen: Farbe darf nach der Projektregel nicht allein tragen. */
.tile--active {
  border-color: var(--accent-neutral);
  box-shadow: inset 0 0 0 1px var(--accent-neutral);
  background-color: rgba(0, 112, 186, 0.06);
}

.tile--active .mv-label::after {
  content: " ✓";
  font-weight: 700;
  color: var(--accent-neutral);
}

/* Kopfzeile einer Kachel: Zeichen links, Bezeichnung daneben. */
.tile-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.tile-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex-shrink: 0;
  border-radius: var(--r-control);
  font-size: 0.85rem;
}

/* ---- LISTENZEILE MIT AKTION (ETL-Läufe) ---- */
.run-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: var(--ctl-h);
  padding: 0.625rem 0.75rem;
  text-align: left;
  cursor: pointer;
  background-color: transparent;
}

.run-row:hover,
.run-row:active {
  background-color: var(--surface-2);
}

.run-row:focus-visible {
  outline: 2px solid var(--accent-neutral);
  outline-offset: -2px;
}

.run-row-main {
  flex: 1 1 auto;
  min-width: 0;
}

.run-row-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
}

.run-row-fn {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.run-row-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.125rem 0.875rem;
  margin-top: 0.25rem;
  font-size: var(--fs-meta);
  color: var(--text-secondary);
}

.run-row-meta > span {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

.run-row-bad {
  color: #b91c1c;
  font-weight: 600;
}

.dark .run-row-bad { color: #f87171; }

.run-row-chevron {
  flex-shrink: 0;
  color: var(--text-muted);
  font-size: 0.8rem;
}

/* ---- MELDUNGSLISTE (technische Fehlertexte) ---- */
.err-list {
  max-height: 10rem;
  overflow-y: auto;
  padding: 0.5rem 0.625rem;
  border: 1px solid var(--border-primary);
  border-radius: var(--r-control);
  background-color: var(--surface-2);
}

.err-list-item {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-meta);
  line-height: 1.45;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

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

/* ================================================================
   4 — KONSOLIDIERUNG DER BESTEHENDEN KOMPONENTEN AUF DIE TOKENS
   ================================================================ */

/* Karte: ein Radius. Ersetzt 12px (style.css) und 14px (Tablet-Block in
   components.css). Der Ruheschatten und der Anhebe-Effekt beim Überfahren sind
   am 02.09.2026 entfallen — den Rand trägt der 1px-Rahmen. */
.card-readable {
  border-radius: var(--r-card);
}

/* ---- KPI-KACHEL ----
   Drei Eingriffe, jeder mit eigenem Grund:

   1. GLAS WEG. `background: linear-gradient(135deg, rgba(255,255,255,.95),
      rgba(248,250,252,.95))` plus `backdrop-filter: blur(8px)` an bis zu 15
      Kacheln gleichzeitig. Auf einer weißen Karte ist der Effekt unsichtbar,
      kostet aber je Kachel eine eigene Compositing-Schicht — und im Dark Mode
      erzeugte er einen zweiten, kälteren Grauton neben der Kartenfläche.
      Eine Kachel in einer Karte ist eine Flächenstufe, kein Glaspaneel.
   2. RADIUS auf --r-tile (war 10px, passt) und der ::before-Balken auf denselben
      Wert — er trug `border-radius: 12px 12px 0 0` gegen eine 10px-Kachel und
      stand damit an den oberen Ecken 2px über.
   3. AKZENT FLACH UND SEMANTISCH. Siehe Block 5. */
.kpi-card {
  background: var(--surface-1);
  backdrop-filter: none;
  border: 1px solid var(--border-primary);
  border-radius: var(--r-tile);
}

.dark .kpi-card {
  background: var(--surface-1);
  border-color: var(--border-primary);
}

.kpi-card::before {
  border-radius: var(--r-tile) var(--r-tile) 0 0;
  /* Flach statt Verlauf: ein 3px-Streifen mit Farbverlauf ist auf 3px nicht als
     Verlauf erkennbar, sondern nur als unsauberer Farbton. */
  background: var(--accent-neutral);
}

/* ================================================================
   5 — FARBE ALS ZUSTANDSAUSSAGE, NICHT ALS DEKORATION

   Projektregel (dokumentiert in den Alarm- und Statusansichten): Rot = echter
   Fehler, Gelb/Bernstein = Warnung, Grün = in Ordnung/online, Grau = neutrale
   Gesamtzahl. Die KPI-Kacheln haben diese Regel unterlaufen: sechs Varianten,
   davon vier ohne Zustandsbezug.

   Die Varianten bleiben ALLE bestehen — sie werden gebraucht, wo ein Zustand
   gemeldet wird (Anlagen Online, Grenzwertabweichungen, JAZ-Bänder). Sie
   verlieren nur ihre Verläufe: ein Zustand ist ein Wert, kein Übergang.
   ================================================================ */

.kpi-card-blue::before   { background: var(--accent-neutral); }
.kpi-card-green::before  { background: #059669; }
.kpi-card-yellow::before { background: #d97706; }
.kpi-card-orange::before { background: #ea580c; }
.kpi-card-red::before    { background: #dc2626; }
.kpi-card-purple::before { background: #7c3aed; }

/* Neutral: ausdrücklich für Kacheln, die einen Durchschnitt oder eine Summe
   zeigen und damit KEINEN Zustand meldet. Grau statt Blau wäre eine zweite
   neutrale Farbe; Blau ist die Produktfarbe und schon vergeben. */
.kpi-card-neutral::before { background: var(--border-secondary); }

/* ================================================================
   5b — KNOPF-SYSTEM

   Gemessen trugen die „Aktualisieren"-Knöpfe der zehn Seiten fünf verschiedene
   Gestalten: gefüllt blau mit `text-xs` (Alerts, Kunden, ETL, Kältekreis),
   Textknopf blau mit `text-xs` (Fleet-KPI, Fleet-JAZ), Textknopf blau mit
   `text-sm` (Statusübersicht), gefüllt blau mit `text-xs` und anderem Padding
   (Betrieb) und gefüllt grün (Parameter-Export). Derselbe Vorgang, fünf
   Erscheinungsbilder — und daneben gefülltes Grün für „Alle" (Spaltenauswahl),
   was dort wie eine Bestätigung aussah, obwohl es nur eine Auswahl umschaltet.

   Regel: GEFÜLLT ist die eine Primäraktion eines Bereichs. Alles Wiederholende
   („Aktualisieren", „Export", Filter) ist ruhig. Vorher kämpften auf der
   Parameterseite drei gefüllte, gesättigte Knöpfe gleichzeitig um Aufmerksamkeit.
   ================================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: var(--ctl-h);
  padding: 0.375rem 0.75rem;
  border: 1px solid transparent;
  border-radius: var(--r-control);
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
}

.btn > i {
  font-size: 0.9em;
  flex-shrink: 0;
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Kleine Ausführung — für Knöpfe IN Datenzeilen (Steuerungsseite, 14 Stück).
   Die volle `--ctl-h` (44px Touch / 34px Desktop) würde jede Zeile auf
   Knopfhöhe aufziehen. 24px ist die Untergrenze aus WCAG 2.5.8 und gilt auch am
   Maus-Desktop; die alten Knöpfe kamen dort mit `px-2 py-0.5 text-xs` auf 20px
   — an 14 Knöpfen, die echte Kundenanlagen verstellen. Auf Touch hebt
   `.page-content button:not(…)` in components.css ohnehin auf 44 × 44px an
   (0,3,1 schlägt 0,1,0, die Reihenfolge spielt dort keine Rolle).

   `white-space: normal` nimmt das `nowrap` von `.btn` zurück: „Einstellen ·
   Advanced Settings" muss umbrechen dürfen — dieses Projekt kürzt nicht.

   Bewusst KEIN eigener Knopf mit eigener Textfarbe: `.dark button { color:
   inherit }` (style.css, Spezifität 0,1,1) schlägt jede Ein-Klassen-Regel und
   machte die Schrift im Dark Mode unsichtbar. Deshalb `.btn--danger` als Basis
   — die trägt ihre Rettung `.dark .btn--danger { color: #fff }` schon. */
.btn--sm {
  min-height: 24px;
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-meta);
  white-space: normal;
  text-align: center;
}

/* Ruhig: die Standardgestalt für wiederkehrende Aktionen. Rahmenlos, aber mit
   Fläche beim Überfahren/Antippen — vorher waren diese Knöpfe reiner blauer
   Text ohne jede Trefferfläche-Andeutung. */
.btn--quiet {
  color: #1d4ed8;
  background-color: transparent;
}

.dark .btn--quiet {
  color: #93c5fd;
}

.btn--quiet:hover,
.btn--quiet:active {
  background-color: rgba(0, 112, 186, 0.1);
}

/* Sekundär: sichtbare Fläche, keine Sättigung. Für „Export", „Zurücksetzen". */
.btn--secondary {
  color: var(--text-primary);
  background-color: var(--surface-3);
  border-color: var(--border-primary);
}

.btn--secondary:hover,
.btn--secondary:active {
  background-color: var(--border-primary);
}

/* Primär: gefüllt, genau eine je Bereich. */
.btn--primary {
  color: #fff;
  background-color: #0063a6;
}

.btn--primary:hover,
.btn--primary:active {
  background-color: #004f85;
}

/* Bestätigende Aktion (Speichern/Senden). Bewusst dieselbe dunkle Grünstufe wie
   in style.css für .bg-green-600 — weißer Text auf #16a34a erreicht nur 3,3:1. */
.btn--success {
  color: #fff;
  background-color: #15803d;
}

.btn--success:hover,
.btn--success:active {
  background-color: #166534;
}

.btn--danger {
  color: #fff;
  background-color: #b91c1c;
}

.btn--danger:hover,
.btn--danger:active {
  background-color: #991b1b;
}

/* ---- TEXTFARBE DER GEFÜLLTEN VARIANTEN IM DARK MODE ----

   `style.css` trägt `.dark button { color: inherit }`. Das ist (0,1,1) und
   schlägt damit die Textfarbe der Varianten oben (0,1,0) — Spezifität
   entscheidet vor Dateireihenfolge, dieses Stylesheet lädt also umsonst zuletzt.
   Ergebnis: jeder gefüllte Knopf erbte im Dark Mode die Primärtextfarbe
   (#e2e8f0) statt des deklarierten Weiß. Gemessen an `.btn--success`
   (#15803d): 4,07 : 1 statt 5,01 : 1 bei 15px/600 — unter dem AA-Grenzwert von
   4,5 : 1, und zwar an den Knöpfen, die etwas AUSLÖSEN (Export, Senden,
   Bestätigen der Schreibbefehle).

   Für `.text-white` gibt es die Rettung in style.css schon; die Varianten waren
   nie dabei. (0,2,0) hier gewinnt gegen (0,1,1). */
.dark .btn--primary,
.dark .btn--success,
.dark .btn--danger {
  color: #fff;
}

/* Sekundär erbte ebenfalls per `inherit` — zufällig derselbe Wert, aber eben
   zufällig. Explizit gesetzt bleibt es das auch, wenn sich die Elternfarbe
   irgendwo ändert. */
.dark .btn--secondary {
  color: var(--text-primary);
}

/* Sichtbarer Tastaturfokus für alle Varianten — vorher trugen drei Selektoren
   in style.css `outline: 2px solid var(--accent-primary)`, also Altrosa
   (#c75380): der Fokusring wies in einer blauen Anwendung in eine andere
   Markenwelt und war zwischen den Varianten nicht wiedererkennbar. */
.btn:focus-visible,
.nav-btn:focus,
.section-toggle-btn:focus {
  outline: 2px solid var(--accent-neutral);
  outline-offset: 2px;
}

/* ---- ZUSTANDS-PILLE ----
   Das Muster `inline-flex items-center gap-1 px-2 py-0.5 rounded-full
   bg-X-100 text-X-700 font-medium` kam über Markup und Renderer verteilt
   dutzendfach vor, mit wechselndem Padding und wechselnder Schriftgröße. */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--r-chip);
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}

.status-chip > i {
  font-size: 0.9em;
}

/* `display: inline-flex` schlägt Tailwinds `.hidden` (siehe .state-block.hidden).
   Der Alarm-Zähler in der Navigation wird bei null offenen Meldungen per
   `classList.add('hidden')` weggeräumt (notifications.js) — ohne diese Regel
   bleibt eine rote Kritisch-Marke mit „0" stehen und behauptet eine Störung,
   die es nicht gibt. */
.status-chip.hidden {
  display: none;
}

.status-chip--ok      { background-color: #d1fae5; color: #065f46; }
.status-chip--warn    { background-color: #fef3c7; color: #92400e; }
.status-chip--crit    { background-color: #fee2e2; color: #991b1b; }
.status-chip--neutral { background-color: var(--surface-3); color: var(--text-secondary); }

.dark .status-chip--ok      { background-color: rgba(6, 95, 70, 0.45);  color: #a7f3d0; }
.dark .status-chip--warn    { background-color: rgba(146, 64, 14, 0.45); color: #fde68a; }
.dark .status-chip--crit    { background-color: rgba(153, 27, 27, 0.45); color: #fecaca; }
.dark .status-chip--neutral { background-color: var(--surface-3); color: var(--text-secondary); }

/* ---- INFO-PUNKT ----
   Der Erklärungs-Tooltip neben einem Abschnittstitel. Vorher ein
   `text-[11px] bg-blue-100 px-1 py-0.5 rounded-full` — eine 11px-Pille, die
   wie ein Zähler aussah und keine Trefferfläche hatte. */
.info-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  margin-left: 0.25rem;
  border-radius: var(--r-chip);
  background-color: var(--surface-3);
  color: var(--text-muted);
  font-size: 0.8em;
  vertical-align: middle;
  cursor: help;
}

/* ---- GRUPPENBESCHRIFTUNG ----
   Beschriftet einen Kachelstreifen INNERHALB einer Karte („Flottenübersicht",
   „Temperatur", „Betrieb"). Sie stand auf 11px in Versalien und war damit
   leiser als die Kacheltitel, die sie gruppiert — die Gruppierung war
   vorhanden, aber nicht ablesbar. */
.group-label {
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: 0.375rem;
  padding-bottom: 0.25rem;
  border-bottom: 1px solid var(--border-primary);
}

/* ================================================================
   6 — BEDIENELEMENTE: EINE HÖHE, EIN RADIUS

   Gemessen standen in einer einzigen Kopfzeile Knöpfe mit 24 / 28 / 30 / 41 /
   48px Höhe. Die Touch-Regeln in components.css heben Knöpfe auf 48px an,
   Selects auf 48px, ließen aber Radius und Schriftgrad je Fundstelle offen.
   ================================================================ */

.page-content button:not([class*="tabulator"]):not(.scroll-affordance-btn):not(.alarm-tab):not(.panel-tab),
.page-content select,
.page-content input[type="text"],
.page-content input[type="search"],
.page-content input[type="number"] {
  border-radius: var(--r-control);
}

/* ---- 24px-UNTERGRENZE FÜR KÄSTCHEN-ZIELE, AUCH AUF DEM DESKTOP ----
   Die 48px-Regel in components.css gilt bewusst nur im Touch-Zweig
   (<= 1279px bzw. grobes Zeigegerät); die dichte Desktop-Darstellung bleibt
   erhalten. WCAG 2.5.8 verlangt aber 24px ÜBERALL, und dort fehlten sie:
   gemessen bei 1425px trugen die Kästchen der Diagramm-Legende 12 × 12px in
   einem Label von 14 × 161px — die Trefferfläche ist das Label, und 14px
   liegen unter der Grenze. Die Kästchen der Spaltenauswahl sind mit 34px
   Label in Ordnung; betroffen waren nur diese sechs.
   24px, nicht 48px: das ist die Untergrenze, keine Touch-Vergrößerung. */
.page-content label:has(> input[type="checkbox"]),
.page-content label:has(> input[type="radio"]) {
  min-height: 24px;
  display: flex;
  align-items: center;
}

/* ---- … UND DIE 48px DES TOUCH-ZWEIGS MÜSSEN SIE WIEDER SCHLAGEN ----

   Die Regel direkt darüber war als reine UNTERGRENZE für den Desktop gedacht.
   Sie hat aber die 48px-Touch-Regel aus components.css ausgeschaltet, und zwar
   überall: `label:has(> input[type="checkbox"])` dort ist (0,1,2), der Selektor
   hier mit `.page-content` davor (0,2,2). Spezifität entscheidet vor Medien-
   bedingung und vor Dateireihenfolge — der Touch-Zweig kam nie zum Zug.

   Gemessen bei 800x480 (Schienen-Stufe, Touch-Politik gilt): die 17 Labels der
   Spaltenauswahl standen auf 176 x 29px statt 48px. Betroffen war JEDES
   Kästchen-/Radio-Label der Anwendung — Spaltenauswahl, Diagramm-Legenden,
   „Nicht mehr anzeigen", die Exportumfang-Auswahl.

   Dieselbe Bedingung wie der Zweig in components.css (Zeile ~3540); ändert sich
   die eine Schwelle, muss die andere mitgehen (der Regressionstest hält das
   fest). Gleiche Spezifität, aber später in dieser Datei ⇒ gewinnt. */
@media (any-pointer: coarse), (max-width: 1279.98px) {
  .page-content label:has(> input[type="checkbox"]),
  .page-content label:has(> input[type="radio"]) {
    min-height: 48px;
  }
}

/* ================================================================
   5c — FORMULAR- UND SUCHBAUSTEINE

   Sichtbare Beschriftungen: die Zeitraum-Auswahl der Betrieb-Seite und der
   Funktionsfilter der ETL-Seite trugen nur ein `aria-label`. Sehende Nutzer
   sahen einen Wert („Letzte 24 Stunden", „Alle Funktionen") ohne die Angabe,
   WORAUF er sich bezieht — bei einer Zeitraumwahl ist das der Unterschied
   zwischen „Meldungen der letzten 24 Stunden" und „Anlagen, die 24 Stunden
   offline sind".

   Suchfelder: dreimal dieselbe 9-Klassen-Kette mit absolut positioniertem Icon
   (Alerts, Kunden, Kältekreis), jeweils mit `max-w-sm` — auf einem 1024px
   breiten Tablet ein 384px-Feld in einer 900px-Zeile.
   ================================================================ */

.field-label {
  flex-shrink: 0;
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.field-select,
.field-input {
  min-height: var(--ctl-h);
  padding: 0.375rem 0.625rem;
  border: 1px solid var(--border-secondary);
  border-radius: var(--r-control);
  background-color: var(--surface-1);
  color: var(--text-primary);
  font-size: var(--fs-body);
}

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

/* Gruppierter Feldblock (Spaltenauswahl-Kategorien). Vorher trugen acht Blöcke
   `bg-gray-50 rounded-lg` mit p-1 an einem und p-2 an sieben. */
.field-group {
  padding: var(--sp-2);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-tile);
  background-color: var(--surface-2);
}

.field-group-title {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
  font-size: var(--fs-tile-title);
  font-weight: 700;
  color: var(--text-secondary);
}

/* Die acht Kategorie-Icons waren blau, rot, grün, violett, cyan, orange, grau
   und gelb — in einer Spaltenauswahl, wo keine dieser Farben einen Zustand
   meldet, während Rot, Grün und Orange im Rest der Anwendung genau das tun.
   Die Icon-FORM unterscheidet die Kategorien; die Farbe tat es nicht. */
.field-group-title > i {
  width: 1rem;
  text-align: center;
  color: var(--text-muted);
}

/* ---- WERKZEUGLEISTE ----
   Ruhige Fläche über einer Tabelle, die Filter/Spalten/Export gruppiert.
   Ersetzt das Verlaufspaneel der Statusübersicht. */
.toolbar {
  padding: var(--sp-2);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-card);
  background-color: var(--surface-2);
}

/* ---- KÄSTCHEN-ZEILE ---- */
.check-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--ctl-h);
  padding: 0.25rem 0.5rem;
  border-radius: var(--r-control);
  cursor: pointer;
}

.check-item:hover {
  background-color: var(--surface-3);
}

.check-item > input[type="checkbox"] {
  flex-shrink: 0;
}

/* Die Beschriftung eines Kästchens sagt, was es ein- oder ausschaltet — sie ist
   nie Nebeninformation und stand auf 12px.
   Sie MUSS umbrechen dürfen: die Anhebung auf Lesegröße hat die längsten
   Beschriftungen („Außentemp.", „Wasserdruck") über die 120px-Rasterspalte
   geschoben (nachgemessen 138 > 120 und 140 > 120 auf der Übersicht). Kürzen
   wäre hier falsch — die Beschriftung ist der ganze Inhalt der Zeile. Die Zeile
   ist auf 48px Mindesthöhe gesetzt und wächst einfach mit. */
.check-item-label {
  min-width: 0;
  font-size: var(--fs-body);
  line-height: 1.3;
  color: var(--text-primary);
  user-select: none;
  overflow-wrap: anywhere;
}

/* Und die Rasterspalte bekommt etwas mehr Grundbreite, damit die meisten
   Beschriftungen weiterhin einzeilig bleiben (7,5rem waren auf die alte
   12px-Beschriftung zugeschnitten). */
.checkbox-grid {
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
}

/* ---- SUCHFELD ---- */
.search-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.search-box {
  position: relative;
  display: flex;
  align-items: center;
  /* Auf dem Tablet darf das Feld die Zeile nutzen; `max-w-sm` (384px) ließ auf
     1024px zwei Drittel der Zeile leer, während der Platzhaltertext
     („Kunde, Ort, WP- oder Gateway-Seriennummer suchen…") nicht hineinpasste. */
  flex: 1 1 18rem;
  max-width: 34rem;
  min-width: 0;
}

.search-box-icon {
  position: absolute;
  left: 0.75rem;
  color: var(--text-muted);
  font-size: 0.85rem;
  pointer-events: none;
}

.search-input {
  width: 100%;
  min-width: 0;
  min-height: var(--ctl-h);
  padding: 0.5rem 0.75rem 0.5rem 2.25rem;
  border: 1px solid var(--border-secondary);
  border-radius: var(--r-control);
  background-color: var(--surface-1);
  color: var(--text-primary);
  font-size: var(--fs-body);
}

/* ---- HINWEISZEILE ----
   Vorbehalte und Einschränkungen standen als drittes <span class="text-gray-400">
   im Fließtext der Seitenbeschreibung: derselbe Absatz, kleinere Farbe, kein
   Zeichen. Wer die Beschreibung überfliegt, überliest genau den Satz, der die
   Aussagekraft der Seite einschränkt. */
.note-line {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  margin-top: 0.375rem;
  padding: 0.375rem 0.5rem;
  border-left: 3px solid var(--border-secondary);
  border-radius: 0 var(--r-control) var(--r-control) 0;
  background-color: var(--surface-2);
  font-size: var(--fs-meta);
  line-height: 1.45;
  color: var(--text-secondary);
}

.note-line > i {
  margin-top: 0.15em;
  flex-shrink: 0;
  color: var(--text-muted);
}

/* 1em, nicht 0.95em: `<code>` in einer 12px-Hinweiszeile landete damit bei
   11,4px — unter der Untergrenze für Kartentext, und zwar an den beiden Wörtern
   (`registeredAt`, `modifiedAt`), auf die der Hinweis gerade hinweist. */
.note-line code {
  font-size: 1em;
  padding: 0 0.2em;
  border-radius: 3px;
  background-color: var(--surface-3);
}

/* ---- HINWEISBANNER ----
   Für Zustandsaussagen, die eine ganze Seite betreffen (Schreibsperre der
   Steuerung, Installateur-Sperre, Datenaktualität). Farbe UND Kante links als
   zweites Signal — die Projektregel verlangt, dass Farbe nie allein trägt. */
.notice {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border-primary);
  border-left-width: 4px;
  border-radius: var(--r-control);
  background-color: var(--surface-2);
  font-size: var(--fs-body);
  line-height: 1.45;
  color: var(--text-primary);
}

.notice > i {
  margin-top: 0.15em;
  flex-shrink: 0;
}

.notice code {
  font-size: 0.95em;
  padding: 0 0.25em;
  border-radius: 3px;
  background-color: var(--surface-3);
  overflow-wrap: anywhere;
}

.notice--crit {
  border-color: #fca5a5;
  border-left-color: #b91c1c;
  background-color: #fef2f2;
  color: #7f1d1d;
}

.notice--warn {
  border-color: #fcd34d;
  border-left-color: #b45309;
  background-color: #fffbeb;
  color: #78350f;
}

.notice--neutral {
  border-left-color: var(--border-secondary);
  color: var(--text-secondary);
}

.dark .notice--crit {
  border-color: rgba(185, 28, 28, 0.5);
  border-left-color: #f87171;
  background-color: rgba(127, 29, 29, 0.28);
  color: #fecaca;
}

.dark .notice--warn {
  border-color: rgba(180, 83, 9, 0.5);
  border-left-color: #fbbf24;
  background-color: rgba(120, 53, 15, 0.28);
  color: #fde68a;
}

/* ---- ICON IM KARTENTITEL ----
   Vorher stand an jedem Kartentitel `text-blue-600 dark:text-blue-400 mr-1` —
   außer an denen, wo Bernstein für „Achtung" gemeint war. Zwei Rollen, ein
   Erscheinungsbild, je Fundstelle neu geschrieben. */
.card-title-icon {
  margin-right: 0.4rem;
  color: var(--accent-neutral);
  font-size: 0.9em;
}

.card-title-icon--warn { color: #b45309; }
.dark .card-title-icon--warn { color: #fbbf24; }

/* ---- LEGENDE ----
   Erklärt eine Farbkodierung und ist damit Inhalt. Sie stand auf 12px. */
.legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.875rem;
  font-size: var(--fs-meta);
  color: var(--text-secondary);
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
}

/* ================================================================
   6b — ANLAGENWAHL (Seite „Anlagendetails")

   Bleibt blau: dasselbe Blau trägt der klebende Anlagen-Banner, und beides sagt
   dasselbe („um welche Anlage geht es"). Was sich ändert, ist die Rangfolge —
   Bedienung oben, Kennung und Zustand als ruhige Zeile darunter — und der
   Schatten: `shadow-lg` an einem gesättigten Block wirkt wie ein Dialog, der
   sich über die Seite legt.
   ================================================================ */

.plant-picker {
  background-color: #0063a6;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--r-card);
  padding: var(--sp-2) var(--sp-3);
}

.dark .plant-picker {
  background-color: #0b4a75;
}

.plant-picker-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.plant-picker-label {
  flex-shrink: 0;
  font-size: var(--fs-body);
  font-weight: 600;
  color: #fff;
}

.plant-picker-actions {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  flex-shrink: 0;
}

/* Beide Knöpfe gleich: vorher war „Neu laden" gefülltes Grün und „Auswahl
   aufheben" durchscheinendes Weiß. Grün ist in dieser Anwendung „in Ordnung /
   online"; an einem Aktualisieren-Knopf behauptet es einen Zustand. */
.plant-picker-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--ctl-h);
  min-height: var(--ctl-h);
  border-radius: var(--r-control);
  background-color: rgba(255, 255, 255, 0.16);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.2);
}

.plant-picker-btn:hover:not(:disabled),
.plant-picker-btn:active:not(:disabled) {
  background-color: rgba(255, 255, 255, 0.28);
}

.plant-picker-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* `display: inline-flex` schlägt Tailwinds `.hidden` (siehe .state-block.hidden).
   „Neu laden" und „Auswahl aufheben" werden erst freigeschaltet, wenn eine Anlage
   geladen ist (gateway-manager.js) — ohne diese Regel standen beide schon vorher
   in der Leiste: einer als ausgegrauter Blindknopf, der andere anklickbar ohne
   Auswahl, die er aufheben könnte. */
.plant-picker-btn.hidden {
  display: none;
}

/* ---- FORTSCHRITTSSCHRITTE ----
   Zwei gemessene Fehler, beide in components.css entstanden und hier korrigiert,
   weil diese Schicht die autoritative ist:

   1. KONTRAST. Die noch nicht erreichten Schritte („Daten laden", „Bereit")
      standen auf `rgba(255,255,255,0.38)`. Gemessen auf der Fläche der
      Anlagenwahl: 2,17 : 1 — der WCAG-AA-Grenzwert für Text unter 24px ist
      4,5 : 1. 80 % Deckkraft ergaben nachgemessen erst 4,32 : 1 (die
      Handrechnung war zu optimistisch), 88 % ergeben 4,95 : 1. Der aktive
      Schritt bleibt reinweiß und fett und ist dadurch weiter unterscheidbar.
   2. VERSCHWINDENDE BESCHRIFTUNG. `@media (max-width: 500px) { .details-step
      span { display: none } }` hat auf Telefonen die Wörter entfernt und drei
      Icons übrig gelassen (Zeigefinger, Kreis, Häkchen). „Anlage auswählen →
      Daten laden → Bereit" ist aus diesen drei Zeichen nicht ablesbar, und die
      Zeile bricht ohnehin um — sie muss nichts verstecken. */
.details-step {
  font-size: var(--fs-meta);
  color: rgba(255, 255, 255, 0.88);
}

.details-step--active {
  font-size: var(--fs-meta);
}

.details-step--done {
  color: rgba(190, 242, 210, 0.95);
}

@media (max-width: 500px) {
  .details-step span {
    display: inline;
  }
}

.plant-picker-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.75rem;
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid rgba(255, 255, 255, 0.15);
  font-size: var(--fs-meta);
  color: rgba(255, 255, 255, 0.85);
}

/* Die Kennung der geladenen Anlage ist die wichtigste Angabe dieser Zeile und
   stand vorher auf 12px in Blau-100 neben gleich großem Beiwerk. */
.plant-picker-ident {
  min-width: 0;
  font-size: var(--fs-body);
  font-weight: 600;
  color: #fff;
}

/* ================================================================
   6c — SEKTIONS-PANEL (Seite „Anlagendetails")

   Die vier Sektionen trugen vier verschiedene Akzentfarben: Systemkomponenten
   violett, Verlaufsdaten blau, Detailansicht smaragd, Erweiterte Diagnose
   bernstein — jeweils als 4px-Kante links PLUS getönte Kopffläche. Keine dieser
   Farben sagt etwas über einen Zustand; Bernstein und Smaragd sind in dieser
   Anwendung Warnung und „in Ordnung". Vier gleichrangige Abschnitte sahen
   dadurch aus wie vier verschiedene Zustände.
   Unterschieden werden sie jetzt durch Icon und Titel — also durch die Angaben,
   die die Information tatsächlich tragen.
   ================================================================ */

.panel {
  background-color: var(--surface-1);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-card);
  overflow: hidden;
}

.panel + .panel {
  margin-top: var(--sp-2);
}

.panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border-primary);
  background-color: var(--surface-2);
}

.panel-head-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex-shrink: 0;
  border-radius: var(--r-control);
  background-color: rgba(0, 112, 186, 0.12);
  color: var(--accent-neutral);
  font-size: 0.9rem;
}

.panel-head-text {
  min-width: 0;
  flex: 1 1 auto;
}

.panel-body {
  padding: var(--sp-2) var(--sp-3);
}

.panel-body--flush {
  padding: 0.375rem;
}

/* Auf Telefonen kosten 14px seitliches Polster je Seite 7,5 % der Breite. Die
   Panels der Anlagendetails enthalten Kartenreihen und Tabellen, denen jeder
   Pixel fehlt (gemessen: mit 14px blieben der Diagnose-Karte 320px statt 332px
   Inhaltsbreite). Ab 480px gilt wieder das größere Polster. */
@media (max-width: 479.98px) {
  .panel-head,
  .panel-body {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
}

/* ================================================================
   5d — KPI-KACHEL-INNENLEBEN

   Das Icon rechts oben trug je Renderer eine eigene Farbe: Grün an der
   Online-Kachel, Bernstein an der Grenzwert-Kachel, Blau an allen
   Durchschnitten, Grau an den Platzhaltern. Vier Bedeutungen für ein
   Schmuckelement, das keine hat — die Kachel sagt über Titel und Wert schon
   alles. Es bleibt (es hilft beim Wiederfinden), wird aber gedämpft.
   ================================================================ */

.kpi-card-icon {
  flex-shrink: 0;
  font-size: 1.1rem;
  color: var(--text-muted);
  opacity: 0.55;
}

.kpi-card-icon--muted {
  opacity: 0.35;
}

/* Anklickbare Kachel als <button>: die Grenzwert-Kachel öffnete einen Dialog,
   war aber ein <div> mit onclick — mit der Tastatur unerreichbar und ohne
   erkennbare Anklickbarkeit außer `cursor: pointer`. */
.kpi-card--action {
  width: 100%;
  text-align: left;
  cursor: pointer;
  font: inherit;
}

.kpi-card--action:focus-visible {
  outline: 2px solid var(--accent-neutral);
  outline-offset: 2px;
}

/* Der Hinweis „N Anlagen ansehen" ist die Beschriftung der Aktion dieser Kachel,
   nicht Beiwerk — deshalb Lesegröße und nicht die Metadaten-Stufe. */
.kpi-card-hint {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.125rem;
  font-size: var(--fs-body);
  color: #1d4ed8;
  font-weight: 600;
}

.dark .kpi-card-hint {
  color: #93c5fd;
}

/* Aufschlüsselung im Kachelfuß (Grenzwert-Kachel). */
.kpi-breakdown {
  border-top: 1px solid var(--border-primary);
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.kpi-breakdown-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: var(--fs-meta);
  color: var(--text-secondary);
}

.kpi-breakdown-num {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

/* Aktiver Zeitraum in der Aufschlüsselung: nicht nur Farbe und Fettung, auch ein
   Balken links und der Zusatz „(gewählt)" im Text — die Projektregel verlangt,
   dass Farbe nie das alleinige Signal ist. */
.kpi-breakdown-row--active {
  color: var(--text-primary);
  font-weight: 700;
  border-left: 3px solid var(--accent-neutral);
  padding-left: 0.375rem;
  margin-left: -0.375rem;
}

/* Trendzeile: jetzt ein ganzer Satz statt „Verbesserung +2,3 %“ — dort stand
   nirgends, WOMIT verglichen wurde. `.jaz-trend-pct` ist damit entfallen: eine
   Prozentzahl auf zwei Nachkommastellen war die Genauigkeit, die genau
   niemanden weiterbringt. */
.jaz-trend {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: var(--sp-1);
  font-size: var(--fs-body);
  line-height: 1.35;
  color: var(--text-secondary);
}

/* Erklärung, warum bei manchen Anlagen nichts steht. Stand vorher nur als
   `title` an einem farbigen Wort — also unsichtbar auf Touch und unsichtbar
   für jeden, der nicht weiß, dass er dort hovern müsste. */
.jaz-hinweis {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  margin-top: var(--sp-2);
  font-size: var(--fs-meta);
  line-height: 1.4;
  color: var(--text-muted);
}

/* Werkzeugzeile über dem Ring (Zeitraumwahl der JAZ-Karte). */
.jaz-periodbar {
  padding: var(--sp-2);
  margin-bottom: var(--sp-1);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-tile);
  background-color: var(--surface-2);
}

/* „Keine Daten" / „Konfiguration ausstehend" — steht an der Stelle des Wertes
   und muss deshalb erkennbar KEIN Wert sein: gleiche Zeile, gedämpft, kursiv. */
.mv-empty {
  font-size: var(--fs-body);
  font-weight: 600;
  font-style: italic;
  color: var(--text-muted);
}

/* ================================================================
   5e — DIALOG

   Vier Dialoge, vier Gestalten: ETL-Detail (`rounded-xl`, max-w-4xl, p-4,
   Titel 18px), Grenzwert-Abweichungen (`rounded-xl`, max-w-lg, px-5 py-4,
   Titel 16px), Produkt-Tour (`rounded-2xl`, max-w-2xl, eigener Farbkopf),
   Fehler-Pop-up (eigenes Hand-CSS). Schließkreuze zwischen 32 und 48px,
   zweimal ohne `aria-label`, einmal ohne Rolle und ohne Fokusfalle.

   Die Tour behält ihren Farbkopf — sie ist bewusst eine Ausnahme (Einführung,
   kein Arbeitsdialog). Alle Arbeitsdialoge teilen ab hier eine Gestalt.
   ================================================================ */

/* `:not(.hidden)` ist hier zwingend, nicht Kosmetik: dieses Stylesheet lädt NACH
   tailwind.css, und `.dialog-overlay { display: flex }` hätte mit gleicher
   Spezifität (0,1,0) das `.hidden` (display: none) der geschlossenen Dialoge
   geschlagen — ETL-Detailfenster und Steuerungs-Dialog wären beim Seitenaufbau
   dauerhaft offen gewesen. Mit 0,2,0 gewinnt die Regel gegen `.hidden` nur dann,
   wenn `.hidden` gar nicht gesetzt ist. */
.dialog-overlay:not(.hidden) {
  display: flex;
}

/* ---- SCHICHTORDNUNG DER OVERLAYS ----

   Dialoge lagen auf z-index 50 — genau so hoch wie `#app-sidebar` in der
   Tablet-Schiene (600–1279px). Bei Gleichstand entscheidet die Dokument-
   reihenfolge, und `#welcome-intro-modal` steht VOR `#main-dashboard`: die
   Schiene lag also über der Produkt-Tour. Gemessen bei 1024x576 mit 150 %
   Systemschrift — dort reicht der Dialog bis an den linken Rand — traf
   `elementFromPoint` auf dem Kontrollkästchen „Nicht mehr anzeigen" die
   Sidebar-Profilzeile, bei 200 % den Hilfe-Knopf. Der Klick ging an die
   Navigation hinter dem Dialog.

   Die Ordnung darüber und darunter, damit der nächste Dialog nicht wieder rät:
   untere Leiste 44 · Sidebar-Backdrop 45 · Sidebar 50 · Sidebar-Tooltip 60 ·
   DIALOGE 500 · Fehler-Pop-up und Toasts 10000 (sie müssen über einem Dialog
   erscheinen — eine Fehlermeldung zur gerade bestätigten Aktion gehört gesehen). */
/* ---- WARUM JEDE HÖHE AUS --app-vh EINE UNTERGRENZE HAT ----

   `var(--app-vh, 100dvh)` fällt nur dann auf `100dvh` zurück, wenn die Variable
   UNGESETZT ist. Steht dort ein unplausibler Wert, rechnet das Layout damit.
   Genau das ist passiert: `visualViewport.height` meldet 0, solange das Dokument
   nicht angezeigt wird (Hintergrundtab, Prerender, Rückkehr aus dem
   Verlaufscache) und kurzzeitig während `orientationchange`. Der Wächter schrieb
   diese 0 durch — Overlay 0px hoch, `max-height` der Box negativ und damit 0.
   Ergebnis: der Dialog ist im DOM, aber unsichtbar und unbedienbar. Das ist der
   gemeldete Feldfehler „Dialog kann nicht weiter geklickt werden, wenn Auflösung
   zu niedrig" in seiner härtesten Form.

   Behoben ist das an der Quelle (js/viewport-metrics.js verwirft Messungen unter
   MIN_PLAUSIBLE_HEIGHT). Die `max(100px, …)` hier ist das Netz darunter: kein
   Wert von --app-vh — woher auch immer er kommt — kann einen Dialog noch auf 0
   zusammenfallen lassen. 100px ist bewusst niedrig; die Grenze soll eine
   Fehlmessung abfangen und keine echte Höhe ersetzen (bei offener Tastatur im
   Querformat bleiben real 150–200px, die als sie selbst durchkommen müssen). */
.dialog-overlay {
  position: fixed;
  inset: 0;
  /* top/height statt inset: 0 — siehe §5f. Bei offener Bildschirmtastatur ist
     der sichtbare Ausschnitt kleiner und nach oben verschoben; ein Overlay über
     die volle Layouthöhe zentriert seinen Dialog dann hinter der Tastatur. */
  top: var(--app-vt, 0px);
  height: max(100px, var(--app-vh, 100dvh));
  z-index: 500;
  align-items: center;
  justify-content: center;
  padding: var(--dialog-inset);
  background-color: rgba(0, 0, 0, 0.55);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.dialog-box {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 40rem;
  /* Reihenfolge ist Absicht: vh als Rückfall für Browser ohne dvh, dvh für die
     ein-/ausfahrende Browserleiste, --app-vh als genauester Wert (kennt auch die
     Bildschirmtastatur, siehe §5f). */
  max-height: calc(100vh - 2 * var(--dialog-inset));
  max-height: calc(100dvh - 2 * var(--dialog-inset));
  /* max(100px, …) als Netz gegen eine unplausible Messung — Begründung über
     `.dialog-overlay`. Die Grenze liegt AUSSERHALB des calc(): innen wäre sie
     nach dem Abzug des Rands wieder negativ. */
  max-height: max(100px, calc(var(--app-vh, 100dvh) - 2 * var(--dialog-inset)));
  background-color: var(--surface-1);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-card);
  /* `auto`, NICHT `hidden`. Im Normalfall ist der Unterschied keiner: Kopf,
     Körper und Fuß füllen die Box genau aus, es gibt nichts zu scrollen und
     keine Leiste erscheint. Der Unterschied zählt in dem Fall, für den es keine
     Kompaktstufe mehr gibt — 320px Bildbreite mit 200 % Systemschrift, wo schon
     der Fuß allein die Box überschreitet. Mit `hidden` war der Rest dort
     endgültig weg; mit `auto` scrollt der Nutzer hin. `auto` schneidet die
     Ecken weiterhin ab, der Radius bleibt also erhalten. */
  overflow: auto;
}

/* Breite Variante für Dialoge mit Tabelle (ETL-Lauf-Details). */
.dialog-box--wide {
  max-width: 60rem;
}

/* Schmale Variante für Formulardialoge (Steuerung): eine Spalte Felder liest
   sich in begrenzter Breite besser als über 40rem hinweg. */
.dialog-box--form {
  max-width: 34rem;
}

/* Anlagenkennung im Dialogkopf. Auf der Steuerungsseite ist das die Angabe, die
   den Unterschied zwischen der richtigen und einer fremden Heizung macht —
   deshalb Lesegröße und halbfett, nicht 12px grau. */
.dialog-plant {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: 0.125rem;
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.dialog-plant > i {
  color: var(--accent-neutral);
  font-size: 0.9em;
}

/* `flex: 0 1 auto` mit `min-height: 0` statt `0 0 auto`: der Kopf gibt Platz ab,
   BEVOR die Box überläuft und ihr `overflow: hidden` den Fuß mit der Hauptaktion
   abschneidet. Das ist das Sicherheitsnetz aus §5f — es greift nur, wenn Kopf
   plus Fuß sonst höher wären als der Dialog. Reihenfolge des Schrumpfens ergibt
   sich aus den Basisgrößen: der Körper ist grösser und gibt zuerst nach, und weil
   er `min-height: 0` hat, kann er bis auf 0 nachgeben, ehe der Kopf dran ist. */
.dialog-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  flex: 0 1 auto;
  /* 44px statt 0: der Kopf darf schrumpfen, aber nicht unter eine Bedienzeile —
     sonst wurde er bei 320px Breite und 150 % Schrift zu einem 31px-Streifen, in
     dem das Schließkreuz nur zur Hälfte stand. In px, nicht in rem: bei
     vergrößerter Wurzelschrift wäre eine rem-Untergrenze mitgewachsen und hätte
     genau in dem Fall Platz gekostet, in dem er fehlt. */
  min-height: 44px;
  overflow-y: auto;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border-primary);
  background-color: var(--surface-2);
}

.dialog-head-text {
  min-width: 0;
}

.dialog-title {
  font-size: var(--fs-section);
  font-weight: 700;
  line-height: 1.3;
  color: var(--text-primary);
}

.dialog-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  min-width: var(--ctl-h);
  min-height: var(--ctl-h);
  border-radius: var(--r-control);
  background-color: transparent;
  color: var(--text-muted);
  font-size: 1.1rem;
}

.dialog-close:hover,
.dialog-close:active {
  background-color: var(--surface-3);
  color: var(--text-primary);
}

.dialog-close:focus-visible {
  outline: 2px solid var(--accent-neutral);
  outline-offset: 1px;
}

/* Nur der Körper scrollt — Kopf und Fuß behalten ihren Platz, sonst scrollt der
   Dialogtitel aus dem Bild und man weiß nicht mehr, was man gerade bearbeitet. */
.dialog-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-2) var(--sp-3);
}

.dialog-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
  flex: 0 0 auto;
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--border-primary);
  background-color: var(--surface-2);
}

/* ---- HÖHENBEGRENZUNG FÜR DIALOGE MIT EIGENEM KOPF ----

   Die fünf Parameter-Dialoge (Export, „Alle Parameter", Hinweis, Ladeanzeige,
   Fehler) bringen einen eigenen Farbkopf mit und laufen deshalb nicht über
   `.dialog-box`. Ihre Höhe war unbegrenzt: bei 1280x600 wuchs die Box des
   Export-Dialogs auf 650px, stand von -25px bis 625px, und „Abbrechen" lag bei
   609px unter dem Bildrand. Nachscrollen ging nicht — der Overlay hatte
   `overflow: visible`.

   Diese Klassen liefern nur die Struktur. Farbe, Breite und Radius bleiben bei
   der jeweiligen Box, sonst wären es andere Dialoge als vorher.

   Warum die Box begrenzt wird und nicht einfach der Overlay scrollt: der
   Overlay zentriert mit `align-items: center`. Ist der Inhalt höher als der
   Container, schiebt das Zentrieren den oberen Teil über den Scroll-Ursprung
   hinaus — er wäre dann unerreichbar. Genau deshalb hat auch `.dialog-box`
   eine Höhenbegrenzung statt eines scrollenden Overlays. */
.dialog-fit-overlay {
  /* top/height/z-index wie bei `.dialog-overlay`: schlägt `inset-0` und `z-50`
     der Utility-Kette im Markup, weil dieses Stylesheet zuletzt lädt. */
  top: var(--app-vt, 0px);
  /* Untergrenze wie bei `.dialog-overlay` — Begründung dort. */
  height: max(100px, var(--app-vh, 100dvh));
  z-index: 500;
  padding: var(--dialog-inset);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.dialog-fit {
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 2 * var(--dialog-inset));
  max-height: calc(100dvh - 2 * var(--dialog-inset));
  max-height: max(100px, calc(var(--app-vh, 100dvh) - 2 * var(--dialog-inset)));
  /* Schlägt das `overflow-hidden` der Utility-Kette im Markup — Begründung bei
     `.dialog-box`. Ohne das blieb bei 320px Breite und 200 % Schrift der Fuß
     dieses Dialogs 96px weit ausserhalb und unerreichbar. */
  overflow: auto;
}

/* Die fünf Parameter-Dialoge bringen ihren eigenen Farbkopf und Fuß mit. Beide
   trugen `shrink-0` aus der Utility-Kette — damit konnte NUR der Körper
   nachgeben, und sobald Kopf plus Fuß höher waren als die Box, schnitt
   `overflow-hidden` den Fuß samt „Abbrechen"/„Schließen" ab. Gemessen bei
   900x260: Box 228px hoch, Inhalt 253px — 25px fehlten, und zwar unten.
   `dialog-fit-head`/`-foot` machen den Kopf nachgebend und den Fuß unantastbar. */
.dialog-fit-head {
  flex: 0 1 auto;
  min-height: 44px;
  overflow-y: auto;
}

/* `flex-wrap` fehlte: bei 320px Breite und vergrößerter Schrift liefen die
   Zeilen des Fußes („Gesamt: 40 Parameter" | CSV-Export + Schließen) seitlich
   auseinander statt umzubrechen. Umbrechen ist hier richtig — der Fuß darf
   höher werden, er wird ja nie abgeschnitten. */
.dialog-fit-foot {
  flex: 0 0 auto;
  flex-wrap: wrap;
}

/* Das Schließkreuz im Farbkopf trug nur `p-2` und war als Flex-Kind schrumpfbar:
   auf 320px Breite quetschte die Kopfzeile es auf 24x37px zusammen — bei der
   Ladeanzeige und dem Fehlerdialog ist es der einzige Ausweg. Jetzt fest 48px,
   in den flachen Stufen 36px (die Höhe ist dort das knappe Gut, nicht die
   Breite — und 36px bleibt deutlich über der 24px-Untergrenze). */
.dialog-fit-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-width: var(--ctl-h);
  min-height: var(--ctl-h);
}

/* In den flachen Stufen 36px — und Polster wie Symbolgröße in px, nicht in rem:
   bei 200 % Systemschrift wuchs das `p-2 text-xl` der Utility-Kette auf 69px,
   also höher als der 44px-Kopf, in dem das Kreuz sitzt. Es stand dann dauerhaft
   3px über dessen Oberkante und war nur teilweise zu treffen. */
:root[data-vh='sm'] .dialog-fit-close,
:root[data-vh='xs'] .dialog-fit-close {
  min-width: 36px;
  min-height: 36px;
  width: 36px;
  height: 36px;
  padding: 4px;
}

:root[data-vh='sm'] .dialog-fit-close i,
:root[data-vh='xs'] .dialog-fit-close i {
  font-size: 16px;
  line-height: 1;
}

/* Der scrollende Bereich zwischen Kopf und Fuß. `min-height: 0` ist zwingend:
   ein Flex-Kind schrumpft sonst nicht unter seine Inhaltshöhe und die
   Begrenzung der Box liefe ins Leere. */
.dialog-fit-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Variante für Dialoge ohne getrennten Kopf/Fuß (Ladeanzeige): dort scrollt
   die Box selbst, es gibt nichts, was stehen bleiben müsste. */
.dialog-fit--scroll {
  overflow-y: auto;
}

/* ================================================================
   5g — ANLAGEN-KONTEXTLEISTE

   Höhe gemessen bei 1024x600, Anlage „Olaf Wißemann" (typischer Datensatz mit
   Adresse, Telefon, Deal-Name und Deal-ID):

   | Teil                            | vorher | jetzt |
   |---------------------------------|--------|-------|
   | Bereichsbezeichnung             |   16   |   \\    Zeile 1 zusammen 22px
   | Name + vier weitere Kennungen   |   50   |   /    Zeile 2 (Nebenzeile) 17px
   | Knopf „Ändern" in eigener Zeile |   48   |    0  (steht rechts daneben)
   | Polster                         |   24   |   12
   | SUMME                           |  145   |   ~62 |

   Keine Angabe entfällt. Was sich ändert, ist die Rangfolge: vorher standen
   Name, Adresse, Telefon, Deal-Name und Deal-ID alle in 16px halbfett weiss
   nebeneinander — fünf gleich laute Angaben ohne Rangfolge, die zusammen zwei
   Zeilen füllten. Jetzt trägt der Name die Lesegröße und die übrigen vier
   stehen als Nebenzeile darunter.
   ================================================================ */

.plant-bar {
  /* z-index unter den Dialogen (500) und unter der Sidebar (50), aber über dem
     Seiteninhalt — sie klebt, während darunter gescrollt wird. */
  z-index: 40;
  padding: 0.375rem 1rem;
}

.plant-bar-inner {
  display: grid;
  /* Der Knopf rechts über ALLE Zeilen: als Flex-Kind rutschte er in eine eigene
     Zeile und kostete 48px Höhe, obwohl rechts Platz frei war. */
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'id      change'
    'meta    change'
    'cascade change';
  align-items: center;
  column-gap: 0.75rem;
  row-gap: 0.0625rem;
}

.plant-bar-id {
  grid-area: id;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.125rem 0.5rem;
  min-width: 0;
}

.plant-bar-label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex: 0 0 auto;
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #dbeafe;
}

/* Der Name ist die Angabe, die die richtige von einer fremden Anlage
   unterscheidet — Lesegröße und halbfett, und er darf umbrechen statt zu
   kürzen (Politik „keine Truncation"). */
.plant-bar-name {
  min-width: 0;
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.25;
  color: #ffffff;
  overflow-wrap: anywhere;
}

.plant-bar-meta {
  grid-area: meta;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.0625rem 0.375rem;
  min-width: 0;
  font-size: var(--fs-meta);
  line-height: 1.35;
  color: #eff6ff;
}

/* blue-200 statt blue-300: auf blue-600 kommt blue-300 auf 3,7 : 1 und liegt
   damit unter AA. Die Trenner sind `aria-hidden`, aber sie werden gelesen —
   von Augen. */
.plant-bar-sep {
  color: #bfdbfe;
}

.plant-bar-idlabel {
  color: #bfdbfe;
}

/* Die Deal-ID ist eine Kennung, keine Prosa — Ziffern in gleicher Laufweite
   lassen sich vorlesen und vergleichen. */
.plant-bar-serial {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  overflow-wrap: anywhere;
}

.plant-bar-cascade {
  grid-area: cascade;
  align-items: center;
  gap: 0.3rem;
  min-width: 0;
  font-size: var(--fs-meta);
  color: #dbeafe;
}

/* `.hidden` (display:none) muss gewinnen, solange keine Kaskade vorliegt —
   gateway-manager.js schaltet die Klasse. Deshalb hier KEIN display, sondern
   erst in der sichtbaren Variante. */
.plant-bar-cascade:not(.hidden) {
  display: flex;
}

.plant-bar-change {
  grid-area: change;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  flex: 0 0 auto;
  min-height: var(--ctl-h);
  padding: 0 0.75rem;
  border-radius: var(--r-control);
  background-color: rgba(255, 255, 255, 0.2);
  color: #ffffff;
  font-size: var(--fs-meta);
  font-weight: 600;
  white-space: nowrap;
}

.plant-bar-change:hover,
.plant-bar-change:active {
  background-color: rgba(255, 255, 255, 0.32);
}

.plant-bar-change:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}

/* Flach: Polster runter, Knopf auf 36px. Die Angaben bleiben unverändert. */
:root[data-vh='sm'] .plant-bar,
:root[data-vh='xs'] .plant-bar {
  padding: 0.25rem 0.625rem;
}

:root[data-vh='sm'] .plant-bar-change,
:root[data-vh='xs'] .plant-bar-change {
  min-height: 36px;
}

/* ================================================================
   5f — HÖHENSTUFEN (data-vh)

   AUSLÖSER: Feldmeldung „Dialog im WP Monitoring kann nicht weiter geklickt
   werden wenn Auflösung zu niedrig".

   NACHGESTELLT UND GEMESSEN (Produkt-Tour, Schritt 1):

   | Viewport | Box max-height | Inhalt | „Weiter" liegt bei | im Bild |
   |----------|----------------|--------|--------------------|---------|
   | 1280x600 | 568px          | 568px  | 532–568            | ja      |
   |  800x480 | 448px          | 448px  | 400–448            | ja      |
   |  900x260 | 228px          | 331px  | 283–331            | NEIN    |
   |  512x300 | 268px          | 385px  | 337–385            | NEIN    |

   512x300 ist 1024x600 bei 200 % Zoom, 900x260 ein Tablet quer mit offener
   Tastatur. Ursache war nicht die Höhenbegrenzung — die war richtig — sondern
   dass Kopf (194px, unveränderlich) und Fuß (81–135px, wächst beim Umbrechen)
   zusammen höher wurden als die erlaubte Box. `overflow: hidden` schnitt dann
   genau den Fuß ab, und der Overlay hatte nichts zu scrollen, weil die Box
   sich ja brav begrenzt hatte. Der Knopf war nicht „schwer erreichbar",
   sondern nicht vorhanden.

   ZWEI MITTEL, IN DIESER REIHENFOLGE:

   1. Der Rahmen wird kleiner, statt den Inhalt zu verdrängen (dieser Block).
      Bei 300px Bildhöhe kostet ein 28px-Polster mehr als es ordnet.
   2. Sicherheitsnetz in der Flexbox (oben bei `.dialog-head` /
      `.dialog-fit-head`): reicht auch das nicht, gibt der KOPF nach und wird
      selbst scrollbar. Der Fuß mit der Hauptaktion ist damit unantastbar.

   Warum `data-vh` und nicht `@media (max-height: …)`: siehe
   js/viewport-metrics.js — die Bildschirmtastatur verkleinert den
   Layout-Viewport nicht, Media Queries sehen sie also gar nicht.
   ================================================================ */

/* ---- DER ARBEITSBEREICH MUSS DEN BILDSCHIRM BEHERRSCHEN ----

   Gemessen: senkrechte Position, an der auf jeder Seite der erste echte
   technische Inhalt beginnt (1024x600, Anlage gewählt):

   | Seite       | vorher | nach Kontextleiste | Ziel |
   |-------------|--------|--------------------|------|
   | Dashboard   |   278  |        193         |      |
   | Details     |   419  |        334         |      |
   | Betrieb     |   463  |        283         |      |
   | Alerts      |   514  |        429         | <300 |
   | Kunden      |   537  |        367         | <300 |
   | Kältekreis  |   310  |        310         | <300 |

   Bei 600px Bildhöhe waren auf „Kunden" 90 % der Fläche Rahmung. Übrig sind die
   Seitenköpfe: Titel, ein bis zwei erklärende Absätze und ein Hinweiskasten.
   Diese Texte bleiben vollständig — sie werden nur enger gesetzt (Titelgröße
   eine Stufe kleiner, Zeilenabstand 1,5 → 1,35, Abstände halbiert). Nichts wird
   gekürzt und nichts eingeklappt: ein „…" oder ein Aufklapper hätte Information
   verborgen, und die Politik dieses Projekts ist, dass Text vollständig
   dasteht. */
/* Ab „md" (unter 760px sichtbare Höhe) UND ab Tablet-Breite. Beide Bedingungen
   sind nötig: 1024x600, 1280x600 und 1366x650 — die Fenster aus dem Feldbericht —
   liegen alle in „md", ein Handy im Hochformat (375x667) aber auch. Dort ist
   Höhe kein knappes Gut, und der Seitentitel soll nicht ohne Grund von 20 auf
   16px fallen. Die Enge entsteht im Querformat: flach UND breit. */
@media (min-width: 600px) {
  :root[data-vh='md'] .page-title {
    font-size: var(--fs-section);
  }

  :root[data-vh='md'] .section-desc {
    font-size: var(--fs-meta);
    line-height: 1.4;
  }

  :root[data-vh='md'] .note-line {
    margin-top: 0.25rem;
    padding: 0.25rem 0.4375rem;
    line-height: 1.4;
  }
}

/* Die Statuszeile der Betrieb-Seite trägt viel: Aufschlüsselung je Kategorie,
   Summe, sichtbare gegen vorhandene Zeilen, Statusfilter, Meldungen außerhalb
   des Zeitraums, Zeitstempel, neu/behoben und der nächste Abruf. Alles davon
   bleibt — es wird nur eng gesetzt statt als Fließtext mit Zeilenabstand 1,5.
   Gemessen mit voller Meldungslage: 91px auf vier Zeilen → 62px auf drei. */
.alarm-status-line {
  display: block;
  font-size: var(--fs-meta);
  line-height: 1.35;
}

@media (min-width: 600px) {
  :root[data-vh='md'] .alarm-status-line {
    line-height: 1.3;
  }
}

:root[data-vh='sm'] .alarm-status-line,
:root[data-vh='xs'] .alarm-status-line {
  line-height: 1.25;
}

/* Die Abschnittsleiste („Abschnitte: ÜH/UK-Fehlermatrix · Schwellwert-Alarme
   · Korrelationen · Plausibilität") ist auf einer 8,8 Bildschirmhöhen langen
   Seite nützlich, kostete aber bei 1024x600 zwei Reihen zu 66px, weil ihre
   Verweise `min-height: 48px` und Lesegröße tragen. Im flachen Querformat: eine
   Reihe, 36px, Beschriftungen unverändert. Alle Verweise bleiben.

   Dazu die Ränder zwischen Suchzeile, Abschnittsleiste und erster Karte: dreimal
   12px sind bei 600px Bildhöhe 6 % der Fläche für Luft zwischen drei Bauteilen. */
@media (min-width: 600px) {
  :root[data-vh='md'] .section-nav,
  :root[data-vh='sm'] .section-nav,
  :root[data-vh='xs'] .section-nav {
    gap: 0.25rem;
    margin-bottom: 0.375rem;
    padding: 0.25rem 0.375rem;
  }

  /* 40px, nicht 36px: 40px ist die Kompaktgröße, auf die sich die flachen Stufen
     überall sonst geeinigt haben — Sidebar-Ziele (`--sidebar-item-h`) und
     Kategorie-Reiter stehen beide dort. Mit 36px war die Abschnittsleiste das
     einzige Bedienelement der Anwendung, das im flachen Querformat darunter lag,
     also die kleinste Trefferfläche überhaupt — gemessen bei 700x500 vier
     Verweise mit 36px Höhe. Die 4px kosten eine EINZIGE Reihe, weil die Leiste
     genau eine ist; der Grund für die Verdichtung (bei 1024x600 waren es zwei
     Reihen à 66px) bleibt damit erfüllt. */
  :root[data-vh='md'] .section-nav a,
  :root[data-vh='sm'] .section-nav a,
  :root[data-vh='xs'] .section-nav a {
    min-height: 40px;
    padding: 0.125rem 0.5rem;
    font-size: var(--fs-meta);
  }

  :root[data-vh='md'] .search-row,
  :root[data-vh='sm'] .search-row,
  :root[data-vh='xs'] .search-row {
    margin-bottom: 0.375rem !important;
  }
}

/* Die Kategorie-Reiter tragen in der Touch-Stufe 48px Mindesthöhe. Umbrechend
   sind das zwei Reihen, also 101px Seitenkopf. Im flachen Querformat auf 40px:
   noch komfortabel für den Finger, 17px weniger Rahmung. */
@media (min-width: 600px) {
  :root[data-vh='md'] .alarm-tabnav .alarm-tab,
  :root[data-vh='sm'] .alarm-tabnav .alarm-tab,
  :root[data-vh='xs'] .alarm-tabnav .alarm-tab {
    min-height: 40px;
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
  }
}

/* Unter 560px sichtbarer Höhe gilt es auf JEDER Breite — dort ist auch das
   Hochformat keine Ausrede mehr. */
:root[data-vh='sm'] .page-title,
:root[data-vh='xs'] .page-title {
  font-size: var(--fs-section);
}

:root[data-vh='sm'] .section-desc,
:root[data-vh='xs'] .section-desc {
  font-size: var(--fs-meta);
  line-height: 1.35;
}

:root[data-vh='sm'] .note-line,
:root[data-vh='xs'] .note-line {
  margin-top: 0.1875rem;
  padding: 0.1875rem 0.375rem;
  line-height: 1.35;
}

/* sm: Tablet quer, Split Screen, 200 % Zoom auf einem Notebook. */
:root[data-vh='sm'],
:root[data-vh='xs'] {
  --dialog-inset: 0.5rem;
}

:root[data-vh='xs'] {
  --dialog-inset: 0.25rem;
}

:root[data-vh='xs'] .dialog-head,
:root[data-vh='xs'] .dialog-foot,
:root[data-vh='xs'] .dialog-body {
  padding: 0.375rem 0.625rem;
}

/* Der Titel darf schrumpfen, die ANLAGENKENNUNG nicht: sie unterscheidet die
   richtige von einer fremden Heizung (siehe `.dialog-plant`). Deshalb geht hier
   die Titelzeile auf Abschnittsgröße zurück und die Kennung bleibt Lesegröße. */
:root[data-vh='xs'] .dialog-title {
  font-size: var(--fs-tile-title);
}

/* Die Farbköpfe der Parameter-Dialoge tragen ihr Polster als Utility (px-6 py-4)
   und ihre Titelgröße als text-xl. Beides hier zurücknehmen — die Klasse sitzt
   am selben Element, dieses Stylesheet lädt zuletzt, also gewinnt es. */
:root[data-vh='sm'] .dialog-fit-head,
:root[data-vh='sm'] .dialog-fit-foot {
  padding: 0.5rem 0.75rem;
}

:root[data-vh='xs'] .dialog-fit-head,
:root[data-vh='xs'] .dialog-fit-foot {
  padding: 0.375rem 0.625rem;
}

:root[data-vh='xs'] .dialog-fit-head h2 {
  font-size: var(--fs-section);
}

/* Untertitel und Herkunftszeile der Farbköpfe tragen `text-sm`/`text-xs` — bei
   150 % Wurzelschrift sind das 21px bzw. 18px und der Kopf war dreizeilig.
   Feste px halten ihn einzeilig, ohne unter die 11px-Untergrenze zu gehen. */
:root[data-vh='xs'] .dialog-fit-head p {
  font-size: var(--fs-meta);
}

:root[data-vh='xs'] .dialog-fit-head i {
  font-size: 1rem;
}

/* ---- ABWEICHUNGSLISTE IM DIALOG ---- */
.viol-plant {
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--border-primary);
}

.viol-plant:last-child {
  border-bottom: 0;
}

.viol-plant-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 0.75rem;
  margin-bottom: 0.375rem;
}

.viol-plant-name {
  font-size: var(--fs-body);
  font-weight: 700;
  color: var(--text-primary);
}

/* Die Seriennummer identifiziert die Anlage eindeutig und darf deshalb nicht
   auf 11px stehen — bei zwölf gleichnamigen Anlagen ist sie die Kennung. */
.viol-plant-serial {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-meta);
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.viol-row {
  display: grid;
  grid-template-columns: minmax(6rem, 1fr) auto;
  align-items: baseline;
  gap: 0.125rem 0.75rem;
  padding: 0.25rem 0;
}

.viol-row-label {
  font-size: var(--fs-body);
  color: var(--text-secondary);
}

.viol-row-figure {
  display: inline-flex;
  align-items: baseline;
  gap: 0.2rem;
  justify-self: end;
}

/* Der Grenzwert steht unter dem Istwert und rechtsbündig darunter — er ist der
   Vergleichsmaßstab, nicht ein zweiter Messwert. */
.viol-row-limit {
  grid-column: 2;
  justify-self: end;
  font-size: var(--fs-meta);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.viol-row .mv-value {
  font-size: var(--fs-value);
}

/* ================================================================
   6d — LEER-, LADE- UND FEHLERZUSTÄNDE

   Vorher baute jede Stelle ihren eigenen: ein zentrierter Spinner mit
   `text-gray-400` hier, ein rot umrandetes Kärtchen dort, ein 24rem hoher
   Leerbereich mit grauem Kreis daneben — und auf der ETL-Seite gar keiner: dort
   landete die rohe Fehlerantwort der Datenbank als Text im Dokument, samt
   ActivityId und interner Azure-Partitions-URI. Ein Nutzer sah einen
   JSON-Auszug, keinen Hinweis und keinen Weg zurück.

   Ein Zustand besteht immer aus vier Teilen: Zeichen, Aussage, Erklärung,
   Ausweg. Fehlt der Ausweg, ist es eine Sackgasse.
   ================================================================ */

.state-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  padding: var(--sp-3);
  text-align: center;
}

/* `display: flex` oben schlägt Tailwinds `.hidden { display: none }` — beide sind
   eine Klasse, und diese Datei lädt NACH tailwind.css, also gewinnt die spätere
   Regel. Ein Zustandsblock, den das JS per `classList.add('hidden')` wegräumt,
   blieb deshalb stehen: auf der Anlagenseite stand „Keine Verlaufsdaten" dauerhaft
   unter dem fertig gezeichneten Diagramm. Zwei Klassen schlagen eine, damit gilt
   `hidden` wieder — für jeden Zustandsblock der Anwendung, nicht nur den einen.
   (Die Klassenliste allein beweist nichts: sie enthielt `hidden`, während
   `getComputedStyle().display` `flex` lieferte. Immer das berechnete `display`
   messen.) */
.state-block.hidden {
  display: none;
}

/* Kompakte Variante für Zustände innerhalb einer Kachel oder eines Panels. */
.state-block--inline {
  padding: var(--sp-2);
}

.state-block-icon {
  font-size: 1.5rem;
  color: var(--text-muted);
}

.state-block-title {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text-primary);
}

.state-block-msg {
  font-size: var(--fs-meta);
  line-height: 1.45;
  max-width: 34rem;
  color: var(--text-muted);
}

.state-block--error .state-block-icon { color: #b91c1c; }
.dark .state-block--error .state-block-icon { color: #f87171; }

.state-block--warn .state-block-icon { color: #b45309; }
.dark .state-block--warn .state-block-icon { color: #fbbf24; }

/* Technische Einzelheiten eines Fehlers gehören nicht in die Hauptaussage, aber
   sie dürfen auch nicht verschwinden — wer das Backend betreut, braucht sie. */
.state-block-detail {
  width: 100%;
  max-width: 40rem;
  margin-top: 0.25rem;
  text-align: left;
}

.state-block-detail > summary {
  font-size: var(--fs-meta);
  color: var(--text-muted);
  cursor: pointer;
  padding: 0.375rem 0;
  min-height: 2rem;
}

.state-block-detail pre {
  margin: 0;
  padding: 0.5rem 0.625rem;
  border-radius: var(--r-control);
  background-color: var(--surface-3);
  color: var(--text-secondary);
  font-size: var(--fs-meta);
  line-height: 1.4;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 12rem;
  overflow-y: auto;
}

/* ================================================================
   6e — VERBRAUCHSTAFEL DER ANLAGENDETAILS: EINHEITEN LESBAR MACHEN

   Gemessen auf der Anlagendetail-Seite mit geladener Anlage, Light Mode,
   753px Viewport — acht Textknoten unter dem AA-Grenzwert 4,5 : 1:

   | Knoten                        | Klasse            | gemessen  |
   |-------------------------------|-------------------|-----------|
   | „kWh" (Strom)                 | .cons-total-unit  | 2,33 : 1  |
   | „100 %" / „0 %"               | .cons-total-pct   | 2,33/2,39 |
   | „(100P)" (Datenpunkte)        | .cons-header-dim  | 2,43 : 1  |
   | „kWh" (Gas)                   | .cons-total-unit  | 2,39 : 1  |

   Alle drei Klassen stehen auf `#9ca3af` (Tailwind gray-400) auf einer hellen
   Fläche. Das ist ausgerechnet die EINHEIT des Energiewerts und ihr Anteil —
   die Angaben, ohne die die Zahl daneben nichts bedeutet. Die 12px waren
   zusätzlich die Untergrenze; die Einheit steht neben einem 16px-Wert und darf
   eine Stufe darüber liegen.

   Nur Farbe und Grad, keine Zahl und keine Einheit selbst.
   ================================================================ */

.cons-total-unit,
.cons-total-pct,
.cons-header-dim,
.cons-inactive {
  color: var(--text-secondary);
}

.dark .cons-total-unit,
.dark .cons-total-pct,
.dark .cons-header-dim,
.dark .cons-inactive {
  color: var(--text-secondary);
}

.cons-total-unit {
  font-size: 13px;
}

/* Der Verbrauchswert selbst ist der Leitwert dieser Tafel und stand mit 16px
   nur 4px über seiner Einheit. */
.cons-total-val {
  font-size: 20px;
  font-variant-numeric: tabular-nums;
}

/* ================================================================
   6g — KEIN EINGRIFF AN DER ÜH/UK-MATRIXZELLE (dokumentierter Nicht-Befund)

   Die Messung meldete hier über alle Breiten hinweg „Inhalt breiter als Box":
   `.alert-matrix-cell` 122 > 114 bei 500px, 134 > 126 bei 600px, 177 > 169 bei
   753px — immer genau 8px. Zwei Erklärungsversuche und beide waren falsch:

   1. „Der Diagnosetext bricht nicht um." Nachgemessen: er tut es. Der Text
      belegt 102px in einer 114px-Zelle.
   2. „Die Flex-Zeile bricht nicht um." Die Zelle ist `flex-direction: column`;
      es gibt keine Zeile, die umbrechen könnte.

   Die 8px stammen von `.alert-matrix-count` — der Zähler-Plakette, die per
   `position: absolute; top: -8px; right: -8px` ABSICHTLICH über die Ecke der
   Zelle hinausragt, wie eine Benachrichtigungsblase. Sie liegt im Rasterabstand
   und überdeckt nichts.

   Zwei Regeln dagegen (`flex-wrap: wrap` an der Zelle, `overflow-wrap: anywhere`
   am Text) standen hier bereits im Code und sind wieder entfernt: sie haben ein
   Messartefakt behandelt. Der Eintrag bleibt als Warnung stehen, damit der
   nächste Durchgang nicht dieselbe Runde dreht.
   ================================================================ */

/* Die letzten Streu-Radien. Nachgemessen über alle zehn Seiten blieben nach der
   Konsolidierung noch 3px (Verbrauchstafel: Kopf, Summenkästen,
   Kategorietabelle), 4px (Diagnose-Kategoriekasten, 13-mal) und 6px
   (Diagnose-Container) übrig — alle in den Altbeständen der
   Anlagendetail-Seite, alle ohne Verschachtelungsgrund.
   Damit bleiben vier Radien: Karte 12, Kachel 10, Bedienelement 8, Pille. */
.cons-cat-table,
.cons-header,
.cons-total,
.diagnostics-compact-container,
.diagnostics-category-compact {
  border-radius: var(--r-control);
}

/* ---- ACHSENBESCHRIFTUNG DER ÜH/UK-MATRIX ----
   „UK niedrig / normal / hoch" und „ÜH niedrig / normal / hoch" standen auf 11px.
   Das ist der Schlüssel, ohne den die neun Zellen der Matrix nicht zu lesen
   sind — keine Nebeninformation, sondern die Beschriftung der beiden Achsen.
   Ebenso die Gruppenüberschriften der Panel-Tabs („Protokolle", „Analyse"): sie
   sagen, was die Reiter daneben gruppiert. */
.alert-matrix-colhead,
.alert-matrix-rowhead,
.panel-tab-group-label {
  font-size: var(--fs-meta);
}

/* ================================================================
   6j — DIAGRAMM-LEGENDE: KOPFZEILE DARF SCHRUMPFEN

   Gemessen bei 1024px Fensterbreite (iPad quer — die häufigste
   Tablet-Arbeitshaltung), Anlagendetail-Seite mit geladener Anlage:
   `#plot-legend-panel` ist 192px breit (12rem, ab dieser Breite steht die
   Legende neben dem Diagramm), sein Kopf aber 328px. Der Kopf ist ein Flex-Item
   der Spalte und trägt damit `min-width: auto` — er schrumpft also nicht unter
   seine Min-Content-Breite, und die ergibt sich aus „Parameter" plus zwei
   Knöpfen, die über die 44px-Touch-Regel eine Mindestbreite haben.

   Das Panel trägt `overflow: hidden`. Der Überhang wird deshalb nicht
   scrollbar, sondern ABGESCHNITTEN: 136px der Kopfzeile samt dem Knopf
   „Keine auswählen" waren weg — und zwar unerreichbar, nicht nur unschön.
   Dieselbe Ursache wie beim Befund B8 des Responsive-Audits, nur eine Ebene
   tiefer und deshalb damals nicht mitgefunden.

   `min-width: 0` allein genügt hier NICHT — nachgemessen blieb die Kopfzeile
   danach unverändert bei 328px (`min-width` stand auf 0px, die Breite nicht).
   Das Panel ist wegen `overflow: hidden` ein Scroll-Container, und die
   Quer-Streckung (`align-items: stretch`) greift an diesem Flex-Item nicht;
   es wird auf seine Inhaltsbreite gesetzt. Eine ausdrückliche Breite von 100 %
   ergibt die 190px des Panels — verifiziert, nicht hergeleitet.
   ================================================================ */

#plot-legend-panel > * {
  width: 100%;
  min-width: 0;
}

/* Nachdem die Kopfzeile das Panel nicht mehr aufspannt, fiel die eigentliche
   Legendenliste auf die 190px zurück — und dort liefen die Parameternamen über
   (nachgemessen 175 > 104 und dreimal 127 > 122). components.css lässt die
   Namen schon umbrechen (`#plot-legend label span { overflow-wrap: anywhere }`),
   aber die Zeilen sind Flex-Container, und ein Flex-Item schrumpft ohne
   `min-width: 0` nicht unter seine Min-Content-Breite — das Umbrechen kam also
   nie zum Tragen. */
#plot-legend .flex,
#plot-legend .flex > * {
  min-width: 0;
}

/* Und die Namen selbst dürfen nicht gekürzt werden. Sie trugen `truncate`
   (Tailwind: overflow hidden + ellipsis + nowrap), und `white-space: nowrap`
   schlägt jedes `overflow-wrap` — die Regel in components.css war damit
   wirkungslos. Gemessen: „Sollwert Raumtemperatur" braucht 175px in einer
   104px-Zeile, „Wasserdruck (Kessel)" 149px. Der Name ist die Kennung der
   Messreihe im Diagramm; von „Sollwert Raumtempe…" ist nicht ablesbar, ob es
   der Soll- oder der Istwert ist. Die Legende scrollt senkrecht — Höhe ist
   hier billiger als Bedeutung. */
#plot-legend span {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
}

/* ================================================================
   6i — ERWEITERTE DIAGNOSE: DER MESSWERT WIRD DOMINANT

   Der schwerste Verstoß gegen die Kernregel „der Messwert muss stärker sein als
   seine Umgebung", gemessen auf der Anlagendetail-Seite:

   • 49 Bezeichnungen (`.diagnostics-label-compact`) auf 12px
   • 47 Messwerte     (`.diagnostics-value-compact`) auf 12px

   Also GENAU dieselbe Größe für „Raumtemperatur:" und „20.8 °C". Der Wert war
   nur an `font-weight: 600` und der Monospace-Schrift zu erkennen; in einer
   Spalte aus 96 solchen Zeilen ist das kein ablesbarer Unterschied. Die Zeile
   ist ein Raster aus Bezeichnung und Wert — beide gleich laut heißt: keine
   Rangfolge.

   Zweiter Befund in derselben Regel: die Bezeichnung trug
   `white-space: nowrap` + `text-overflow: ellipsis`. Parameternamen wurden also
   gekürzt („Vorlauftemperatur Heizk…"), und zwar die Angabe, die sagt, WAS der
   Wert daneben ist. Sie darf umbrechen; das kostet Höhe und gewinnt Bedeutung.
   ================================================================ */

.diagnostics-item-compact {
  align-items: baseline;
  gap: 2px 8px;
  padding: 3px 6px;
  line-height: 1.3;
}

.diagnostics-label-compact {
  font-size: 13px;
  font-weight: 500;
  /* Kürzung zurückgenommen — siehe Begründung oben. */
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
}

.diagnostics-value-compact {
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* Ab Desktop wieder dichter — dort ist die Rangfolge über 15px gegen 12px schon
   deutlich, und die Tafel zeigt vier Spalten nebeneinander. */
@media (min-width: 1280px) and (any-pointer: fine) {
  .diagnostics-label-compact { font-size: 12px; }
  .diagnostics-value-compact { font-size: 14px; }
}

/* Die Erklärung eines Leer-/Fehlerzustands ist der Satz, der sagt was zu tun
   ist — auf der Metadaten-Stufe (11px am Desktop) war sie das Kleingedruckte
   ihres eigenen Hinweises. */
.state-block-msg {
  font-size: var(--fs-body);
}

/* ================================================================
   6h — DIAGNOSE-KARTENKOPF BRICHT UM

   Derselbe Fehler, den der Responsive-Umbau als Hauptursache B1 beschrieben und
   für alle Karten-Header über `.card-head` behoben hat — diese eine Kopfzeile
   war nicht dabei: `.diagnostics-compact-header` ist `display: flex` ohne
   `flex-wrap`, mit Gerätekennung links und Zeitstempel rechts.

   Gemessen bei 375px: Gerätekennung „CONTROL/VRC-720: 2125380010045478…" 273px
   plus „Aktualisiert: 10.08.2026, 15:07:28" 79px ergeben 352px + Abstand in
   einer 320px-Box (nachgemessen scrollWidth 361 > clientWidth 320, in zwei
   Karten). Der Zeitstempel endete bei x = 388 und war damit hinter dem rechten
   Rand — die Karte trägt `overflow: hidden`, er war also nicht nur unschön,
   sondern nicht lesbar.
   ================================================================ */

.diagnostics-compact-header {
  flex-wrap: wrap;
  gap: 0.125rem 0.5rem;
}

.diagnostics-compact-header > * {
  min-width: 0;
}

/* ================================================================
   6f — LIGHT-MODE-KONTRAST, WEITERE FÄLLE

   style.css trägt bereits eine solche Liste („LIGHT-MODE-KONTRAST, NACHTRAG")
   mit .text-blue-500 und .text-amber-600. Nachgemessen auf frisch geladenen
   Seiten im Light Mode fehlten diese:

   | Utility           | als Text bei             | gemessen | Ziel     |
   |-------------------|--------------------------|----------|----------|
   | .text-orange-500  | „Gas" (Verbrauchstafel)  | 2,68 : 1 | 4,5 : 1  |
   | .text-emerald-500 | Herkunftslegende         | 2,54 : 1 | 4,5 : 1  |
   | .text-green-500   | Statuszeilen             | 2,28 : 1 | 4,5 : 1  |
   | .text-purple-500  | Legenden/Marker          | 3,31 : 1 | 4,5 : 1  |
   | .text-teal-500    | Plausibilitätsmarker     | 2,41 : 1 | 4,5 : 1  |

   Nur die TEXTFARBE wird angehoben, nicht die Flächenfarbe: dieselben Töne
   markieren an anderer Stelle Punkte, Balken und Kachelränder, wo sie als
   Fläche genug Kontrast haben und die eingeführte Farbzuordnung tragen.
   Deshalb `html:not(.dark)` und ausschließlich `color` — genau das Muster, das
   style.css für die ersten beiden Fälle schon nutzt.
   ================================================================ */

html:not(.dark) .text-orange-500  { color: #c2410c; }   /* 2,68 → 4,89 : 1 */
html:not(.dark) .text-emerald-500 { color: #047857; }   /* 2,54 → 4,88 : 1 */
html:not(.dark) .text-green-500   { color: #15803d; }   /* 2,28 → 4,60 : 1 */
html:not(.dark) .text-purple-500  { color: #7e22ce; }   /* 3,31 → 5,58 : 1 */
html:not(.dark) .text-teal-500    { color: #0f766e; }   /* 2,41 → 5,04 : 1 */
html:not(.dark) .text-yellow-500  { color: #a16207; }   /* 1,92 → 4,55 : 1 */

/* Die 600er-Stufen sind knapp darunter und tauchen an Messwerten auf
   („40 %", „41 %" in der Kältekreis-Tabelle: gemessen 3,77 : 1). */
html:not(.dark) .text-emerald-600 { color: #047857; }   /* 3,77 → 4,88 : 1 */
html:not(.dark) .text-green-600   { color: #15803d; }   /* 3,55 → 4,60 : 1 */

/* ================================================================
   7 — SCHRIFT-UNTERGRENZE FÜR KARTENTEXT (TABLET UND TOUCH)

   Die 11px des Projekts sind für Tabellenzellen dokumentiert und dort auch
   richtig: eine Zeile mit 20 Spalten lebt von Dichte. In einer Karte ist 11px
   dagegen nur klein. Gemessen auf der Kundenseite: 52 Knoten auf 11px und 222
   auf 12px gegen 59 auf 14px — die Seite bestand aus Kleingedrucktem.

   Die zweite Regel nimmt Tabellen ausdrücklich wieder heraus; sie ist
   spezifischer und gewinnt. Ohne sie würde diese Schicht die mühsam
   eingestellte Tabellendichte und die Kartendarstellung der vier
   Tabulator-Tabellen verschieben.
   ================================================================ */

@media (max-width: 1279.98px), (any-pointer: coarse) {
  .page-content [class*="text-[11px]"],
  .page-content [class*="text-[10px]"] {
    font-size: 12px;
  }

  .page-content .text-xs {
    font-size: 13px;
  }

  /* Tabellen behalten ihre dokumentierte Dichte — aber GENAU ihre, nicht eine
     neue. Beim ersten Versuch stand hier `font-size: 11px` für alle drei
     Selektoren zusammen, und `text-xs` ist in Tabellen 12px: die Regel hat die
     Tabellen der Kundenseite von 12px auf 11px GESENKT (nachgemessen: 181
     Textknoten auf 11px, vorher 52). Eine Ausnahme darf den Ausgangswert nicht
     verändern, sondern nur die Anhebung zurücknehmen — deshalb zwei Regeln mit
     den jeweils ursprünglichen Werten. */
  .page-content .tabulator .text-xs,
  .page-content table .text-xs {
    font-size: 0.75rem;   /* Tailwind-Ausgangswert von .text-xs = 12px */
  }

  .page-content .tabulator [class*="text-[11px]"],
  .page-content .tabulator [class*="text-[10px]"],
  .page-content table [class*="text-[11px]"],
  .page-content table [class*="text-[10px]"] {
    font-size: 11px;
  }
}

/* ================================================================
   7b — ABSCHNITTSNAVIGATION FÜR LANGE SEITEN

   Gemessen bei 753px Viewport: die Kundenseite ist 9025px hoch, also 8,8
   Bildschirmhöhen — Consent-Watchlist 1557px, Kunden & Anlagen 3242px,
   Differenzen 2930px, Datenqualität 394px. Vier gleichrangige Abschnitte
   hintereinander, ohne jede Möglichkeit zu sehen, was noch kommt oder gezielt
   dorthin zu springen. Die Alerts-Seite hat dasselbe Muster mit vier
   Abschnitten.

   Kein Inhalt wird versteckt oder eingeklappt: die Leiste nennt die Abschnitte
   und springt hin. Damit ist erstens sichtbar, dass es sie gibt, und zweitens
   kostet der Weg dorthin keine acht Wischbewegungen.

   NICHT GEBAUT: ein klebender Tabellenkopf. Er wäre für die 147 Zeilen der
   Anlagen-Tabelle das Richtige, kann in dieser Struktur aber nicht wirken —
   `.card-readable` trägt `overflow: hidden` und der Tabellenrahmen
   `overflow-x: auto`; beide sind Scroll-Container, und `position: sticky`
   bezieht sich auf den nächsten davon, nicht auf den Viewport. Die Regel wäre
   wirkungslos, und in dieser Preview-Umgebung lässt sich Scrollen nicht
   auslösen — also auch nicht nachweisen. Bleibt als offener Punkt im Bericht
   stehen, statt als unbelegte Verbesserung im Code.
   ================================================================ */

.section-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  margin-bottom: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-card);
  background-color: var(--surface-2);
}

.section-nav-label {
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-right: 0.25rem;
}

.section-nav a {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--ctl-h);
  padding: 0.25rem 0.625rem;
  border: 1px solid var(--border-primary);
  border-radius: var(--r-control);
  background-color: var(--surface-1);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text-primary);
  text-decoration: none;
}

.section-nav a:hover,
.section-nav a:active {
  border-color: var(--accent-neutral);
  color: var(--accent-neutral);
}

.section-nav a:focus-visible {
  outline: 2px solid var(--accent-neutral);
  outline-offset: 2px;
}

/* Sprungziel nicht unter die mobile Topbar bzw. den Anlagen-Banner setzen. */
.card-readable[id] {
  scroll-margin-top: calc(var(--topbar-h, 0px) + 0.75rem);
}

/* ================================================================
   8 — SEITENKOPF ALS EIN BAUSTEIN

   Jede der zehn Seiten hat ihren Kopf einzeln gebaut: mal
   `flex flex-col sm:flex-row justify-between`, mal
   `flex flex-wrap items-start justify-between gap-3 mb-3`, mal ein nacktes
   `<div>`. Folge: unterschiedliche Abstände zum ersten Inhalt (0,25rem bis
   0,75rem), unterschiedliche Titelgrößen und eine Werkzeugleiste, die auf
   manchen Seiten neben dem Titel und auf anderen darunter saß.
   ================================================================ */

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-1) var(--sp-2);
  margin-bottom: var(--sp-1);
}

.page-head-text {
  min-width: 0;
  flex: 1 1 22rem;
}

.page-head-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  flex: 0 1 auto;
  min-width: 0;
}

/* Der Titel trägt sein Icon in einer eigenen, nicht schrumpfenden Spalte —
   vorher stand es als `mr-1` im Textfluss und rutschte beim Umbruch in die
   zweite Zeile des Titels. */
.page-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.page-title > i {
  flex-shrink: 0;
  color: var(--accent-neutral);
  font-size: 0.9em;
}


/* ================================================================
   9 — ABSTÄNDE DER HÜLLEN

   Erste Verdichtung (02.09.2026, erster Durchgang): Karte 32→16px Polster und
   32→8px Außenabstand — gemessen lagen 64px Leerraum zwischen zwei
   Karteninhalten, ein Fünftel der Tablet-Bildhöhe.

   Zweite Verdichtung (02.09.2026): jede Hülle eine Stufe der Skala tiefer.
   Ein Wert für alle Breiten; die sechs Seitenrand-Stufen am <main>
   (640/768/1024/1280/1920/2560px in style.css) sind entfallen — sechs Werte
   für einen Rand, der bei max-width 1400px ohnehin zentriert wird.

   Unverändert bleiben: 11px Schriftuntergrenze, 16px in Eingabefeldern auf
   Touch, 44px Trefferflächen (--ctl-h), die 12rem-Mindestspalte des
   Kachelgitters (darunter lief Text über, gemessen) und der 8px-Abstand
   zwischen zwei Karten — bei 4px lesen sich zwei weiße Karten mit Rahmen
   auf dem grauen Seitengrund als Doppelrahmen, nicht als Abstand.

   Diese Regeln stehen bewusst hier und nicht an den Fundstellen: die alten
   Werte liegen über style.css und components.css verstreut, und die
   autoritative Schicht schlägt sie ohne !important, weil sie zuletzt lädt.
   KEIN `display` an .page-content oder .card-readable: elf Seiten sind
   `page-content hidden`, Tailwinds .hidden muss dort gewinnen.
   ================================================================ */

.card-readable {
  padding: var(--sp-3);
  margin-bottom: var(--sp-2);
}

/* Kartenkopf und Karteninhalt gehören zusammen — der Abstand dazwischen ist
   der kleinste der Skala. */
.card-head {
  gap: var(--sp-1) var(--sp-2);
  margin-bottom: var(--sp-1);
}

.tile-grid {
  gap: var(--sp-1);
}

.tile {
  padding: var(--sp-2);
  gap: var(--sp-1);
}

.kpi-card {
  padding: var(--sp-2) var(--sp-3);
}

/* Die Reihenbeschriftungen der Kennzahlkarte standen 12px über ihrer Reihe und
   12px unter der vorigen — dadurch klebte jede Beschriftung optisch an der
   falschen Reihe. */
.group-label {
  margin-bottom: var(--sp-1);
}


/* ================================================================
   11 — STEUERUNG: DREI SPALTEN, KLARTEXT UNTER DER FACHBEZEICHNUNG

   Gemessen bei 1500px Fensterbreite: fünf Spalten über 1376px Kartenbreite.
   Der Inhalt braucht etwa 700px, der Rest wurde zu Luft INNERHALB der Spalten —
   zwischen „Bivalenzpunkt" und seinem Wert lagen mehrere hundert Pixel, die das
   Auge auf jeder der 14 Zeilen neu überbrücken musste. Zwei der fünf Spalten
   trugen dabei 14-mal dasselbe: „Einstellen" und denselben Zeitstempel.

   Der Zeitstempel ist nicht zufällig gleich: die Vaillant-Settings-API stempelt
   die GANZE Antwort mit einem Wert. Nachgezählt in
   backend/tests/fixtures/settings-responses.json: 37 `timestamp`-Felder, ein
   einziger eindeutiger Wert. Er steht deshalb nur noch da, wo er etwas sagt.

   Drei Spalten, Lesebreite statt Kartenbreite, und unter jeder Fachbezeichnung
   ein Satz im Klartext — SICHTBAR, nicht im title-Attribut: auf einem Tablet
   gibt es kein Überfahren, und diese Seite verstellt echte Kundenanlagen.

   RASTER, KEINE TABELLE. Drei Gründe:
   • Umbruch: drei feste Spalten müssen auf einem 375px-Telefon zu gestapelten
     Zeilen werden. Eine Tabelle kann das nur über `display: block` an den
     Zellen — dann ist die Tabellensemantik ohnehin weg.
   • Ohne <thead> entfallen fünf Kopfzeilen (5 × ~24px). Die Spaltennamen
     stehen jetzt einmal je Regler statt einmal je Gruppe.
   • Ohne `overflow-x-auto` gibt es keinen Scroll-Container mehr, in dem etwas
     verschwinden könnte: die erste Spalte ist `minmax(0, 1fr)` und schrumpft
     bis 0, die beiden festen Spalten entfallen unter 768px.

   KEINE `text-xs`/`text-sm`/`text-[11px]`-Utilities in diesem Markup. §7 hebt
   `.text-xs` auf dem Tablet auf 13px und nimmt nur TABELLEN wieder aus —
   außerhalb einer Tabelle wären die kleinen Zeilen also 13px geworden. Die
   Stufen kommen aus `--fs-*`; damit greift die Ausnahme nicht und die
   11px-Untergrenze des Projekts gilt an genau einer Stelle.
   ================================================================ */

/* Die beiden festen Spaltenbreiten stehen als Variablen an der Gruppe:
   Parameterzeile UND Sonderfunktionszeile müssen dieselbe Aktionsspalte
   treffen, sonst stehen die Knöpfe versetzt. */
.ctrl-group {
  /* Beide Breiten sind gemessen, nicht geschätzt.

     WERT 15rem: bei 12rem (192px) brach die Bereichszeile bei ACHT von 14
     Parametern um („erlaubt: 5,0 bis 30,0 °C, Schritte von 0,5" braucht 210px)
     und machte jede dieser Zeilen 15px höher.

     AKTION 9rem: „Einstellen" ist 71px breit; die Sonderfunktionen setzen
     „Aktivieren"/„Beenden" daneben, und die Begründung eines gesperrten Knopfes
     bricht darunter um. Die Namen der Schnittstellen standen hier früher mit im
     Knopf („Einstellen · Advanced Settings", 168px) — die sind raus, der
     doppelt geführte Wert hat jetzt einen Knopf (CONTROL_SYSTEM.md §4).

     LESEBREITE 72rem: bei 60rem blieben der Bezeichnung 576px, und fast jeder
     Erklärsatz brach auf zwei Zeilen um (gemessen: 14 Zeilen zwischen 46 und
     73px Höhe). Mit 72rem sind es 744px, und alle 15 Sätze bleiben einzeilig.

     Die Grenze sitzt HIER und nicht an den Kindern: die Kopfzeile liegt in
     einem eigenen .ctrl-group ohne .ctrl-table und war sonst die einzige
     unbegrenzte Zeile — ihre Spalten standen dann woanders als die der
     Zeilen darunter. */
  --ctrl-col-value: 15rem;
  --ctrl-col-action: 9rem;
  --ctrl-max: 72rem;
  max-width: var(--ctrl-max);
  margin-bottom: var(--sp-2);
}

/* Gruppentitel (Heizzone 0, Heizkreis 0, Warmwasser …). Die Kante unten
   ersetzt die entfallene <thead>-Linie — netto null Höhe. */
.ctrl-group-title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 var(--sp-2);
  margin-bottom: var(--sp-1);
  padding-bottom: 2px;
  border-bottom: 1px solid var(--border-primary);
  font-size: var(--fs-section);
  font-weight: 700;
  line-height: 1.3;
  color: var(--text-primary);
}

.ctrl-table {
  /* Grenze steht an .ctrl-group — hier bliebe sie ohne Wirkung. */
  min-width: 0;
}

.ctrl-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--ctrl-col-value) var(--ctrl-col-action);
  align-items: start;
  column-gap: var(--sp-3);
  row-gap: var(--sp-1);
  padding: var(--sp-1) 0;
  border-bottom: 1px solid var(--border-primary);
}

.ctrl-row:last-child {
  border-bottom: 0;
}

/* Spaltennamen — EINMAL je Regler, nicht je Gruppe. */
.ctrl-row--head {
  padding-bottom: 2px;
  border-bottom: 0;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ---- Spalte 1: Fachbezeichnung, darunter der Klartext ---- */
.ctrl-name {
  min-width: 0;
}

.ctrl-term {
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-primary);
}

.ctrl-expl {
  display: block;
  font-size: var(--fs-meta);
  line-height: 1.3;
  color: var(--text-secondary);
}

/* ---- Spalte 2: Wert, darunter der zulässige Bereich ---- */
.ctrl-value {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* `data-param-values` hängt an DIESEM Element, nicht an `.ctrl-value`:
   `_renderPendingBadges` setzt `cell.textContent` und würde die Bereichs- und
   Standzeile sonst bei jedem Schreibvorgang mitlöschen. */
.ctrl-num {
  display: block;
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.3;
  color: var(--text-primary);
}

.ctrl-range,
.ctrl-stand,
.ctrl-hint {
  display: block;
  font-size: var(--fs-meta);
  font-weight: 400;
  line-height: 1.35;
  color: var(--text-secondary);
}

/* Der Zeitstempel steht nur da, wenn er vom Lesezeitpunkt der Karte abweicht.
   Uhr-Symbol als zweites Signal — Farbe trägt in diesem Projekt nie allein.
   `1em`, nicht `0.9em`: auf 11px wäre das Symbol 9,9px. */
.ctrl-stand {
  color: var(--text-muted);
}

/* Auch der Grenzen-Hinweis in der Wertespalte trägt ein Symbol — eine
   Abweichung ist ein Befund, und Farbe allein signalisiert hier nie. */
.ctrl-stand > i,
.ctrl-value .ctrl-hint > i {
  font-size: 1em;
  margin-right: 0.25em;
}

/* ---- Spalte 3: Aktion ----
   Bewusst OHNE die Klasse `flex`: components.css trägt
   `.page-content div.flex:not(.tabulator *):has(button) { flex-wrap: wrap }`
   (abnahmetestfest). `display: flex` per Klassenregel ist davon nicht betroffen. */
.ctrl-action {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  min-width: 0;
}

/* ---- Regler-Zeile: Herkunftsangabe, keine Überschrift ---- */
.ctrl-device {
  margin-bottom: var(--sp-2);
}

.ctrl-device-head {
  margin-bottom: var(--sp-1);
  font-size: var(--fs-meta);
  line-height: 1.35;
  color: var(--text-muted);
}

.ctrl-empty {
  margin-bottom: var(--sp-1);
  font-size: var(--fs-meta);
  color: var(--text-muted);
}

/* ---- Sonderfunktionen ---- */
.ctrl-special {
  margin-top: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--border-primary);
  border-radius: var(--r-control);
}

.ctrl-special-title {
  margin-bottom: var(--sp-1);
  font-size: var(--fs-meta);
  font-weight: 700;
  color: var(--text-secondary);
}

/* Dasselbe Raster wie die Parameterzeile, damit die Knöpfe in derselben Spalte
   stehen. Rahmen und Polster des Kastens verschieben sie um 9px — nicht
   sichtbar, und der eigene Kasten ist die Aussage „das ist kein Messwert". */
.ctrl-sf-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--ctrl-col-action);
  align-items: start;
  column-gap: var(--sp-3);
  row-gap: var(--sp-1);
  padding: var(--sp-1) 0;
}

.ctrl-sf-name {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  min-width: 0;
  font-size: var(--fs-body);
  line-height: 1.3;
  color: var(--text-primary);
}

/* ---- Protokoll der Schreibversuche ---- */
.ctrl-audit {
  margin-top: var(--sp-2);
}

.ctrl-audit > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text-primary);
  cursor: pointer;
}

/* <summary> ist kein <button> und fällt durch alle vier Selektoren der
   44px-Absicherung in components.css. */
@media (any-pointer: coarse), (max-width: 1279.98px) {
  .ctrl-audit > summary {
    min-height: 44px;
  }
}

.ctrl-audit-body {
  padding: 0 var(--sp-2) var(--sp-2);
}

/* ---- Dialog: Beschriftung/Wert als echtes Paar ----
   Ersetzt `w-28` (7rem harte Breite) an der linken Spalte: bei vergrößerter
   Systemschrift brach „Parameter" dort um, während rechts Platz frei blieb.
   `max-content` nimmt genau die Breite der längsten Bezeichnung. */
.ctrl-dl {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  column-gap: var(--sp-3);
  row-gap: var(--sp-1);
  align-items: baseline;
}

.ctrl-dl > dt {
  font-size: var(--fs-meta);
  color: var(--text-secondary);
}

.ctrl-dl > dd {
  margin: 0;
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.ctrl-lead {
  margin-bottom: var(--sp-2);
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--text-secondary);
}

.ctrl-fields {
  display: grid;
  gap: var(--sp-2);
}

.ctrl-note {
  margin-top: var(--sp-2);
}

/* ---- UMBRUCH ----
   Unter 768px passen drei feste Spalten nicht: 12rem + 10,5rem sind 360px, ein
   375px-Telefon hat nach dem Kartenpolster 351px übrig. Gestapelt statt
   seitwärts scrollend — `main` trägt `overflow-x: hidden`, und ein Knopf, der
   eine Kundenanlage verstellt, darf nicht seitlich aus dem Bild rutschen.
   Bezeichnung samt Satz über die ganze Breite, darunter Wert links und Knopf
   rechts: eine Zeile weniger als volles Stapeln, und der Knopf bleibt am Rand
   des Daumens. */
@media (max-width: 767.98px) {
  .ctrl-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .ctrl-row--head {
    display: none;
  }

  .ctrl-name {
    grid-column: 1 / -1;
  }

  .ctrl-value {
    grid-column: 1;
  }

  .ctrl-action {
    grid-column: 2;
    align-self: center;
    align-items: flex-end;
  }

  .ctrl-sf-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Tablet-Hochformat bis Notebook: die feste Wertespalte eine Stufe schmaler,
   damit der Erklärsatz nicht auf drei Zeilen läuft — bei 768px blieben der
   Bezeichnung sonst nur 344px. */
@media (min-width: 768px) and (max-width: 1023.98px) {
  .ctrl-group {
    --ctrl-col-value: 10rem;
    --ctrl-col-action: 9.5rem;
  }
}

@media (max-width: 400px) {
  .ctrl-dl {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0;
  }

  .ctrl-dl > dt {
    margin-top: var(--sp-1);
  }
}


/* ================================================================
   10 — KEINE BEWEGUNG, KEINE SCHATTEN AUS UTILITIES

   Bis zum 02.09.2026 stand hier nur der Rückzug für „Bewegung reduzieren".
   Jetzt gilt er immer. Was in den Hand-Stylesheets noch animierte oder
   überblendete, ist dort gelöscht; diese Regel ist das Netz für alles, was
   CSS nicht an der Quelle erreicht: Tailwind-Utilities im Markup
   (transition-colors, duration-200, animate-pulse), Inline-Styles aus JS
   und Fremd-CSS (Leaflet-Zoom und -Fade, Tabulator).

   Gefahrlos, weil kein Skript im Projekt auf transitionend/animationend
   hört (geprüft 02.09.2026); Leaflet beendet seinen Zoom per setTimeout,
   nicht per Übergangsereignis — die Karte springt und räumt trotzdem auf.

   Warum `!important`: unter wichtigen Deklarationen entscheidet die
   Spezifität. `*` ist (0,0,0), `.fa-spin` (0,1,0), `#step-2… i` (1,1,1) —
   die Ausnahmen unten gewinnen. Ein Inline-Style aus JS verliert gegen jede
   wichtige Autor-Deklaration; das ist gewollt.

   Was weiterläuft: Ladekreisel. Ein stehender Kreisel meldet „fertig",
   während noch geladen wird — eine falsche Aussage.
   ================================================================ */

*,
*::before,
*::after {
  transition: none !important;
  animation: none !important;
  scroll-behavior: auto !important;
}

/* Font Awesome 6 (CDN, lädt vor style.css): Keyframes `fa-spin` aus all.min.css. */
.fa-spin {
  animation: fa-spin 2s linear infinite !important;
}

.fa-pulse,
.fa-spin-pulse {
  animation: fa-spin 1s steps(8) infinite !important;
}

/* Schritt 2 der Anlagenwahl („Daten laden"); Keyframes `spin` in components.css. */
#step-2.details-step--active i {
  animation: spin 0.8s linear infinite !important;
}

/* Tailwind-Schatten über die Variable neutralisieren — NICHT über
   `box-shadow: none`: Tailwind setzt `box-shadow: var(--tw-ring-offset-shadow),
   var(--tw-ring-shadow), var(--tw-shadow)`; ein `none` nähme dem Element auch
   den Fokusring (focus:ring-*, rund 30 Fundstellen). Gleiche Spezifität wie
   die Utility, diese Datei lädt später — gewinnt. */
.shadow-sm,
.shadow-md,
.shadow-lg,
.shadow-xl,
.shadow-2xl,
.shadow-inner,
.hover\:shadow-md:hover,
.hover\:shadow-lg:hover {
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
}

/* backdrop-blur an Dialog-Overlays: je eine Filterschicht über dem ganzen
   Viewport, die bei jedem Frame neu rechnet. Der Overlay ist ohnehin zu
   50–60 % schwarz — die Trübung trägt nichts. */
.backdrop-blur-sm,
.backdrop-blur {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
