/* dashboard/static/dashboard/panel.css — panel's design system.
 *
 * Quiet ops-tool surface: flat, solid-ink cards on a plain dark ground, one
 * deliberate accent (the brand's own ink, black on light and white on dark —
 * see "Marke" at the end of this header; before that a single confident
 * blue — and never violet/pink, rainbow button gradients or a drifting
 * gradient-blob backdrop; that combination reads as generic AI-generated
 * chrome, not as this panel's own identity).
 * Semantic color (ok/warn/bad) stays strictly separate from the accent —
 * it means something (health, danger), the accent never doubles as a
 * status color. Sans for UI chrome, mono only for *data* (slugs, domains,
 * branches, timestamps, logs) and, since the brand, for what is big (page
 * titles, key figures — the brand's rule) — this split is deliberate, see README.md/
 * the PR that introduced this file for the reasoning. No external fonts,
 * no CDN, no icon font (the two brand fonts are self-hosted files, see
 * brand.css): every icon here is an inline <svg> in the
 * templates themselves, colored via currentColor so it always matches its
 * button's text color automatically.
 *
 * Tokens: this file no longer invents its own color/radius/shadow/glass
 * values. They come from pablo-roso/style_guide's shared @talvesa/tokens
 * package — dashboard/static/dashboard/ds-tokens.css is a vendored,
 * regenerate-on-demand snapshot of that package's plain-CSS token export
 * (see that file's own header for exactly how to refresh it). The "Design
 * tokens" block right below this comment is the adapter layer: it aliases
 * panel's own semantic names (--bg/--accent/--ok/--radius/… — the ones
 * every rule in the rest of this file actually uses) onto that package's
 * --talvesa-* custom properties, the same "adapter, not search-and-
 * replace" pattern style_guide's own docs/integration-bestehendes-repo.md
 * documents for a React consumer, translated to plain CSS custom
 * properties since this Django app has no React/vanilla-extract runtime.
 * Nothing below the token block references --talvesa-* directly except
 * where a raw token is the point (scrim, z-index, motion easing) —
 * everything else stays decoupled from where the values come from. This does move the accent a shade — style_guide's brand
 * accent is still explicitly a placeholder (`ds.config.ts` says so) rather
 * than panel's previously hand-picked blue, but panel is a *consumer* of
 * the shared brand, not its owner, so it takes that placeholder as-is
 * rather than forking its own value back in. If the placeholder becomes
 * real, this file needs no change — regenerate ds-tokens.css and it's
 * everywhere at once.
 *
 * Marke (seit der Brand Identity V1.0): Der Akzent ist kein Platzhalter mehr,
 * sondern die Marke selbst — und die ist schwarz-weiß. Sie steht nicht im
 * Design-System, sondern in brand.css (Abschrift von talvesa/_brand.scss in
 * pablo-roso/paralympus). Der Adapter unten legt deshalb Papier, Tinte, Akzent
 * und Schriften auf --tv-* und mischt Flächenstufen und Trenner aus der Tinte;
 * alles Übrige (Status, Radien, Abstände, Bewegung) bleibt beim
 * Design-System. Die Trennung von Akzent und Bedeutung gilt weiter, und jetzt
 * erst recht: Der Akzent ist Tinte, also trägt allein ok/warn/bad noch Farbe —
 * Farbe heißt hier immer etwas.
 */

:root {
  /* Papier und Tinte der Marke (brand.css). */
  --bg: var(--tv-paper);
  /* Die zweite Grundfläche: vertieft ins Papier, für Chips und Mulden auf
     einer Karte. Das ist das Papier selbst (#F5F5F5 bzw. das dunkle
     Papier) — vorher der Hintergrund des Design-Systems, #F7F7F8 und
     #0C0C0D, also im Hellen sogar heller als das Papier und bläulich. */
  --bg-2: var(--tv-paper);
  --surface: var(--tv-surface);
  /* Flächenstufen und Trenner aus der Tinte gemischt, deckend auf der
     Kartenfläche — nicht die Systemgraus des Design-Systems. Die sind
     Apples (#2C2C2E, #48484A, #EFEFF2, #D1D1D6) und leicht bläulich; neben
     Papier und Tinte der Marke stand das Panel damit in zwei Sorten Grau.
     Die Stufen bleiben ungefähr, wo sie waren. Nur --border-strong ist
     jetzt in beiden Themes kräftiger als --border: im hellen war es
     heller (#E3E3E7 gegen #D1D1D6), im dunklen gleich — ein Hover auf
     einem Rand war damit keiner. */
  --surface-2: color-mix(in srgb, var(--tv-ink) 7%, var(--tv-surface));
  --surface-solid: color-mix(in srgb, var(--tv-ink) 4%, var(--tv-surface));
  --border: color-mix(in srgb, var(--tv-ink) 19%, var(--tv-surface));
  --border-strong: color-mix(in srgb, var(--tv-ink) 32%, var(--tv-surface));
  /* Rand eines Eingabefelds: Tinte zu 52 % auf der Fläche, wie
     --m-field-line auf talvesa.de — mindestens 3:1 gegen Karte und Papier
     in beiden Themes. Aus --text/--surface gemischt statt aus --tv-*,
     damit die Seitenleiste (eigener Kontext, siehe .app-nav) sie mit ihren
     eigenen Werten neu rechnen kann. */
  --field-line: color-mix(in srgb, var(--text) 52%, var(--surface));
  --field-line-hover: color-mix(in srgb, var(--text) 75%, var(--surface));
  --field-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
  --text: var(--tv-ink);
  /* Das Grau des Markenblatts (#444444) ist die gedämpfte Schrift, wie
     --m-fg-2 auf talvesa.de; im Dunklen dessen Gegenstück (#BDBDBD). Hier
     stand bis zum Farbabgleich Stufe 3 (#666), mit der Begründung, #444
     stehe zu nah an der Tinte — aber dann steht die eine Graustufe der
     Marke nirgends im Panel. Stufe 3 bleibt für das, was noch leiser sein
     soll als gedämpft: Platzhalter in Feldern (--text-faint). */
  --text-dim: var(--tv-ink-2);
  --text-faint: var(--tv-ink-3);
  /* Der Akzent ist die Tinte — schwarz auf hell, weiß auf dunkel, wie
     „Projekt starten →" auf dem Markenblatt. Die Farbwahl unter
     Einstellungen überschreibt --accent/--accent-ink weiterhin zur Laufzeit
     (panel.js, applyAccent); ohne gespeicherte Wahl gilt die Marke. */
  --accent: var(--tv-ink);
  /* Not --talvesa-color-accent-dark: that's the *dark-theme variant* of
     the accent (deliberately brighter than the light one, for contrast
     against a dark background — see ds.config.ts's accentDark comment),
     not a hover/pressed shade of whichever accent is currently showing.
     What this file actually wants for hover/active feedback is "a bit
     deeper than --accent, in either theme" — color-mix() derives exactly
     that from the live --accent instead of a second, easily-mismatched
     token. */
  /* Zur Fläche hin statt zu Schwarz: Bei Tinte als Akzent ist „ein
     bisschen tiefer" im Hellen schwarz auf schwarz — der Hover sähe aus wie
     keiner. Ein Stück Papier hinein wirkt in beiden Themes, auch für eine
     selbst gewählte Farbe. */
  --accent-strong: color-mix(in srgb, var(--accent) 84%, var(--bg));
  /* Früher das Türkis der Info-Meldungen. Auf dem Markenblatt steht keine
     vierte Farbe: Ein Hinweis ist Tinte, Farbe bleibt ok/warn/bad. */
  --accent-2: var(--accent);
  --accent-ink: var(--tv-paper);
  --ok: var(--talvesa-color-status-success);
  --warn: var(--talvesa-color-status-warning);
  --bad: var(--talvesa-color-status-danger);
  /* Dieselben drei als Schrift. #FF3B30, #FF9500 und #34C759 sind Farben
     für Punkte, Balken und Ränder: Als Text auf Papier oder weißer Karte
     erreichen sie 3,5:1, 2,2:1 und 2,2:1 — ein roter Fehlersatz, den man
     im hellen Theme kaum lesen konnte. Das Design-System hat für Text
     eigene, dunklere (bzw. im Dunklen hellere) Stufen, die 4,5:1 halten;
     jede Schrift in einer Statusfarbe liest diese. */
  --ok-text: var(--talvesa-color-status-text-success);
  --warn-text: var(--talvesa-color-status-text-warning);
  --bad-text: var(--talvesa-color-status-text-danger);
  /* No pre-mixed "glow" role exists in the shared system (just solid
     status/accent colors) — color-mix() derives one here instead of baking
     in four more rgba() literals that could drift from --ok/--warn/--bad/
     --accent above. --glow-strength is the one real per-theme number this
     file still owns: the same alpha reads as tasteful in dark mode and as
     way too loud over a light page, see the [data-theme] blocks below. */
  --ok-glow: color-mix(in srgb, var(--ok) var(--glow-strength), transparent);
  --warn-glow: color-mix(in srgb, var(--warn) var(--glow-strength), transparent);
  --bad-glow: color-mix(in srgb, var(--bad) var(--glow-strength), transparent);
  --accent-glow: color-mix(in srgb, var(--accent) var(--glow-strength), transparent);
  /* --panel-radius-scale (settings.html) mirrors dsConfig.shape.radiusScale
     exactly — the same multiplier primitives.ts's own radius values are
     already derived through (see style_guide's packages/tokens/src/
     primitives.ts: `r = (px) => Math.round(px * dsConfig.shape.radiusScale)`),
     just applied here at the CSS layer instead of in the token build,
     since this app reads the compiled px values, not the multiplier
     itself. `full` (999px, pills/dots) is deliberately not scaled here
     either, same as upstream: pill shape is the point of that one. */
  --radius: calc(var(--talvesa-radius-lg) * var(--panel-radius-scale) / 100);
  --radius-sm: calc(var(--talvesa-radius-sm) * var(--panel-radius-scale) / 100);
  --radius-md: calc(var(--talvesa-radius-md) * var(--panel-radius-scale) / 100);
  --shadow: var(--talvesa-shadow-3);
  --shadow-sm: var(--talvesa-shadow-2);
  /* Instrument Sans für die Oberfläche, Talvesa Mono für Daten (Slugs,
     Domains, Logs) und für das Große (Seitentitel, Kennzahlen, Wortmarke).
     Beide lokal, siehe brand.css. */
  --font-sans: var(--tv-font-sans);
  --font-mono: var(--tv-font-mono);

  /* --- Abstandsskala ---------------------------------------------------
     ds-tokens.css liefert eine vollstaendige, nicht-lineare Abstandsskala
     (--talvesa-space-0 bis -12) — diese Datei hat davon bisher nichts
     benutzt: Jeder senkrechte Abstand war eine handgesetzte rem-Zahl
     (.page-header 1.8rem, .toasts 1.2rem, Abschnittskoepfe .6rem inline),
     und deshalb hatten keine zwei Seiten denselben Rhythmus. Hier stehen
     Namen fuer genau die Stufen, die Huelle und Abschnitte brauchen —
     keine neuen Werte, nur die Entscheidung, aus wie wenigen Stufen ein
     Abstand ueberhaupt gewaehlt werden darf. */
  --space-3: var(--talvesa-space-3);   /*  8px — Kopf zu seinem Inhalt */
  --space-4: var(--talvesa-space-4);   /* 12px */
  --space-5: var(--talvesa-space-5);   /* 16px — Karten-Kopf zum Inhalt */
  --space-6: var(--talvesa-space-6);   /* 20px — Meldungen zum Inhalt */
  --space-7: var(--talvesa-space-7);   /* 24px — Karte zu Karte */
  --space-8: var(--talvesa-space-8);   /* 32px — Abschnitt zu Abschnitt */
  --space-9: var(--talvesa-space-9);   /* 40px — Seitenkopf zum Inhalt */

  /* Die EINE Lesebreite fuer Fliesstext. Vorher hatte .lage-erklaerung
     62ch, .k-kosten-note 52rem und der Rest gar keine — in der
     1200px-Spalte von `main` sind das bei .85rem rund 150 Zeichen je
     Zeile, weit jenseits der Laenge, ab der man beim Zeilensprung die
     Zeile verliert. Eine Zahl an einer Stelle, damit „erklaerender
     Absatz" ueberall dasselbe heisst. */
  --mass-text: 68ch;
}

/* --- Design settings (dashboard/settings.html) ---------------------------
   Every multiplier below mirrors an actual field in style_guide's own
   ds.config.ts (shape.radiusScale, motion.speed) — config-time
   multipliers there; this file just applies the same multipliers at the
   CSS layer instead of the token build, since this app reads compiled
   px/ms values, not the source config. panel.js's settings page sets
   these --panel-*-scale custom properties on <html> (percent, 100 = the
   shipped spec values, untouched), persisted to localStorage and
   re-applied pre-paint — same mechanism the theme toggle already
   established, see base.html's inline script.

   Every *-fast/-base pair below is DERIVED from the real --talvesa-*
   values, never redefined in place — a custom property can't reference
   its own prior value without creating a cycle, so each needed a second
   name. The "appleOut" vs "standard" easing curves themselves stay
   pinned to the shared spec always — scaling *those* too risks genuinely
   broken motion a settings slider has no business causing.

   Die drei Glas-Regler (Deckkraft, Weichzeichnung, Sättigung) und der
   Glas-Schalter sind entfallen: Das Panel hat keine durchscheinende
   Fläche mehr, siehe „Karten: deckend, mit Haarlinie". */
:root {
  --panel-radius-scale: 100;

  /* Only the transitions already written against --talvesa-motion-*
     tokens (.btn, the Accordion trigger/chevron, .info-tip, the settings
     page's own .switch/.slider — see each rule) actually read these and
     therefore respond to this slider; this file's many bespoke one-off
     animations (rise-in, count-up, the status-pill flash) use
     their own hand-tuned literal durations by design — see panel.css's
     own header comment on why those stayed out of scope for a mechanical
     token sweep. A real, if partial, effect, not a cosmetic one. */
  --panel-motion-scale: 100;
  --panel-motion-duration-fast: calc(var(--talvesa-motion-duration-fast) * var(--panel-motion-scale) / 100);
  --panel-motion-duration-base: calc(var(--talvesa-motion-duration-base) * var(--panel-motion-scale) / 100);

  --panel-text-scale: 100;
}

/* This panel's default is dark regardless of the visitor's OS setting —
   an ops tool reads better committing to one identity than silently
   forking its look off something unrelated (see this file's own header
   comment on avoiding generic/inconsistent chrome) — with an explicit,
   remembered opt-out via the topbar's light/dark toggle (see panel.js)
   instead of a silent @media (prefers-color-scheme) fork. base.html
   renders data-theme="dark" server-side so first paint is already
   correct, plus a small inline script there that flips it to "light"
   pre-paint if panel.js previously saved that choice — see base.html's
   own comment. ds-tokens.css already defines every --talvesa-color-*
   value for both [data-theme="dark"] and [data-theme="light"]; the two
   blocks below only add what that file can't cover: --glow-strength
   (panel-only, no shared equivalent) and the select arrow's baked-in
   stroke color (an embedded SVG data URI can't read currentColor/var()
   the way inline markup can, so the whole data URI is redefined per
   theme instead — same reasoning as always here, just no longer a raw
   hex guess: the stroke matches --text-dim, i.e. --tv-ink-3 of brand.css,
   in each theme). */
:root, [data-theme="dark"] {
  --glow-strength: 30%;
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23949494' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.5 8l4.5 4.5L14.5 8'/%3E%3C/svg%3E");
}
[data-theme="light"] {
  --glow-strength: 16%;
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23666666' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.5 8l4.5 4.5L14.5 8'/%3E%3C/svg%3E");
}

* { box-sizing: border-box; }
/* Follows the explicit toggle, not prefers-color-scheme (see above) — so
   native chrome (scrollbars, form controls the rules below don't already
   restyle) matches whichever theme is actually showing instead of
   guessing off the OS. */
/* --panel-text-scale (settings.html, dsConfig.type.baseSize's runtime
   counterpart) drives both of these together: html's own font-size sets
   the root every rem measurement in this file resolves against, body's
   own restates --talvesa-font-size-base scaled the same way so plain
   inherited text (not itself sized in rem) moves with the rest of the
   page instead of staying fixed. 100% resolves to *exactly* today's
   behavior on both — html at the browser's untouched 100% (never a fixed
   px baseline, which would have silently shifted every existing rem
   value the moment this shipped), body at the token's own 15px — so
   shipping this changed nothing for anyone who never opens Settings. */
html { color-scheme: dark; overflow-x: clip; font-size: calc(var(--panel-text-scale) * 1%); } /* see body's own overflow-x comment below */
html[data-theme="light"] { color-scheme: light; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: calc(var(--talvesa-font-size-base) * var(--panel-text-scale) / 100);
  line-height: var(--talvesa-font-line-height-relaxed);
  -webkit-font-smoothing: antialiased;
  position: relative;
  /* Belt-and-suspenders against document-level horizontal scroll, set here
     and on <html> above: every genuinely wide thing on this page (the
     DNS/resource tables) already scrolls inside its own .table-wrap, so
     the page itself never needs to scroll sideways — but a flex-wrap row
     of several nowrap buttons (.hetzner-actions on a narrow phone) can
     still end up contributing a few pixels more to the page's scrollable
     area than its own final, correctly-positioned layout box would
     suggest, in a way that traces back to a Chromium flex/scrollable-
     overflow interaction rather than to any single element actually
     being mis-sized. `clip`, not `hidden`/`auto`: it clips without
     turning <html>/<body> into a scroll container, which is what would
     otherwise break .app-nav's position:sticky (confirmed both ways while
     fixing this). Doesn't touch .table-wrap's own overflow-x:auto — a
     separate, nested scroll container, still scrollable exactly as
     intended. */
  overflow-x: clip;
}

/* --- Hintergrund ---------------------------------------------------------
   Das Papier der Marke, flach. Hier lagen bis zum Abschied vom Glas zwei
   Lichtflecken und ein senkrechter Verlauf zu --bg-2: Glas über einer
   einfarbigen Fläche ist unsichtbar, also brauchte es etwas dahinter. Ohne
   Glas waren sie nur noch Farben, die nicht auf dem Markenblatt stehen —
   ein Grau, das oben links heller ist als unten rechts. body trägt --bg
   (weiter unten), mehr braucht es nicht. */

/* Shared entrance for the overview tiles and site cards (each sets its own
   --i via an inline style in the template, so a whole row rises in as one
   short cascade instead of popping in all at once). */
/* Ends on the same expression .overview-tile/.site-card's own base rule
   rests at (not `transform: none`) — animation fill-forward keeps
   *applying* this keyframe's declared value indefinitely once the
   animation completes, and since it's still a live var() reference (not
   a baked-in snapshot), the tilt handlers in panel.js writing to
   --rx/--ry keep working after the entrance finishes instead of being
   permanently pinned flat. */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to { opacity: 1; transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); }
}
@media (prefers-reduced-motion: reduce) {
  .overview-tile, .site-card, .hetzner-server-card { animation: none; }
}

/* A status pill flashes this once when panel.js's polling actually
   changes its health value (not on every poll — most polls see no
   change) — makes a live update legible instead of a silent text swap. */
@keyframes pill-flash {
  0% { box-shadow: 0 0 0 0 currentColor; }
  35% { box-shadow: 0 0 0 5px currentColor; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.status-pill.just-changed { animation: pill-flash .7s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .status-pill.just-changed { animation: none; }
}

a { color: inherit; }

/* --- Ueberschriften ------------------------------------------------------
   Bis hierher hatte keine Ueberschrift eine Regel ohne Klasse: Ein blankes
   <h2> fiel auf die Browser-Voreinstellung und war damit auf dem Handy
   groesser als ein .page-title. Drei Stufen mit sichtbaren Spruengen, alle
   im Satzkasten und in voller Textfarbe — die leise Etikett-Optik ist eine
   KLASSE (h2.card-title), keine Eigenschaft des Elements. Jede Ueberschrift
   dieser Oberflaeche traegt heute eine Klasse mit eigenem Abstand; diese
   Regeln sind das Netz darunter, nicht der Normalfall. */
/* Die Seitenueberschrift steht in der Hausschrift, wie H1 und H2 der
   oeffentlichen Seiten (talvesa/_foundation.scss, mkt-display): Talvesa
   Mono im Normalgewicht — die Marke setzt „Talvesa" duenn, und eine fett
   gerechnete Mono waere eine andere Schrift (font-synthesis: none) —, eng
   gesetzt, weil die Schrift 0,6 em breit laeuft. Nur das Grosse: h2/h3 und
   alles darunter bleiben in der Textschrift, sonst liest sich die Seite wie
   ein Terminal. Auf dem Telefon darf ein langes deutsches Wort trennen,
   statt die Spalte zu sprengen. */
h1, .page-title {
  font-family: var(--tv-font-mono); font-weight: 400; font-synthesis: none;
  letter-spacing: -.035em; text-wrap: balance;
}
@media (max-width: 640px) {
  h1, .page-title { hyphens: auto; overflow-wrap: break-word; }
}
h1 { font-size: clamp(1.6rem, 1.3rem + 1vw, 2.2rem); line-height: 1.1; margin: 0 0 .6rem; }
h2 { font-size: 1.15rem; font-weight: 700; letter-spacing: -.015em; line-height: 1.25; margin: 0 0 .6rem; }
h3 { font-size: .98rem; font-weight: 650; letter-spacing: -.01em; line-height: 1.3; margin: 0 0 .5rem; }

/* Kein border-radius in dieser Regel: Sie setzte sonst den ECHTEN Radius
   jedes fokussierten Elements auf 3px — beim Durchtabben der Aktionsleiste
   rastete jeder Knopf (--radius-md) und jede Pille der Sprungleiste (999px)
   sichtbar in ein fast eckiges Rechteck und sprang beim Weitertabben
   zurueck. Der Ring folgt in allen Zielbrowsern von sich aus dem eigenen
   Radius des Elements; die Zeile war nur fuer radiuslose Elemente gedacht
   und kostet dort nichts. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.mono { font-family: var(--font-mono); }

/* --- App shell / navigation --------------------------------------------
   .app-nav is the one navigation surface for this app: a compact bar on
   narrow viewports (mobile-first default, same shape the old .topbar
   had), a full-height sidebar from the min-width:960px block further
   down. In beiden Formen das deckende Nachtband der Marke (siehe den
   Block direkt darunter), kein Glas.

   Below 960px, the bar itself holds only brand + hamburger (.nav-toggle);
   the actual links (.app-nav-links) and the theme/logout row
   (.app-nav-foot) live in .app-nav-panel, a wrapper that opens as a
   fullscreen, OPAQUE sheet (see its own rule further down) — the same
   call Wiland's own mobile menu makes (wiland_website's index.scss .site-header__nav, background:
   var(--bg) at max-width:640px) rather than a made-up-for-this-app one.
   From min-width:960px, .app-nav-panel dissolves via display:contents
   (see that breakpoint below) so .app-nav-links/.app-nav-foot go back to
   being direct flex children of the sidebar — one markup shape, two
   completely different layouts, same as this file always did it. On the
   login screen (no `.has-menu`, nothing to open) .app-nav-panel never
   becomes an overlay at all — see its base rule below — it just keeps
   flowing inline in the bar exactly as before this pass. */
.app-shell { display: flex; flex-direction: column; min-height: 100vh; }

.app-nav {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .5rem 1.6rem; padding: .85rem 1.4rem;
  /* Safe-Area statt fester Werte — Notch/Dynamic Island/Home-Indicator
     schneiden sonst in Marke und Hamburger (style_guide,
     docs/layout-system.md §5; base.html setzt dafür viewport-fit=cover). */
  padding-top: calc(.85rem + env(safe-area-inset-top));
  padding-left: calc(1.4rem + env(safe-area-inset-left));
  padding-right: calc(1.4rem + env(safe-area-inset-right));
  position: sticky; top: 0; z-index: var(--talvesa-z-index-sticky);
  border-bottom: 1px solid var(--border);
  /* Das Nachtband der Marke (brand.css): In beiden Themes dunkel, wie Kopf
     und Fuß der öffentlichen Seiten — schwarz neben Papier im Hellen, eine
     Stufe unter dem Papier im Dunkeln. Die Leiste liest ihre Rollen über
     dieselben Namen wie alles andere und bekommt sie hier einfach andere
     Werte (dasselbe Muster wie --m-* in talvesa/_foundation.scss): Jede
     Regel darunter — Links, Suchfeld, Theme-Knopf, Fußzeile, das geöffnete
     Mobilmenü — kippt mit, ohne eine zweite Klasse.

     --accent ausdrücklich mit: Im Hellen wäre die Tinte schwarz, der
     Fokusring auf schwarzer Leiste also unsichtbar. Eine unter
     Einstellungen gewählte Akzentfarbe gilt deshalb im Inhalt, nicht in der
     Leiste — die Marke auf ihrem eigenen Band bleibt die Marke.

     Deckend, wie die Kopfleisten der öffentlichen Seiten (paralympus,
     „Kopfleiste und mobile Leiste sind deckend"). Das Korn darüber ist das
     der dunklen Bänder dort: eine Fläche statt eines toten Schwarz. */
  --bg: var(--tv-night);
  --surface: var(--tv-night-2);
  --surface-2: color-mix(in srgb, var(--tv-on-night) 9%, transparent);
  --border: var(--tv-night-line);
  --border-strong: color-mix(in srgb, var(--tv-on-night) 26%, transparent);
  --field-line: color-mix(in srgb, var(--tv-on-night) 52%, var(--tv-night-2));
  --field-line-hover: color-mix(in srgb, var(--tv-on-night) 75%, var(--tv-night-2));
  --text: var(--tv-on-night);
  --text-dim: var(--tv-on-night-2);
  --accent: var(--tv-on-night);
  --accent-ink: var(--tv-night);
  color-scheme: dark;
  color: var(--text);
  background: var(--tv-grain), var(--tv-night);
  background-size: 220px 220px, auto;
}
/* Das Lockup der Marke: Bildmarke (_logomark.html, ein Pfad in
   currentColor), Wortmarke „Talvesa" als Text in Talvesa Mono und dahinter,
   was dieses Werkzeug ist. Alles einfarbig, alles erbt die Farbe der Leiste
   — weiß auf dem Nachtband, ohne zweite Datei. Maße wie .logo/.logo-word in
   talvesa/_layout.scss: Die Versalhöhe der Wortmarke (≈ 0,66 em) steht etwa
   so hoch wie das Zeichen daneben.

   Der Ring mit dem einmaligen „Ping" ist mit dem alten Zeichen gegangen:
   Er war ein Radar, und das neue Zeichen ist keins. */
.brand {
  display: flex; align-items: center; gap: .6rem; min-width: 0;
  text-decoration: none; color: var(--text);
  border-radius: var(--radius-sm); padding: .3rem .4rem; margin: -.3rem -.4rem;
  transition: background var(--panel-motion-duration-fast);
}
.brand:hover, .brand:focus-visible { background: var(--surface-2); }
.brand .logomark { display: block; flex: none; width: auto; height: 17px; }
.brand-wort {
  font-family: var(--tv-font-mono); font-weight: 400; font-synthesis: none;
  font-size: 1.28rem; line-height: 1; letter-spacing: .04em; white-space: nowrap;
}
/* „Panel" ist keine zweite Marke, sondern eine Angabe: klein, gedämpft, in
   der Textschrift, als Etikett mit Kante. Satzkasten wie überall hier. */
.brand-zusatz {
  flex: none; align-self: center;
  font-size: .68rem; font-weight: 600; letter-spacing: .04em; line-height: 1;
  color: var(--text-dim);
  padding: .28rem .5rem; border: 1px solid var(--border-strong); border-radius: 999px;
}
/* .app-nav-panel: the wrapper around links + foot (see the header comment
   above .app-shell for why it exists at all). Base rule is the login-
   screen/no-menu shape and the min-width:960px starting point alike —
   just an inline continuation of the bar's own flex row, exactly what
   used to sit directly in .app-nav before this pass. The max-width:959px
   block right below overrides this into a fullscreen sheet, but only
   under `.app-nav.has-menu` (nothing to open on the login screen). */
.app-nav-panel { display: flex; align-items: center; gap: .5rem 1.6rem; flex: 1 1 auto; min-width: 0; }

/* Hamburger ⇄ close, shown only where there's a menu to open (see
   base.html's own comment on why the button is conditional on
   `user.is_authenticated`) and only below the sidebar breakpoint — see
   the max-width:959px block for the part that actually reveals it.
   44×44px is style_guide's own touch-target target
   (brand/08-barrierefreiheit.md, docs/hig-adaption.md "Touch-Targets"),
   not a guess — the one control in this bar that's tapped on every
   single mobile page load earns hitting it exactly, not the 34px
   .theme-toggle settles for elsewhere. */
.nav-toggle {
  display: none; flex-direction: column; justify-content: center; align-items: center;
  gap: 5px; width: 44px; height: 44px; margin-left: auto; flex: none;
  padding: 0; border: none; border-radius: var(--radius-sm); background: none; cursor: pointer;
  transition: background-color var(--panel-motion-duration-fast);
}
/* Nur mit echtem Zeiger: Auf dem Touchscreen bleibt :hover nach dem
   Antippen stehen, und das ✕ des offenen Menüs stand als grauer Kasten da,
   bis man woanders hintippte. Rückmeldung beim Tippen gibt :active. */
@media (hover: hover) {
  .nav-toggle:hover { background: var(--surface-2); }
}
.nav-toggle:active { background: var(--surface-2); }
.nav-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nav-toggle-bar {
  display: block; width: 20px; height: 2px; border-radius: 2px;
  background: var(--text); transition: transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out), opacity var(--panel-motion-duration-fast);
}
/* Hamburger → X, the same three-bar mechanic as wiland_website's own
   .nav-toggle (website/static/website/scss/index.scss) — middle bar
   fades, outer two rotate into a cross. */
.app-nav.is-open .nav-toggle-bar:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.app-nav.is-open .nav-toggle-bar:nth-child(2) { opacity: 0; }
.app-nav.is-open .nav-toggle-bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
@media (prefers-reduced-motion: reduce) {
  .nav-toggle-bar { transition: opacity var(--panel-motion-duration-fast); }
  .app-nav.is-open .nav-toggle-bar:nth-child(1),
  .app-nav.is-open .nav-toggle-bar:nth-child(3) { transform: none; }
}

.app-nav-links { display: flex; gap: .3rem; flex-wrap: wrap; }
.app-nav-links a { display: flex; align-items: center; gap: .5rem; text-decoration: none; color: var(--text-dim); font-weight: 600; font-size: .92rem; padding: .5rem .8rem; border-radius: 999px; transition: color var(--panel-motion-duration-fast), background var(--panel-motion-duration-fast); }
.app-nav-links a svg { width: 16px; height: 16px; flex: none; }
@media (hover: hover) {
  .app-nav-links a:hover { color: var(--text); background: var(--surface-2); }
}
/* „Hier bin ich" als umgekehrte Pille: Tinte auf dem Band, Schrift in der
   Farbe des Bandes — dieselbe Form wie die Knöpfe der Marke. Auf einer
   einfarbigen Leiste ist Umkehren das eine Mittel, das ohne Farbe laut ist. */
.app-nav-links a.active { color: var(--bg); background: var(--text); }
.app-nav-links a.active:hover { color: var(--bg); background: var(--text); }

/* Zehn gleichgewichtete Zeilen sind eine Halde: taegliche Betriebssicht,
   Geschaeft und Infrastruktur waren optisch nicht zu trennen. Eine Linie
   zwischen den Bloecken reicht — eine zusaetzliche Beschriftung waere
   Navigation ueber der Navigation. In der aufgeklappten Mobilflaeche traegt
   ohnehin jede Zeile schon eine Trennlinie, dort faellt die zweite weg. */
.app-nav-gruppe { display: flex; flex-direction: column; gap: .3rem; min-width: 0; }
.app-nav-gruppe[hidden] { display: none; }
.app-nav-gruppe + .app-nav-gruppe { margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--border); }

/* Trailing group (toggle + who) — margin-left:auto pushes it to
   .app-nav-panel's own right edge on the inline/login layout;
   min-width:960px switches this to margin-top:auto instead, once
   .app-nav itself becomes a column (see that block near the end of this
   file). */
.app-nav-foot { display: flex; align-items: center; gap: .7rem; margin-left: auto; }
.who { display: flex; align-items: center; gap: .7rem; color: var(--text-dim); font-size: .88rem; }
/* Der eigene Name ist der Weg zum Profil (base.html). Deshalb sieht er
   aus wie das, was er ist: gepunktet unterstrichen im Ruhezustand, voll
   unterstrichen beim Ueberfahren — nicht wie ein Link mitten im Text und
   nicht wie ein Knopf. */
.who-name {
  font-weight: 600; color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
}
.who-name:hover { text-decoration-color: currentColor; }
button.link { background: none; border: none; color: var(--text-dim); font: inherit; font-size: .88rem; cursor: pointer; padding: 0; text-decoration: underline; text-underline-offset: 2px; }
button.link:hover { color: var(--text); }

/* Das Kunden-Portal hat seine eigene Datei: dashboard/static/dashboard/
   portal.css. Hier stand einmal seine Kopfzeile — sie ist mit dem
   Umbau des Portals dorthin gezogen, zusammen mit allem anderen, was
   nur dort vorkommt. Diese Datei ist das Staff-Panel. */

/* --- Mobile menu: .app-nav-panel as a fullscreen sheet -------------------
   Scoped to `.has-menu` (nothing to open on the login screen, see
   base.html) and to below the sidebar breakpoint — from 960px this whole
   block stops applying and .app-nav-panel goes back to its inline base
   rule above, then display:contents (see that breakpoint) hands its
   children straight to the sidebar. `.app-nav` itself — not
   .app-nav-panel — becomes the fixed fullscreen box when open: that way
   the sheet's top edge is wherever the bar's own (variable) height
   happens to end, in normal flex flow, instead of a hand-measured pixel
   offset that would drift the moment brand/toggle sizing changes (the
   trick wiland_website's own mobile nav needs a hardcoded --header-h
   custom property for — this app doesn't, since brand+toggle stay in the
   same flow as the sheet instead of the sheet being positioned
   independently against them). */
@media (max-width: 959.98px) {
  .app-nav.has-menu .nav-toggle { display: flex; }

  .app-nav.has-menu.is-open {
    position: fixed; inset: 0; height: 100dvh;
    /* Grid statt Flex-Spalte: Marke und Knopf müssen nebeneinander in der
       ersten Zeile bleiben (eine Flex-Spalte stellt sie übereinander),
       die Fläche darunter bekommt den ganzen Rest — `minmax(0, 1fr)`,
       damit die Zeile auch wirklich schrumpfen darf und das eigene
       Scrollen der Fläche greift, statt sie über den Bildschirmrand
       hinauszuschieben. Ohne zusätzliche Hülle im Markup: die drei
       Kinder von .app-nav werden direkt platziert. */
    display: grid;
    /* Drei Spalten, nicht zwei: Zwischen Marke und Hamburger sitzt der
       Suchknopf. Ohne eigene Platzierung landete er in einer impliziten
       dritten Rasterzeile und war wegen `overflow: hidden` + `height:
       100dvh` unter dem Menue abgeschnitten — auf dem Handy ist er der
       einzige Weg in die Suche. */
    grid-template-columns: 1fr auto auto;
    grid-template-rows: auto minmax(0, 1fr);
    /* stretch, nicht das align-items:center der Basisregel weiter oben:
       zentriert würde die Fläche in ihrer Rasterzeile schweben, statt sie
       zu füllen — auf einem hohen Display säße das Menü dann sichtbar in
       der Mitte, mit Lücken über und unter sich. Die Mitte bekommen nur
       die beiden Kinder der Kopfzeile, einzeln (align-self unten). */
    align-items: stretch;
    overflow: hidden;
    /* Offen ist die Fläche eine einzige deckende Ebene über dem ganzen
       Viewport. Wiland macht es an derselben Stelle genauso (index.scss,
       .site-header__nav → background: var(--bg)). */
    background: var(--bg);
  }
  .app-nav.has-menu.is-open .brand { grid-column: 1; grid-row: 1; align-self: center; justify-self: start; }
  .app-nav.has-menu.is-open .nav-suche { grid-column: 2; grid-row: 1; align-self: center; justify-self: end; margin: 0 .3rem 0 0; }
  .app-nav.has-menu.is-open .nav-toggle { grid-column: 3; grid-row: 1; align-self: center; }
  .app-nav.has-menu .app-nav-panel { display: none; }
  .app-nav.has-menu.is-open .app-nav-panel {
    grid-column: 1 / -1; grid-row: 2;
    display: flex; flex-direction: column; align-items: stretch;
    min-height: 0; width: auto;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    gap: 0;
    /* Randlos: die negativen Ränder heben die seitliche Polsterung von
       .app-nav wieder auf, die eigene Polsterung setzt sie innen
       identisch wieder ein. Sonst stünde die Fläche als eingerückter
       Kasten in der Leiste, statt bis an die Bildschirmkante zu gehen. */
    margin-left: calc(-1.4rem - env(safe-area-inset-left));
    margin-right: calc(-1.4rem - env(safe-area-inset-right));
    padding-top: 1rem;
    /* Unten kein Polster: Die Fußzeile klebt dort und bringt den Abstand
       zum Home-Indikator selbst mit (siehe .app-nav-foot unten). */
    padding-bottom: 0;
    padding-left: calc(1.4rem + env(safe-area-inset-left));
    padding-right: calc(1.4rem + env(safe-area-inset-right));
  }
  /* Dieselbe Sprache wie die Seitenleiste, nur fingerbreit: Pillen statt
     Zeilen mit einer Linie unter jeder, die Gruppen wie am Rechner durch
     eine Linie getrennt, der aktive Punkt als umgekehrte Pille. Vorher
     trug jede der zehn Zeilen eine Trennlinie, war 57px hoch und der
     aktive Punkt hatte Fläche UND Unterstrich — zehn Linien, zwei Signale
     für eins, und Konto und Theme standen unter dem Bildrand.

     46px je Punkt: über den 44px, die ein Finger braucht, und klein genug,
     dass zehn Punkte samt Fußzeile auf ein Telefon passen, auch mit der
     Werkzeugleiste des Browsers darunter. Kein „Menü"
     darüber: Was die Fläche ist, sagt das ✕ daneben. */
  .app-nav.has-menu.is-open .app-nav-links {
    flex-direction: column; flex-wrap: nowrap; gap: 0; width: 100%;
  }
  .app-nav.has-menu.is-open .app-nav-gruppe { width: 100%; gap: .15rem; }
  .app-nav.has-menu.is-open .app-nav-gruppe + .app-nav-gruppe {
    margin-top: .5rem; padding-top: .5rem;
  }
  .app-nav.has-menu.is-open .app-nav-links a {
    width: 100%; min-height: 46px; gap: .85rem;
    padding: .65rem 1rem; font-size: 1.02rem;
    -webkit-tap-highlight-color: transparent;
  }
  .app-nav.has-menu.is-open .app-nav-links a svg { width: 19px; height: 19px; }
  .app-nav.has-menu.is-open .app-nav-links a:active { color: var(--text); background: var(--surface-2); }
  .app-nav.has-menu.is-open .app-nav-links a.active,
  .app-nav.has-menu.is-open .app-nav-links a.active:active { color: var(--bg); background: var(--text); }
  /* Theme und Konto kleben am unteren Rand der Fläche, auch wenn sie auf
     einem kleinen Gerät scrollt: Abmelden und Hell/Dunkel sucht man nicht
     unter dem Bildrand. Die Fläche dahinter deckt, was darunter wegrollt. */
  .app-nav.has-menu.is-open .app-nav-foot {
    position: sticky; bottom: 0;
    margin: auto 0 0; width: 100%; justify-content: space-between;
    padding: .8rem 0 calc(1rem + env(safe-area-inset-bottom));
    border-top: 1px solid var(--border);
    background: var(--bg);
  }
  /* Luft zwischen der letzten Gruppe und der Fußzeile, wenn die Liste bis
     an sie heranreicht (margin-top: auto ist dann 0). */
  .app-nav.has-menu.is-open .app-nav-links { padding-bottom: 1rem; }
  .app-nav.has-menu.is-open .theme-toggle { width: 44px; height: 44px; }
  .app-nav.has-menu.is-open .theme-toggle svg { width: 19px; height: 19px; }
  .app-nav.has-menu.is-open button.link { padding: .6rem 0; }
  /* Ein kurzer Auftritt beim Öffnen: Die Fläche blendet ein und rückt
     6px nach oben — die Antwort auf den Tipp, nicht Dekoration. */
  .app-nav.has-menu.is-open .app-nav-panel { animation: menue-auf .18s var(--talvesa-motion-easing-apple-out); }
}
@keyframes menue-auf { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  .app-nav.has-menu.is-open .app-nav-panel { animation: none; }
}
/* Locks background scroll while the fullscreen sheet is open — same
   reasoning as .confirm-dialog's own scroll lock (panel.js,
   docs/performance-und-glas.md §2: "Beim Öffnen eines Sheets den
   Hintergrund-Scroll sperren"), set via panel.js's initMobileNav(). */
/* EINE Klasse fuer jede Ueberlagerung statt dreier Wege: Mobilmenue,
   Bestaetigungsdialog und Befehlssuche sperren denselben Hintergrund-
   Scroll. Solange jede ihren eigenen setzte, raeumte die zuletzt
   geschlossene ihn auch der noch offenen weg. `body.nav-open` bleibt als
   Zweitname bestehen — panel.js setzt ihn heute. */
body.hat-overlay, body.nav-open { overflow: hidden; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Die erste fokussierbare Stelle jeder Seite. Ohne sie tabbt man sich erst
   durch Marke, Suchknopf, bis zu zehn Navigationspunkte, Erscheinungsbild,
   Sicherheit und Abmelden, bevor der erste Inhalt kommt. Unsichtbar wie
   .visually-hidden, bis der Fokus darauf steht — dann ein echter, opaker
   Knopf oben links, ueber allem und mit voller Trefferflaeche.

   `fixed` und nicht `absolute`: Der Sprunglink steht ganz oben im
   Dokument, mit `absolute` klebt er also am SEITENANFANG. Wer weiter
   unten mit Umschalt+Tabulator zurueck nach oben laeuft, faende ihn dann
   zwar mit dem Fokus, saehe ihn aber ausserhalb des Bildschirms liegen —
   ein Knopf, der sich nicht zeigt, ist keiner.

   :focus NEBEN :focus-visible, wie weiter unten bei .reveal-*: Ein
   Sprunglink wird ausschliesslich per Tastatur erreicht (1px, geclippt,
   nicht anklickbar), beide Selektoren treffen hier also dieselbe Lage —
   der zweite ist nur der Rueckfall fuer Browser ohne :focus-visible. */
.sprunglink {
  position: fixed; top: .5rem; left: .5rem;
  z-index: var(--talvesa-z-index-toast);
  width: 1px; height: 1px; padding: 0; margin: 0;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
}
.sprunglink:focus,
.sprunglink:focus-visible {
  width: auto; height: auto; clip: auto; overflow: visible;
  display: inline-flex; align-items: center; min-height: 44px;
  padding: .55rem 1.1rem;
  top: calc(.5rem + env(safe-area-inset-top));
  left: calc(.5rem + env(safe-area-inset-left));
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-weight: 600; text-decoration: none; box-shadow: var(--shadow);
}

/* Light/dark toggle (see base.html/panel.js), grouped with .who inside
   .app-nav-foot above. Shows the *current* theme, not the target — a moon
   while dark is active, a sun once switched to light, same "state
   indicator" reading as a physical switch. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: none;
  /* Kontur statt grauer Scheibe, wie jeder Zweitknopf im Panel. */
  border-radius: 999px; border: 1px solid var(--border-strong);
  background: none; color: var(--text-dim); cursor: pointer;
  transition: color var(--panel-motion-duration-fast), background var(--panel-motion-duration-fast), border-color var(--panel-motion-duration-fast);
}
@media (hover: hover) {
  .theme-toggle:hover { color: var(--text); background: var(--surface-2); }
}
.theme-toggle:active { color: var(--text); background: var(--surface-2); }
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle-icon-sun { display: none; }
[data-theme="light"] .theme-toggle-icon-sun { display: block; }
[data-theme="light"] .theme-toggle-icon-moon { display: none; }

/* Centered 1200px column, reverted back from a brief full-width
   experiment. Padding here is still mobile-first (this is the
   phone-width default, smaller than the desktop value); the
   min-width:641px block near the end of this file opens it back up once
   there's room to spare, same convention as .card/.overview-tile/th,td
   below. Once .app-nav becomes a real sidebar at min-width:960px, this
   column sits centered *within* .app-content, i.e. within whatever width
   remains next to the sidebar — see that block's own comment. */
main {
  max-width: 1200px; margin: 0 auto; padding: 1.5rem 1rem 3rem;
  /* Landscape phones put the notch on a *side*, not the top .app-nav
     already guards — safe-area-inset-left/right stack onto the existing
     1rem so content never sits flush against the cutout (base.html's own
     comment on viewport-fit=cover lists this file as one of the three
     places that matter). */
  padding-left: calc(1rem + env(safe-area-inset-left));
  padding-right: calc(1rem + env(safe-area-inset-right));
}

/* --- Page header ------------------------------------------------------ */

/* Stacked by default (mobile-first) — a title next to a right-aligned
   control (e.g. dashboard.html's "Letzte Aktivität" / "Ganzer Verlauf")
   has nowhere to go on a narrow screen but on top of each other; the
   min-width:641px block near the end of this file puts them back on
   one baseline once there's room. */
/* Ein Seitenkopf, drei Bauarten — das war der Stand: .page-header mit
   verschachteltem <div>, dieselben Klassen ohne Huelle im Inneren einer
   Login-Karte, und .site-actions (eine Kartenfuss-Klasse mit
   margin-top:auto, die den Knopfblock an die Unterkante statt auf die
   Grundlinie des Titels zieht) als Aktionsablage auf der Logseite. Ab hier
   ist es einer. `.seiten-kopf` ist der sprechende Name, `.page-header`
   bleibt als Zweitname bestehen — die Testsuite liest ihn.
   Der Abschnittskopf INNERHALB einer Karte ist ausdruecklich NICHT dieser
   Baustein, sondern .karten-kopf weiter unten. */
.seiten-kopf, .page-header { display: flex; flex-direction: column; align-items: stretch; gap: .9rem; margin-bottom: var(--space-9); }
/* Der Titelblock links (Ueberschrift plus erklaerende Zeile). Bisher ein
   nacktes <div>; benannt, damit die Ablage rechts einen Gegenpart hat. */
.seiten-kopf-titel { min-width: 0; }
/* Fluid, not a fixed size — grows a little on a wider canvas instead of
   sitting the same 1.6rem whether the page is 375px or 3000px wide. The
   low end matches the old fixed size exactly, so mobile is unchanged. */
.page-title { font-size: clamp(1.6rem, 1.3rem + 1vw, 2.2rem); line-height: 1.1; margin: 0; }
/* Lesebreite auch hier: Der Beisatz ist Fliesstext und lief sonst ueber
   die volle 1200px-Spalte. */
.page-sub, .seiten-kopf-bei { color: var(--text-dim); font-size: .92rem; margin: .35rem 0 0; max-width: var(--mass-text); }
/* Die Ablage rechts: Knoepfe, Statuspillen und Zaehlertexte in einer
   umbrechenden Reihe — Status-Pille und Test-Branch-Abzeichen im Seitenkopf
   von site_detail.html waren dafuer einmal ein style="display:flex; …".
   .page-header-meta ist derselbe Baustein unter seinem bisherigen Namen. */
.seiten-kopf-aktionen, .page-header-meta { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.seiten-kopf-aktionen[hidden] { display: none; }

/* Erklärender Kleintext: die Zeile, die einer Anzeige ihren Kontext gibt
   („Restore noch nicht aktiviert“, „Letztes Backup ist 12 Tage alt“).
   Eine Klasse statt des bisherigen style="font-size:.85rem" an jeder
   einzelnen Stelle — dieselbe Größe soll dieselbe Bedeutung haben. */
.note { font-size: .85rem; line-height: 1.5; color: var(--text-dim); margin: 0; max-width: var(--mass-text); }
/* Eine Stufe kleiner, fuer die Nachbemerkung unter genau EINEM Kasten
   (Befehlsausgabe, Feld) statt fuer den Abschnitt darueber. Zwei Groessen,
   zwei Bedeutungen — mehr braucht erklaerender Kleintext nicht. */
.note-klein { font-size: .8rem; }
/* Die zwei Abstaende, die dieser Absatz wirklich hat: direkt unter einer
   Ueberschrift oder einem Wert, und abgesetzt am Fuss einer Karte. Vorher
   standen dafuer vier margin-Kombinationen als Inline-Style nebeneinander. */
.note-unter { margin-top: .35rem; }
.note-fuss { margin-top: .9rem; }
/* Aus „Kleintext“ wird „Kleintext, der etwas will“ — dieselbe Warnfarbe
   wie .status-pill.starting und .branch-badge, siehe deren Kommentare:
   Aufmerksamkeit, kein Fehler. */
.is-warn { color: var(--warn-text); }
/* Dieselbe Idee wie .is-warn, nur für einen echten Ausfall statt bloßer
   Aufmerksamkeit — dashboard/services/uptime.py: eine Site, die über
   HTTPS gerade nicht erreichbar ist oder deren Zertifikat ungültig ist,
   nicht nur bald abläuft. */
.is-bad { color: var(--bad-text); }
/* Reine Massklasse: begrenzt die Zeile, sonst nichts. Fuer Absaetze, die
   ihre Groesse schon selbst mitbringen (.kasse-erklaerung, .lage-erklaerung)
   oder in voller Textfarbe stehen sollen. */
.fliesstext { max-width: var(--mass-text); }
/* Der Einleitungsabsatz unter einer Kartenueberschrift — dieselbe Rolle,
   die bisher in .9rem, .88rem und .85rem nebeneinander stand, mit vier
   verschiedenen Randwerten. Wie .note, plus die eine Zeile Abstand nach
   unten, die er immer braucht. */
.karten-text { font-size: .85rem; line-height: 1.5; color: var(--text-dim); margin: 0 0 1rem; max-width: var(--mass-text); }
.karten-text strong { color: var(--text); font-weight: 600; }
/* Eine nummerierte Anleitung im selben Ton — die Einrueckung kommt vom
   <ol>, nicht von einem zweiten Absatzmass. */
.karten-schritte { padding-left: 1.2rem; }
.karten-schritte li { margin-bottom: .3rem; }
/* Ein zweiter Abschnitt in derselben Karte, abgesetzt ohne Trennlinie:
   Eine Linie hiesse „andere Sache" — hier ist es dieselbe, nur weiter. */
.karten-abschnitt { margin-top: 1.1rem; }
/* Laufzeit einer Aktion, direkt hinter ihrem Zeitpunkt. Tabellenziffern,
   damit „· 4 s" und „· 12 s" untereinander dieselbe Kante halten. */
.dauer { font-size: .82em; color: var(--text-dim); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Fussnote unter der Uebersichtsleiste. Der fruehere negative obere Abstand
   war auf .overview-grid getunt (margin-bottom 1.8rem); darueber steht
   heute .zahlenband mit 1.1rem, und zwei angrenzende Aussenabstaende
   kollabieren zu exakt 0 — die Fussnote klebte am Rahmen des Bandes. Dazu
   die gemeinsame Lesebreite: ohne sie lief dieser Absatz in der
   1200px-Spalte auf rund 190 Zeichen je Zeile. */
.overview-note { font-size: .78rem; margin: .55rem 0 1.6rem; max-width: var(--mass-text); }

/* --- Karten: deckend, mit Haarlinie ------------------------------------
   Bis zur Marke waren Karten Glas: ein halb durchsichtiger Grauton über dem
   Seitengrund, weichgezeichnet. Auf Papier und Tinte wurde daraus Nebel —
   im dunklen Theme ein trübes Grau über einem anderen Grau, und darauf
   noch einmal graue Felder und graue Knöpfe. Talvesa löst dasselbe mit
   deckenden Flächen und einer Linie (paralympus, „Kopfleiste und mobile
   Leiste sind deckend"), und dasselbe gilt jetzt hier: eine Karte ist ein
   Blatt auf dem Papier, abgesetzt durch ihre Kante, nicht durch den
   Hintergrund, der gerade hinter ihr liegt.

   Dasselbe gilt für alles, was über dem Inhalt schwebt — Dialog, Popover,
   Hinweisblase, die Kopfleiste des Kunden-Portals: deckend, mit Kante und
   Schatten, einheitlich. Glas gibt es im Panel nicht mehr, und mit ihm
   sind der Schalter und die drei Regler in den Einstellungen gegangen.

   Die Linie ist dieselbe wie an den Panels in geschaeft.css (--g-linie),
   damit alte Karten und neue Seiten eine Kante haben. Keine Lichtkante
   mehr oben links: Die Fläche ist genau die Farbe der Marke, sonst nichts. */
.card, .site-card, .hetzner-server-card {
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
}

.card {
  border-radius: var(--radius);
  /* Mobile-first default; min-width:641px block near the end of this
     file opens it up once there's room to spare. */
  /* Abstand aus der Skala (siehe :root): Karte zu Karte ist eine Stufe,
     nicht eine Zahl, die jede Seite neu waehlt. */
  padding: 1.15rem 1.2rem; margin-bottom: var(--space-7);
  box-shadow: var(--shadow-sm);
}
/* Der Kartentitel ist eine Ueberschrift, kein Etikett. Vorher: .78rem,
   VERSALIEN, .08em gesperrt und in --text-dim — also kleiner, blasser und
   schlechter lesbar als der Fliesstext, den er ueberschreibt; die Hierarchie
   stand auf dem Kopf. Denselben Musterfehler hat diese Datei an
   .overview-tile-label und an `th` bereits benannt und korrigiert („same
   over-invented eyebrow label pattern … nowhere in the real component"),
   hier war er der letzte Rest. Satzkasten, weil die Vorlagen ihn ohnehin so
   schreiben („Letzte Aktivitaet", „Weitere Projekte auf dem Server") — nur
   das CSS schrie ihn an. Die Flex-Zeile bleibt: einige Karten setzen
   Beiwerk neben den Titel. */
h2.card-title {
  font-size: .95rem; color: var(--text); margin: 0 0 1.2rem;
  letter-spacing: -.01em; font-weight: 650;
  display: flex; align-items: center; gap: .5rem;
}

/* --- Kartenkopf ----------------------------------------------------------
   Der Abschnittskopf INNERHALB einer Karte: Titel links, eine Angabe oder
   ein kleiner Knopf rechts. Bis hierher wurde dafuer .page-header
   zweckentfremdet — die Komponente fuer den SEITENkopf, mit 1.8rem
   Aussenabstand und einer Grundlinie, die zu einem h1 gehoert — und jedes
   Mal per style="margin-bottom:.6rem" und style="margin:0" wieder
   zurechtgebogen. Dieselbe Kopfform bedeutete damit an drei Stellen einer
   Seite drei verschiedene Dinge.

   Kein Rahmen, keine eigene Flaeche: Die Karte ist schon die Flaeche. Den
   Abstand nach unten bringt der Kopf selbst mit, damit keine Stelle ihn per
   style-Attribut zuruecknehmen muss. */
/* `.seiten-kopf.ist-abschnitt` steht als Zweitname im Selektor, damit eine
   Vorlage, die den Seitenkopf zum Abschnittskopf herunterstuft, nicht
   ungestylt endet. Der Name, der gilt, ist .karten-kopf. */
.karten-kopf, .seiten-kopf.ist-abschnitt {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: .35rem; margin: 0 0 var(--space-5);
}
.karten-kopf[hidden] { display: none; }
/* Der Titel bringt seinen Abstand nicht mehr selbst mit — den macht der
   Kopf, fuer beide Anordnungen gleich. */
.karten-kopf > .card-title, .karten-kopf > h2, .karten-kopf > h3, .karten-kopf > p { margin: 0; }

/* --- Leitkarte -----------------------------------------------------------
   Der eine Block, der die Hauptfrage einer Seite beantwortet (auf der Kasse
   der Ausgleich). Deutlicher als seine Nachbarn, aber ohne Zustandsfarbe:
   sonst konkurriert er mit .kasse-warnung, und „wichtig" saehe aus wie
   „Achtung". Eine kraeftigere Kante und ein groesserer Kopftitel, mehr
   nicht. */
.karte-leit { box-shadow: 0 0 0 1.5px var(--border-strong) inset, var(--shadow-sm); }
.karte-leit > .karten-kopf > .card-title { font-size: 1.15rem; font-weight: 700; }

/* --- Selbst-Redeploy des Panels ---------------------------------------
   Der eine hart verdrahtete Knopf ganz oben auf dem Dashboard (siehe
   dashboard.html und views.py:panel_redeploy). Er darf laut sein: Er ist
   die einzige Aktion auf dieser Seite, die nicht eine der Sites betrifft,
   sondern das Werkzeug selbst — steckt man ihn in die Reihe der
   Site-Karten, sucht man ihn dort jedes Mal.

   Eine deckende Fläche mit Akzent-Einfärbung — auffällig durch Farbe und
   Kontur.

   Mobile-first wie der Rest dieser Datei: Symbol und Text nebeneinander,
   der Knopf rutscht auf schmalen Schirmen in eine eigene, volle Zeile
   (flex-basis:100%). Erst ab 641px sitzt er rechts in derselben Zeile —
   siehe den min-width-Block weiter unten. */
/* flex-start, nicht center: Auf einem schmalen Schirm wird der Text
   mehrzeilig, und ein mittig dazu schwebendes Symbol sieht aus, als
   gehöre es zu nichts. Ab 641px, wo die Zeile kurz ist und der Knopf
   danebensteht, ist center wieder richtig — siehe unten. */
.selfdeploy {
  display: flex; align-items: flex-start; flex-wrap: wrap; gap: .8rem 1rem;
  margin: 0 0 1.6rem; padding: 1rem 1.15rem;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
  /* Von der Akzentfarbe angehauchte Fläche statt voller Akzentfüllung:
     Ein komplett eingefärbter Balken über der Übersichtsleiste würde die
     Kacheln darunter erschlagen — hier soll ein Knopf hervorstechen,
     nicht die halbe Seite. */
  background:
    linear-gradient(135deg,
      color-mix(in srgb, var(--accent) 14%, var(--surface)) 0%,
      var(--surface) 62%);
  box-shadow: 0 8px 26px -20px var(--accent-strong);
}
.selfdeploy-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex: none; border-radius: var(--radius-md);
  color: var(--accent); background: var(--accent-glow);
}
.selfdeploy-icon svg { width: 20px; height: 20px; }
/* min-width:12rem, damit die Textspalte auf schmalen Schirmen nicht auf
   zwei Wörter pro Zeile zusammenfällt, bevor der Knopf umbricht. */
.selfdeploy-text { flex: 1 1 12rem; min-width: 0; }
.selfdeploy-title { margin: 0; font-weight: 700; font-size: 1rem; letter-spacing: -.01em; }
.selfdeploy-desc { margin: .2rem 0 0; font-size: .82rem; line-height: 1.5; color: var(--text-dim); }
.selfdeploy-btn { flex: 1 1 100%; justify-content: center; padding: .7rem 1.4rem; font-size: .92rem; }
.selfdeploy-btn svg { width: 16px; height: 16px; }
/* Die Signatur — klein, gedämpft, und bewusst am Ende des erklärenden
   Textes statt neben der Überschrift: eine Notiz, kein zweiter Titel. */
.selfdeploy-sig { white-space: nowrap; opacity: .75; font-size: .78em; }

/* --- Overview strip (dashboard-wide stats) ---------------------------
   One surface for the whole strip, like every card (see „Karten: deckend,
   mit Haarlinie"); .overview-tile below is a cell inside it, each subtly
   separated by its own --surface-2 tint rather than a divider line
   (robust regardless of how many tiles wrap per row at a given width,
   unlike a border-based divider would be).

   Two fixed columns by default (mobile-first) — the fluid auto-fit
   pattern only takes over at min-width:641px (see that block near the
   end of this file). At a phone's width, minmax(190px,1fr) would often
   only fit one column, and five tiles stacked in a single column reads
   as a long scroll of near-identical rows rather than an overview; two
   columns keeps it scannable as an actual grid even at the narrowest
   width this panel supports. */
/* --- Ressourcen: der Balken -----------------------------------------------
   Gemessen liegt INNERHALB von gebucht, gebucht kann ueber den echten
   Speicher hinausgehen. Genau dieses Verhaeltnis ist der Punkt der Seite,
   und aus drei Zahlen nebeneinander liest es niemand ab.

   Zwei uebereinanderliegende Balken statt gestapelter Abschnitte: Gebucht
   und gemessen sind keine Teile einer Summe, sondern zwei Messungen
   derselben Sache. Nebeneinander gelegt saehe es aus, als addierten sie
   sich. */
.res-balken { margin: 0 0 1.4rem; }
.res-balken-spur {
  position: relative; height: 12px; overflow: hidden;
  border-radius: 999px; background: var(--surface-2);
}
.res-balken-gebucht, .res-balken-gemessen {
  position: absolute; left: 0; top: 0; bottom: 0;
  border-radius: 999px; max-width: 100%;
}
/* Gebucht liegt hinten und ist blass: eine Reservierung, kein Verbrauch. */
.res-balken-gebucht { background: color-mix(in srgb, var(--accent) 32%, transparent); }
/* Ueber 100% gebucht: Der Balken kann das nicht zeigen (er endet am Rand),
   also sagt es die Farbe. */
.res-balken-gebucht.ist-ueber { background: color-mix(in srgb, var(--warn) 40%, transparent); }
.res-balken-gemessen { background: var(--accent); }
.res-balken-legende {
  display: flex; gap: 1.1rem; flex-wrap: wrap;
  margin-top: .5rem; font-size: .8rem; color: var(--text-dim);
}
.res-punkt {
  display: inline-block; width: 8px; height: 8px; margin-right: .35rem;
  border-radius: 999px; vertical-align: baseline;
}
.res-punkt-gemessen { background: var(--accent); }
.res-punkt-gebucht { background: color-mix(in srgb, var(--accent) 32%, transparent); }

/* --- Verlaufsseite -------------------------------------------------------
   Eine Tabelle aus fuenfzig gleich aussehenden Zeilen, in der die eine
   fehlgeschlagene in Spalte 5 steht. Beim Ueberfliegen liest man die erste
   Spalte und den linken Rand -- also gehoert der Ausgang dorthin. */
.verlauf-lage { margin: 0 0 .9rem; font-size: .95rem; color: var(--text-dim); }
.verlauf-schlecht { color: var(--bad-text); }
.verlauf-laufend { margin-left: .6rem; font-size: .85rem; }
/* Das Suchfeld ist das breiteste Element der Filterzeile: Ein Suchwort
   braucht Platz, die drei Auswahlfelder daneben zeigen nur ihren Wert.
   Auf dem Telefon nimmt es wie alles in .filters die ganze Breite. */
@media (min-width: 641px) {
  .filters > .verlauf-suche { flex: 2 1 16rem; min-width: 0; }
}

/* Nur ein Rand, keine eingefaerbte Zeile: Eine rote Flaeche ueber die
   ganze Breite macht den Text darin schlechter lesbar -- und lesen will
   man gerade bei einem Fehlschlag. */
.verlauf-zeile > td:first-child { border-left: 2px solid transparent; padding-left: .6rem; }
.verlauf-failure > td:first-child { border-left-color: var(--bad); }
.verlauf-pending > td:first-child { border-left-color: var(--accent); }

@media (max-width: 760px) {
  /* In der Kartenansicht (siehe .table-wrap) gibt es keine erste Spalte
     mehr -- dort traegt die ganze Karte den Rand. */
  .verlauf-zeile > td:first-child { border-left: none; padding-left: 0; }
  .verlauf-failure { border-left: 2px solid var(--bad); }
  .verlauf-pending { border-left: 2px solid var(--accent); }
}

/* --- Logseite ------------------------------------------------------------
   Vorher war alles gleich grau und die Suche lief ueber Strg+F. Strg+F
   kann aber nicht ausblenden, und genau das braucht man bei 300 Zeilen
   Zugriffslog. */
.logbar {
  display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
  margin: 0 0 .8rem;
}
.log-filter {
  flex: 1 1 14rem; min-width: 0;
  padding: .45rem .7rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); font: inherit; font-size: .88rem;
}
.log-nurfehler { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--text-dim); }
.log-zahlen { display: inline-flex; align-items: center; gap: .7rem; font-size: .8rem; color: var(--text-dim); }
.log-zahl-bad { color: var(--bad-text); font-weight: 600; }
.log-zahl-warn { color: var(--warn-text); font-weight: 600; }
.log-zahl-ok { color: var(--text-dim); }
.log-treffer { font-size: .8rem; color: var(--text-dim); }

/* Der Log-Koerper scrollt selbst, statt die Seite lang zu machen: So
   bleibt die Leiste mit Filter und Zahlen beim Lesen sichtbar. */
/* Die Schrift muss hier stehen und nicht an `pre.log`: Der Logkoerper der
   Logseite ist ein <div>, das die pre-Regel nicht trifft — ausgerechnet die
   Logseite rendert sonst 300 Zeilen Serverausgabe in der UI-Sans, waehrend
   dieselbe Ausgabe im aufklappbaren <details> der Verlaufstabelle mono ist.
   Bewusst NICHT `pre.log` auf `.log` verallgemeinert: dessen Hintergrund
   und Rahmen saessen als zweiter Kasten in der ohnehin rahmenden .log-card. */
.log-card .log {
  display: block; max-height: 68vh; overflow: auto;
  margin: 0; padding: .2rem 0;
  font-family: var(--font-mono); font-size: .83rem; line-height: 1.5;
}
/* Passt keine Zeile, blieb die Flaeche einfach leer — und „der Filter hat
   nichts gefunden" sah damit aus wie „das Log ist leer". :has() ist in
   dieser Datei bereits im Einsatz (siehe .table-wrap td.cell-block:has()). */
.log-card .log:not(:has(.log-zeile:not([hidden])))::after {
  content: "Keine Zeile passt zu diesem Filter.";
  display: block; padding: 1.6rem .2rem; text-align: center;
  font-family: var(--font-sans); font-size: .85rem; color: var(--text-dim);
}
.log-zeile {
  display: block; padding: 0 .2rem;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* Dieselbe Kaskadenfalle wie bei .palette-backdrop und .empty-state, hier
   von JavaScript aus: Der Filter setzt zeile.hidden, und ohne diese Zeile
   schlaegt das `display: block` darueber das `[hidden]` des Browsers --
   der Filter blendete dann nichts aus, sondern taete nur so. */
.log-zeile[hidden] { display: none; }
/* Nur ein Farbstich, kein Kasten: Eine eingefaerbte Zeile heisst "hier
   lohnt der Blick", nicht "hier ist der Fehler" -- die Muster sind grob
   (siehe logzeilen.py), und eine Auszeichnung, die mehr behauptet als
   sie weiss, waere schlechter als keine. */
.log-bad { color: var(--bad-text); }
.log-warn { color: var(--warn-text); }

/* --- Befehlssuche (Cmd/Strg+K) -------------------------------------------
   Der Dialog sitzt im oberen Drittel statt mittig: Die Trefferliste
   waechst nach unten, und ein mittig zentrierter Dialog wandert bei jedem
   Tastendruck.

   z-index: modal. Hier stand eine 200 von Hand — also exakt der Wert von
   --talvesa-z-index-sticky, den die klebende .app-nav traegt. Dass der
   Dialog trotzdem darueber lag, entschied allein die Reihenfolge im
   Markup; wer die Suche in base.html nach oben zoege, haette sie hinter
   der Navigationsleiste. Seit dieser Dialog wirklich modal ist
   (Fokusfalle und body.hat-overlay in panel.js), gehoert er auf die
   Modal-Stufe: ueber die Leiste (200), aber weiter unter Ladebalken,
   Warte-Banner und Sprunglink (toast, 500) — die duerfen auch bei
   offener Suche sichtbar bleiben. */
.palette-backdrop {
  position: fixed; inset: 0; z-index: var(--talvesa-z-index-modal);
  display: flex; justify-content: center; align-items: flex-start;
  padding: 10vh 1rem 1rem;
  background: color-mix(in srgb, #000 55%, transparent);
}
/* DIESE ZEILE IST DER GRUND, WARUM DIE SUCHE UEBERHAUPT VERSCHWINDET.
   `display: flex` daruber schlaegt das `[hidden] { display: none }` des
   Browsers -- ohne diese Regel steht der Dialog auf JEDER Seite offen und
   laesst sich nicht schliessen. Dieselbe Falle wie bei .reveal-block und
   .tool-grid label weiter oben; sie ist hier zum dritten Mal zugeschnappt. */
.palette-backdrop[hidden] { display: none; }
.palette {
  /* 38rem statt 34: Seit die Suche Befehle zeigt, steht in einer Zeile
     auch ein Pfad wie /opt/deploy/sites/<slug>/.env — bei 34rem brach er
     um, bevor der Slug kam. */
  width: 100%; max-width: 38rem;
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: 0 28px 70px -24px rgba(0, 0, 0, .8);
  overflow: hidden;
}
.palette-eingabe {
  display: flex; align-items: center; gap: .7rem;
  padding: .85rem 1rem;
  border-bottom: 1px solid var(--border);
}
.palette-eingabe svg { width: 17px; height: 17px; color: var(--text-dim); flex: 0 0 auto; }
/* Zwei Klassen, damit die Regel gegen die geteilten Feldregeln weiter
   unten gewinnt (input[type="text"] hat dieselbe Spezifität wie
   `.palette-eingabe input` und steht später) — bisher trug das Feld
   deren Fläche, Rand und Fokusrahmen, und in der Zeile stand ein Kasten
   im Kasten. */
.palette .palette-eingabe input {
  flex: 1 1 auto; min-width: 0;
  padding: 0; border: none; border-radius: 0; background: transparent; color: var(--text);
  font: inherit; font-size: 1rem;
}
.palette .palette-eingabe input:focus-visible { outline: none; box-shadow: none; }
/* Hier stand `outline: none` und damit gar nichts: Tabbte man im offenen
   Dialog vom Schliessen-Knopf zurueck ins Feld, war nirgends zu sehen, wo
   der Fokus steht (WCAG 2.4.7) — obwohl diese Datei sonst ueberall
   :focus-visible zeigt. Der Ring sitzt an der ZEILE, nicht am Feld: Das
   Feld hat keine eigene Kante, ein Ring um seinen Textbereich saehe darin
   aus wie ein zweites Bauteil. */
.palette-eingabe:focus-within { border-bottom-color: var(--accent); box-shadow: inset 0 -1px 0 var(--accent); }
.palette-liste { list-style: none; margin: 0; padding: .3rem; max-height: min(28rem, 60vh); overflow-y: auto; }
.palette-treffer a {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .5rem .7rem; border-radius: var(--radius-sm);
  text-decoration: none; color: var(--text);
}
.palette-titel, .palette-unter { overflow-wrap: anywhere; }
/* Die Hervorhebung haengt am li und nicht an :hover des a: Sie folgt den
   Pfeiltasten, und Tastatur und Maus duerfen nicht zwei verschiedene
   "aktive" Zeilen zeigen.
   
   NICHT --surface-2, und das war ein echter Fehler: Im dunklen Thema ist
   --surface-2 exakt die Farbe der Dialogflaeche selbst. Die markierte
   Zeile war damit von den anderen nicht zu unterscheiden — Pfeiltasten
   und Enter taten genau das Richtige, man sah es nur nicht, und das las
   sich als "die Tastatur funktioniert hier nicht". Im hellen Thema war
   es ein sehr blasses Grau, also dasselbe Problem in schwach.
   
   Jetzt der Akzent, eingefaerbt statt voll: eine Flaeche, die sich in
   beiden Themen von der Dialogflaeche abhebt, plus eine Kante links, die
   auch dann traegt, wenn jemand Farben reduziert. */
.palette-treffer a { border-left: 2px solid transparent; }
.palette-treffer.is-aktiv a {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-left-color: var(--accent);
}
.palette-treffer.is-aktiv .palette-titel { color: var(--text); }
/* Der Teil, auf den die Eingabe gepasst hat. Ohne ihn muss man bei
   "wilandkummer.de" selbst suchen, warum dieser Treffer da ist. */
.palette-markiert { color: var(--accent); font-weight: 700; }
/* Woher der Treffer kommt — Site, Logs oder Seite. Bei einer Liste, in
   der "wiland" und "Logs von wiland" untereinander stehen, ist das der
   Unterschied zwischen Lesen und Raten. */
/* Satzkasten und eine Spur groesser: .68rem VERSALIEN plus .06em
   Sperrung waren 11px — das am schwersten lesbare Textstueck der ganzen
   Oberflaeche. Die Unterscheidung zur Trefferbeschriftung traegt weiter
   Gewicht, Farbe und die Position rechts. */
.palette-art {
  margin-left: auto; flex: 0 0 auto;
  font-size: .74rem; letter-spacing: .01em;
  color: var(--text-dim);
}
.palette-kopf { display: flex; align-items: baseline; gap: .6rem; }
.palette-titel { font-size: .95rem; font-weight: 600; }
.palette-unter { font-size: .78rem; color: var(--text-dim); }
/* Ein Befehl zum Kopieren (panel.js, befehleFinden): unter Titel und Satz
   der Befehl selbst, so wie er gleich im Terminal steht — als kleines
   Stück Terminal im Nachtband der Marke, in beiden Themes dunkel wie die
   Befehlsblöcke der Werkzeuge-Seite. Man soll sehen, was man kopiert,
   bevor man es einfügt. Umgebrochen statt seitwärts gescrollt: In einer
   Trefferliste scrollt man mit den Pfeiltasten, nicht in einer Zeile. */
.palette-befehl {
  display: block; margin-top: .4rem;
  padding: .45rem .6rem; border-radius: var(--radius-sm);
  background: var(--tv-night); color: var(--tv-on-night);
  font-family: var(--font-mono); font-size: .76rem; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.palette-treffer.ist-befehl .palette-art { color: var(--text); font-weight: 600; }
/* Die Rueckmeldung sitzt in der Zeile, die man gerade gewaehlt hat — nicht
   in einem Toast irgendwo am Rand, den man mit dem Blick erst suchen
   muesste, bevor der Dialog sich schliesst. */
.palette-treffer.ist-kopiert .palette-art { color: var(--talvesa-color-status-text-success, var(--ok)); }
.palette-treffer.ist-fehler .palette-art { color: var(--talvesa-color-status-text-danger, var(--bad)); }
.palette-leer { margin: 0; padding: 1.2rem 1rem; font-size: .9rem; color: var(--text-dim); text-align: center; }
.palette-fuss {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .6rem 1rem; border-top: 1px solid var(--border);
  font-size: .75rem; color: var(--text-dim);
}
.palette-hinweis { margin-left: auto; }
.palette kbd {
  font-family: var(--font-mono, ui-monospace, monospace);
  background: var(--surface-2); border: 1px solid var(--border-strong);
  border-radius: 4px; padding: 0 .35rem; font-size: .72rem;
}

/* Der Knopf, der die Suche auf dem Handy ueberhaupt erreichbar macht.
   Ohne ihn gab es genau einen Weg hinein -- Cmd/Strg+K -- und den hat ein
   Telefon nicht. Ab 960px verschwindet er wieder: Dort steht die
   Tastenkombination im Fuss des Dialogs, und ein Dauerknopf in der
   Seitenleiste waere eine Schaltflaeche fuer etwas, das man mit zwei
   Tasten schneller hat. */
/* Bis 960px eine Lupe, darüber ein Feld. Derselbe Knopf, zwei Gestalten
   — nicht zwei Elemente, von denen eines immer falsch im DOM steht. */
.nav-suche {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-left: auto;
  border: none; background: transparent; color: var(--text-dim);
  border-radius: var(--radius-sm); cursor: pointer;
}
.nav-suche:hover, .nav-suche:focus-visible { color: var(--text); background: var(--surface-2); }
.nav-suche svg { width: 19px; height: 19px; flex: 0 0 auto; }
.nav-suche + .nav-toggle { margin-left: 0; }
/* Text und Taste gehören zur Feld-Gestalt und sind auf dem Handy weg —
   dort ist für beides kein Platz, und die Tastenkombination gibt es
   ohnehin nicht. */
.nav-suche-text, .nav-suche-taste { display: none; }

@media (min-width: 960px) {
  .nav-suche {
    width: auto; min-width: 200px; height: 34px;
    justify-content: flex-start; gap: .5rem;
    margin: 0 0 0 auto; padding: 0 .55rem;
    border: 1px solid var(--border);
    background: var(--surface-2);
    /* Kein reiner Text-Knopf: Er soll wie ein Feld aussehen, in das man
       gleich tippt — das ist die ganze Botschaft. */
    border-radius: var(--radius-sm);
    font: inherit; font-size: .85rem;
  }
  .nav-suche:hover, .nav-suche:focus-visible {
    border-color: var(--border-strong); color: var(--text);
  }
  .nav-suche svg { width: 15px; height: 15px; }
  .nav-suche-text { display: inline; }
  .nav-suche-taste {
    display: inline-block; margin-left: auto;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: .72rem; line-height: 1.5;
    padding: 0 .3rem; border-radius: 4px;
    border: 1px solid var(--border-strong); background: var(--surface);
    color: var(--text-dim);
  }
}

/* Schliessknopf im Dialog. Auf dem Handy gibt es kein Escape und keinen
   Rand zum Danebentippen, der gross genug waere -- ohne ihn war der
   Dialog dort eine Sackgasse. */
.palette-schliessen {
  display: none;
  align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: 0 0 auto;
  border: none; background: transparent; color: var(--text-dim);
  border-radius: var(--radius-sm); cursor: pointer;
}
.palette-schliessen svg { width: 16px; height: 16px; }

@media (max-width: 640px) {
  /* Weiter oben und schmaler gepolstert: Auf dem Handy nimmt die
     Bildschirmtastatur die untere Haelfte. */
  .palette-backdrop { padding: 4vh .7rem .7rem; }
  .palette-hinweis { display: none; }
  /* Die Tastenhinweise im Fuss sind auf einem Telefon sinnlos -- uebrig
     bleibt der eine Satz, der etwas aussagt. */
  .palette-fuss kbd, .palette-fuss span:not(.palette-hinweis) { display: none; }
  .palette-fuss { justify-content: center; }
  .palette-fuss .palette-hinweis { display: inline; margin: 0; text-align: center; }
  .palette-schliessen { display: inline-flex; }
  .palette-eingabe kbd { display: none; }
  /* 16px, nicht kleiner: Darunter zoomt iOS beim Fokus in das Feld hinein
     und die Seite steht danach verschoben da. */
  .palette-eingabe input { font-size: 16px; }
  /* Fingerbreite Treffer statt Mausbreite. */
  .palette-treffer a { padding: .7rem .8rem; min-height: 44px; justify-content: center; }
  .palette-liste { max-height: 60vh; }
}

/* --- Mini-Verlauf auf den Karten der Uebersicht -------------------------
   Ein Feld je Tag, dreissig davon. Absichtlich winzig: Der Streifen soll
   die Karte nicht anfuehren, sondern beantworten, ob der Zustand oben
   neu ist. Wer Tageswerte braucht, findet sie auf der Detailseite.

   `flex` mit gleichen Anteilen statt fester Breite -- die Karte ist im
   Raster mal breiter, mal schmaler, und ein Streifen mit fester Breite
   liesse rechts eine Luecke oder liefe ueber. */
.mini-strip {
  display: flex; gap: 1px; height: 14px;
  margin: 0 0 .7rem;
}
.mini-day {
  flex: 1 1 0; min-width: 0;
  border-radius: 1px;
  background: var(--border);
}
.mini-strip .mini-day:first-child { border-radius: 2px 1px 1px 2px; }
.mini-strip .mini-day:last-child { border-radius: 1px 2px 2px 1px; }
/* Dieselben Zustandsnamen wie beim grossen Streifen (uptime_history.py:
   DayBucket.state), damit beide dieselbe Sprache sprechen. „leer" bleibt
   in der Randfarbe: kein Messwert ist kein Ergebnis. */
.mini-ok { background: color-mix(in srgb, var(--ok) 55%, transparent); }
.mini-warn { background: var(--warn); }
.mini-bad { background: var(--bad); }

/* --- Lagebericht + Zahlenband (Übersicht) --------------------------------
   Vorher standen hier sieben Kacheln, alle gleich gross und gleich laut.
   Die Frage, mit der man das Panel aufmacht, ist aber „muss ich etwas
   tun?" — und die beantwortet ein Satz, keine Kachelreihe. Die Zahlen
   sind nicht weg, sie sind vom Aufmacher zum Beleg geworden.

   Farbe traegt hier den Ton, nicht die Dekoration: „befund" warnfarben,
   „still" gedaempft, „gut" ohne jede Auszeichnung. Ein gruener Kasten um
   „Alles laeuft." waere eine Feier fuer den Normalfall. */
.lage { margin: 0 0 1.1rem; }
/* Die Obergrenze liegt bewusst UNTER der von h1/.page-title (2.2rem):
   Der Lagebericht ist der lauteste Satz der Seite, aber nicht ihr Titel.
   Vorher endete er bei 2.1rem und ueberholte damit ab knapp 1200px
   Fensterbreite die Ueberschrift „Sites" — die Seite schrie ihre
   Ausnahme lauter als ihren Namen. Das Gewicht (700 in der Textschrift
   gegen den Titel in Mono) und die Warnfarbe tragen die Dringlichkeit
   weiter; dafuer braucht es
   nicht auch noch die groesste Schrift der Seite. */
.lage-satz {
  margin: 0 0 .35rem;
  font-size: clamp(1.35rem, 1.05rem + 1.3vw, 1.6rem);
  line-height: 1.15; font-weight: 700; letter-spacing: -.02em;
}
.lage-erklaerung {
  margin: 0; max-width: 62ch;
  font-size: .95rem; line-height: 1.6; color: var(--text-dim);
}
.lage-befund .lage-satz { color: var(--warn-text); }
.lage-still .lage-satz { color: var(--text-dim); }

/* Ein Band, kein Raster: Die Zahlen gehoeren zusammengelesen, nicht
   einzeln betrachtet. Waagerecht scrollbar statt umbrechend — eine
   Kennzahl, die in der zweiten Reihe allein steht, sieht aus wie ein
   Fehler im Layout (genau das Problem der achten Kachel, #58). */
.zahlenband {
  display: flex; align-items: stretch; gap: 0;
  margin: 0 0 1.1rem; padding: .2rem 0;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  overflow-x: auto;
  /* Siehe „Waagerecht scrollende Leisten" weiter unten: die beiden
     deckenden Verlaeufe scrollen mit (`local`), die beiden Kanten stehen
     still (`scroll`) — die Kante erscheint damit von selbst nur dort, wo
     wirklich noch etwas liegt. */
  background:
    linear-gradient(to right, var(--surface), transparent) left center / 2rem 100% no-repeat local,
    linear-gradient(to left, var(--surface), transparent) right center / 2rem 100% no-repeat local,
    linear-gradient(to right, color-mix(in srgb, var(--border-strong) 80%, transparent), transparent) left center / 1rem 100% no-repeat scroll,
    linear-gradient(to left, color-mix(in srgb, var(--border-strong) 80%, transparent), transparent) right center / 1rem 100% no-repeat scroll,
    var(--surface);
}
.zahl {
  flex: 1 0 auto; min-width: 7.5rem;
  padding: .7rem 1.1rem;
  border-right: 1px solid var(--border);
}
.zahl:last-child { border-right: none; }
/* tabular-nums wie bei jedem anderen Geldbetrag dieser Datei
   (.k-tile-wert, .k-card-kosten-wert, .pagination-count): Ohne sie
   wechselt die Breite mit der Ziffer, und panel.js' Hochzaehlen laesst
   das Band bei jedem Schritt zappeln. */
.zahl-wert {
  display: block; font-size: 1.45rem; font-weight: 700;
  letter-spacing: -.02em; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.zahl-label {
  display: block; margin-top: .15rem;
  font-size: .76rem; color: var(--text-dim);
}
/* Eine optionale Unterzeile („nur Server", „ohne Kundenanteil"). Bewusst
   ohne eigene Hoehenwirkung — sonst macht ein laengerer Untertitel eine
   Kachel hoeher als ihre Nachbarn, und das Band saehe aus, als bedeute
   die Hoehe etwas. */
.zahl-sub { display: block; margin-top: .15rem; font-size: .73rem; color: var(--text-dim); }
/* „Gehoert uns selbst": dieselbe Aussage wie das gestrichelte
   „intern"-Abzeichen, nur in Kachelform. Eine gestrichelte Kante statt
   einer Farbe — ein roter oder gruener Ton waere eine Wertung, die
   niemand gemeint hat (siehe .k-card-eigen). */
.zahl-eigen { border-left: 1px dashed var(--border-strong); }
.zahl-eigen .zahl-wert { color: var(--text-dim); }
.zahl-befund .zahl-wert { color: var(--warn-text); }
.zahl-still .zahl-wert { color: var(--text-dim); }

@media (max-width: 640px) {
  /* Auf dem Handy schmaler, damit mehr als zwei Zahlen gleichzeitig
     sichtbar sind — sonst scrollt man das Band durch, statt es zu lesen. */
  .zahl { min-width: 6.2rem; padding: .6rem .8rem; }
  .zahl-wert { font-size: 1.25rem; }
}

.overview-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem;
  margin-bottom: 1.8rem; padding: .6rem;
  border-radius: var(--radius);
  /* Deckend wie jede Karte (siehe „Karten: deckend, mit Haarlinie"). */
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
  box-shadow: var(--shadow-sm);
}
.overview-tile {
  border-radius: var(--radius-sm);
  padding: 1rem 1.1rem;
  /* A cell within .overview-grid, not its own surface — background only,
     no shadow of its own. */
  background: color-mix(in srgb, var(--surface-2) 55%, transparent);
  display: flex; flex-direction: column; gap: .7rem;
  position: relative; overflow: hidden;
  /* Cursor-tracked tilt (panel.js's initTiltCards) — --rx/--ry/--mx/--my
     default to a flat, centered "no tilt" state so this is a no-op until
     JS actually starts writing to them. */
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform var(--panel-motion-duration-fast) ease, background-color var(--panel-motion-duration-fast) ease;
  /* Staggered rise-in on page load, one tick per --i (set inline per tile
     in dashboard.html) — see .site-card below for the shared keyframe. */
  animation: rise-in .5s cubic-bezier(.16, 1, .3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 70ms);
}
/* Der Lichtfleck, der dem Zeiger folgte (der Glanz des Glas-Materials),
   ist mit dem Glas gegangen; der Hover ist die leichte Fläche, sonst nichts. */
.overview-tile:hover { background: color-mix(in srgb, var(--surface-2) 80%, transparent); }
.overview-tile-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
/* Matches style_guide's real Stat.css.ts label (these tiles are this
   app's own Stat — label/value/hint) exactly: font-size.xs, weight
   *medium*, letter-spacing 0.02em — not the bold-uppercase treatment
   this had before, which doesn't exist anywhere in the actual component,
   just an assumption an earlier pass made. */
.overview-tile-label { font-size: .76rem; font-weight: 500; color: var(--text-dim); letter-spacing: .02em; }
.overview-tile-icon {
  width: 30px; height: 30px; border-radius: 9px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--text);
}
.overview-tile-icon svg { width: 16px; height: 16px; }
/* Fluid, same reasoning as .page-title above — the low end matches the
   old fixed 1.9rem exactly, only the upper end grows on a wider canvas. */
/* tabular-nums — Stat.css.ts's own value style always sets this: without
   it, panel.js's count-up animation and the 15s status poll both cause a
   visible, jittery reflow every time a digit width changes mid-count. */
.overview-tile-value { font-size: clamp(1.9rem, 1.7rem + 0.6vw, 2.4rem); font-weight: 800; letter-spacing: -.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.overview-tile-value small { font-size: 1rem; font-weight: 700; color: var(--text-dim); margin-left: .2rem; }
/* „Nicht gewusst" darf nie aussehen wie „nichts da": Eine harte 0 ueber
   einer roten Fehlermeldung behauptet eine Messung, die es nicht gab. */
.overview-tile-value.is-unbekannt { color: var(--text-dim); font-weight: 600; }
/* „· 1 gestoppt" in der Kachel „Sites gesamt" — leiser als die „/ 4"-Angabe
   der Nachbarkachel, weil es eine Nebenauskunft ist und keine Bezugsgröße.
   `nowrap`, damit die Zahl und ihre Erläuterung nicht auf zwei Zeilen
   auseinanderfallen: .overview-tile-value gibt line-height 1 vor, was für
   eine Ziffer richtig und für eine umgebrochene Wortzeile unbrauchbar ist.
   Bewusst inline und nicht als eigene Zeile — eine zusätzliche Zeile in
   einer Kachel macht die ganze Rasterreihe höher, also auch die sechs
   Kacheln daneben, die nichts damit zu tun haben. */
.overview-tile-note { font-size: .78rem; font-weight: 600; color: var(--text-dim); margin-left: .3rem; white-space: nowrap; }
/* fill.tertiary, not fill.secondary (--surface-2) — ProgressBar.css.ts's
   own track color is one step subtler than this app's default input/hover
   fill, so the empty track reads as quieter than the surrounding chrome. */
.overview-tile-bar { height: 4px; border-radius: 999px; background: var(--surface-solid); overflow: hidden; }
.overview-tile-bar span {
  display: block; height: 100%; border-radius: inherit;
  /* panel.js's initBarFills() zeroes this out on load, then restores the
     server-rendered target width one frame later so it grows in instead
     of just appearing at full length. */
  transition: width 1s cubic-bezier(.16, 1, .3, 1);
}
.overview-tile.accent-blue .overview-tile-icon, .overview-tile.accent-blue .overview-tile-bar span { background: var(--accent); color: var(--accent-ink); }
.overview-tile.accent-blue .overview-tile-icon { color: var(--accent); background: var(--accent-glow); }
.overview-tile.accent-green .overview-tile-bar span { background: var(--ok); }
.overview-tile.accent-green .overview-tile-icon { color: var(--ok-text); background: var(--ok-glow); }
.overview-tile.accent-amber .overview-tile-bar span { background: var(--warn); }
.overview-tile.accent-amber .overview-tile-icon { color: var(--warn-text); background: var(--warn-glow); }
/* "violet" name kept from before this file's palette pass (see header
   comment) so dashboard.html/hetzner.html's existing class names still
   apply — the hue itself is now the accent-2 teal, not violet. */
.overview-tile.accent-violet .overview-tile-bar span { background: var(--accent-2); }
.overview-tile.accent-violet .overview-tile-icon { color: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 18%, transparent); }

/* --- Site card grid ------------------------------------------------ */

/* auto-fill, not auto-fit — deliberately: with a handful of sites (the
   normal case for this panel, not hundreds), auto-fit would collapse the
   unused tracks on a wide screen and stretch the few real cards to fill
   the whole row edge to edge, each one mostly empty space around a few
   lines of text. auto-fill keeps every card at a sane, readable width
   and lets a wide screen show *more* cards per row instead of *wider*
   ones — any width the grid can't fill becomes quiet trailing
   whitespace on the right, not a stretched card. */
/* min(300px, 100%) statt einer nackten 300px-Untergrenze: Bei einem
   Viewport, der nach den Seitenrändern schmaler als 300px bleibt (kleine
   Handys ab 320px Gerätebreite), wäre die Spur breiter als ihr Container
   — die Karte ragt hinaus und die ganze Seite scrollt waagerecht. Mit
   min() darf die Spur in genau diesem Fall mitschrumpfen; überall sonst
   verhält sie sich unverändert. */
.site-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 1.25rem; margin-bottom: 1.8rem;
  /* start statt des Grid-Defaults stretch: Sonst zieht die höchste Karte
     einer Reihe alle anderen auf ihre Höhe — und seit die Karten ein
     aufklappbares „Mehr" haben, heißt das, dass das Aufklappen EINER
     Karte klaffende Lücken in ihre Nachbarn reißt. Eine Karte soll so
     hoch sein wie ihr eigener Inhalt, sonst wird das Aufklappen zu einer
     Aktion, die die halbe Seite umbaut. */
  align-items: start;
}
.site-card {
  border-radius: var(--radius);
  padding: 1.4rem 1.5rem; display: flex; flex-direction: column; gap: .85rem;
  box-shadow: var(--shadow-sm);
  position: relative;
  /* Cursor tilt (panel.js's initTiltCards), same --rx/--ry contract as
     .overview-tile above; :hover below adds the lift on top of whatever
     tilt is currently live instead of fighting it on the same property. */
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform var(--panel-motion-duration-fast) ease, box-shadow var(--panel-motion-duration-fast) ease;
  animation: rise-in .5s cubic-bezier(.16, 1, .3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 70ms);
}
/* box-shadow: var(--shadow) on hover — shadow-2 to shadow-3, not a
   border-color change (there's no resting border to darken any more) —
   is Card.css.ts's real `interactive` variant hover state verbatim
   (`&:hover: { boxShadow: shadow[3] }`); the translateY lift on top is
   this app's own restrained addition, same spirit, not in conflict with it. */
.site-card:hover {
  box-shadow: var(--shadow);
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(-3px);
}
/* Oben Monogramm | Name | Status, darunter Slug und Zugehörigkeit über
   die volle Breite neben dem Monogramm. Grid statt Flex mit
   space-between: Die Namensspalte darf schrumpfen (minmax(0, 1fr)),
   Monogramm und Statuspille nie — und der Kundenname teilt sich seine
   Zeile nicht mehr mit der Pille, er hat die ganze Kartenbreite. */
.site-card-head {
  --avatar: 2.6rem;
  position: relative;
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "avatar titel" "avatar meta";
  align-items: start; gap: .3rem .85rem;
}
.site-card-head > .site-avatar { grid-area: avatar; }
.site-card-head > .site-card-ident { grid-area: titel; align-self: center; }
.site-card-head > .site-card-meta { grid-area: meta; }
/* Der Status als Punkt an der unteren rechten Ecke des Monogramms (siehe
   _site_statuspunkt.html) — die Pille oben rechts kostete dem Namen eine
   ganze Spalte. Das Wort bleibt im Markup für Screenreader und steht im
   Tooltip; der Ring in Kartenfarbe löst den Punkt vom Monogramm. */
.site-card-head > .status-pill {
  position: absolute; left: calc(var(--avatar) - .6rem); top: calc(var(--avatar) - .6rem);
  padding: 0; gap: 0; background: none; box-shadow: none; cursor: help;
}
.site-card-head > .status-pill .status-text {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.site-card-head > .status-pill .status-dot {
  width: 12px; height: 12px;
  outline: 2.5px solid var(--surface-solid);
}
/* Die Huelle, die aus dem Kartentitel eine echte Ueberschrift macht, ohne
   dass sich optisch etwas aendert: Die Ueberschriftengliederung der
   Uebersicht sprang vorher von „h1 Sites" direkt zu „h2 Weitere Projekte
   auf dem Server" — der gesamte Hauptinhalt tauchte in der
   Ueberschriftenliste eines Screenreaders gar nicht auf, und es gab keinen
   Weg, von Karte zu Karte zu springen. Groesse und Gewicht erbt sie,
   damit .site-domain darin exakt so aussieht wie vorher. */
.site-card-title { margin: 0; font-size: inherit; font-weight: inherit; line-height: 1.25; }
/* overflow-wrap statt word-break: break-word — Letzteres fügt zwischen
   JEDEM Buchstaben eine Umbruchstelle ein, und der Browser füllt die Zeile
   dann bis zum Rand („hochzeitsplaner.paralympu / s.de"). So bricht er an
   den <wbr> nach den Punkten (Filter umbruch_nach_punkt) und nur im
   Notfall mitten im Wort. */
.site-domain { font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em; text-decoration: none; color: var(--text); overflow-wrap: break-word; }
.site-domain:hover { color: var(--accent); }
.site-slug { display: block; font-family: var(--font-mono); font-size: .78rem; color: var(--text-dim); margin-top: .15rem; }

/* --- Site-Karte: Zustand oben, Kontext unten ---------------------------
   Ersetzt die frühere `.site-meta` — eine einzige Reihe aus sechs
   gleichwertigen „Label: Wert“-Paaren in Monospace. Alles darin hatte
   dieselbe Größe, dieselbe Farbe und dasselbe Gewicht, also musste man
   jeden Wert einzeln lesen, um festzustellen, dass keiner ein Problem
   ist. Genau das soll eine Übersichtskarte abnehmen.

   Jetzt tragen nur die drei überwachbaren Werte das visuelle Gewicht
   (Beschriftung klein darüber, Wert darunter), und Farbe erscheint
   ausschließlich im Problemfall. Eine gesunde Karte ist damit ruhig —
   und eine kranke fällt auf, ohne dass man sie liest. */
.site-stats {
  /* Flex mit Umbruch, nicht Grid mit gleich breiten Spalten.
     Drei gleiche Spuren klingen ordentlich, teilen den Platz aber nach
     Zahl statt nach Bedarf: „210 ms“ braucht halb so viel wie „vor 3
     Tagen“, bekam aber genauso viel — und der längste Wert wurde
     abgeschnitten, während daneben Platz frei blieb. So nimmt sich jeder
     Wert, was er braucht, und der letzte rutscht auf einem schmalen
     Gerät lieber eine Zeile tiefer, als unleserlich zu werden. */
  display: flex; flex-wrap: wrap;
  /* 1.15rem, nicht 1.5rem: Die Karten sind in der Spaltenansicht rund
     360px breit — bei 1.5rem passten die drei Werte um wenige Pixel NICHT
     mehr nebeneinander, und „Backup“ rutschte allein in eine zweite
     Zeile. Der Abstand ist hier also kein Geschmack, sondern das, was
     über ein- oder zweizeilig entscheidet. */
  gap: .5rem 1.15rem;
  padding: .7rem .85rem;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border) inset;
}
.site-stat { display: flex; flex-direction: column; gap: .12rem; min-width: 0; flex: 0 1 auto; }
/* Satzkasten und eine Stufe groesser: .66rem VERSALIEN sind 10,5px und
   damit das am schwersten lesbare Textstueck der Karte — ausgerechnet die
   Beschriftung, ohne die der Wert daneben nichts bedeutet. Gedaempft bleibt
   sie, klein auch; nur schreien tut sie nicht mehr. */
.site-stat-label {
  font-size: .72rem; font-weight: 600; letter-spacing: .015em;
  color: var(--text-dim);
}
.site-stat-value {
  font-size: .92rem; font-weight: 650; color: var(--text);
  /* Nicht Monospace: Diese Zahlen werden gelesen, nicht in einer Spalte
     verglichen — und proportional bleibt bei den langen deutschen
     Wörtern deutlich mehr Platz. */
  display: flex; align-items: center; gap: .3rem; min-width: 0;
}
/* Der Text kürzt sich, das ⓘ nicht. Ohne diese Trennung schob ein zu
   langer Wert entweder das Symbol aus der Spalte oder legte sich mitsamt
   Symbol über die Nachbarspalte — beides war im ersten Entwurf zu sehen,
   weil `white-space: nowrap` auf dem ganzen Wert saß und ein Grid-Track
   seinen Inhalt nicht von sich aus beschneidet. */
.site-stat-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.site-stat-value .info-tip { flex: 0 0 auto; }
/* Farbe sitzt am Container, nicht am Wert: So färbt sich die Beschriftung
   mit, und ein Problem ist auch dann als solches erkennbar, wenn der Wert
   selbst neutral klingt („keins“, „Fehler“). */
.site-stat.is-warn .site-stat-value, .site-stat.is-warn .site-stat-label { color: var(--warn-text); }
.site-stat.is-bad .site-stat-value, .site-stat.is-bad .site-stat-label { color: var(--bad-text); }

/* Die Kontextzeile: nachschlagen, nicht überwachen. Deshalb klein,
   gedämpft und mit Trennpunkten statt eigener Zeilen je Wert. */
.site-facts {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .2rem .55rem;
  margin: 0; font-size: .78rem; color: var(--text-dim);
}
.site-facts span { display: inline-flex; align-items: center; gap: .25rem; }
/* Trennpunkt als ::after INNERHALB des vorangehenden Elements, nicht als
   ::before des folgenden: Beim Umbruch bliebe ein ::before sonst allein am
   Anfang der neuen Zeile stehen („· mit Datenbank“) — als Aufzählungszeichen
   lesbar, das keines ist. So hängt er am Wort davor und wandert nie an den
   Zeilenanfang. Über :not(:last-child) statt eines Zeichens im Markup, damit
   ein wegfallender Wert (keine Datenbank, noch nie ausgerollt) seinen
   Trenner mitnimmt. */
.site-facts span:not(:last-child)::after {
  content: "·"; color: var(--border-strong); margin-left: .55rem;
}
.site-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: auto; padding-top: .5rem; }

/* --- Site-Karte: Statuskante, Monogramm, Kennzahlen, Aktionsleiste -------
   Die Karte der Übersicht (_site_card.html) mit ihrem Befund
   (_site_befund.html) — jede Site gleich groß, auch die eigene
   Vorzeige-Site (PANEL_FLAGSHIP_SITE), die nur ihr Platz ganz vorn unter
   „Intern" heraushebt.

   1. Der Zustand steht im Punkt am Monogramm, und nur dort. Hier stand
      einmal eine farbige Oberkante mit einem Schein derselben Farbe, dazu
      bei einem Fehler ein roter Rahmen um die ganze Karte — dreimal
      dieselbe Auskunft. Auf einer Seite mit mehreren gestörten Sites
      färbte das das halbe Raster rot, und die Karten sahen lauter aus als
      das, was auf ihnen steht. Seit die Marke schwarz-weiß ist, trägt
      Farbe hier nur noch einzelne Zeichen; der Punkt reicht. Das Wort
      dazu steht im Tooltip und für Screenreader im Markup (siehe den
      Statuspunkt oben), Gelb meldet zusätzlich der Befund auf der Karte.
      Die Klassen .is-error/.is-auffaellig bleiben am Markup —
      Kartenfilter und Tests lesen sie. */
/* Ohne diese Zeile hätte `hidden` (Kartenfilter, panel.js) gegen das
   display: flex der Karte keine Chance. */
.site-card[hidden] { display: none; }
/* Eine gestoppte Site tritt zurück: Ihre Kennzahlen beschreiben einen
   Zustand, der gerade nicht gilt. Der Name bleibt voll lesbar. */
.site-card[data-lifecycle="stopped"] :is(.site-avatar, .site-verlauf, .site-stats, .site-facts) { opacity: .55; }

/* 2. Kopf: Monogramm, Name, Zugehörigkeit. */
.site-avatar {
  width: var(--avatar, 2.6rem); height: var(--avatar, 2.6rem); border-radius: var(--radius-md);
  display: grid; place-items: center;
  font-weight: 800; font-size: 1.1rem; line-height: 1; color: var(--text);
  background: linear-gradient(145deg, var(--surface-2), transparent);
  box-shadow: 0 0 0 1px var(--border) inset;
  transition: opacity var(--panel-motion-duration-base) ease;
}
.site-card-intern .site-avatar {
  color: var(--accent);
  background: linear-gradient(145deg, color-mix(in srgb, var(--accent) 22%, transparent), color-mix(in srgb, var(--accent) 6%, transparent));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent) inset;
}
.site-card-ident { min-width: 0; }
.site-card-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .5rem;
  min-width: 0;
}
.site-card-meta .site-slug { display: inline; margin: 0; }

/* 3. Der 30-Tage-Streifen mit seiner Zahl daneben. */
.site-verlauf { display: flex; align-items: center; gap: .75rem; }
.site-verlauf .mini-strip { flex: 1 1 auto; margin: 0; height: 16px; }
.site-verlauf .mini-day { border-radius: 2px; }
.site-verlauf-wert {
  display: inline-flex; align-items: baseline; gap: .3rem;
  font-size: .82rem; font-weight: 750; font-variant-numeric: tabular-nums;
  color: var(--text); white-space: nowrap;
}
.site-verlauf-wert.is-warn { color: var(--warn-text); }
.site-verlauf-wert.is-bad { color: var(--bad-text); }
.site-verlauf-einheit { font-size: .68rem; font-weight: 600; color: var(--text-dim); }

/* 4. Kennzahlen in drei gleich breiten Spalten mit Trennlinie. Eine
      Spalte je Kind (grid-auto-flow: column), damit auch zwei Werte (keine
      Domain, also keine Erreichbarkeit) die Breite füllen. Der
      Flex-Umbruch von früher teilte den Platz nach Wertlänge — jede Karte
      hatte ihre Spalten woanders, und über das Raster hinweg stand nichts
      untereinander. */
.site-card .site-stats {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: 0; padding: 0;
}
.site-card .site-stat { padding: .65rem .7rem .7rem; gap: .18rem; border-radius: var(--radius-md); }
.site-card .site-stat + .site-stat { box-shadow: inset 1px 0 0 var(--border); border-top-left-radius: 0; border-bottom-left-radius: 0; }
.site-card .site-stat:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.site-card .site-stat.is-warn { background: color-mix(in srgb, var(--warn) 9%, transparent); }
.site-card .site-stat.is-bad { background: color-mix(in srgb, var(--bad) 9%, transparent); }
.site-card .site-stat-value { font-size: .98rem; font-weight: 700; font-variant-numeric: tabular-nums; gap: .35rem; line-height: 1.25; }
/* Umbrechen statt kürzen: „vor 14 …" verschwieg genau den Teil, auf den es
   ankommt. Zwei Zeilen „vor 12 / Tagen" sind lesbar, eine abgeschnittene
   nicht. */
.site-card .site-stat-text { white-space: normal; overflow: visible; }
.site-card .site-stat-value .info-tip { margin-left: 0; }
.site-stat-sub {
  font-size: .7rem; color: var(--text-dim); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* 5. Kontext als Chips. Jeder Wert ist ein eigenes Kästchen und bricht als
      Ganzes um — kein Trennpunkt, der allein am Zeilenende stehen bleibt. */
ul.site-facts { list-style: none; padding: 0; margin: 0; gap: .4rem; }
ul.site-facts li {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .24rem .6rem .24rem .5rem; border-radius: 999px;
  background: var(--surface-2); color: var(--text-dim);
  font-size: .74rem; font-weight: 550; line-height: 1.3;
}
ul.site-facts li svg { width: 13px; height: 13px; flex: none; }

/* 6. Die letzte Aktion — eine Zeile, die in den Verlauf dieser Site führt. */
.site-letzte {
  display: flex; align-items: center; gap: .55rem; min-width: 0;
  font-size: .76rem; color: var(--text-dim); text-decoration: none;
}
.site-letzte-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.site-letzte strong { color: var(--text); font-weight: 650; }
.site-letzte:hover .site-letzte-text { color: var(--text); }
.site-letzte:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.site-letzte-punkt { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--warn); box-shadow: 0 0 0 3px var(--warn-glow); }
.site-letzte.is-success .site-letzte-punkt { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-glow); }
.site-letzte.is-failure .site-letzte-punkt { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-glow); }
.site-letzte.is-failure .site-letzte-text { color: var(--bad-text); }

/* 7. Aktionsleiste: EINE Reihe. Logs, Redeploy (und Start, wenn die Site
      steht) teilen sich die Breite gleichmäßig; Restart und Stop sind
      quadratische Symbolknöpfe am Ende. Vorher brachen vier beschriftete
      Knöpfe unterschiedlich breit in zwei ausgefranste Reihen um. */
.site-card .site-actions {
  flex-wrap: wrap; gap: .5rem;
  margin-top: auto; padding-top: .95rem;
  border-top: 1px solid var(--border);
}
.site-card .site-actions > .btn,
.site-card .site-actions > form:not([data-lifecycle-only="running"]) { flex: 1 1 5.5rem; min-width: 0; }
.site-card .site-actions > form > .btn,
.site-card .site-actions > .btn { width: 100%; justify-content: center; }
.site-card .site-actions > form[data-lifecycle-only="running"] { flex: none; width: auto; }
.site-card .btn-lifecycle { padding: .55rem; min-width: 2.5rem; height: 100%; justify-content: center; }
.site-card .btn-lifecycle .btn-text {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.site-card .btn-lifecycle svg { width: 17px; height: 17px; }

/* --- Kartengruppen: Kunden, Intern, ohne Zuordnung (_site_cards.html) -----
   Eine Überschrift je Gruppe beantwortet „wem gehört das?" für alle
   Karten darunter auf einmal. Die interne Gruppe steht in einer eigenen,
   leicht akzentgetönten Fläche — abgehoben von den Kundenprojekten, ohne
   einen zweiten Farbton einzuführen. */
.karten-gruppe { margin-bottom: 2rem; }
.karten-gruppe .site-grid { margin-bottom: 0; }
.karten-gruppe-kopf {
  display: flex; flex-wrap: wrap; align-items: center; gap: .45rem .65rem;
  margin: 0 0 1rem; padding-bottom: .7rem;
  border-bottom: 1px solid var(--border);
}
.karten-gruppe-icon {
  width: 1.9rem; height: 1.9rem; border-radius: var(--radius-sm);
  display: grid; place-items: center; flex: none;
  background: var(--surface-2); color: var(--text-dim);
}
.karten-gruppe-icon svg { width: 16px; height: 16px; }
.karten-gruppe-titel { margin: 0; font-size: 1.05rem; font-weight: 750; letter-spacing: -.01em; }
.karten-gruppe-zahl {
  font-size: .74rem; font-weight: 700; font-variant-numeric: tabular-nums;
  padding: .08rem .5rem; border-radius: 999px;
  background: var(--surface-2); color: var(--text-dim);
}
.karten-gruppe-befund {
  font-size: .74rem; font-weight: 700;
  padding: .08rem .55rem; border-radius: 999px;
  background: var(--warn-glow); color: var(--warn-text);
}
.karten-gruppe-hinweis { margin-left: auto; font-size: .78rem; color: var(--text-dim); }
.karten-gruppe-link { color: var(--accent); text-decoration: none; font-weight: 600; }
.karten-gruppe-link:hover { text-decoration: underline; text-underline-offset: 2px; }
.karten-gruppe-intern {
  padding: 1.1rem 1.15rem 1.25rem;
  border-radius: calc(var(--radius) + 4px);
  background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 8%, transparent), transparent 60%);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent) inset;
}
.karten-gruppe-intern .karten-gruppe-kopf { border-bottom-color: color-mix(in srgb, var(--accent) 20%, var(--border)); }
.karten-gruppe-intern .karten-gruppe-icon { color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent); }
@media (max-width: 640px) {
  .karten-gruppe-hinweis { margin-left: 0; flex-basis: 100%; }
  .karten-gruppe-intern { padding: .9rem .75rem 1rem; margin-inline: -.25rem; }
}

/* --- Kartenfilter über dem Raster (dashboard.html, panel.js) ------------- */
.karten-filter { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem .8rem; margin: 0 0 1.4rem; }
.karten-filter[hidden] { display: none; }
.karten-suche { flex: 1 1 16rem; max-width: 28rem; margin: 0; }
.karten-suche input.tool-search-input { padding-right: 2.6rem; }
.karten-suche:has(input:not(:placeholder-shown)) .tool-search-kbd { display: none; }
@media (hover: none) { .karten-suche .tool-search-kbd { display: none; } }
.karten-filter-chips {
  display: inline-flex; flex-wrap: wrap; gap: .25rem;
  padding: .25rem; border-radius: 999px;
  background: var(--surface-2);
}
.filter-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  font: inherit; font-size: .8rem; font-weight: 650;
  padding: .4rem .85rem; border: none; border-radius: 999px;
  background: transparent; color: var(--text-dim); cursor: pointer;
  transition: color var(--panel-motion-duration-fast) ease, background-color var(--panel-motion-duration-fast) ease, opacity var(--panel-motion-duration-fast) ease;
}
.filter-chip:hover { color: var(--text); }
.filter-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.filter-chip[aria-pressed="true"] { background: var(--bg); color: var(--text); box-shadow: var(--shadow-sm); }
.filter-chip.is-leer:not([aria-pressed="true"]) { opacity: .45; }
.filter-chip-zahl {
  min-width: 1.35rem; padding: 0 .38rem; border-radius: 999px; text-align: center;
  font-size: .7rem; font-variant-numeric: tabular-nums;
  background: var(--border); color: inherit;
}
.filter-chip-zahl:empty { display: none; }
.filter-chip-befund:not(.is-leer) .filter-chip-zahl { background: var(--warn-glow); color: var(--warn-text); }
.karten-filter-leer { margin: 0 0 1.6rem; }
@media (max-width: 640px) {
  .karten-suche { max-width: none; flex-basis: 100%; }
  .karten-filter-chips { width: 100%; justify-content: space-between; border-radius: var(--radius-md); }
  .filter-chip { flex: 1 1 auto; justify-content: center; padding-inline: .5rem; min-height: 40px; }
}

/* --- Aktionsleiste, Sprungleiste, Abschnitte (Detailseite) ---------------
   Die Detailseite hatte vierzehn gleich aussehende Karten in einer Spalte
   und "Aktionen" als Nummer sieben. Drei Dinge raeumen das auf, und alle
   drei sind Anordnung, nicht Dekoration. */

/* 1. Die Aktionsleiste. Keine Karte: Sie traegt keinen eigenen Inhalt,
      sondern gehoert zum Kopf darueber — deshalb nur eine Trennlinie nach
      unten statt Rahmen und Flaeche. Ein Kasten mehr an dieser Stelle
      haette genau das erzeugt, was hier weg soll. */
.site-actionbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  margin: 0 0 1.4rem; padding: 0 0 1.2rem;
  border-bottom: 1px solid var(--border);
}
/* Innerhalb der Leiste faellt der Kartenabstand weg, den .site-actions
   fuer seinen alten Platz am Kartenfuss mitbringt. */
.site-actionbar .site-actions { margin: 0; padding: 0; }
.site-actionbar .note { flex-basis: 100%; margin: 0; }

/* 2. Die Sprungleiste. Klebt oben, damit sie auch nach dem Scrollen noch
      da ist — sonst muesste man zum Springen erst hochscrollen, also
      genau das tun, was sie ersparen soll. Der Hintergrund ist deckend,
      nicht durchscheinend: Sie liegt beim Scrollen ueber Kartenraendern,
      und Glas darueber macht daraus Matsch (docs/muster-und-antimuster.md
      #7 — Glas traegt die Navigation, nicht Listeninhalt). */
.site-jump {
  display: flex; gap: .4rem; overflow-x: auto;
  margin: 0 0 1.4rem; padding: .6rem 0;
  border-bottom: 1px solid var(--border);
  /* Dieselben selbstausblendenden Kanten wie am .zahlenband — siehe
     „Waagerecht scrollende Leisten" weiter unten. */
  background:
    linear-gradient(to right, var(--bg), transparent) left center / 2rem 100% no-repeat local,
    linear-gradient(to left, var(--bg), transparent) right center / 2rem 100% no-repeat local,
    linear-gradient(to right, color-mix(in srgb, var(--border-strong) 80%, transparent), transparent) left center / 1rem 100% no-repeat scroll,
    linear-gradient(to left, color-mix(in srgb, var(--border-strong) 80%, transparent), transparent) right center / 1rem 100% no-repeat scroll,
    var(--bg);
}
/* Kleben erst ab dem Punkt, an dem .app-nav zur Seitenleiste wird
   (min-width: 960px, siehe dort). Darunter ist .app-nav selbst eine
   klebende Leiste am oberen Rand mit hoeherem z-index — eine zweite
   klebende Leiste mit `top: 0` verschwaende sich darunter, statt unter
   ihr zu stehen. Das mit einem gemessenen Pixelwert auszugleichen hiesse,
   die Hoehe einer fremden Leiste hier ein zweites Mal zu behaupten; sie
   aendert sich mit Schriftgroesse und Safe-Area. Auf dem Handy scrollt
   die Sprungleiste deshalb einfach mit weg. */
@media (min-width: 960px) {
  .site-jump { position: sticky; top: 0; z-index: 5; }
}
/* --- Waagerecht scrollende Leisten: sagen, dass rechts noch etwas steht --
   .zahlenband und .site-jump scrollen beide seitwaerts und blendeten dabei
   ihren Scrollbalken vollstaendig aus. Auf einem 375px-Telefon sind sieben
   Kacheln a 6.2rem rund 700px breit — es waren also immer ein bis zwei
   Kennzahlen unsichtbar, und nichts am Rand deutete darauf hin.

   Drei Dinge zusammen loesen das, keines davon eine neue Farbe:
   1. Zwei Kanten als Hintergrund-Verlaeufe (siehe die Regeln oben). Der
      Trick ist `background-attachment`: die deckenden Verlaeufe scrollen mit
      dem Inhalt (`local`) und schieben sich am jeweiligen Ende ueber die
      feststehende Kante (`scroll`). Die Kante erscheint damit nur, wenn
      dort wirklich noch etwas liegt, und bleibt in einer Leiste, die gar
      nicht scrollt, unsichtbar — anders als eine feste Maske, die auch
      den ersten Wert anschneidet, wenn es nichts zu verbergen gibt.
   2. Ein Fokusring nach INNEN, sobald die Leiste tabindex="0" traegt: ohne
      Fokus kommt man mit der Tastatur an die verdeckten Zahlen gar nicht
      heran (WCAG 2.1.1), und ein Ring nach aussen raegte aus der Karte.
   3. Ein duenner, sichtbarer Balken auf Zeigergeraeten. Auf dem Telefon
      erscheint er ohnehin nur beim Wischen und kostet Hoehe; am Rechner
      ist er der einzige Dauerhinweis. */
.zahlenband, .site-jump { scrollbar-width: none; overscroll-behavior-x: contain; }
.zahlenband::-webkit-scrollbar, .site-jump::-webkit-scrollbar { display: none; }
.zahlenband:focus-visible, .site-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
@media (pointer: fine) {
  .zahlenband, .site-jump { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
  .zahlenband::-webkit-scrollbar, .site-jump::-webkit-scrollbar { display: block; height: 8px; }
  .zahlenband::-webkit-scrollbar-thumb, .site-jump::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
  .zahlenband::-webkit-scrollbar-track, .site-jump::-webkit-scrollbar-track { background: transparent; }
}

.site-jump a {
  flex: 0 0 auto;
  padding: .4rem .85rem; border-radius: 999px;
  font-size: .86rem; font-weight: 600; text-decoration: none;
  color: var(--text-dim); background: var(--surface-2);
  transition: color var(--panel-motion-duration-fast), background var(--panel-motion-duration-fast);
}
.site-jump a:hover, .site-jump a:focus-visible { color: var(--text); box-shadow: 0 0 0 1px var(--border-strong) inset; }

/* 3. Die Abschnitte. `scroll-margin-top` ist hier keine Feinheit, sondern
      der Unterschied zwischen funktioniert und funktioniert nicht: Ohne
      ihn springt der Anker unter die klebende Leiste, und die erste
      Ueberschrift des Abschnitts ist verdeckt. */
.site-section { scroll-margin-top: 1rem; }
@media (min-width: 960px) {
  /* Ohne das springt der Anker unter die klebende Leiste und die erste
     Ueberschrift des Abschnitts ist verdeckt — der Sprung sieht dann aus,
     als haette er das falsche Ziel getroffen. */
  .site-section { scroll-margin-top: 3.6rem; }
}
.site-section + .site-section { margin-top: var(--space-8); }
/* Eine eigene Stufe zwischen Seitentitel und Kartenetikett. „Betrieb" —
   eine Klammer um vier Karten — sah vorher genau aus wie „Cronjobs", eine
   einzelne Karte darin: beide .78rem, VERSALIEN, gesperrt, gedaempft. Die
   Vierteilung, die die Sprungleiste verspricht, war beim Scrollen nicht zu
   sehen; die Seite las sich weiter als Halde gleich gewichteter Karten.
   Die Haarlinie neben dem Wort laeuft ueber die Restbreite und macht den
   Beginn eines Abschnitts sichtbar, ohne eine zweite Flaeche zu bauen. */
.site-section-title {
  display: flex; align-items: center; gap: .8rem;
  margin: 0 0 .8rem; font-size: 1rem; font-weight: 700;
  letter-spacing: -.01em; color: var(--text);
}
.site-section-title::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--border); }

@media (max-width: 640px) {
  /* Auf dem Handy ist die Leiste das, was man anfassen will — die Knoepfe
     bekommen dieselbe Mindesthoehe wie ueberall sonst (siehe .wartung-an). */
  .site-actionbar .btn { min-height: 44px; }
  .site-jump { margin-bottom: 1.1rem; }
}
.site-error-msg { color: var(--bad-text); font-size: .85rem; }
/* „In neuem Tab öffnen“ neben dem Titel — der Titel selbst führt auf die
   Detailseite im Panel, auf die echte Seite kam man von hier bisher gar
   nicht. Klein und zurückhaltend, bis man hinfährt: ein Sprung nach
   außen ist ein Nebenweg, keine Panel-Aktion. */
.site-open {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: var(--radius-sm);
  color: var(--text-dim); text-decoration: none; vertical-align: middle;
  transition: color var(--panel-motion-duration-fast) ease, background-color var(--panel-motion-duration-fast) ease;
}
.site-open svg { width: 14px; height: 14px; }
.site-open:hover, .site-open:focus-visible { color: var(--accent); background: var(--surface-2); }

/* --- Zustandsabhängige Lifecycle-Knöpfe ------------------------------
   Start ↔ Stop/Restart. Welche davon sichtbar sind, entscheidet
   `data-lifecycle` am umgebenden Element (Site-Karte bzw. Aktionen-Karte,
   Wert aus deploy_client.SiteStatus.lifecycle) — serverseitig gerendert,
   von panel.js beim Status-Poll aktualisiert. Bewusst rein über CSS:
   Damit stimmen die Knöpfe auch ohne JS, und der Poll muss nichts am DOM
   umbauen, nur ein Attribut setzen.

   „unknown“ (Status nicht abrufbar) blendet nichts aus — wer nicht weiß,
   ob eine Site läuft, soll beides anbieten dürfen, statt zu raten.
   Und was hier verschwindet, ist nur unsichtbar, nicht verboten: Start/
   Stop/Restart prüfen serverseitig weiterhin Flag und MANAGED_SITES
   (dashboard/views.py). */
[data-lifecycle="running"] [data-lifecycle-only="stopped"],
[data-lifecycle="stopped"] [data-lifecycle-only="running"] { display: none; }
/* Ist eine Site gestoppt, ist „Start“ die eine naheliegende Aktion.
   „Redeploy“ bleibt daneben stehen (es startet die Site ja auch, nur mit
   Rebuild), tritt aber auf die Sekundärfläche zurück, damit die Zeile
   nicht zwei gleich laute Knöpfe hat. */
[data-lifecycle="stopped"] .btn-primary[data-lifecycle-demote] {
  background: transparent; color: var(--text); box-shadow: 0 0 0 1px var(--border-strong) inset;
}
[data-lifecycle="stopped"] .btn-primary[data-lifecycle-demote]:hover {
  background: var(--surface-2); color: var(--accent);
}

/* --- Status pill with glow dot -------------------------------------- */

.status-pill { display: inline-flex; align-items: center; gap: .45rem; font-size: .78rem; font-weight: 700; padding: .28rem .65rem .28rem .55rem; border-radius: 999px; background: var(--surface-2); box-shadow: 0 0 0 1px var(--border) inset; white-space: nowrap; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.status-pill.ok .status-dot, .status-pill.healthy .status-dot, .status-pill.running .status-dot, .status-pill.success .status-dot { background: var(--ok); box-shadow: 0 0 0 4px var(--ok-glow); }
.status-pill.ok, .status-pill.healthy, .status-pill.running, .status-pill.success { color: var(--ok-text); }
/* „exited" gehört NICHT mehr in den roten Topf: Eine Site, die jemand
   selbst gestoppt hat, ist kein Fehler — rot behauptete das aber, und
   zwar genau an der Stelle, an der man nachsieht, ob etwas kaputt ist.
   Sie ist jetzt still (siehe .status-pill.exited weiter unten), während
   „restarting" und „dead" neu dazukommen: eine Neustart-Schleife und ein
   abgestürzter Container sind echte Befunde. Beide wurden erst sichtbar,
   seit das Toolkit den stehengebliebenen Health-Wert eines nicht
   laufenden Containers nicht mehr weiterreicht (pablo-roso/deploy,
   container_health). */
.status-pill.bad .status-dot, .status-pill.unhealthy .status-dot, .status-pill.restarting .status-dot, .status-pill.dead .status-dot, .status-pill.failure .status-dot { background: var(--bad); box-shadow: 0 0 0 4px var(--bad-glow); }
.status-pill.bad, .status-pill.unhealthy, .status-pill.restarting, .status-pill.dead, .status-pill.failure { color: var(--bad-text); }

/* Angehalten, nicht kaputt — dieselbe ruhige Sprache wie „intern" auf der
   Kunden-Seite: eine Einordnung, keine Wertung. */
.status-pill.exited .status-dot, .status-pill.paused .status-dot, .status-pill.created .status-dot { background: var(--text-dim); box-shadow: 0 0 0 4px color-mix(in srgb, var(--text-dim) 18%, transparent); }
.status-pill.exited, .status-pill.paused, .status-pill.created { color: var(--text-dim); }
.status-pill.starting .status-dot, .status-pill.pending .status-dot { background: var(--warn); box-shadow: 0 0 0 4px var(--warn-glow); }
.status-pill.starting, .status-pill.pending { color: var(--warn-text); }
.status-pill.unknown .status-dot { background: var(--text-dim); }
.status-pill.unknown { color: var(--text-dim); }
/* „Angefangen, aber noch nicht im Betrieb" — der Stand eines Kunden oder
   Leads, bei dem gerade wir am Zug sind (Neu, Kontaktiert, Geplant).
   Weder grün (nichts läuft) noch gelb (es wartet niemand auf eine
   Antwort): der Akzentton, in dem dieses Panel ohnehin sagt „hier geht
   es weiter". Siehe dashboard/templatetags/dashboard_extras.py. */
.status-pill.offen .status-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-glow); }
.status-pill.offen { color: var(--accent); }

/* --- Recent-outcomes pulse row (site detail) -------------------------
   A compact row of dots, oldest → newest, one per recent DeployAction on
   this site — a cheap "how's it been going lately" read built entirely
   from the audit log this panel already writes (see views.py's
   _overview_stats docstring: no separate uptime tracker exists, so this
   reuses what's real instead of inventing a metric). */

.pulse-row { display: flex; align-items: center; gap: .3rem; }
.pulse-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--text-dim); opacity: .5;
}
.pulse-dot.success { background: var(--ok); opacity: 1; box-shadow: 0 0 0 3px var(--ok-glow); }
/* Ein Ring statt einer gefuellten Scheibe: „fehlgeschlagen" soll sich auch
   dann von „erfolgreich" abheben, wenn jemand Rot und Gruen nicht
   unterscheidet — die Reihe traegt ihre Bedeutung sonst ausschliesslich in
   der Farbe, und die Einzelauskunft steckt in einem title-Attribut, das auf
   dem Telefon nie erscheint (siehe .finding-badge fuer dasselbe Argument). */
.pulse-dot.failure { background: transparent; opacity: 1; box-shadow: inset 0 0 0 2px var(--bad), 0 0 0 3px var(--bad-glow); }
.pulse-dot.pending { background: var(--warn); opacity: 1; box-shadow: 0 0 0 3px var(--warn-glow); }
.pulse-label { font-size: .78rem; color: var(--text-dim); margin-left: .4rem; }

/* --- Buttons --------------------------------------------------------
   Against style_guide's real Button.css.ts, not just DESIGN_SYSTEM.md's
   summary: radius.md (not a full pill — an actual, checkable divergence
   the first pass here got wrong), the exact :active/:disabled mechanics
   (scale(0.97), not a translateY press; opacity 0.4, not 0.5), and the
   "secondary" variant's real shape — a filled surface with NO border,
   fill.secondary resting / fill.primary on hover (the same escalation
   --border-strong's own alias already models). Size stays this app's own
   call, not md's literal 15px/space[6]: Button.tsx ships a real `sm`
   variant for exactly this — dense rows of many actions — and panel's
   existing size already sits close to it (font-size.sm, tighter padding);
   forcing every row in .hetzner-actions to md-sized buttons would blow
   out layouts this file already spent real effort keeping compact on a
   narrow screen. .btn-danger and .btn-warn not taking a solid fill until
   hover is this app's own considered call too — Danger Zone rows hold
   several buttons at once (Löschen, Rebuild, Hard-Reset), and an
   always-red one among them fights the "two deliberate clicks" restraint
   that section already documents further down this file. They do carry a
   resting outline, though: withholding the fill is a statement about
   emphasis, withholding every edge made them stop reading as buttons at
   all — see .btn-danger's own comment. */

/* Der Zweitknopf ist eine Kontur, keine graue Fläche — wie „ghost" auf
   talvesa.de. Neben dem gefüllten Hauptknopf (Tinte) bleibt so genau eine
   Fläche pro Zeile, die ruft, und die Seite hat ein Grau weniger. Die
   Kontur ist ein innerer Schatten statt eines Rands, damit kein Knopf
   durch den Wechsel breiter wird; Varianten mit eigenem box-shadow
   (Haupt-, Gefahr-, Warnknopf, die stillen) ersetzen ihn. */
.btn {
  display: inline-flex; align-items: center; gap: .45rem; font: inherit; font-weight: 600; font-size: var(--talvesa-font-size-sm);
  padding: .55rem 1.1rem; border-radius: var(--radius-md); border: none;
  background: transparent; color: var(--text); cursor: pointer; text-decoration: none;
  box-shadow: 0 0 0 1px var(--border-strong) inset;
  transition: color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard), background-color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard), transform var(--panel-motion-duration-fast) var(--talvesa-motion-easing-apple-out);
  /* A button inside a flex row (.hetzner-actions etc.) is still a
     flex item — without this, a cramped row (a long label like "Neu
     aufsetzen (Rebuild)" next to a select+input in a narrow card) can
     shrink it below its content width instead of wrapping the row, and
     the label reflows into a tall, barely-legible blob. Buttons keep
     their natural size; the row around them wraps instead. */
  flex-shrink: 0; white-space: nowrap;
}
.btn svg { width: 14px; height: 14px; flex: none; }
.btn:hover { background: var(--surface-2); color: var(--accent); }
.btn:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
  .btn:active { transform: none; }
}
/* Solid, not a gradient — one confident color reads as a deliberate
   choice; a diagonal accent→accent-2 blend was two unrelated hues doing
   a marketing-page move for a button that just submits a form. */
.btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: 0 4px 14px -6px color-mix(in srgb, var(--accent-strong) 55%, transparent); }
.btn-primary:hover { background: var(--accent-strong); color: var(--accent-ink); }
/* Kontur im Ruhezustand, Flaeche erst beim Ueberfahren. Ohne die Kontur
   war das hier kein Knopf, sondern ein Stueck roter Text: Man sah erst
   beim Zufallstreffer mit der Maus, dass da etwas zum Anklicken steht —
   und auf einem Telefon, wo es kein Ueberfahren gibt, nie. Bei
   „Wartungsmodus einschalten", das allein in seiner Karte steht und
   nicht in einer Reihe gleichartiger Knoepfe, fiel das am deutlichsten
   auf; zwei Stellen dieser Datei hatten die Kontur fuer genau diesen
   Fall auch schon nachtraeglich per Media Query wieder eingesetzt
   (.table-wrap .btn-danger auf dem Telefon).
   Was die Kontur NICHT tut, und das ist der Punkt der Begruendung
   darueber: die Flaeche fuellen. Eine Danger-Zone-Zeile mit drei
   zerstoerenden Knoepfen leuchtet weiterhin nicht dreifach rot — sie
   zeigt drei Umrisse. Mechanik und Staerke wie bei .btn-warn direkt
   darunter, damit die beiden semantischen Varianten sich gleich
   verhalten und sich nur in der Farbe unterscheiden. */
.btn-danger { background: transparent; color: var(--bad-text); box-shadow: 0 0 0 1px color-mix(in srgb, var(--bad) 55%, transparent) inset; }
.btn-danger:hover { background: var(--bad); color: var(--accent-ink); box-shadow: none; }
/* Die Stufe zwischen „primaer" und „gefaehrlich": Aufmerksamkeit, kein
   Befehl und keine Gefahr. Ohne sie trug die Akzentfarbe eine
   Statusaussage — ein ueberfaelliges Backup machte „Backup jetzt" zu einem
   zweiten btn-primary neben „Redeploy", und in einer Leiste mit zwei bis
   drei gleich lauten Primaerknoepfen sagt keiner mehr, was die Hauptaktion
   ist. Genau das schliesst der Kopfkommentar dieser Datei aus: Semantische
   Farbe bleibt strikt vom Akzent getrennt. Mechanik exakt wie
   .btn-danger darueber — Kontur im Ruhezustand, Flaeche beim Ueberfahren
   —, sodass sich die beiden semantischen Varianten nur in der Farbe
   unterscheiden und nicht auch noch im Verhalten. */
.btn-warn { background: transparent; color: var(--warn-text); box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 55%, transparent) inset; }
.btn-warn:hover { background: var(--warn); color: var(--accent-ink); box-shadow: none; }
.btn:disabled { opacity: .4; cursor: not-allowed; }
form.inline { display: inline; }

/* --- Form controls (shared) ---------------------------------------------
   One <select> rule for the whole panel instead of styling it per form —
   before this, only .filters/.hetzner-inline-form's own selects got any
   styling at all and everything else (Server erstellen's Typ/Image/
   Standort, every per-action extra field in _hetzner_action_fields.html,
   dns.html's Record-Typ) fell back to bare browser chrome next to
   already-styled pill buttons and text inputs. appearance:none drops the
   native arrow, so select:not([multiple]) draws its own via --select-arrow
   (defined per theme block above) — select[multiple] (server_create's
   SSH-key list) is a listbox, not a dropdown, and keeps its native rows. */
select {
  /* Gewicht ausdrücklich 400: `font: inherit` erbt sonst die 600 der
     Beschriftung, in der ein Feld meist steckt (.tool-grid label u. a.) —
     Talvesa Mono gibt es nur in 400 und 700, also wurde daraus Fettdruck,
     und ein Platzhalter sah aus wie ein eingetragener Wert. */
  font: inherit; font-weight: 400; font-size: .88rem; color: var(--text);
  background-color: var(--surface);
  border: 1px solid var(--field-line); border-radius: var(--radius-sm);
  padding: .55rem .75rem;
  cursor: pointer;
  transition: border-color var(--panel-motion-duration-fast), box-shadow var(--panel-motion-duration-fast);
  /* A <select> sizes its closed box to its widest <option> by default —
     appearance:none only drops the OS chrome, not that. A server-type
     option ("cx22 (2 Cores, 4 GB RAM, 40 GB Disk)") is long enough on its
     own to overflow a phone-width card. `max-width: 100%` alone doesn't
     reliably fix this: this select's own immediate parent is a flex item
     with no explicit width of its own (.hetzner-inline-form etc.), so it
     resolves percentages against a containing block that just grows to
     fit this exact content in the first place — 100% of "however wide I
     need to be" is a no-op. The calc() is a hard ceiling relative to the
     viewport instead, independent of any ancestor's own auto-sizing,
     which is what actually stops it here; 100% still wins in every
     properly width-constrained context, this is only a backstop.
     min-width:0 is the other half — a flex item's automatic minimum is
     content-based otherwise, so it wouldn't shrink no matter how tight
     the row gets (see .hetzner-actions' own row of these). Clipped, not
     wrapped: a select's native closed state doesn't wrap the way ordinary
     text does, so a plain hard cutoff without the ellipsis would just
     look broken mid-word. */
  max-width: min(100%, calc(100vw - 5rem)); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
select:not([multiple]) {
  appearance: none; -webkit-appearance: none;
  padding-right: 2.25rem;
  background-image: var(--select-arrow);
  background-repeat: no-repeat; background-position: right .7rem center; background-size: 15px;
}
select[multiple] { padding: .4rem; white-space: normal; }
select:hover { border-color: var(--field-line-hover); }
select:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--field-ring); }
select:disabled { opacity: .5; cursor: not-allowed; }
select option { background: var(--surface-solid); color: var(--text); }

/* Same reasoning, for every plain text-like input/textarea — before this,
   only inputs inside a handful of named form classes (.branch-form,
   .hetzner-form, .field, ...) were styled at all. Anything outside those
   fell back to the browser's own dark-mode form chrome: close enough to
   not look broken, not close enough to actually match this file's border/
   radius/padding/focus tokens — dns.html's per-record inline-edit row is
   the concrete case (each input posts via form="dns-rec-<id>" rather than
   sitting inside a <form> of its own, so it never picked up any of the
   form-scoped rules below). The form-scoped rules that remain now only
   override what's genuinely different there (width/flex sizing, the mono
   font for technical values) rather than repeating the base look. */
/* `date` steht in dieser Liste mit: Ohne das zeichnet der Browser sein
   eigenes Feld — anderer Rahmen, andere Hoehe, andere Flaeche —, und in
   einem Formular daneben sieht es aus, als gehoere es nicht dazu. Genau
   so stand es in der Kasse, in den Vertraegen und an der Leadkarte. */
/* Das Datumsfeld zeichnet der Browser selbst, und ohne Zutun sieht es in
   diesem Panel wie ein Fremdkoerper aus: Der Kalenderknopf steht als
   weisses Symbol da, die leere Vorlage („TT.MM.JJJJ") in voller
   Textfarbe — ein Feld, in dem nichts steht, ruft damit lauter als eins,
   in dem etwas steht. Vier Regeln richten das; mehr geht ohne
   JavaScript nicht, denn WELCHE Vorlage dort steht, entscheidet die
   Spracheinstellung des Browsers, nicht diese Seite.

   Die Klasse `.leer` kommt aus der Vorlage und nicht aus einer
   CSS-Bedingung: Ob ein Wert drinsteht, kann ein Stylesheet nicht sehen —
   fuer `date` gibt es kein `:placeholder-shown`. Die Vorlage weiss es und
   sagt es. */
input[type="date"]::-webkit-calendar-picker-indicator {
  /* Der Knopf traegt die Textfarbe wie jedes andere Symbol dieser
     Oberflaeche — `filter` ist der einzige Weg dorthin, der Browser
     laesst an dieses Symbol keine `color` heran. */
  opacity: .45; cursor: pointer;
  filter: invert(var(--panel-datepicker-invert, 1));
  transition: opacity var(--panel-motion-duration-fast);
}
input[type="date"]:hover::-webkit-calendar-picker-indicator,
input[type="date"]:focus-visible::-webkit-calendar-picker-indicator { opacity: .9; }
:root[data-theme="light"] { --panel-datepicker-invert: 0; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) { --panel-datepicker-invert: 0; }
}
/* Ein leeres Feld zeigt seine Vorlage so leise wie ein Platzhalter —
   denn genau das ist sie. */
input[type="date"].leer::-webkit-datetime-edit { color: var(--text-faint); }
input[type="date"]:focus::-webkit-datetime-edit { color: var(--text); }

/* Felder wie auf talvesa.de (paralympus, _contact.scss): die Fläche, auf
   der sie liegen, und eine Linie aus Tinte statt eines grauen Kastens mit
   grauem Rand. Die Linie (--field-line) hält mindestens 3:1 gegen Karte
   und Papier (WCAG 1.4.11) — ein Trenner (--border) reicht für den Rand
   eines Eingabefelds nicht, das war dort ein Review-Befund. Fokus: der
   Rand wird Akzent, dazu ein weicher Ring, statt eines zweiten Rahmens mit
   Abstand um den ersten. Gewicht 400 aus demselben Grund wie bei select. */
input[type="text"], input[type="number"], input[type="email"], input[type="password"], input[type="search"], input[type="date"], input[type="url"], textarea {
  font: inherit; font-weight: 400; font-size: .88rem; color: var(--text);
  background-color: var(--surface);
  border: 1px solid var(--field-line); border-radius: var(--radius-sm);
  padding: .55rem .75rem;
  transition: border-color var(--panel-motion-duration-fast), box-shadow var(--panel-motion-duration-fast);
}
/* Kein opacity mehr: --text-dim auf --surface-2 erreicht im hellen Thema
   mit .8 nur noch rund 3,2:1 — und ein Platzhalter, der ein Beispiel zeigt
   („z.B. 10.0.1.0/24"), muss lesbar sein, sonst ist er keines. */
input[type="text"]::placeholder, input[type="number"]::placeholder, input[type="email"]::placeholder,
input[type="password"]::placeholder, input[type="search"]::placeholder,
input[type="url"]::placeholder, textarea::placeholder { color: var(--text-faint); }
input[type="text"]:hover, input[type="number"]:hover, input[type="email"]:hover,
input[type="password"]:hover, input[type="search"]:hover, input[type="date"]:hover,
input[type="url"]:hover, textarea:hover { border-color: var(--field-line-hover); }
input[type="text"]:focus-visible, input[type="number"]:focus-visible, input[type="email"]:focus-visible,
input[type="password"]:focus-visible, input[type="search"]:focus-visible,
input[type="date"]:focus-visible, input[type="url"]:focus-visible, textarea:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: var(--field-ring);
}
input:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
/* Ein gescheitertes Feld ist selbst gekennzeichnet, nicht nur der Satz
   ueber dem Formular — wer per Tastatur direkt ins Feld springt, bekommt
   den Satz nie zu sehen. */
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"],
input[aria-invalid="true"]:hover, select[aria-invalid="true"]:hover, textarea[aria-invalid="true"]:hover { border-color: var(--bad); }

/* --- Confirm dialog ---------------------------------------------------
   Replaces window.confirm() for data-confirm forms (see panel.js) with a
   native <dialog>, styled from this file's own tokens — no external
   library, this project stays framework-free. */

/* Deckend wie jede Karte, mit Kante und Schatten — kein Glas mehr (siehe
   „Karten: deckend, mit Haarlinie"). */
.confirm-dialog {
  padding: 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  color: var(--text); box-shadow: var(--shadow);
  max-width: min(26rem, calc(100vw - 2rem));
}
/* --talvesa-color-scrim — the shared "abdunkelnde Fläche hinter Modal/
   Sheet" role (contract.css.ts), the same darkening this dialog always
   used, just sourced from the shared system now instead of a hand-picked
   rgba(). Ohne Weichzeichner: Abgedunkelt reicht, um zu sagen „das
   dahinter ist gerade nicht dran". */
.confirm-dialog::backdrop { background: var(--talvesa-color-scrim); }
.confirm-dialog-body { padding: 1.3rem; display: flex; flex-direction: column; gap: 1.1rem; }
.confirm-dialog-message { margin: 0; font-size: .92rem; line-height: 1.5; white-space: pre-line; }
.confirm-dialog-actions { display: flex; justify-content: flex-end; gap: .5rem; }

/* --- Toasts -----------------------------------------------------------
   Against style_guide's real Toast.css.ts: an opaque elevated card with a
   small tone-colored dot, not a full tone-tinted background — its own
   code comment is explicit about why ("Toasts liegen über allem, aber
   nicht auf Glas: laut DESIGN_SYSTEM.md ist über einfarbigem Grund
   background.elevated + Schatten die richtige Wahl", the same "no
   contrast behind it" reasoning .card already follows elsewhere in this
   file). Body text stays this app's own ink at every tone — same as the
   real component, where only the dot carries color and the message text
   is always label.primary — a whole line tinted danger-red or ok-green
   reads *louder* than the dot-only version, not clearer. */

.toasts { list-style: none; padding: 0; margin: 0 0 var(--space-6); display: flex; flex-direction: column; gap: .5rem; }
/* Unlike .card, this DOES keep its border — against style_guide's real
   Toast.css.ts, a toast is background.elevated + a 1px separator border +
   shadow[3] (var(--shadow), not var(--shadow-sm)/shadow[2] this had
   before): floating over arbitrary page content, not sitting flat on this
   app's own plain background the way a .card does, so it earns both the
   border AND the stronger of the two shadow tiers. The .4s fade stays a
   fixed duration rather than following --panel-motion-scale like most
   other transitions here — panel.js's own setTimeout(…, 400) that removes
   the element from the DOM is hard-coded to match it, and desyncing the
   two would either cut the fade off mid-animation or leave a fully faded,
   still-present toast lingering, depending on which way the slider moved. */
.toast {
  display: flex; align-items: flex-start; gap: .65rem;
  padding: .75rem 1rem; border-radius: var(--radius); font-size: .88rem; font-weight: 600;
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  box-shadow: var(--shadow);
  transition: opacity .4s, transform .4s;
}
/* Form statt nur Farbe: Erfolg, Warnung und Fehler unterschieden sich
   ausschliesslich durch die Farbe eines 8px-Punktes — fuer jemanden mit
   Rot-Gruen-Schwaeche waren „Redeploy erfolgreich" und „Redeploy
   fehlgeschlagen" optisch dieselbe Meldung. Das Symbol ist ein inline-<svg>
   in base.html und erbt seine Farbe ueber currentColor, wie jedes andere
   Icon dieser Oberflaeche; vier Toene, vier Formen.

   .toast-body ist der innere Flex-Behaelter: Er nimmt die Live-Rolle vom
   <li> herunter, damit die <ul> wieder nur Listenelemente enthaelt (axe:
   „list" / aria-required-children) — .toast selbst bleibt der Rahmen.

   Der fruehere .toast-dot ist ersatzlos weg: Beide Huellen — base.html und
   portal/base.html — rendern dasselbe .toast-icon, und zwar woertlich
   dasselbe Markup. Wer eine Form aendert, aendert beide. */
.toast-body { display: flex; align-items: flex-start; gap: .65rem; flex: 1 1 auto; min-width: 0; }
.toast-icon { flex: none; width: 16px; height: 16px; margin-top: .1em; color: var(--text-dim); }
.toast.success .toast-icon { color: var(--ok-text); }
.toast.error .toast-icon { color: var(--bad-text); }
/* Django's messages.warning() level — used by dashboard/views_hetzner.py
   for a Hetzner action that timed out waiting on Hetzner's own verdict
   (genuinely still unknown, not a failure). Same amber token as
   .status-pill.pending/.branch-badge, for the same "attention, not an
   error" semantic. */
.toast.warning .toast-icon { color: var(--warn-text); }
.toast-text { flex: 1 1 auto; min-width: 0; }
.toast.fade-out { opacity: 0; transform: translateY(-4px); }

/* Schließen-Knopf am Toast (base.html) — sichtbar, aber leise: ein Toast
   ist eine Meldung, kein Dialog, der Knopf soll den Text nicht
   überstimmen. Fehler/Warnungen verschwinden nicht mehr von allein (siehe
   panel.js' initToasts), deshalb ist das hier der einzige Weg, sie
   wegzubekommen — entsprechend muss er auch per Tastatur erreichbar sein,
   also ein echter <button>, kein ::after-Kreuz. */
.toast-close {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; margin: -.1rem -.25rem 0 0; padding: 0;
  border: none; border-radius: var(--radius-sm); background: transparent;
  color: var(--text-dim); cursor: pointer;
  transition: color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard), background-color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard);
}
.toast-close svg { width: 13px; height: 13px; }
.toast-close:hover { color: var(--text); background: var(--surface-2); }
.toast-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* --- Rückmeldung, solange eine Aktion läuft ------------------------------
   Diese Klassen sind inzwischen der Vertrag des Designsystems: sie stehen
   dort als packages/vanilla/src/pending-action.{css,js} (die Fassung für
   server-gerenderte Seiten) und als PendingActionProvider/<Button loading>
   für React — entstanden sind sie hier, und die Namen sind absichtlich
   dieselben geblieben, damit man beim Wechsel zwischen den Repositories
   nicht übersetzen muss. Wer eine Regel hier ändert, gehört dort mit
   hingeschaut (style_guide, DESIGN_SYSTEM.md §5 und docs/rezepte.md,
   Rezept 9).

   Gegenstück zu panel.js' "Rückmeldung, solange eine Aktion läuft" — die
   Begründung steht dort ausführlich: ein Redeploy blockiert die Antwort
   bis zu DEPLOY_SSH_COMMAND_TIMEOUT Sekunden lang, und ohne diese drei
   Elemente (Spinner im Knopf, Balken oben, Banner unten) sieht die Seite
   in dieser ganzen Zeit exakt so aus wie vor dem Klick.

   Bewusst kein zweites Farbschema: Spinner erben currentColor, Balken und
   Banner nutzen --accent bzw. dieselben --surface/--border/--shadow-Werte
   wie .toast. Der Fortschritt ist unbestimmt (kein Prozentbalken) — das
   Panel *weiß* nicht, wie weit ein Rebuild ist; eine gefüllte Prozentzahl
   wäre eine erfundene Zahl. Was es weiß, steht daneben: wie lange es
   schon läuft. */

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

/* Ein Ring statt eines Icons: gleiche Kantenlänge wie die 14px-Icons in
   .btn, damit ein Knopf beim Klick nicht seine Breite ändert (siehe
   .is-busy > svg unten — das eigene Icon geht dafür weg). */
.btn-spinner {
  width: 14px; height: 14px; flex: none; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  animation: panel-spin .7s linear infinite;
}
.is-busy > svg { display: none; }
.is-busy { cursor: progress; }

/* Sperre, solange etwas läuft (panel.js setzt <html data-busy>): der
   laufende POST würde durch einen zweiten Klick abgebrochen, während der
   Server ihn zu Ende ausführt. pointer-events reicht dafür als *Optik*,
   die echte Sperre ist der submit-Handler in panel.js — Tastatur-
   bedienung kommt an pointer-events vorbei. Der laufende Knopf selbst
   bleibt normal sichtbar: er ist die Antwort auf "was passiert gerade".

   Gesperrt wird JEDER Link, der im selben Tab wegführt, nicht nur die
   Navigation links: wer während eines laufenden Redeploys auf "Logs
   ansehen" klickt, verlässt die Seite, auf der das Ergebnis als Meldung
   angekommen wäre — danach steht es nur noch im Verlauf. Ein Link mit
   target="_blank" bleibt frei, der nimmt einem nichts weg.

   Zwei Ausnahmen mit Absicht: .toast-close (eine Meldung wegklicken zu
   können ist keine Aktion gegen den Server) und der laufende Knopf. */
[data-busy] .btn:not(.is-busy),
[data-busy] button:not(.is-busy):not(.toast-close),
[data-busy] a:not(.is-busy):not([target="_blank"]),
[data-busy] input, [data-busy] select, [data-busy] textarea, [data-busy] summary {
  pointer-events: none;
}
[data-busy] .btn:not(.is-busy),
[data-busy] button:not(.is-busy):not(.toast-close) { opacity: .45; }
[data-busy] a:not(.is-busy):not(.btn) { opacity: .6; }

/* Dünner, unbestimmter Balken an der oberen Fensterkante — dieselbe
   Konvention wie in jeder App mit langsamen Seitenwechseln, und das
   einzige Element hier, das auch bei einem reinen Seitenwechsel erscheint
   (der Hetzner-Tab holt beim Rendern ein Dutzend API-Listen). z-index:
   toast, nicht sticky: der Balken muss über der klebrigen .app-nav
   liegen, sonst verschwindet er auf Handybreite genau hinter ihr. */
.route-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  z-index: var(--talvesa-z-index-toast);
  background: color-mix(in srgb, var(--accent) 24%, transparent);
  overflow: hidden; pointer-events: none;
  opacity: 0; transition: opacity var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard);
}
.route-progress.is-active { opacity: 1; }
.route-progress::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 35%;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: route-progress-slide 1.15s var(--talvesa-motion-easing-standard) infinite;
}
@keyframes route-progress-slide {
  from { transform: translateX(-110%); }
  to { transform: translateX(400%); }
}

/* Der Banner ist das, was auch dann noch sichtbar ist, wenn der geklickte
   Knopf längst aus dem Sichtfeld gescrollt ist (die Hetzner-Seite ist
   lang). Opak wie .toast, nicht auf Glas — dieselbe Begründung wie dort:
   ein Element, das über beliebigem Inhalt schwebt, hat keinen ruhigen
   Hintergrund zum Aufnehmen (siehe .toast weiter oben). Unten mittig, weil
   oben schon Balken, .app-nav und die Meldungsliste liegen. */
.busy-banner {
  /* left+right+margin-inline:auto statt left:50% + translateX(-50%): ein
     fixed positioniertes Element mit nur `left` bekommt als verfügbare
     Breite nur den Rest *rechts davon* (auf einem 420px-Display also
     210px) und bricht seinen Text entsprechend klein um — mit beiden
     Kanten gesetzt ist die verfügbare Breite die ganze Zeile, und
     `width: fit-content` + auto-Ränder zentrieren wie gehabt. */
  /* bottom rechnet den Home-Indicator mit ein (base.html setzt dafür
     viewport-fit=cover) — sonst liegt der Banner auf iPhones genau unter
     dem Balken, den das System dort selbst zeichnet. */
  position: fixed; bottom: calc(1.1rem + env(safe-area-inset-bottom)); left: 1rem; right: 1rem;
  width: fit-content; margin-inline: auto; transform: translateY(1rem);
  z-index: var(--talvesa-z-index-toast);
  display: none; align-items: center; gap: .75rem;
  max-width: min(30rem, calc(100vw - 2rem));
  padding: .7rem 1rem; border-radius: var(--radius);
  background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);
  color: var(--text); font-size: .86rem; font-weight: 600;
  opacity: 0;
  transition: opacity var(--panel-motion-duration-base) var(--talvesa-motion-easing-standard), transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
.busy-banner.is-visible { display: flex; opacity: 1; transform: translateY(0); }
.busy-banner .btn-spinner { color: var(--accent); }
.busy-banner-body { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.busy-banner-label { overflow-wrap: anywhere; }
/* Leer, bis panel.js nach 10s bzw. 40s etwas hineinschreibt — :empty
   sorgt dafür, dass die Zeile bis dahin auch keinen Platz einnimmt. */
.busy-banner-hint { font-size: .78rem; font-weight: 500; color: var(--text-dim); }
.busy-banner-hint:empty { display: none; }
.busy-banner-elapsed { margin-left: auto; font-size: .8rem; color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Reduzierte Bewegung: der Spinner bleibt (er *ist* die Information, und
   14px Rotation ist keine der Bewegungen, die jemandem schlecht werden
   lassen — er wird nur langsamer), der Balken hört auf zu wandern und
   pulsiert stattdessen an Ort und Stelle, der Banner erscheint ohne
   Hochschieben. */
@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation-duration: 1.6s; }
  .route-progress::after {
    width: 100%; background: var(--accent);
    animation: route-progress-pulse 2s ease-in-out infinite;
  }
  @keyframes route-progress-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
  .busy-banner { transition: none; transform: none; }
}

/* Getippte Bestätigung (data-match-value, panel.js): grüner Rahmen, sobald
   der Text exakt passt — dieselbe Semantik wie überall sonst in dieser
   Datei, --ok heißt "in Ordnung", nie der Akzent. */
input.is-valid, input.is-valid:hover { border-color: var(--ok); }

/* --- Ausgabe einer Aktion (Verlaufstabellen) ----------------------------
   dashboard.models.DeployAction/HetznerAction speichern seit jeher die
   komplette Ausgabe des Befehls (`output`) — sichtbar war sie bisher
   nirgends, obwohl jede fehlgeschlagene Aktion als Meldung "Details im
   Verlauf unten" verspricht. Genau das ist hier jetzt: ein natives
   <details> pro Zeile, das nur aufklappt, wer die Ausgabe wirklich
   sehen will — eine Tabelle mit ausgeklapptem Traceback in jeder Zeile
   wäre unlesbar. */
.output-details { max-width: 18rem; }
.output-details > summary {
  display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; list-style: none;
  font-size: .78rem; font-weight: 600; color: var(--text-dim);
  padding: .2rem .5rem; margin: -.2rem -.5rem; border-radius: var(--radius-sm);
  user-select: none; white-space: nowrap;
  transition: color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard), background-color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard);
}
.output-details > summary::-webkit-details-marker { display: none; }
.output-details > summary::before {
  content: ""; width: 6px; height: 6px; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
.output-details[open] > summary::before { transform: rotate(45deg); }
.output-details > summary:hover { color: var(--text); background: var(--surface-solid); }
.output-details > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .output-details > summary, .output-details > summary::before { transition: none; }
}
/* Erbt bewusst pre.log (siehe "Logs" weiter unten) statt eigener Werte —
   es ist derselbe Inhaltstyp, nur kleiner und niedriger gedeckelt, weil
   er in einer Tabellenzelle steht. */
.output-details pre.log { margin: .55rem 0 .2rem; max-height: 16rem; font-size: .76rem; padding: .7rem .8rem; }
.output-empty { font-size: .78rem; color: var(--text-dim); }

/* --- Aufklapper ----------------------------------------------------------
   Vier <summary> in diesem Panel, vier Macharten: drei unterdrueckten den
   nativen Marker und zeichneten je ein eigenes Zeichen, eines („Zuordnen",
   ein summary, das wie ein Knopf aussieht) vergass es — in WebKit stand das
   Dreieck mitten im blauen Knopf. Hier steht die gemeinsame Basis: Marker
   weg, eigenes Chevron, das sich bei [open] dreht, Ausstieg bei reduzierter
   Bewegung (denselben Ausstieg hat .output-details > summary schon, den
   beiden Kasse-Marken fehlte er). `summary.btn` ist dieselbe Regel fuer den
   Knopf-Fall: dort traegt der Knopf schon Form und Farbe, es fehlt nur das
   Abschalten des Markers. */
summary.aufklapper, summary.btn { list-style: none; cursor: pointer; user-select: none; }
summary.aufklapper::-webkit-details-marker, summary.btn::-webkit-details-marker { display: none; }
summary.aufklapper { display: inline-flex; align-items: center; gap: .4rem; color: var(--text-dim); font-size: .82rem; font-weight: 600; }
summary.aufklapper[hidden] { display: none; }
summary.aufklapper::before {
  content: ""; width: 6px; height: 6px; flex: none;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
[open] > summary.aufklapper::before { transform: rotate(45deg); }
summary.aufklapper:hover { color: var(--text); }
summary.aufklapper:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  summary.aufklapper::before { transition: none; }
}

/* --- Tables (history / audit log) ------------------------------------
   Against style_guide's real Table.css.ts: `th` is background.secondary
   (--bg-2), font-size.xs/weight *semibold*/letter-spacing 0.02em — not
   the bold-uppercase-wide-tracking treatment this had before, same
   over-invented "eyebrow label" pattern already found and fixed on
   .overview-tile-label above (nowhere in the real component). Left as-is
   on purpose: the real `scroller`'s own border/radius/background — every
   table in this app already sits inside a .card, so wrapping .table-wrap
   in a second, near-identical border immediately inside the card's own
   would read as a redundant nested box, not an intentional one; th's
   sticky background instead just needs to *look* right against .card's
   surface, which the color fix above already does. Zebra striping
   (fill.tertiary on even rows) is real Table.css.ts too — an explicit
   opt-in variant there, made the default here since it costs one rule
   and every table in this app is exactly the kind of multi-row resource/
   audit list it's meant for. */

/* overscroll-behavior-x: eine breite Tabelle bis an ihren Rand zu wischen
   darf nicht in die Zurück-Geste des Browsers umschlagen — auf einem
   Telefon ist das sonst der schnellste Weg, die Seite zu verlieren, die
   man gerade liest. */
.table-wrap { overflow-x: auto; overscroll-behavior-x: contain; }
table { width: 100%; border-collapse: collapse; }
/* Mobile-first default padding; min-width:641px block near the end of
   this file opens it up once there's room to spare. */
th, td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--border); font-size: .88rem; }
th { color: var(--text-dim); font-weight: 600; font-size: .72rem; letter-spacing: .02em; position: sticky; top: 0; background: var(--bg-2); }
tbody tr { transition: background var(--panel-motion-duration-fast); }
tbody tr:nth-child(even) { background: var(--surface-solid); }
tbody tr:hover { background: var(--surface-2); }
td.mono, th.mono { font-family: var(--font-mono); }

/* --- Zahlenspalten -------------------------------------------------------
   `th, td { text-align: left }` galt fuer jede Spalte, auch fuer die, die
   man untereinander vergleicht: Einer, zehn und hundert begannen damit an
   derselben Kante statt an derselben Stelle, und eine Gesamtsumme stand
   nicht unter den Posten, die sie summiert. Dass das Projekt das kennt,
   zeigt .pagination-count.

   Tabellenziffern immer, rechtsbuendig erst ab 641px (siehe den
   min-width-Block am Ende dieser Datei): Darunter ist jede Zeile eine Karte,
   in der der Wert ohnehin rechts steht — dort wuerde die Regel nur mit der
   Kartenform streiten. Gilt fuer <th> UND <td>, damit Kopf und Werte an
   derselben Kante enden. */
th.num, td.num { font-variant-numeric: tabular-nums; }

/* --- Detail page ------------------------------------------------------ */

.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1.1rem 1.5rem; }
.detail-field { background: var(--surface-2); border-radius: var(--radius-sm); padding: .7rem .85rem; }
/* Same label treatment as .overview-tile-label (Stat.css.ts) — this is
   the same "small label above a value" role, just in a definition list
   instead of a stat tile. */
.detail-field dt { font-size: .72rem; font-weight: 500; color: var(--text-dim); letter-spacing: .02em; margin: 0 0 .3rem; }
.detail-field dd { margin: 0; font-family: var(--font-mono); font-size: .92rem; }

/* --- Info tooltip (CSS-only — no Bootstrap/CDN/JS, matches this
   project's "no framework, no build step" design already established
   elsewhere, see this file's own header comment). A small "i" badge next
   to a technical value (Typ, Static-Modus, Standard-Branch) that reveals
   an explanation on hover/focus. `content: attr(data-tooltip)` reads the
   text straight from the element's own attribute — no JS needed to
   position or populate it. `tabindex="0"` on the element itself (in the
   template) is what makes :focus-visible reachable for keyboard/touch,
   not just mouse hover.

   The bubble is a solid surface with an edge, like every other floating
   thing in this panel (no glass any more), with style_guide's Tooltip.tsx
   fade+scale entrance
   (opacity 0→1, scale .96→1, duration.fast, easing.appleOut) — done here
   via `transition` rather than `@keyframes` since this stays CSS-only,
   :hover/:focus-visible driven, no JS state to key an animation off. */

.info-tip {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--surface-2); color: var(--text-dim);
  font-size: .68rem; font-weight: 700; font-style: normal;
  cursor: help; flex: none; margin-left: .35rem; vertical-align: middle;
}
.info-tip::before { content: "i"; }
/* Dieselbe Blase, anderer Anlass: „!“ in Warnfarbe steht an einer
   Information, die man kennen *sollte* (ein überfälliges Backup), nicht
   nur nachschlagen kann. Eigene Variante statt eines zweiten Bauteils —
   Position, Blase und Fokusverhalten sind identisch, nur Zeichen und
   Farbe unterscheiden sich. */
.info-tip.is-warn { background: var(--warn-glow); color: var(--warn-text); cursor: help; }
.info-tip.is-warn::before { content: "!"; }
/* Dieselbe Blase noch einmal, für einen echten Ausfall statt bloßer
   Aufmerksamkeit (dashboard/services/uptime.py: Site nicht erreichbar,
   Zertifikat ungültig) — eigenes Zeichen, damit sich "läuft bald ab" und
   "ist gerade kaputt" auch auf einen Blick unterscheiden. */
.info-tip.is-bad { background: var(--bad-glow); color: var(--bad-text); cursor: help; }
.info-tip.is-bad::before { content: "×"; }
.info-tip::after {
  content: attr(data-tooltip);
  position: absolute; bottom: 135%; left: 50%; transform: translateX(-50%) scale(.96);
  width: max-content; max-width: 240px;
  padding: .6rem .75rem; border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text); font-family: var(--font-sans);
  font-size: .78rem; font-weight: 400; line-height: 1.4;
  white-space: normal; text-align: left; box-shadow: var(--shadow-sm);
  opacity: 0; visibility: hidden;
  transition: opacity var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard), transform var(--panel-motion-duration-fast) var(--talvesa-motion-easing-apple-out);
  z-index: var(--talvesa-z-index-tooltip); pointer-events: none;
}
/* :focus NEBEN :focus-visible: Auf einem Telefon loest ein Tipp auf ein
   tabindex="0"-<span> weder :hover noch :focus-visible aus — die zehn
   Erklaerungen der Detailseite und der komplette Erklaertext jeder
   Hetzner-Aktion waren dort schlicht nicht zu oeffnen. */
.info-tip:hover::after, .info-tip:focus::after, .info-tip:focus-visible::after { opacity: 1; visibility: visible; transform: translateX(-50%) scale(1); }
@media (prefers-reduced-motion: reduce) {
  .info-tip::after { transition: opacity var(--panel-motion-duration-fast) linear; transform: translateX(-50%); }
  .info-tip:hover::after, .info-tip:focus::after, .info-tip:focus-visible::after { transform: translateX(-50%); }
}

/* --- Branch-preview badge ----------------------------------------------
   Reuses the "warn" (amber) token, same semantic as status-pill.starting/
   pending: an attention state, not an error — a site legitimately sits on
   a non-default branch while someone's testing a PR, that's the point. */

.branch-badge {
  display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; font-weight: 700;
  padding: .26rem .65rem; border-radius: 999px; background: var(--warn-glow); color: var(--warn-text);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 25%, transparent) inset;
  white-space: nowrap;
}
.branch-badge svg { width: 13px; height: 13px; flex: none; }

/* --- Branch-deploy card (site detail) ----------------------------------- */

.branch-form, .restore-form, .cron-form { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-bottom: 1rem; }
/* Base look/focus ring already comes from the global text-input rule
   above — these three forms only need the mono font (technical values:
   branch names, slugs, cron schedules) and to grow/shrink as flex items. */
.branch-form input[type="text"], .restore-form input[type="text"], .cron-form input[type="text"] {
  flex: 1 1 220px; font-family: var(--font-mono);
}
/* Schedule/description fields stay narrower than the free-form command
   field — three equal-width flex items would leave "0 6 * * *" awkwardly
   stretched. */
.cron-form input[name="schedule"], .cron-form input[name="description"] { flex-basis: 160px; }
.cron-form input[name="command"] { flex: 2 1 260px; }
/* .btn:disabled above already dims it; this just stops .btn-danger's own
   hover (solid red fill) from firing while it's still disabled and
   un-clickable — the button shouldn't look "armed" until panel.js's
   typed-confirmation check (data-match-* — see site_detail.html/panel.js)
   actually enables it. The neutral outline is spelled out rather than
   left off: .btn-danger:hover clears its own resting box-shadow, so
   without this the button would lose its last edge on hover — exactly
   the "is this even a button?" problem .btn-danger's outline fixes. */
.restore-form button:disabled:hover { background: none; border-color: var(--border); color: inherit; box-shadow: 0 0 0 1px var(--border) inset; }

.pr-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
/* --surface-2 (fill.secondary), not --surface: this row already sits
   inside a .card, so it gets the same "one tier lighter than its parent"
   treatment as .hetzner-subcard rather than a second elevated+shadow box
   nested inside the first one — a shadow doesn't read as "elevated" when
   it's floating inside another shadowed surface, it just reads as another
   gray box. No border either, same reasoning as .card's own fix above. */
.pr-row {
  display: flex; align-items: center; justify-content: space-between; gap: .8rem;
  padding: .6rem .85rem; border-radius: var(--radius-sm);
  flex-wrap: wrap;
  background: var(--surface-2);
}
.pr-row-main { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.pr-row-title { font-weight: 600; font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pr-row-meta { font-family: var(--font-mono); font-size: .76rem; color: var(--text-dim); }
.pr-row-meta a { color: var(--text-dim); text-decoration: underline; text-underline-offset: 2px; }
.pr-row-meta a:hover { color: var(--accent); }

/* --- Auto-Redeploy bei Merge (site detail) ------------------------------
   Reuses the global input/select/.btn rules; everything below is layout
   plus the two things this card needs that nothing else has: a checkbox
   row that reads as a switch label, and a value row (webhook URL/secret)
   whose long mono string must never push the card into a horizontal
   scroll on a phone. */

.auto-deploy-form { display: flex; flex-direction: column; gap: .9rem; }
.toggle-row { display: flex; align-items: center; gap: .55rem; font-size: .9rem; cursor: pointer; }
.toggle-row input[type="checkbox"] { width: 1.05rem; height: 1.05rem; accent-color: var(--accent); cursor: pointer; }

/* The disclosed half (panel.js: data-reveal-target). Indented and
   rule-marked rather than boxed — it belongs to the checkbox above it,
   and another nested surface inside an already-elevated .card is exactly
   the "gray box in a gray box" .pr-row's own comment argues against. */
.reveal-block {
  display: flex; flex-direction: column; gap: .9rem;
  padding-left: .95rem; border-left: 2px solid var(--border);
}
/* Any `display` declaration outranks the UA stylesheet's own
   [hidden] { display: none } — without this line the block above would
   stay visible with the attribute set, i.e. the whole "erst sichtbar,
   wenn angehakt" behaviour would silently do nothing. */
.reveal-block[hidden] { display: none; }

.auto-deploy-fields { display: flex; flex-wrap: wrap; gap: .9rem; }
.auto-deploy-fields > label { display: flex; flex-direction: column; gap: .3rem; flex: 1 1 240px; min-width: 0; }
.auto-deploy-fields input[type="text"] { font-family: var(--font-mono); }
.field-label { font-size: .78rem; font-weight: 600; color: var(--text-dim); letter-spacing: .01em; }
.field-hint { font-size: .74rem; color: var(--text-dim); }

.webhook-setup { display: flex; flex-direction: column; gap: .5rem; }
.webhook-setup-title { font-size: .82rem; font-weight: 600; margin: 0; }
.webhook-field { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
/* min-width:0 + break-all: a webhook URL and a 64-char hex secret are
   both single unbreakable tokens — without this they'd widen the flex
   row past the card instead of wrapping inside it. */
.webhook-field code {
  flex: 1 1 260px; min-width: 0;
  padding: .35rem .5rem; border-radius: var(--radius-sm);
  background: var(--surface-2); font-size: .78rem; word-break: break-all;
}
.webhook-field .btn { flex: none; }

.auto-deploy-actions { display: flex; flex-wrap: wrap; gap: .6rem; }

/* --- Filters (history page) ---------------------------------------------- */

.filters { display: flex; flex-wrap: wrap; gap: .6rem; margin-bottom: 1rem; align-items: center; }
/* Mobile-first: jedes Bedienelement der Filterzeile nimmt die ganze
   Breite. Nebeneinander bleiben auf einem Telefon zwei Auswahlfelder
   übrig, die beide zu schmal sind, um ihren eigenen ausgewählten Wert
   noch zu zeigen (der Basisregel von `select` weiter oben genügt dafür
   die Ellipse — lesbar wird es davon nicht). Der min-width:641px-Block
   am Ende dieser Datei stellt die Zeile wieder her, sobald Platz da
   ist. */
.filters > select,
.filters > input,
.filters > .btn { flex: 1 1 100%; }

/* --- Pagination --------------------------------------------------------- */

.pagination { display: flex; gap: .6rem; margin-top: 1rem; align-items: center; font-size: .85rem; color: var(--text-dim); }
/* Pagination.css.ts's real controls apply tabular-nums so the buttons
   don't shift width page to page — this app's own prev/next are already
   .btn (fixed shape regardless of label), but the page count text next
   to them benefits from the same digit stability. */
.pagination-count { font-variant-numeric: tabular-nums; }

/* --- Empty / error states -------------------------------------------
   Against style_guide's real EmptyState: icon, title and description are
   three separate pieces, not one paragraph next to an icon — and the
   component ships two densities (EmptyState.css.ts: "compact" für Karten/
   Panels/Seitenleisten, "normal" für ganze Seiten). This app's own
   call sites are almost all the compact case — one resource list inside
   an already-titled card ("Server", "Volumes", …) coming up empty — so
   that's the default here; .empty-state-lg opts into the roomier,
   page-level treatment for the handful of places (a zero-sites dashboard,
   a "nothing matched" search) where the empty state IS the page. */
.empty-state {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: .7rem; padding: 1.6rem 1.1rem;
}
/* Dieselbe Kaskadenfalle wie bei .reveal-block und .palette-backdrop: Das
   `display: flex` daruber schlaegt `[hidden] { display: none }` des
   Browsers. Auf der Werkzeuge-Seite haengt der "nichts gefunden"-Block an
   genau diesem Attribut (tools.js setzt emptyState.hidden) -- er stand
   deshalb dauerhaft da, auch wenn die Suche Treffer hatte. */
.empty-state[hidden] { display: none; }
.empty-state svg { width: 26px; height: 26px; color: var(--text-dim); }
.empty-state-title { margin: 0; font-size: .92rem; font-weight: 600; color: var(--text); }
.empty-state-desc { margin: 0; font-size: .82rem; color: var(--text-dim); max-width: 34ch; }
.empty-state .btn { margin-top: .3rem; }

.empty-state-lg { gap: 1rem; padding: 3.2rem 1.5rem; }
.empty-state-lg svg { width: 34px; height: 34px; }
.empty-state-lg .empty-state-title { font-size: 1.05rem; }

/* Leer und Fehler sind derselbe Baustein in zwei Toenen. Ein Fehler als
   graues Kleingedrucktes ist die schlechteste Nachricht der Seite in ihrer
   leisesten Schrift. Gefaerbt wird deshalb das SYMBOL, nicht der ganze
   Text: Mehrere Zeilen in Warn- oder Fehlerfarbe lesen sich schlechter als
   der Hinweis, den sie tragen sollen (dieselbe Begruendung wie bei .toast
   und .alert-message weiter oben). */
.empty-state.ist-warnung svg { color: var(--warn-text); }
.empty-state.ist-fehler svg { color: var(--bad-text); }

/* --- Fehlerzustand mit Rohmeldung ----------------------------------------
   Gegenstueck zu .empty-state fuer den Fall, in dem eine fremde Meldung
   mitlaeuft. Vorher war der Fehlerzustand einer ganzen Seite eine nackte
   stderr-Zeile in Rot: kein Titel, kein Satz, der einordnet, kein naechster
   Schritt. Drei Teile, immer in dieser Reihenfolge — was ist, was der
   Server gesagt hat, was jetzt hilft.

   Linksbuendig statt zentriert wie .empty-state: Diese Meldung liest man,
   statt sie zu ueberfliegen. Und ausdruecklich OHNE .card-title —
   test_site_detail_aufbau.py zaehlt die in site_detail.html auf genau 13. */
.fehler-block { display: flex; flex-direction: column; align-items: flex-start; gap: .55rem; }
.fehler-block[hidden] { display: none; }
.fehler-titel { margin: 0; font-size: .95rem; font-weight: 650; color: var(--text); }
.fehler-meldung {
  margin: 0; width: 100%; padding: .55rem .7rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  border-left: 2px solid var(--bad);
  background: var(--surface-2);
  font-family: var(--font-mono); font-size: .8rem; line-height: 1.5;
  color: var(--bad-text); white-space: pre-wrap; overflow-wrap: anywhere;
}
/* Dieselbe Meldung, wenn sie nur Aufmerksamkeit will statt einen Ausfall
   zu melden (Zeitueberschreitung, „gerade nicht abrufbar"). */
.fehler-meldung.is-warn { border-left-color: var(--warn); color: var(--warn-text); }

/* Die kleinen Abwesenheiten — „keine Projekte zugeordnet", „kein
   Portalzugang", „nichts frei" — hatten je eine eigene Klasse mit demselben
   Inhalt. Eine reicht; die alten Namen zeigen hierher, damit die Vorlagen
   umziehen koennen, ohne dass etwas dazwischen bricht. */
.leer-zeile, .k-chips-leer, .k-portal-leer, .k-picker-leer, .weitere-sites-fallback {
  margin: 0; font-size: .8rem; line-height: 1.5; color: var(--text-dim);
}

/* --- Logs -------------------------------------------------------------- */

/* Das Nachtband, aus demselben Grund wie .cmd-output. */
pre.log {
  background: var(--tv-night); border: 1px solid var(--tv-night-line); color: var(--tv-on-night); color-scheme: dark;
  border-radius: var(--radius-sm); padding: 1rem;
  overflow: auto; white-space: pre-wrap; word-break: break-word; max-height: 72vh;
  font-family: var(--font-mono); font-size: .83rem;
}

/* --- Login form --------------------------------------------------------- */

.login-shell { display: flex; justify-content: center; padding-top: 3rem; }
.login-card { width: 100%; max-width: 360px; }
.field { margin-bottom: .9rem; }
.field label { display: block; font-size: .82rem; color: var(--text-dim); margin-bottom: .3rem; }
.field input { width: 100%; font-size: .95rem; }
.form-error { color: var(--bad-text); font-size: .85rem; margin: -.4rem 0 .9rem; }
/* Der negative obere Abstand setzt voraus, dass der Satz direkt UNTER einem
   Feld sitzt. Auf der Login- und der Bestaetigungsseite steht er ueber dem
   Formular und braucht dort einen normalen Abstand. */
.form-error.ist-oben { margin: 0 0 .9rem; }

/* Eine schmale Spalte fuer Zugangsformulare (Passwort aendern, Code
   eingeben). Vorher stand dafuer fuenfmal style="max-width:320px" am
   einzelnen Feld — die Breite gehoert an das Formular, nicht an jedes Feld
   einzeln, sonst entscheidet sie jede Stelle neu. */
.form-schmal { max-width: 320px; }
.form-schmal input, .form-schmal select { width: 100%; }
/* Ein kurzes Feld darf kurz sein: Ein sechsstelliger Code in einer
   320px-Spalte sieht aus, als fehle etwas. */
.field-kurz { max-width: 220px; }
@media (max-width: 640px) {
  .form-schmal, .field-kurz { max-width: 100%; }
}

/* Beschriftetes Feld in einer umbrechenden Formularzeile. Ohne es bleibt
   der Platzhalter die einzige Beschriftung — und der verschwindet genau
   dann, wenn man ihn braucht, naemlich beim Tippen; der pflichtige
   <select multiple> fuer SSH-Keys hatte gar keinen und damit sichtbar
   ueberhaupt keine Beschriftung. Bleibt Flex-Element, damit es in
   .hetzner-form und .hetzner-inline-form mitwandert und mit umbricht. Die
   Beschriftung selbst ist .field-label, die es schon gibt. */
.feld-inline { display: flex; flex-direction: column; gap: .3rem; flex: 1 1 14rem; min-width: 0; }
.feld-inline[hidden] { display: none; }
.feld-inline > select, .feld-inline > input, .feld-inline > textarea { width: 100%; }

/* --- Hinweisblock --------------------------------------------------------
   Zwischen Kleintext und Meldung: ein Satz, der Aufmerksamkeit will, mit
   Platz fuer ein Symbol links. Diese Rolle trug bisher .tool-warn — nach
   der Werkzeuge-Seite benannt und reine Farbe plus Schriftgroesse. Farbe
   allein traegt hier nichts: Symbol, Kante und Flaeche sagen dasselbe noch
   einmal. Der Text bleibt in voller Textfarbe, weil mehrere Zeilen in
   Warnfarbe schlechter zu lesen sind als der Hinweis, den sie tragen. */
.hinweis {
  display: flex; align-items: flex-start; gap: .55rem;
  margin: .9rem 0 0; padding: .6rem .8rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: .85rem; line-height: 1.5; color: var(--text);
}
.hinweis[hidden] { display: none; }
.hinweis > svg { width: 16px; height: 16px; flex: none; margin-top: .12rem; color: var(--text-dim); }
/* Der Text gehoert in EIN Element neben das Zeichen. Ohne das wird
   jedes <strong> und jedes <a> im Hinweis zu einer eigenen Spalte des
   Flex-Kastens — der Satz zerfaellt dann in drei Blöcke nebeneinander.
   Genau so ist es der erste Hinweis mit Auszeichnung im Text
   geworden. */
.hinweis > .hinweis-text { min-width: 0; }
.hinweis-warn { border-color: color-mix(in srgb, var(--warn) 40%, transparent); background: color-mix(in srgb, var(--warn) 10%, transparent); }
.hinweis-warn > svg { color: var(--warn-text); }
.hinweis-bad { border-color: color-mix(in srgb, var(--bad) 40%, transparent); background: var(--bad-glow); }
.hinweis-bad > svg { color: var(--bad-text); }

/* --- Hetzner tab -------------------------------------------------------
   dashboard/templates/dashboard/hetzner.html, dns.html, and their shared
   includes (_hetzner_action_button.html / _hetzner_action_fields.html).
   Reuses .btn/.inline/.table-wrap/.status-pill/.info-tip/select wherever
   they already fit (see those templates) — this is only the handful of
   shapes that don't already exist elsewhere: a wrapped row of small
   per-resource action forms, a compact typed-confirm form that fits
   inside a table cell (unlike .restore-form's own block layout), and —
   further below — the Server card grid that replaces a table for the one
   resource type with enough actions to need one. Each action's
   explanation rides along on its own button as an .info-tip badge (see
   _hetzner_action_button.html) rather than a separate legend block. */

.hetzner-actions { display: flex; flex-wrap: wrap; gap: .55rem; align-items: center; }
/* flex-wrap here too, not just on .hetzner-actions — one action's own
   select-plus-button can be wider than a server card is (e.g. Größe
   ändern's Typ-select + button), and without this the row would rather
   shrink its button (see .btn's own flex-shrink:0 comment) than wrap.
   min-width:0 on the form ITSELF, not just on the select inside it
   (below) — a flex item's automatic min-width is its content's min-width
   otherwise, and a <select> generally has no smaller min-content than its
   max-content (form controls don't text-wrap), so without this the form
   still claims room for the select's full unclamped width regardless of
   the select's own max-width — an ancestor flex item's floor overrides a
   descendant's ceiling when the two disagree. */
.hetzner-inline-form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; min-width: 0; max-width: 100%; }
/* Smaller than the shared `select` default — this one sits inline in a
   table cell (SSH-key/volume/floating-IP/snapshot/network rows), not in
   open form space. */
.hetzner-inline-form select { font-size: .82rem; padding: .4rem 2rem .4rem .55rem; }

/* Same min-width:0/max-width:100% reasoning as .hetzner-inline-form above
   — server_rebuild's "image" select lives in a form of this class too. */
.hetzner-confirm-inline { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; min-width: 0; max-width: 100%; }
.hetzner-confirm-inline input[type="text"] { width: 9rem; font-family: var(--font-mono); font-size: .8rem; padding: .5rem .65rem; }
/* Same "don't look armed while disabled" reasoning as .restore-form
   button:disabled:hover above. */
.hetzner-confirm-inline button:disabled:hover { background: none; border-color: var(--border); color: inherit; }

.hetzner-form { display: flex; flex-wrap: wrap; gap: .75rem; align-items: flex-start; margin-bottom: .8rem; }
.hetzner-form input[type="text"], .hetzner-form input[type="number"], .hetzner-form textarea { font-family: var(--font-mono); }
.hetzner-form textarea { resize: vertical; }
.hetzner-form input[type="text"] { flex: 1 1 220px; }
.hetzner-form textarea { flex: 2 1 320px; min-height: 2.6rem; }

/* Eine Stufe UNTER dem Kartentitel, aber ueber dem Fliesstext. Mit .85rem
   in --text-dim und Gewicht 500 war „Server erstellen" vom Absatz darunter
   nicht mehr zu unterscheiden — eine Unterueberschrift, die nichts
   unterteilt. Die Reihenfolge ist jetzt: h2.card-title (.95rem, --text),
   .hetzner-subhead (.92rem, --text), Fliesstext (.85rem, --text-dim). */
.hetzner-subhead {
  font-size: .92rem; color: var(--text); font-weight: 600; margin: 2rem 0 .9rem;
}

/* --- Alert ------------------------------------------------------------
   Against style_guide's real Alert.tsx/css.ts: four tones, each with its
   OWN icon shape (circle+check, triangle, circle+x, circle+i) — muster-
   und-antimuster.md #10's own rule is that a status is never carried by
   color alone, and Alert already builds that in, so this port keeps it
   rather than reusing one triangle for everything. `role` follows the
   same split as the real component: danger interrupts (role="alert"),
   everything else waits its turn (role="status") — set per instance in
   the templates, since a static stylesheet can't know which. A hidden
   text prefix (.visually-hidden, already defined above) carries the same
   "Fehler:"/"Warnung:"/etc. word for anyone not seeing the icon or the
   tint, same reasoning as .alert-banner's old single hard-coded case, now
   generalized to all four.

   This app currently only ever instantiates .alert-danger (the unhealthy-
   servers/failed-zones banner) — info/success/warning exist here as the
   complete system in case a future banner needs one, not as speculative
   markup already sitting unused in a template. */

.alert { display: flex; align-items: flex-start; gap: .7rem; padding: .8rem 1.1rem; border-radius: var(--radius); font-size: .88rem; line-height: 1.5; margin-bottom: 1.2rem; }
.alert-icon { flex: none; width: 19px; height: 19px; margin-top: .05rem; }
.alert-title { margin: 0 0 .15rem; font-weight: 700; }
.alert-message { margin: 0; }
.alert-info { background: color-mix(in srgb, var(--accent-2) 10%, transparent); border: 1px solid color-mix(in srgb, var(--accent-2) 35%, transparent); color: var(--accent-2); }
.alert-success { background: var(--ok-glow); border: 1px solid color-mix(in srgb, var(--ok) 35%, transparent); color: var(--ok-text); }
.alert-warning { background: var(--warn-glow); border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); color: var(--warn-text); }
.alert-danger { background: var(--bad-glow); border: 1px solid color-mix(in srgb, var(--bad) 35%, transparent); color: var(--bad-text); }
/* Message text stays this app's own primary ink, same as the real
   component (only the icon/tint/border carry the tone color — body copy
   at full tone-color across several lines reads worse than one accent
   line does). Single-line usage today doesn't hit this, but the class
   exists for a future title+message alert. */
.alert-message, .alert-title { color: var(--text); }

.hetzner-legend { list-style: none; margin: 1.1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.hetzner-legend li { font-size: .8rem; color: var(--text-dim); line-height: 1.5; max-width: var(--mass-text); }
.hetzner-legend strong { color: var(--text); font-weight: 600; }

/* --- Server cards -------------------------------------------------------
   Replaces a <table> for Servers specifically — every other resource type
   on this page has one or two actions per row and a table still fits
   there, but a server carries up to ten (Start/Neustarten/Hard-Reset/
   Ausschalten/Snapshot/Löschen/Umbenennen/Rebuild/Größe ändern/Rescue),
   and cramming all of them into one "Aktionen" table cell is what this
   replaces. With a handful of servers — the case this panel is actually
   used for, not hundreds — a card grid also just reads better than a
   very wide table.

   Grouping mirrors dashboard.services.hetzner.HetznerActionSpec's own
   `primary`/`destructive` fields (see that module) instead of anything
   hard-coded here: the everyday power controls stay one click away, the
   rarer or config-shaped actions (Hard-Reset, Umbenennen, Größe ändern,
   Rescue) sit behind a "Verwalten" disclosure, and anything that destroys
   data (Löschen, Rebuild) sits behind its own, separately collapsed,
   red-outlined "Danger Zone". Both start closed on every page load — so
   "Server löschen" is never the button a stray click near Start/Stop
   could land on; reaching it takes two deliberate clicks. */

/* One column by default (mobile-first) — the auto-fill pattern that
   lets this add columns as space allows only takes over at
   min-width:641px (see that block near the end of this file); a
   server card carries a lot of detail (meta row + two disclosures), so
   it gets the phone's full width to itself rather than fighting a
   second column for room. */
.hetzner-server-grid { display: grid; grid-template-columns: 1fr; gap: 1.25rem; margin-bottom: .8rem; }
.hetzner-server-card {
  /* Background and edge come from the shared .card, .site-card,
     .hetzner-server-card rule above (see „Karten: deckend, mit
     Haarlinie") — this rule is only what's actually specific to a server
     card. Beyond that, this one also gets the same tilt treatment as
     .site-card/.overview-tile
     (data-tilt in hetzner.html) rather than staying at the plain .card
     baseline: structurally it's the same kind of thing — one card per
     real resource a person browses (a server, same as a site or a stat),
     not a generic form section like the SSH-Keys/Volumes/Firewalls cards
     around it, of which a page can have several without it feeling like
     a deliberate choice each time. It does NOT get .site-card's :hover
     box-shadow escalation though — that specifically signals "this whole
     surface is one clickable target" (Card.css.ts's real `interactive`
     variant), and a server card isn't; its buttons are the click targets,
     same reasoning .card's own plain cards were left without it. */
  border-radius: var(--radius);
  /* Mobile-first default — deliberately generous, not the tightest
     padding that would technically fit: a server card packs a name,
     status pill, meta row, actions, and two disclosures into one box,
     and cramped padding there read as squished/hard to scan rather than
     compact. min-width:641px block near the end of this file opens it
     up further once there's room to spare. */
  padding: 1.5rem 1.4rem; box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column; gap: 1rem;
  position: relative; overflow: hidden;
  /* Cursor-tracked tilt (panel.js's initTiltCards) — --rx/--ry/--mx/--my
     default to a flat, centered "no tilt" state so this is a no-op until
     JS actually starts writing to them. */
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform var(--panel-motion-duration-fast) ease;
  animation: rise-in .5s cubic-bezier(.16, 1, .3, 1) backwards;
  animation-delay: calc(var(--i, 0) * 70ms);
}
.hetzner-server-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .7rem; }
.hetzner-server-name { font-weight: 700; font-size: 1rem; }

.hetzner-server-card > .hetzner-actions { border-top: 1px solid var(--border); padding-top: 1rem; }

/* Collapsed disclosure for the rarer/config-shaped actions and, separately,
   for anything destructive — native <details>/<summary> carries the open/
   closed state itself, no JS needed for that part (panel.js's
   data-confirm/data-match-* still drive what happens once a form inside is
   actually submitted, same as everywhere else on this page). */
/* Native <details>/<summary> — not a JS-driven grid-template-rows height
   animation like Accordion.tsx's real panel (0fr→1fr), which needs
   aria-expanded/a controlled div this app would have to hand-roll in JS;
   staying native keeps "no JS needed for the open/closed state itself"
   (see comment above) instead of trading it for a smoother open/close.
   The trigger IS matched to Accordion.css.ts's real values though —
   duration.base/appleOut on the chevron, fill.tertiary on hover, not the
   ad hoc .15s/color-only treatment this had before. */
.hetzner-card-section { border-top: 1px solid var(--border); padding-top: .8rem; }
.hetzner-card-section summary {
  display: flex; align-items: center; gap: .5rem; cursor: pointer; list-style: none;
  font-size: .8rem; font-weight: 700; color: var(--text-dim);
  padding: .4rem .5rem; margin: 0 -.5rem; border-radius: var(--radius-sm);
  user-select: none; transition: color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard), background-color var(--panel-motion-duration-fast) var(--talvesa-motion-easing-standard);
}
.hetzner-card-section summary::-webkit-details-marker { display: none; }
.hetzner-card-section summary::before {
  content: ""; width: 7px; height: 7px; flex: none;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(-45deg); transition: transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
.hetzner-card-section[open] > summary::before { transform: rotate(45deg); }
.hetzner-card-section summary:hover { color: var(--text); background: var(--surface-solid); }
.hetzner-card-section summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .hetzner-card-section summary, .hetzner-card-section summary::before { transition: none; }
}
.hetzner-card-section .hetzner-actions { margin-top: .85rem; }

.hetzner-card-section-danger { border-top-color: color-mix(in srgb, var(--bad) 30%, transparent); }
.hetzner-card-section-danger summary { color: var(--bad-text); }
.hetzner-card-section-danger summary:hover { filter: brightness(1.2); }
/* Once actually opened, the section reads unmistakably as the dangerous
   one — a tinted panel around the typed-confirm forms, not just a label. */
/* Mobile-first default bleed; min-width:641px block near the end of
   this file widens it to match the roomier .hetzner-server-card padding
   there. */
.hetzner-card-section-danger[open] { background: var(--bad-glow); border-radius: var(--radius-sm); padding: .7rem .8rem .9rem; margin: 0 -.6rem; }
.hetzner-card-section-danger[open] summary { margin: 0 .6rem; }
.hetzner-card-section-danger[open] .hetzner-actions { margin-left: .6rem; margin-right: .6rem; }

/* One firewall's rules + actions, nested inside the Firewalls card
   (dashboard/templates/dashboard/hetzner.html) — a lighter-weight
   surface than .card itself since it already sits inside one. Also used
   by dns.html for one DNS zone, same reasoning. */
.hetzner-subcard {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  /* Mobile-first default; min-width:641px block near the end of this
     file opens it up once there's room to spare. */
  padding: 1rem 1.1rem; margin-bottom: 1.25rem;
}
.hetzner-subcard-head { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin-bottom: .6rem; }
.hetzner-subcard-title { font-weight: 700; font-size: .92rem; }

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

/* --- Kennwert-Reihe ------------------------------------------------------
   Beschriftung-Wert-Paare, die man nachschlaegt statt ueberwacht (Typ,
   Standort, IPv4, IPv6, Erstellt). Ersetzt die tote Klasse `.site-meta`,
   deren Regel es seit dem Umbau der Site-Karte gar nicht mehr gibt — fuenf
   unformatierte Spans lasen sich als ein einziger Fliesssatz („Typ cpx21
   Standort Nuremberg IPv4 1.2.3.4 …").

   Umbrechende Reihe statt gleich breiter Spuren, aus demselben Grund, den
   .site-stats schon nennt: „—" braucht nicht so viel Platz wie eine IPv6,
   bekaeme ihn in einem Raster aber trotzdem. Das Gewicht kommt aus der
   Klasse, nicht aus einem <strong>. */
.karten-meta { display: flex; flex-wrap: wrap; gap: .6rem 1.4rem; margin: 0; }
.karten-meta[hidden] { display: none; }
.karten-meta-paar { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.karten-meta-label { font-size: .72rem; font-weight: 500; letter-spacing: .02em; color: var(--text-dim); }
.karten-meta-wert { font-size: .88rem; color: var(--text); overflow-wrap: anywhere; }
@media (max-width: 640px) {
  /* Eine Spalte — und weiter umbrechend, damit eine IPv6 die Karte nicht
     ueber den Schirm hinausschiebt. */
  .karten-meta { flex-direction: column; gap: .5rem; }
}

/* --- Befehlsbausteine (Werkzeuge, Site einrichten, Detailseite) ---------
   Die Werkzeuge-Seite selbst steht seit ihrem Umbau zur Werkbank in
   werkzeuge.css (Liste, Bühne, Konsole). Hier bleibt, was sich mehrere
   Seiten teilen: das Feldraster der Formulare (auch im Assistenten „Site
   einrichten") und der schlichte Befehlskasten .cmd-box/.cmd-output, den
   Assistent, Detailseite und Übersicht benutzen. Die aufklappbaren
   Werkzeugkarten (.tool-card, .tool-body, .tool-warn) und ihr Sprung-
   Aufblitzen sind mit der alten Seite gegangen. */

.tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: .9rem 1rem; }
.tool-grid label { display: flex; flex-direction: column; gap: .35rem; font-size: .78rem; color: var(--text-dim); font-weight: 600; }
.tool-grid label.tool-check { flex-direction: row; align-items: center; gap: .5rem; }
/* tools.js toggles conditional fields via the plain `hidden` attribute
   (e.g. site-db-field, collab-sites-field, cron-id-field) — without this,
   the `display: flex` rule right above wins the cascade (author origin
   beats the UA stylesheet's own `[hidden] { display: none }`, regardless
   of specificity), so a "hidden" field would still render, just as an
   empty-ish flex box with its input/select fully visible inside. This is
   the one override that lets the attribute actually do its job here. */
.tool-grid label[hidden] { display: none; }
/* Base look/focus/hover already comes from the shared input[type="text"]/
   select rules above (see "Form controls (shared)") — same reasoning as
   .hetzner-form there, this only adds the mono font for genuinely
   technical values (slug/domain/repo/branch/paths), not selects, whose
   option text is plain prose. */
.tool-grid input[type="text"] { font-family: var(--font-mono); }
/* Das Raster gibt jeder Spalte eine feste Breite, also braucht es den
   Viewport-Deckel der geteilten select-Regel hier nicht — mit ihm war ein
   Auswahlfeld auf dem Telefon schmaler als das Textfeld darüber. */
.tool-grid select { max-width: 100%; }
.tool-grid input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); }

.cmd-box { display: flex; gap: .6rem; align-items: flex-start; flex-wrap: wrap; }
/* Ein Befehl steht im Nachtband der Marke, wie die Konsole der
   Werkzeuge-Seite: in beiden Themes dunkel. Vorher war der Grund Schwarz
   zu 65 % über der Karte, mit der Schrift des Themes darauf — im Hellen
   dunkle Schrift auf dunklem Grau, gemessen um 2:1. */
.cmd-output {
  flex: 1 1 320px; margin: 0;
  background: var(--tv-night); border: 1px solid var(--tv-night-line); color: var(--tv-on-night); color-scheme: dark;
  border-radius: var(--radius-sm); padding: .85rem 1rem; overflow-x: auto;
  font-family: var(--font-mono); font-size: .84rem; white-space: pre;
}
/* Die Rueckmeldung ersetzt die Beschriftung: Aus „Kopieren" wird
   „Kopiert ✓" oder „Text von Hand markieren". Ohne feste Mindestbreite
   springt der Knopf dabei um ein Vielfaches seiner eigenen Breite — und was
   springt, liest sich als Fehler, nicht als Bestaetigung. */
.cmd-copy { min-width: 11rem; justify-content: center; }

/* --- Tools page search ---------------------------------------------------
   One search bar, filters/opens the cards above as you type (see
   tools.js). Sits on top of the shared input[type="search"] styling
   already defined above ("Form controls (shared)") — icon, clear-button
   and the keyboard-shortcut hint are just absolutely positioned inside
   that same padded box, not a bespoke control. */

.tool-search { position: relative; margin-bottom: .6rem; }
/* `input.tool-search-input`, not just `.tool-search-input`: the shared
   rule above matches on `input[type="search"]`, which — tag name plus
   attribute selector — is MORE specific than a bare class, regardless of
   source order. Qualifying this selector with the tag too is what lets
   its own padding-left/-right actually win instead of silently losing to
   the shared rule's shorthand `padding: .55rem .75rem`. */
input.tool-search-input { width: 100%; padding-left: 2.6rem; padding-right: 5.2rem; }
/* This page draws its own clear button (below) — never show two. */
.tool-search-input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
.tool-search-icon {
  position: absolute; left: .9rem; top: 50%; transform: translateY(-50%);
  width: 17px; height: 17px; color: var(--text-dim); pointer-events: none;
  transition: color var(--panel-motion-duration-fast);
}
.tool-search:focus-within .tool-search-icon { color: var(--accent); }
.tool-search-clear {
  position: absolute; right: .5rem; top: 50%; transform: translateY(-50%);
  width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%; background: none; color: var(--text-dim); cursor: pointer;
  transition: background var(--panel-motion-duration-fast), color var(--panel-motion-duration-fast);
}
/* Same cascade gotcha as .tool-grid label[hidden] above: this button's
   own `display: flex` outranks the UA stylesheet's `[hidden] { display:
   none }`, so tools.js setting .hidden = true on it would otherwise still
   render an empty, clickable circle. */
.tool-search-clear[hidden] { display: none; }
.tool-search-clear svg { width: 13px; height: 13px; }
.tool-search-clear:hover { background: var(--surface-2); color: var(--text); }
/* Clear button and the kbd hint share the same corner — tools.js only
   ever shows one of the two (hint while empty, clear once there's a
   query), so they never actually overlap. */
.tool-search-kbd {
  position: absolute; right: .8rem; top: 50%; transform: translateY(-50%);
  font-family: var(--font-mono); font-size: .72rem; font-weight: 700; color: var(--text-dim);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .1rem .45rem; pointer-events: none;
}
.tool-search-status { color: var(--text-dim); font-size: .8rem; margin: 0 0 1.2rem .2rem; min-height: 1.2em; }

mark.search-hit { background: var(--accent-glow); color: inherit; border-radius: 3px; padding: 0 .1em; box-shadow: 0 0 0 1px var(--accent-glow); }

/* --- Settings page (dashboard/settings.html) -----------------------------
   .switch ports style_guide's real Toggle.css.ts (51×31 track, 27px
   thumb, translateX(20px) — Apple's own UISwitch proportions, not a
   guess) and .slider its real Slider.css.ts (4px track, 20px thumb,
   accent-filled progress via a --slider-percent custom property panel.js
   sets on input) — the two controls this page actually needs, hand-built
   in plain CSS/HTML the same way every other component in this file is.
   controlThumb (--talvesa-color-control-thumb) is the one token this
   file didn't have a use for until now — it's the exact role the earlier
   scrim/controlThumb export fix (see ds-tokens.css's own provenance
   header) was made for. */

.settings-row { display: flex; align-items: center; justify-content: space-between; gap: 1.2rem; padding: .9rem 0; border-top: 1px solid var(--border); flex-wrap: wrap; }
.settings-row:first-of-type { border-top: none; padding-top: 0; }
.settings-row-text { min-width: 0; flex: 1 1 220px; }
.settings-row-label { display: block; font-size: .9rem; font-weight: 600; color: var(--text); cursor: pointer; margin-bottom: .2rem; }
.settings-row-desc { margin: 0; font-size: .8rem; color: var(--text-dim); line-height: 1.55; }
.card .btn { margin-top: .3rem; }
/* Die Zeile oben ist der Abstand fuer den EINEN Knopf, der allein unter
   einem Absatz steht. In einer Reihe ist sie falsch: Dort macht der
   Behaelter den Abstand, und .3rem obendrauf verschiebt die Knoepfe gegen
   ihre eigene Zeile. */
.card .aktionszeile .btn, .card .karten-kopf .btn, .card .site-actions .btn,
.card .hetzner-actions .btn, .card .kasse-form .btn, .card .cmd-box .btn,
.card .fehler-block .btn, .card .webhook-field .btn { margin-top: 0; }

/* Die Aktionszeile am Fuss einer Karte: gleicher Abstand nach oben,
   gleicher Zwischenraum, umbrechend auf schmalen Schirmen, linksbuendig.
   .twofa-actions machte das schon fuer zwei Seiten — es bekommt hier nur
   den Namen, der nicht nach einer Seite klingt. */
.aktionszeile, .twofa-actions { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-top: .9rem; }
.aktionszeile[hidden] { display: none; }
.aktionszeile .btn, .twofa-actions .btn { margin-top: 0; }

.switch {
  position: relative; display: inline-flex; align-items: center; flex: none;
  width: 51px; height: 31px; border-radius: 999px; padding: 2px;
  background: var(--border); border: none; cursor: pointer;
  transition: background-color var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
.switch[aria-checked="true"] { background: var(--accent); }
.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch-thumb {
  width: 27px; height: 27px; border-radius: 999px;
  background: var(--talvesa-color-control-thumb); box-shadow: var(--shadow-sm);
  transform: translateX(0);
  transition: transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
/* Auf dem Akzent die Schrift des Akzents: Der Standardknopf ist weiß, und
   der Akzent ist im dunklen Theme ebenfalls weiß (Tinte der Marke) — ohne
   diese Zeile stand ein weißer Knopf auf weißer Spur. */
.switch[aria-checked="true"] .switch-thumb { transform: translateX(20px); background: var(--accent-ink); }
@media (prefers-reduced-motion: reduce) {
  .switch, .switch-thumb { transition: none; }
}

.settings-row-slider { align-items: flex-start; }
.slider-wrap { display: flex; align-items: center; gap: .7rem; flex: none; width: 100%; max-width: 220px; }
.slider {
  appearance: none; -webkit-appearance: none; flex: 1 1 auto;
  width: 100%; height: 20px; margin: 0; padding: 0; background: transparent; cursor: pointer;
}
.slider::-webkit-slider-runnable-track {
  height: 4px; border-radius: 999px;
  background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--slider-percent, 50%), var(--border) var(--slider-percent, 50%), var(--border) 100%);
}
.slider::-moz-range-track { height: 4px; border-radius: 999px; background: var(--border); }
.slider::-moz-range-progress { height: 4px; border-radius: 999px; background: var(--accent); }
.slider::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 20px; height: 20px; border-radius: 999px; margin-top: -8px;
  background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-sm);
  cursor: grab; transition: transform var(--panel-motion-duration-fast) var(--talvesa-motion-easing-apple-out);
}
.slider::-moz-range-thumb {
  width: 20px; height: 20px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-sm); cursor: grab;
}
.slider:active::-webkit-slider-thumb, .slider:active::-moz-range-thumb { transform: scale(1.1); cursor: grabbing; }
.slider:focus-visible { outline: none; }
.slider:focus-visible::-webkit-slider-thumb, .slider:focus-visible::-moz-range-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .slider::-webkit-slider-thumb, .slider::-moz-range-thumb { transition: none; }
  .slider:active::-webkit-slider-thumb, .slider:active::-moz-range-thumb { transform: none; }
}
.slider-value { min-width: 3.4rem; text-align: right; font-family: var(--font-mono); font-size: .82rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }

/* Native <input type="color"> — the swatch itself is OS-drawn and can't be
   restyled beyond its box, so this only shapes that box to match the rest
   of this file's controls (radius/border) rather than fighting the
   platform's own color-picker UI. */
.color-input {
  appearance: none; -webkit-appearance: none; flex: none;
  width: 44px; height: 32px; padding: 3px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); cursor: pointer;
}
.color-input::-webkit-color-swatch-wrapper { padding: 0; }
.color-input::-webkit-color-swatch { border: none; border-radius: calc(var(--radius-sm) - 3px); }
.color-input::-moz-color-swatch { border: none; border-radius: calc(var(--radius-sm) - 3px); }
.color-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- "Site einrichten" wizard -------------------------------------------
   A numbered <ol> of .card steps (dashboard/templates/dashboard/
   site_setup.html). Reuses .card/.tool-grid/.cmd-box/.hetzner-form
   wholesale — the only genuinely new shapes here are the step number,
   the per-step "erledigt" checkbox, and the done state. */

.setup-progress { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 1.8rem 0 .9rem; }
.setup-progress-text { margin: 0; font-size: .82rem; font-weight: 600; color: var(--text-dim); font-variant-numeric: tabular-nums; }

.setup-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1.1rem; }
.setup-step { margin: 0; }
.setup-step-head { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.setup-step-num {
  flex: none; width: 26px; height: 26px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--border);
  font-family: var(--font-mono); font-size: .78rem; font-weight: 700; color: var(--text-dim);
}
.setup-step-title { margin: 0; flex: 1 1 12rem; font-size: .95rem; font-weight: 700; color: var(--text); }
.setup-step-done { display: flex; align-items: center; gap: .4rem; font-size: .78rem; font-weight: 600; color: var(--text-dim); cursor: pointer; }
.setup-step-done input { width: 16px; height: 16px; accent-color: var(--accent); }
.setup-step-body { display: flex; flex-direction: column; gap: .9rem; margin-top: 1rem; }

/* Done steps stay fully readable (they're still the reference for what
   was run) — only the number switches to the accent, so the eye can find
   "where was I" at a glance without hiding anything. */
.setup-step.is-done .setup-step-num {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--text);
}

.setup-checklist { margin: 0; padding-left: 1.1rem; display: flex; flex-direction: column; gap: .35rem; font-size: .85rem; color: var(--text-dim); }
/* Same cascade gotcha as .tool-grid label[hidden] above — these are
   flex/block containers, so the UA stylesheet's [hidden] rule loses. */
.setup-hint[hidden] { display: none; }

/* --- Touch: was sich mit dem Finger bedienen lässt ----------------------
   Nach Eingabemedium getrennt, nicht nach Fensterbreite — genau die
   Aufteilung, die style_guide dafür vorschreibt (docs/hig-adaption.md,
   "Plattform-Konventionen": „Plattformunterschiede lösen wir über
   Input-Medium (@media (pointer: coarse)), nicht über OS-Sniffing"). Ein
   schmales Fenster auf einem Laptop bleibt damit unangetastet, ein Tablet
   in Desktop-Breite bekommt die größeren Flächen trotzdem.

   Zielwert 44px: derselbe, den docs/hig-adaption.md unter „Touch-Targets"
   als Web-Umsetzung von Apples 44pt nennt und den
   brand/08-barrierefreiheit.md als Ziel über der harten WCAG-2.5.8-Grenze
   von 24px führt. Dieselbe Datei nennt die unsichtbare Hit-Area als das
   Mittel der Wahl, wo ein Bauteil optisch klein bleiben soll — genau das
   machen die ::after-Regeln unten für die beiden 22px-Icon-Knöpfe, statt
   sie auf allen Geräten aufzublähen. */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
  /* Der Weg zurueck zur Startseite, auf jeder Seite die erste Station
     der Leiste — und mit 33px die letzte Stelle dieses Abschnitts, die
     unter dem Zielwert lag. Volle 44px sind hier gefahrlos: Die Marke
     steht am Rand der Leiste, links von ihr ist nichts, was ein zu
     grosszuegiges Ziel abfangen koennte (anders als bei .site-open und
     .k-chip-x, siehe deren Begruendung weiter unten). Nur die Flaeche
     waechst, die negativen Raender der Grundregel bleiben. */
  .brand { min-height: 44px; }
  .theme-toggle { width: 44px; height: 44px; }
  .theme-toggle svg { width: 19px; height: 19px; }
  /* Ein <button class="link"> ist Text ohne eigene Fläche (padding: 0) —
     hier bekommt er eine, ohne dass sich am Aussehen etwas ändert. */
  button.link { padding: .55rem 0; }

  /* 16px ist die Grenze, unter der iOS Safari beim Fokussieren eines
     Feldes ungefragt in die Seite hineinzoomt und den Nutzer danach mit
     einem verschobenen Layout sitzen lässt. Die .88rem der Basisregeln
     liegen darunter; auf Zeigergeräten bleiben sie unverändert. */
  select, input[type="text"], input[type="number"], input[type="email"],
  input[type="password"], input[type="search"], textarea {
    font-size: 1rem; min-height: 44px;
  }
  textarea { min-height: 5.5rem; }

  /* Unsichtbare Trefferflächen für die beiden Icon-Knöpfe, die optisch
     22px bleiben sollen (siehe Abschnittskommentar). Bewusst kleiner als
     44px beim .site-open: er sitzt direkt neben dem Domain-Link der
     Karte, und eine volle 44px-Fläche würde dort Tipper abfangen, die
     dem Nachbarn gelten. */
  /* .k-chip-x („Zuordnung aufheben", 18px) kommt dazu und bekommt aus
     demselben Grund wie .site-open KEINE vollen 44px: Es sitzt IM Chip,
     direkt neben dessen Text. 36px liegt deutlich ueber der harten
     WCAG-2.5.8-Grenze von 24px und ist genau der Tausch, den dieser
     Abschnitt fuer .site-open schon einmal begruendet hat. */
  .site-open, .toast-close, .k-chip-x { position: relative; }
  .site-open::after, .toast-close::after, .k-chip-x::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
  }
  .site-open::after, .k-chip-x::after { width: 36px; height: 36px; }
  .toast-close::after { width: 44px; height: 44px; }

  /* Das ⓘ liegt mit 15px unter derselben 24px-Grenze — und daran haengt
     die einzige Stelle, an der steht, was „Hard-Reset" oder „Neu aufsetzen"
     wirklich tut. Die unsichtbare Flaeche kann hier NICHT ueber ::after
     laufen wie bei .site-open: Bei .info-tip IST ::after die Erklaerblase.
     Also ueber Polsterung plus `background-clip: content-box` — die
     greifbare Flaeche waechst auf 32px, gefaerbt wird weiter nur der
     15px-Kern, und die negativen Raender geben den gewonnenen Platz ans
     Layout zurueck, damit sich in .site-stats und .hetzner-actions nichts
     verschiebt. Die beiden Varianten stehen mit im Selektor, weil ihre
     `background`-Kurzform background-clip sonst wieder zuruecksetzt. */
  .info-tip, .info-tip.is-warn, .info-tip.is-bad {
    width: 32px; height: 32px; padding: 8.5px;
    background-clip: content-box;
    margin: -8.5px; margin-left: calc(.35rem - 8.5px);
  }

  /* Schalter und Farbwaehler der Einstellungsseite: 51x31 bzw. 44x32, also
     beide unter der Grenze — uebersehen, obwohl sie in denselben Zeilen
     sitzen wie .slider, der hier laengst steht. Die sichtbare Groesse
     bleibt (Apples 51x31-Proportion des Schalters eingeschlossen), nur die
     greifbare Flaeche waechst. */
  .switch, .color-input { position: relative; }
  .switch::after, .color-input::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: 44px; height: 44px;
  }

  /* Der Regler ist 20px hoch — unter der harten 24px-Grenze. Die Spur
     selbst bleibt 4px, nur die greifbare Fläche wächst. */
  .slider { height: 44px; }
  /* Jeder Aufklapper dieser Oberflaeche, nicht nur der in der
     Verlaufstabelle: „Als Tabelle", „Mehr", „Verwalten", „Danger Zone",
     „Portalzugaenge", „Wie es zustande kommt" lagen alle zwischen 20 und
     32px. Dieselbe Zeile, nur mit Flaeche. */
  .output-details > summary,
  .site-more-toggle,
  .uptime-table-wrap > summary,
  .links-mehr > summary,
  .hetzner-card-section summary,
  .weitere-sites > summary,
  .k-portal > summary,
  .kasse-tabelle > summary,
  summary.aufklapper { min-height: 44px; }

  /* Ankreuzfelder: Das Kaestchen bleibt optisch 16px, die Trefferflaeche
     ist das Label. Betroffen war ausgerechnet das Hauptbedienelement des
     Einrichtungs-Assistenten („erledigt" je Schritt). */
  .setup-step-done, .tool-grid label.tool-check { min-height: 44px; }

  /* Kunden-Seite: die restlichen Bedienelemente unter der Grenze —
     Icon-Knopf (32px), Auswahlzeile (35px), „Zuordnen" (26px). Die Kasse
     filtert seit dem Umbau ueber .filters — ein Formular fuer alle fuenf
     Kriterien statt einer Chip-Reihe fuer eines — und faellt damit unter
     die Regel fuer Formularfelder weiter unten. */
  .k-icon-btn { width: 44px; height: 44px; }
  .k-picker-row, .k-chip-add, .k-neu-toggle { min-height: 44px; }
  .k-chip-add { display: inline-flex; align-items: center; }
}

/* --- Wider screens: the mobile-first enhancement layer ------------------
   Everything above this point is written mobile-first — a bare rule with
   no media query is the phone-width default, correct and complete on its
   own. This one block collects every "given more room, open up" change in
   one place (same single-block convention this file used for the mobile
   overrides it replaces, just inverted) rather than scattering a
   min-width query after each rule above. 641px, not some rounder number,
   because it's the exact complement of the max-width:640px boundary this
   file used before this pass — same breakpoint, same behavior at any
   given width, only the cascade direction changed. main/.page-title/
   .overview-tile-value aren't listed here: those are fluid (clamp()/vw)
   instead of a hard jump, so they already scale smoothly through this
   boundary and beyond it — see each rule's own comment above. */
@media (min-width: 641px) {
  main {
    padding: 2.2rem 1.6rem 4.5rem;
    padding-left: calc(1.6rem + env(safe-area-inset-left));
    padding-right: calc(1.6rem + env(safe-area-inset-right));
  }
  .seiten-kopf, .page-header { flex-direction: row; align-items: baseline; justify-content: space-between; }
  /* Der Kartenkopf legt Titel und Beiwerk erst hier auf eine gemeinsame
     Grundlinie — darunter stehen sie gestapelt und linksbuendig, weil ein
     Titel neben einem rechtsbuendigen Knopf auf einem schmalen Schirm
     nirgends hin kann. */
  .karten-kopf, .seiten-kopf.ist-abschnitt { flex-direction: row; align-items: baseline; justify-content: space-between; gap: .5rem 1rem; }
  /* Rechtsbuendig erst ab hier: Unterhalb wird jede Zeile zur Karte, in der
     der Wert ohnehin rechts steht (siehe „Tabellen auf schmalen Schirmen"). */
  th.num, td.num { text-align: right; }
  /* Zurück in eine Zeile — siehe die Basisregel für den Grund, warum die
     Elemente darunter jeweils die volle Breite nehmen. */
  .filters > select,
  .filters > input,
  .filters > .btn { flex: 0 1 auto; }
  /* Ab hier ist Platz für Text und Knopf in einer Zeile — der Knopf
     hört auf, die volle Breite zu nehmen, und sitzt rechts. */
  .selfdeploy { align-items: center; padding: 1.15rem 1.4rem; }
  .selfdeploy-btn { flex: 0 0 auto; }
  /* Feste Spaltenzahl statt auto-fit, seit es sechs Kacheln sind: auto-fit
     legte bei üblichen Fensterbreiten fünf davon in die erste Zeile und
     ließ die sechste allein in einer zweiten stehen — eine Übersichts-
     leiste mit einem Ausreißer sieht aus, als fehle etwas. Drei Spalten
     gehen für sechs Kacheln immer glatt auf (2×3); ab 1100px ist Platz
     für alle sechs nebeneinander. Ändert sich die Zahl der Kacheln,
     gehört diese Aufteilung mit angefasst — das ist der Preis dafür,
     dass hier nie eine halbe Zeile steht. */
  .overview-grid { grid-template-columns: repeat(3, 1fr); gap: 1.1rem; }
  .overview-tile { padding: 1.3rem 1.4rem; }
  .card { padding: 1.6rem 1.75rem; }
  .hetzner-subcard { padding: 1.25rem 1.4rem; }
  /* Wider than the pre-mobile-first original (1.4rem 1.5rem) on purpose
     — see the base rule's own comment above for why cramped padding
     genuinely hurt legibility on a card this dense. */
  .hetzner-server-card { padding: 1.85rem 1.75rem; }
  th, td { padding: .8rem 1rem; }

  /* Now free to add columns instead of forcing one/two — see .site-grid
     for the sibling grid that already worked this way at every width. */
  .hetzner-server-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
  .hetzner-card-section-danger[open] { margin: 0 -.9rem; padding: .8rem .9rem 1rem; }
  .hetzner-card-section-danger[open] summary { margin: 0 .9rem; }
  .hetzner-card-section-danger[open] .hetzner-actions { margin-left: .9rem; margin-right: .9rem; }
}

/* --- Sidebar ----------------------------------------------------------------
   .app-nav stops being a horizontal bar and becomes a full-height
   sidebar. 960px, not 641px: a fixed 240px rail needs real room next to
   it before it stops competing with main's own 1200px column for space —
   below this, .app-nav stays the horizontal bar the two blocks above
   already describe, unchanged. */
@media (min-width: 960px) {
  .app-shell { flex-direction: row; align-items: stretch; }
  .app-nav {
    /* nowrap, ausdruecklich: Die Grundregel oben laesst die Leiste als
       Zeile umbrechen, und eine Flex-SPALTE mit fester Hoehe (100vh) und
       flex-wrap: wrap bricht nicht nach unten um, sondern nach RECHTS —
       in eine zweite Spalte neben der Leiste, ueber den Inhalt. Genau
       das war der Fehler: In einem niedrigen Fenster standen „Handbuch"
       und „Einstellungen" mitten in der Seite. Dasselbe gilt fuer
       .app-nav-links darunter. Zu lang darf die Leiste sein — dann
       scrollt sie (overflow-y weiter unten), sie klappt nie zur Seite. */
    flex-direction: column; flex-wrap: nowrap; align-items: stretch;
    flex: none; width: 240px; height: 100vh;
    border-bottom: none; border-right: 1px solid var(--border);
    padding: 1.5rem 1.1rem;
    padding-top: calc(1.5rem + env(safe-area-inset-top));
    padding-left: calc(1.1rem + env(safe-area-inset-left));
    gap: 0;
  }
  /* Dissolves the mobile sheet's wrapper — .app-nav-links/.app-nav-foot
     go back to being direct flex children of the now-vertical .app-nav,
     same as before this pass ever introduced .app-nav-panel (see that
     rule's own header comment for why the wrapper exists at all). */
  .app-nav-panel { display: contents; }
  /* Eigenes senkrechtes Scrollen: Die Leiste ist `sticky` mit 100vh, und
     bei zehn Navigationspunkten braucht sie rund 600px. Sobald das Fenster
     niedriger ist oder jemand den Textregler ueber 100% stellt, waren
     „Einstellungen", „Sicherheit" und „Logout" abgeschnitten und mit keiner
     Geste erreichbar — die Seite scrollt, die klebende Leiste nicht.
     Die andere Haelfte ist, dass .app-nav-links unten NICHT schrumpft
     (flex-shrink 0): Nur wenn die Liste ihre volle Hoehe behaelt, wird
     die Leiste ueberhaupt hoeher als 100vh, und erst dann greift dieses
     Scrollen. Frueher stand hier `flex: 1; min-height: 0` — die Liste
     schrumpfte auf den Rest, und ihre Punkte brachen als zweite und
     dritte Spalte nach rechts aus (siehe den nowrap-Kommentar oben). */
  .app-nav { overflow-y: auto; overscroll-behavior: contain; padding-bottom: calc(1.5rem + env(safe-area-inset-bottom)); }
  /* Die Seitenleiste ist eine Spalte mit `gap: 0` — jedes Kind bringt
     seinen Abstand selbst mit. Die Marke bringt einen NEGATIVEN mit
     (margin: -.2rem -.4rem gleicht ihr Hover-Polster aus, damit der
     Schriftzug buendig zur Leiste steht), und das Suchfeld darunter
     brachte gar keinen: gemessen lagen beide 3,2px UEBEREINANDER. Der
     Ausgleichsrand ist richtig, er braucht nur ein Gegenstueck. */
  .nav-suche { margin-top: var(--space-5); }
  .app-nav-links { flex-direction: column; flex-wrap: nowrap; order: 0; width: auto; flex: 1 0 auto; gap: .15rem; margin-top: var(--space-8); }
  /* padding-top zusaetzlich zum auto-Rand: Der Rand faellt auf 0, sobald
     die Leiste hoeher als das Fenster ist (dann scrollt sie, siehe oben)
     — ohne den festen Abstand klebte der Theme-Knopf direkt unter
     „Einstellungen". */
  .app-nav-foot { flex-direction: column; align-items: stretch; margin-left: 0; margin-top: auto; padding-top: var(--space-6); gap: .8rem; }
  .theme-toggle { align-self: flex-start; }
  .who { flex-direction: column; align-items: flex-start; gap: .4rem; }
  .app-content { flex: 1; min-width: 0; }
}

/* Ab hier ist neben der 240px-Seitenleiste Platz für alle sieben Kacheln
   der Übersichtsleiste in einer Zeile — siehe die Drei-Spalten-Regel im
   641px-Block oben für die Begründung, warum die Spaltenzahl hier fest
   ist und nicht auto-fit überlassen wird. */
@media (min-width: 1240px) {
  .overview-grid { grid-template-columns: repeat(7, 1fr); }
}

/* --- Ressourcen-Ansicht ---------------------------------------------- */
/* dashboard/templates/dashboard/resources.html. Ein Balken je Site: Die
   Tabelle daneben hat sieben Zahlenspalten, und "belegt diese Site ihre
   Grenze" ist die einzige davon, die man auf einen Blick sehen soll statt
   zu lesen. */

.res-summary { display: flex; flex-wrap: wrap; gap: 1.6rem 2.4rem; margin-top: .4rem; }
.res-stat { display: flex; flex-direction: column; gap: .25rem; min-width: 10rem; }
.res-stat-label { font-size: .78rem; color: var(--text-dim); }
.res-stat-value { font-size: 1.3rem; font-weight: 700; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.res-stat-sub { font-size: .8rem; font-weight: 600; color: var(--text-dim); margin-left: .35rem; }
.res-warn { color: var(--warn-text); }

.res-bar {
  height: .5rem;
  border-radius: 999px;
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border) inset;
  overflow: hidden;
}
.res-bar-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  /* Die Breite kommt als Inline-Style aus der Vorlage (ein Prozentwert,
     schon serverseitig auf 0-100 begrenzt). Der Übergang ist nur dafür
     da, dass ein Neuladen nicht springt. */
  transition: width var(--panel-motion-duration-base) ease;
}
.res-bar-warn { background: var(--warn); }
.res-bar-label { font-size: .78rem; font-family: var(--font-mono); color: var(--text-dim); }

@media (prefers-reduced-motion: reduce) {
  .res-bar-fill { transition: none; }
}

.res-profile {
  display: inline-block;
  font-size: .74rem;
  font-weight: 700;
  padding: .2rem .5rem;
  border-radius: 999px;
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border) inset;
  white-space: nowrap;
}
.res-profile-heavy { color: var(--accent); }

.res-unlimited {
  margin-top: 1.2rem;
  padding: .9rem 1rem;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border) inset;
}
.res-unlimited-title { margin: 0; font-size: .92rem; color: var(--warn-text); }
.res-unlimited-list { display: flex; flex-wrap: wrap; gap: .4rem; }
/* Slug und Kunde gehoeren zusammen und duerfen deshalb nicht zwischen
   zwei Zeilen auseinanderbrechen. */
.res-unlimited-eintrag { display: inline-flex; align-items: baseline; gap: .35rem; }
.res-unlimited-kunde { font-size: .76rem; color: var(--text-dim); }
/* Der Kunde unter dem Slug in der Tabelle: kleiner, ruhiger, zweite
   Zeile — er beantwortet eine Rueckfrage, er ist nicht die Zeile. */
.res-zeile-kunde { display: block; font-size: .74rem; }
.res-slug {
  font-family: var(--font-mono);
  font-size: .8rem;
  padding: .2rem .5rem;
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--border) inset;
}

/* Die Ressourcen-Tabelle hat sieben Spalten und damit als einzige im
   Panel eine echte Mindestbreite — auf Handybreite scrollt sie in ihrem
   .table-wrap waagerecht, statt die Seite zu sprengen. */
@media (max-width: 640px) {
  .res-summary { gap: 1rem 1.6rem; }
  .res-stat-value { font-size: 1.1rem; }

  /* Auf dem Handy ist die Karte die volle Seitenbreite — die 1.5rem
     Innenabstand links und rechts kosten dort rund ein Siebtel des
     lesbaren Platzes, während sie am Desktop nur hübsch aussehen. */
  .site-card { padding: 1.15rem 1.1rem; gap: .75rem; }
  /* Die drei Werte sollen auch dann nebeneinander bleiben: Untereinander
     wären sie wieder die Liste, die diese Umstellung gerade abgeschafft
     hat. Deshalb hier eine engere Untergrenze statt eines Umbruchs. */
  .site-stats { gap: .5rem 1rem; padding: .6rem .7rem; }
  .site-stat-value { font-size: .88rem; }
  .site-stat-label { font-size: .7rem; }
  /* Die Karten-Kennzahlen bleiben drei Spalten — nur enger gepolstert. */
  .site-card .site-stats { gap: 0; padding: 0; }
  .site-card .site-stat { padding: .6rem .6rem .65rem; }
  .site-card .site-stat-value { font-size: .9rem; }
  .site-card-head { --avatar: 2.3rem; }
  .site-avatar { font-size: 1rem; }
  /* Aktionen: gleich breite Knöpfe statt einer ausgefransten Reihe, in
     der „Redeploy“ doppelt so breit ist wie „Stop“ (siehe .site-actions). */
  .site-actions { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
  .site-actions > form, .site-actions > .btn { width: 100%; }
  /* Volle Breite hatten sie schon, aber keine Hoehe: ~37px, bei .btn-sm
     ~32px. Denselben 44px-Standard setzt diese Datei fuer .site-actionbar
     .btn und .output-details > summary laengst — hier fehlte er nur. */
  .site-actions .btn, .site-more-actions .btn { width: 100%; justify-content: center; min-height: 44px; }
  /* Auf der Karte bleibt es bei der einen Reihe aus 7. oben — Symbolknöpfe
     quadratisch mit 44px, statt im Zweierraster eine halbe Zeile zu füllen. */
  .site-card .site-actions { display: flex; }
  .site-card .btn-lifecycle { width: 44px; min-width: 44px; }
  .site-more-toggle, .uptime-table-wrap > summary { min-height: 44px; }
}

/* --- Site-Karte: „Mehr“ ------------------------------------------------
   Die Karte hatte nur zwei mögliche Zustände: knapp und unvollständig,
   oder vollständig und unlesbar. Ein <details> muss sich für keinen von
   beiden entscheiden — zugeklappt kostet es eine Zeile, offen steht dort,
   was sonst nur die Detailseite zeigte. */

.btn-sm { padding: .4rem .8rem; font-size: .8rem; }

.site-more { margin-top: -.2rem; }
.site-more-toggle {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .8rem; font-weight: 600; color: var(--text-dim);
  cursor: pointer; padding: .25rem .1rem;
  /* Den nativen Dreiecks-Marker abschalten — der eigene Chevron dreht
     sich mit und sitzt auf der Grundlinie des Textes, der Marker nicht. */
  list-style: none;
  transition: color var(--panel-motion-duration-fast) ease;
}
.site-more-toggle::-webkit-details-marker { display: none; }
.site-more-toggle:hover { color: var(--text); }
/* Tastaturbedienung: <summary> ist von sich aus fokussierbar, der Ring
   muss aber sichtbar bleiben — sonst ist der einzige Hinweis darauf, wo
   man gerade steht, die Farbe, und die trägt hier schon Bedeutung. */
.site-more-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.site-more-chevron {
  width: 1em; height: 1em; flex: 0 0 auto;
  transition: transform var(--panel-motion-duration-fast) ease;
}
.site-more[open] .site-more-chevron { transform: rotate(90deg); }

.site-more-panel {
  display: flex; flex-direction: column; gap: .75rem;
  margin-top: .5rem; padding-top: .75rem;
  border-top: 1px solid var(--border);
}

/* Zwei Spalten aus Beschriftung und Wert. Grid statt zweier Listen, damit
   die Werte untereinander an derselben Kante beginnen — genau das, was
   die alte „Label: Wert“-Reihe auf der Karte nicht konnte. */
.site-more-facts { display: grid; gap: .35rem 1rem; margin: 0; font-size: .8rem; }
.site-more-facts > div { display: grid; grid-template-columns: 7.5rem 1fr; gap: .5rem; align-items: baseline; }
.site-more-facts dt { color: var(--text-dim); }
.site-more-facts dd { margin: 0; color: var(--text); word-break: break-word; }
.site-more-facts .is-warn { color: var(--warn-text); }

.site-more-actions { display: flex; flex-wrap: wrap; gap: .5rem; }

@media (max-width: 640px) {
  /* Auf dem Handy reicht der Platz nicht für eine feste
     Beschriftungsspalte — Wert unter Beschriftung statt daneben. */
  .site-more-facts > div { grid-template-columns: 1fr; gap: 0; }
  .site-more-facts dt { font-size: .74rem; }
  .site-more-actions { display: grid; grid-template-columns: 1fr 1fr; }
  .site-more-actions > form, .site-more-actions .btn { width: 100%; }
  .site-more-actions .btn { justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .site-more-chevron { transition: none; }
}

/* --- Rechtstext-Befund -------------------------------------------------
   dashboard/templates/dashboard/compliance.html. Die Einordnung („Befund",
   „Hinweis", „Je nach Rechtsform") steht als Etikett VOR dem Text, nicht
   als Farbe am Rand: Ein Befund, dessen Gewicht man nur an einem Farbton
   ablesen kann, ist für alle unlesbar, die diesen Farbton nicht
   unterscheiden — und hier hängt an der Einordnung, ob jemand handeln
   muss oder nicht. */

.compliance-disclaimer { border-left: 3px solid var(--accent); }
.compliance-disclaimer p { margin: .5rem 0 0; font-size: .86rem; line-height: 1.6; }

.compliance-head { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-start; justify-content: space-between; }

.finding-list { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
.finding {
  display: flex; gap: .7rem; align-items: flex-start;
  padding: .7rem .85rem; border-radius: var(--radius-md);
  background: var(--surface-2); box-shadow: 0 0 0 1px var(--border) inset;
}
/* Das Ergebnis der ganzen Pruefung („3 Punkte zum Nachsehen" / „Keiner der
   geprueften Punkte ist aufgefallen") stand als h2.card-title — also der
   wichtigste Satz der Seite in ihrer leisesten Zeile, leiser als die drei
   Haftungsabsaetze darueber. Eine eigene Stufe, weil hier ein Befund steht
   und kein Kartenetikett. */
.befund-titel { margin: 0; font-size: 1.15rem; font-weight: 700; letter-spacing: -.01em; color: var(--text); }

.finding-badge {
  flex: 0 0 auto; font-size: .72rem; font-weight: 700;
  letter-spacing: .02em; padding: .22rem .5rem; border-radius: 999px;
  background: var(--surface); box-shadow: 0 0 0 1px var(--border) inset;
  white-space: nowrap;
}
.finding-bad .finding-badge { color: var(--bad-text); }
.finding-warn .finding-badge { color: var(--warn-text); }
.finding-info .finding-badge { color: var(--text-dim); }
.finding-title { margin: 0; font-weight: 650; font-size: .9rem; }
.finding-detail { margin: .25rem 0 0; font-size: .82rem; color: var(--text-dim); line-height: 1.55; }

.is-ok { color: var(--ok-text); }

@media (max-width: 640px) {
  /* Etikett über den Text statt daneben — nebeneinander bliebe für den
     Befundtext eine Spalte von rund zwei Dritteln der Kartenbreite. */
  .finding { flex-direction: column; gap: .4rem; }
  .finding-badge { align-self: flex-start; }
}

/* --- Zwei-Faktor (dashboard/views_security.py) --------------------------- */

/* QR-Code links, Erklärung rechts — und bei schmalem Fenster untereinander,
   weil ein QR-Code, der auf Handybreite auf 40% skaliert wird, von der
   eigenen Kamera nicht mehr sauber erfasst wird. */
.twofa-setup { display: flex; flex-wrap: wrap; gap: 1.6rem; align-items: flex-start; }
.twofa-setup-text { flex: 1 1 260px; min-width: 0; }
/* Feste Breite und weißer Grund unabhängig vom Theme: Scanner erwarten
   dunkle Module auf hellem Grund (siehe two_factor.qr_svg). Das Polster
   gehört zur Ruhezone, nicht zur Optik. */
.twofa-qr {
  flex: 0 0 auto; background: #fff; padding: .6rem; border-radius: var(--radius-sm);
  line-height: 0; box-shadow: 0 0 0 1px var(--border) inset;
}
.twofa-qr svg { width: 190px; height: 190px; display: block; }

.twofa-secret {
  margin: 0; font-size: 1rem; letter-spacing: .06em; word-break: break-all;
  background: var(--surface); padding: .55rem .7rem; border-radius: var(--radius-sm);
  box-shadow: 0 0 0 1px var(--border) inset;
}

/* .twofa-actions steht jetzt bei .aktionszeile weiter oben — derselbe
   Baustein, ein Name mehr, damit die Vorlagen umziehen koennen. */

/* Zwei Spalten, damit zehn Codes nicht zu einer Liste werden, die man
   scrollen muss, um sie abzuschreiben. */
.twofa-codes {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: .45rem 1.2rem; margin: 0; padding: 0 0 0 1.6rem;
  font-size: .95rem; letter-spacing: .04em;
}
.twofa-codes li { padding: .15rem 0; }

@media (max-width: 640px) {
  .twofa-qr svg { width: 160px; height: 160px; }
}

/* --- Erreichbarkeits-Historie (dashboard/services/uptime_history.py) ----- */

.uptime-block { display: block; }

.uptime-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .6rem; margin-bottom: .8rem; }
/* Die Kennzahl als Zahl, groß, in Textfarbe mit Status-Akzent — nicht als
   farbiger Balken ohne Beschriftung. Wer Farben nicht unterscheidet, liest
   hier trotzdem alles. */
.uptime-value { font-size: 1.7rem; font-weight: 650; letter-spacing: -.01em; line-height: 1.1; }
.uptime-value.uptime-ok { color: var(--ok-text); }
.uptime-value.uptime-warn { color: var(--warn-text); }
.uptime-value.uptime-bad { color: var(--bad-text); }
.uptime-value.uptime-none { color: var(--text-dim); }
.uptime-head-sub { font-size: .86rem; color: var(--text-dim); }

/* Ein Feld je Tag. Die 2px-Lücke gehört zur Trefferfläche: Die Felder
   sitzen in einem Flex-Container ohne gap, den Abstand macht ein
   durchsichtiger Rand — so ist die anklickbare/hoverbare Fläche so breit
   wie das Feld plus Abstand, statt schmaler als das Feld. */
.uptime-strip { display: flex; width: 100%; height: 34px; align-items: stretch; }
.uptime-day {
  flex: 1 1 0; min-width: 0; border-radius: 3px;
  border-left: 1px solid transparent; border-right: 1px solid transparent;
  background-clip: padding-box;
}
/* background-COLOR, nicht die Kurzform `background`: Die Kurzform setzt
   background-clip mit zurück auf border-box, und dann füllt die Farbe den
   durchsichtigen Rand oben gleich mit aus — der Streifen sieht aus wie ein
   einziger Balken, in dem die Tagesgrenzen verschwinden. */
.uptime-day.uptime-ok { background-color: var(--ok); }
.uptime-day.uptime-warn { background-color: var(--warn); }
.uptime-day.uptime-bad { background-color: var(--bad); }
/* „Keine Messung" ist ausdrücklich kein Grün: Ein Tag, an dem niemand
   nachgesehen hat, ist kein Tag ohne Störung. */
.uptime-day.uptime-none { background-color: var(--border-strong); }
.uptime-day:hover { outline: 2px solid var(--text-dim); outline-offset: 1px; }

.uptime-scale {
  display: flex; justify-content: space-between;
  font-size: .72rem; color: var(--text-dim); margin-top: .35rem;
}
.uptime-note { font-size: .84rem; color: var(--text-dim); margin: .8rem 0 0; line-height: 1.6; }

.uptime-table-wrap { margin-top: .9rem; }
.uptime-table-wrap > summary {
  cursor: pointer; font-size: .82rem; color: var(--text-dim);
  list-style: none; display: inline-flex; align-items: center; gap: .35rem;
}
.uptime-table-wrap > summary::-webkit-details-marker { display: none; }
.uptime-table-wrap > summary::before { content: "▸"; font-size: .7em; transition: transform .15s ease; }
.uptime-table-wrap[open] > summary::before { transform: rotate(90deg); }
.uptime-table-wrap > summary:hover { color: var(--text); }
.uptime-table-wrap .table-wrap { margin-top: .6rem; max-height: 320px; overflow-y: auto; }

@media (max-width: 640px) {
  /* Auf Handybreite bleiben je Feld rund 9 Pixel — schmaler als jede
     brauchbare Trefferfläche, und daran ändert auch mehr Höhe nichts.
     Der Streifen wird trotzdem höher (er bleibt als Überblick lesbar),
     der eigentliche Weg zum einzelnen Tag ist hier aber die Tabelle
     darunter — deshalb steht sie überhaupt da. */
  .uptime-strip { height: 44px; }
  .uptime-value { font-size: 1.45rem; }
}

/* Wie derselbe Streifen im Kunden-Portal aussieht (dort bekommt er mehr
   Höhe, weil er auf dieser Seite die Hauptsache ist), steht in
   portal.css — die Bausteine sind geteilt, ihre Größe ist es nicht. */

/* --- Tabellen auf schmalen Schirmen: aus jeder Zeile wird eine Karte ---

   Bis hierher konnte jede Tabelle dieses Panels auf einem Telefon genau
   eines: waagerecht scrollen. Bei sechs oder sieben Spalten heißt das,
   dass man eine Zeile nie ganz sieht — man wischt nach rechts, verliert
   dabei die erste Spalte aus dem Blick und weiß nicht mehr, zu welcher
   Site der Wert gehört, den man gerade liest. Eine Tabelle, die man nur
   scrollend lesen kann, ist auf dem Handy keine Tabelle, sondern ein
   Hindernis.

   Ab hier wird deshalb unterhalb von 640px aus jeder Zeile eine kleine
   Karte, in der jede Zelle ihre eigene Spaltenüberschrift trägt. Das
   passiert vollständig in CSS: Die Überschrift kommt aus
   `td[data-label]`, das im Template steht — kein JavaScript, genau wie
   beim aufklappbaren <details> der Ausgabe-Spalte. Ohne JS funktioniert
   also alles, und eine Tabelle, die das Attribut vergisst, fällt in
   dashboard/tests/test_tabellen_mobil.py auf.

   Der bewusste Preis: Sobald `display` von den Tabellen-Werten abweicht,
   verlieren Browser die Tabellen-Semantik im Accessibility-Baum („Spalte
   3 von 6"). Für diesen Fall ist das der bessere Tausch — die Spalte
   steht jetzt als Text in jeder Zelle, statt nur in einer Kopfzeile, die
   beim Scrollen nach rechts ohnehin niemand mehr sieht. Am Desktop
   (ab 641px) bleibt alles eine echte Tabelle, unverändert.

   `data-label=""` heißt ausdrücklich „diese Zelle braucht keine
   Beschriftung" — die Aktionsspalte mit dem Knopf darin ist genau das.
   Das leere Attribut ist trotzdem gesetzt, damit „vergessen" und
   „braucht keine" unterscheidbar bleiben (und der Test das prüfen kann). */

@media (max-width: 640px) {
  /* Nichts mehr zu scrollen — die Sperre gegen die Zurück-Geste des
     Browsers (overscroll-behavior-x oben) bleibt trotzdem stehen, weil
     einzelne Zellen (ein <pre> mit Log-Ausgabe) weiterhin breiter
     werden dürfen als der Schirm. */
  .table-wrap { overflow-x: visible; }
  .table-wrap table,
  .table-wrap tbody,
  .table-wrap tr,
  .table-wrap td { display: block; }

  /* Die Kopfzeile ist jetzt in jeder Zelle — sie ein zweites Mal über
     der Liste zu zeigen, wäre dieselbe Angabe doppelt. */
  .table-wrap thead { display: none; }

  .table-wrap tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    margin-bottom: .7rem;
    overflow: hidden;
  }
  .table-wrap tbody tr:last-child { margin-bottom: 0; }
  /* Zebrastreifen trennen Zeilen in einer Liste. Sobald jede Zeile ihre
     eigene Karte mit eigenem Rand ist, trennt der Rand — und der
     Wechsel zwischen zwei Kartenfarben sähe aus, als bedeute er etwas. */
  .table-wrap tbody tr:nth-child(even) { background: var(--surface-2); }
  .table-wrap tbody tr:hover { background: var(--surface-2); }

  .table-wrap td {
    display: flex;
    gap: 1rem;
    align-items: baseline;
    justify-content: space-between;
    padding: .55rem .8rem;
    border-bottom: 1px solid var(--border);
  }
  .table-wrap tr td:last-child { border-bottom: none; }

  .table-wrap td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    color: var(--text-dim);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .02em;
    /* Die Beschriftung ist die Spalte, nicht der Wert — sie darf nie
       umbrechen und damit breiter wirken als das, was sie beschriftet. */
    white-space: nowrap;
  }

  /* Der Wert rechts, und zwar umbrechend: Eine lange Domain oder ein
     Fingerprint darf die Karte dehnen, aber nicht über den Rand des
     Schirms hinausschieben. */
  .table-wrap td > * { min-width: 0; }
  .table-wrap td {
    text-align: right;
    overflow-wrap: anywhere;
  }

  /* Zellen, deren Inhalt selbst ein Block ist — die aufklappbare
     Ausgabe, eine Reihe von Knöpfen, ein ganzes Formular. Nebeneinander
     mit der Beschriftung wird das gequetscht; hier steht die
     Beschriftung darüber und der Inhalt darunter über die volle
     Breite. */
  .table-wrap td.cell-block {
    display: block;
    text-align: left;
  }
  .table-wrap td.cell-block::before {
    display: block;
    margin-bottom: .4rem;
  }

  /* Die Aktionsspalte trägt keine Überschrift (data-label=""): nur der
     Knopf, rechtsbündig, ohne eine leere Zeile davor. */
  .table-wrap td[data-label=""]::before { content: none; }
  .table-wrap td[data-label=""] { justify-content: flex-end; }
  .table-wrap td.cell-block[data-label=""]::before { display: none; }

  /* Eine Zelle, in der am Ende doch nur ein Strich steht („keine
     Ausgabe"), braucht die Blockform nicht — sie sähe als einzige Zeile
     der Karte anders aus als alle darüber. */
  .table-wrap td.cell-block:has(> .output-empty) {
    display: flex;
    text-align: right;
  }
  .table-wrap td.cell-block:has(> .output-empty)::before {
    display: inline;
    margin-bottom: 0;
  }

  /* Den Umriss hat .btn-danger inzwischen ueberall (siehe dort) — hier
     bleibt nur, ihn auf dem Telefon kraeftiger zu ziehen: In einer
     Tabellenzeile steht „Entfernen" zwischen lauter Text, und ein
     halbtransparenter Rand geht darin unter. */
  .table-wrap .btn-danger { box-shadow: 0 0 0 1px var(--bad) inset; }

  /* Eine Trefferfläche, die man auch trifft: 44px ist das Maß, unter dem
     ein Fingertipp zur Glückssache wird. Am Desktop bleiben die Knöpfe
     kompakt, weil dort ein Mauszeiger zielt. */
  .table-wrap td .btn { min-height: 44px; }

  /* Die Kopfzeile ist weg, also braucht auch niemand mehr an ihr zu
     kleben. Sticky mit display:none am <thead> ist harmlos, aber der
     Vollständigkeit halber: */
  .table-wrap th { position: static; }
}

/* --- „Was geht live?" (dashboard/services/github.py) --------------------

   Sitzt über dem Redeploy-Knopf, nicht daneben: Die Liste beantwortet die
   Frage, die man beim Ansehen dieses Knopfes hat, und soll deshalb vor dem
   Klick gelesen werden, nicht danach gefunden. Ein Kasten, keine Kachel —
   eine eigene Kachel würde die Aktionen auseinanderreißen, zu denen die
   Angabe gehört. */

.pending {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  padding: .85rem 1rem;
  margin-bottom: 1.1rem;
}
/* Nur wenn wirklich etwas anliegt, bekommt der Kasten die Akzentkante.
   „Nichts Neues" ist eine beruhigende Auskunft und soll nicht aussehen
   wie eine Aufforderung. */
.pending-ahead { border-left: 3px solid var(--accent); }

.pending-head { margin: 0; font-size: .9rem; line-height: 1.5; }
.pending-head strong { font-size: 1.05rem; }
.pending-current { color: var(--text-dim); }

.pending-list {
  list-style: none; margin: .7rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .35rem;
}
.pending-list li {
  display: flex; gap: .6rem; align-items: baseline;
  font-size: .86rem; line-height: 1.45;
}
.pending-sha { flex: none; font-size: .8rem; color: var(--text-dim); }
.pending-sha:hover { color: var(--accent); }
/* Der Betreff bekommt den Platz und darf umbrechen; Kurz-SHA und Autor
   sind kurz und bleiben dort, wo man sie erwartet. */
.pending-subject { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.pending-author { flex: none; font-size: .78rem; }

.pending-note { margin: .7rem 0 0; font-size: .8rem; line-height: 1.5; }

@media (max-width: 640px) {
  /* Nebeneinander bleibt vom Betreff auf 390px kaum mehr als ein Wort je
     Zeile übrig. Er bekommt deshalb eine eigene Zeile über die volle
     Breite — und Kurz-SHA und Autor teilen sich die Zeile darüber, statt
     je eine eigene zu verbrauchen. Drei Zeilen je Commit waren bei zehn
     Commits eine Bildschirmlänge nur für Metadaten. */
  .pending-list li { flex-wrap: wrap; gap: .1rem .6rem; }
  .pending-author { order: 1; }
  .pending-subject { flex: 1 1 100%; order: 2; }
  /* Etwas Luft zwischen den Commits, wenn jeder zwei Zeilen hoch ist —
     ohne sie fließen sie zu einem Block zusammen. */
  .pending-list { gap: .7rem; }
}

/* --- Backup-Nachweis (deploy_client.get_backup_verify) -----------------

   Bewusst derselbe Kasten wie „Was geht live?" darüber und nicht noch ein
   eigenes Muster: Beides sind Auskünfte, die vor einer Entscheidung
   stehen. Die Farbe der linken Kante trägt hier aber wirklich etwas —
   „nie geprüft" ist kein neutraler Zustand, und die Kachel soll nicht
   beruhigend aussehen, solange niemand den Beweis geführt hat. Deshalb
   bekommt „none" die Warnfarbe und nicht Grau: Grau liest sich wie
   „egal", und das ist es gerade nicht. */

.proof {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  padding: .9rem 1rem;
}
.proof-ok { border-left: 3px solid var(--ok); }
.proof-none { border-left: 3px solid var(--warn); }
.proof-bad { border-left: 3px solid var(--bad); }
/* Bestanden, aber zu lange her: die Kante wechselt auf Warn, der Text
   darunter sagt warum. */
.proof-ok.proof-stale { border-left-color: var(--warn); }

.proof-head { margin: 0; font-size: .95rem; line-height: 1.5; }
.proof-note { margin: .5rem 0 0; font-size: .85rem; color: var(--text-dim); line-height: 1.6; }
.proof-note.is-warn { color: var(--warn-text); }
.proof-note.is-bad { color: var(--bad-text); }
.proof form, .proof .note { margin-top: .9rem; display: inline-block; }

/* --- Zeitstrahl (dashboard/services/timeline.py) -----------------------

   Eine Spur, keine Tabelle: Die Aussage ist die REIHENFOLGE zweier
   verschiedener Sorten Ereignis, und dafür ist eine Linie mit Punkten das
   Bild, das man ohnehin im Kopf hat. Eine Tabelle mit einer Spalte „Art"
   wäre dieselbe Information in einer Form, die das Nebeneinander gerade
   nicht zeigt.

   Die senkrechte Linie kommt aus einem Rand am <li>, nicht aus einem
   eigenen Element — so ist sie automatisch genau so lang wie die Liste
   und braucht keine Höhe, die jemand pflegen muss. */

.tl { list-style: none; margin: 0; padding: 0; }
.tl-item {
  position: relative;
  padding: 0 0 1.1rem 1.4rem;
  border-left: 2px solid var(--border);
}
/* Der letzte Punkt schließt die Linie ab, statt sie ins Leere laufen zu
   lassen. */
.tl-item:last-child { border-left-color: transparent; padding-bottom: 0; }
.tl-item:last-child::after {
  content: ""; position: absolute; left: -2px; top: 0; height: .55rem;
  border-left: 2px solid var(--border);
}

.tl-dot {
  position: absolute; left: -7px; top: .3rem;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--border-strong);
  box-shadow: 0 0 0 3px var(--surface);
}
.tl-ok .tl-dot { background: var(--ok); }
.tl-warn .tl-dot { background: var(--warn); }
.tl-bad .tl-dot { background: var(--bad); }
/* Ein Ausfall ist kein Punkt wie jeder andere — er bekommt einen Ring,
   damit er sich auch ohne Farbe von einer Aktion unterscheidet. */
.tl-outage .tl-dot { box-shadow: 0 0 0 3px var(--surface), 0 0 0 5px var(--bad-glow); }

.tl-head { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .6rem; }
.tl-title { font-weight: 600; font-size: .92rem; }
.tl-when { font-size: .8rem; color: var(--text-dim); }
.tl-ago { font-size: .78rem; }
/* overflow-wrap: Die Fehlermeldung kommt vom fremden Server und kann eine
   URL oder eine Stacktrace-Zeile ohne ein einziges Leerzeichen sein. Ohne
   das drückt sie die Karte über den Schirm hinaus — heute passt sie, das
   ist keine Eigenschaft des Codes. */
.tl-detail {
  margin: .3rem 0 0; font-size: .85rem; color: var(--text-dim);
  line-height: 1.6; overflow-wrap: anywhere;
}
.tl-detail .status-pill { vertical-align: middle; }

/* Die Nähe-Zeile: eingerückt und leiser als der Ausfall darüber, weil sie
   eine Beobachtung zu ihm ist und keine zweite Tatsache. */
.tl-near {
  margin: .4rem 0 0; padding: .4rem .6rem;
  font-size: .82rem; line-height: 1.55;
  border-left: 2px solid var(--warn);
  background: var(--surface-2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.tl-output { margin-top: .4rem; }
/* Der gemeinsame Include heisst „Anzeigen" und begruendet das damit, dass
   die Spaltenueberschrift darueber schon sagt, WAS angezeigt wird. In der
   Verlaufstabelle stimmt das; im Zeitstrahl steckt derselbe Include in
   einem nackten <div> ohne jede Beschriftung. Dieselbe Behandlung wie
   .field-label und die Spaltenkoepfe. */
.tl-output-label { display: block; margin-bottom: .25rem; font-size: .72rem; font-weight: 600; color: var(--text-dim); }

@media (max-width: 640px) {
  /* Datum und „vor 3 Std." nebeneinander unter dem Titel statt in einer
     Zeile mit ihm — sonst bricht der Titel mitten im Wort um. */
  .tl-title { flex: 1 1 100%; }
  .tl-item { padding-left: 1.1rem; }
}

/* --- Zugriffe (deploy_client.get_visitors) -----------------------------

   Ein Balken je Tag, Höhe nach Menge. Bewusst ANDERS als der
   Erreichbarkeits-Streifen weiter oben, obwohl beide „ein Feld je Tag"
   sind: Dort ist die Aussage ein Zustand (erreichbar/nicht), und Zustände
   trägt Farbe. Hier ist sie eine Menge, und Mengen trägt Länge — eine
   Zahl über Farbsättigung abzulesen funktioniert bei niemandem.

   Der höchste Tag ist 100 %. Eine feste Skala gäbe es nur um den Preis
   einer erfundenen Obergrenze; was die Balken beantworten, ist „wann war
   mehr los als sonst", und dafür ist der relative Vergleich genau
   richtig. Die absoluten Zahlen stehen im Tooltip und in der Tabelle. */

.hits-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .6rem; margin-bottom: .9rem; }
.hits-value { font-size: 1.7rem; font-weight: 650; letter-spacing: -.01em; line-height: 1.1; color: var(--accent); }
.hits-sub { font-size: .86rem; color: var(--text-dim); }

.hits-chart {
  display: flex; align-items: flex-end; gap: 2px;
  width: 100%; height: 64px;
}
/* Der äußere Balken ist die volle Höhe und damit die Trefferfläche: So
   trifft der Zeiger auch einen Tag mit zwei Zugriffen, dessen Füllung nur
   ein paar Pixel hoch ist. */
.hits-bar {
  flex: 1 1 0; min-width: 0; height: 100%;
  display: flex; align-items: flex-end;
  border-radius: 2px;
}
.hits-bar:hover { background: var(--surface-2); }
.hits-fill {
  width: 100%; border-radius: 2px;
  background: var(--accent);
  /* Ein Tag mit wenigen Zugriffen darf nicht unsichtbar werden — sonst
     liest sich „wenig" wie „nichts", und das sind verschiedene Tage. */
  min-height: 2px;
}
/* Ein Tag mit Serverfehlern: dieselbe Höhe, andere Farbe. Die Menge
   bleibt die Menge, der Hinweis kommt dazu. */
.hits-fill-err { background: var(--bad); }

.hits-note { margin: .9rem 0 0; font-size: .8rem; line-height: 1.6; }

@media (max-width: 640px) {
  .hits-chart { height: 52px; }
  .hits-value { font-size: 1.45rem; }
}

/* --- Wartungsmodus -------------------------------------------------------
   Der eine Zustand, in dem jede andere Anzeige dieser Seite stimmt und
   trotzdem das Falsche nahelegt: Der Container läuft, die Statuspille
   steht auf „läuft", das Backup ist frisch — und kein Besucher kommt
   durch. Deshalb wird hier nicht gedämpft, sondern markiert.

   --warn und nicht --bad: Wartung ist kein Fehler, sondern eine
   Entscheidung. Rot hieße „hier ist etwas kaputt" und schickte jemanden
   auf die Suche nach einer Ursache, die es nicht gibt. */
.wartung-band {
  /* Oben ausgerichtet und nicht mittig: Der Text bricht auf einer
     schmalen Karte auf zwei bis drei Zeilen um, und ein mittig
     ausgerichtetes Symbol stünde dann mitten in der zweiten. */
  display: flex; align-items: flex-start; gap: .55rem;
  margin: 0 0 .8rem;
  padding: .5rem .7rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 38%, transparent);
  color: var(--warn-text);
  font-size: .82rem; line-height: 1.45;
}
.wartung-band svg { width: 16px; height: 16px; flex: 0 0 auto; margin-top: .12rem; }

.wartung-an {
  padding: .9rem 1rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
}
.wartung-head { margin: 0; line-height: 1.6; }
.wartung-head strong { color: var(--warn-text); }
/* Der Grund steht in einem eigenen Kasten, nicht im selben Absatz: Er
   kommt von einem Menschen und ist das Einzige hier, was der Server
   nicht selbst weiß. */
.wartung-grund {
  margin: .7rem 0 0;
  padding: .55rem .7rem;
  border-left: 2px solid color-mix(in srgb, var(--warn) 55%, transparent);
  background: var(--surface-2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  line-height: 1.6;
}
.wartung-note { margin: .7rem 0 0; font-size: .8rem; line-height: 1.6; }
.wartung-an form { margin: .9rem 0 0; }

/* Die eigene Lage an einer laufenden Wartung. Abgesetzt durch eine Linie
   statt durch eine weitere Farbe: Der Kasten ist schon warnfarben, und ein
   zweiter Ton darin hieße, es gäbe zwei Befunde. Es ist nur einer, aus zwei
   Blickwinkeln. */
.wartung-ich {
  margin: .8rem 0 0;
  padding: .6rem 0 0;
  border-top: 1px solid color-mix(in srgb, var(--warn) 26%, transparent);
  font-size: .84rem; line-height: 1.6;
}
/* Ausgenommen ist eine gute Nachricht und bekommt deshalb keine Warnfarbe.
   Gedämpft, nicht grün: „Du kommst durch" ist eine Feststellung, kein
   Erfolg, den man feiern müsste. */
/* --text-dim, nicht --muted: Das Token gibt es nicht (das Panel hat die
   Klasse .muted und das Token --text-dim). Ein var() ohne Fallback loest zu
   NICHTS auf, die Deklaration fiel aus, und der Satz stand in voller
   Textfarbe da — genau das Gegenteil des Kommentars zwei Zeilen darueber. */
.wartung-ich-durch { color: var(--text-dim); }
.wartung-ich + .cmd-box { margin-top: .6rem; }

@media (max-width: 640px) {
  .wartung-an { padding: .8rem .75rem; }
  /* Eine Trefferflaeche, die man auch trifft: 44px ist das Mass, unter
     dem ein Fingertipp zur Gluecksache wird. Am Desktop bleiben die
     Knoepfe kompakt, weil dort ein Mauszeiger zielt. Die Kante haben
     .btn-danger und .btn-warn inzwischen ohnehin ueberall (siehe dort),
     hier geht es nur noch um die Hoehe. */
  .wartung-an .btn, .card .btn-danger, .card .btn-warn { min-height: 44px; }
}

/* --- Tote Links ----------------------------------------------------------
   Die Adressen sind das Einzige, was hier zählt, und sie sind lang. Also
   bekommen sie die Breite und dürfen umbrechen — abgeschnitten mit „…"
   wären sie zum Nachsehen unbrauchbar, und genau das ist der Zweck
   dieser Tabelle. */
.links-head { margin: 0 0 .9rem; line-height: 1.6; }
/* Die Tabelle erbt ab hier die Basisregeln (siehe „Tables" weiter oben).
   Eigene Polsterung, eigene Schriftgroesse und VERSAL-Kopfzeilen machten
   aus zwei Tabellen auf derselben Seite zwei verschiedene Bauteile fuer
   dieselbe Sache — nebeneinander gescrollt sah das aus wie ein Versehen,
   und keine andere Tabelle des Panels schreibt ihre Kopfzeile gross.
   Uebrig bleibt, was wirklich anders ist: oben ausgerichtete Zellen. Eine
   ueber drei Zeilen umbrechende Adresse neben einer einzeiligen Statuszahl
   saehe sonst aus, als gehoerten die beiden nicht zusammen. */
.links-table td { vertical-align: top; }
/* `anywhere` und nicht `break-all`: Ersteres bricht bevorzugt an den
   Stellen, die eine Adresse ohnehin hat (/ und -), und nur mitten im
   Wort, wenn es nicht anders geht. Mit `break-all` stand in der ersten
   Fassung „katalog-202 / 4.pdf" — lesbar, aber unnötig hässlich. */
.links-url { overflow-wrap: anywhere; }

.links-mehr { margin-top: 1rem; }
.links-mehr > summary { cursor: pointer; font-size: .84rem; color: var(--text-dim); }

/* Auf dem Handy übernimmt die Kartenform, die .table-wrap für jede
   Tabelle dieses Panels mitbringt — hier steht ausdrücklich kein
   eigener @media-Block. Die beiden Adress-Zellen tragen im Template
   `cell-block`, weil eine volle URL neben ihrer Beschriftung nur
   gequetscht würde; das ist genau der Fall, für den es diese Klasse
   gibt. */

/* --- „Weitere Sites": der Aufklapper unter dem Kartenraster --------------
   Die Sites, die nicht in PANEL_FAVORITE_SITES stehen. Ihre Karten holt
   panel.js beim ersten Aufklappen nach; hier steht nur, wie der
   Aufklapper selbst aussieht.

   Bewusst ruhiger als eine Karte: Das ist ein Behälter, kein Inhalt. Er
   soll unter dem Raster stehen, ohne mit ihm um Aufmerksamkeit zu
   konkurrieren. */

.weitere-sites {
  /* Beide Seiten, nicht nur oben: Ohne den unteren Rand klebte die
     naechste Karte gemessen mit 0px daran — jede .card bringt unten
     --space-7 mit, dieses <details> war das einzige Geschwister auf
     dieser Ebene ohne. Dieselbe Stufe, damit die Uebersicht einen
     Rhythmus hat statt zweier verschiedener Abstaende. */
  margin-top: var(--space-7);
  margin-bottom: var(--space-7);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.weitere-sites > summary {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .85rem 1.1rem;
  cursor: pointer;
  font-weight: 600;
  /* Eigener Chevron statt des nativen Markers, gleiche Entscheidung wie
     bei .site-more-toggle weiter oben. */
  list-style: none;
}
.weitere-sites > summary::-webkit-details-marker { display: none; }
.weitere-sites > summary::before {
  content: "";
  width: .5rem; height: .5rem; flex: 0 0 auto;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--panel-motion-duration-fast) ease;
}
.weitere-sites[open] > summary::before { transform: rotate(45deg); }
.weitere-sites > summary:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
  border-radius: var(--radius);
}
.weitere-sites-zahl {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.5rem; padding: .1rem .4rem;
  border-radius: 999px;
  background: var(--surface-2, rgba(127, 127, 127, .16));
  font-size: .78rem; font-variant-numeric: tabular-nums;
}
/* Die Slug-Liste ist Beiwerk: Sie sagt, was drinsteckt, ohne dass man
   aufklappen muss — und verschwindet, sobald der Platz knapp wird,
   statt die Überschrift umzubrechen. */
.weitere-sites-slugs {
  font-weight: 400; font-size: .8rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 1 1 12rem;
}
@media (max-width: 640px) { .weitere-sites-slugs { display: none; } }
.weitere-sites-inhalt { padding: 0 1.1rem 1.1rem; }
.weitere-sites-fallback { margin: 0; font-size: .85rem; }
/* Während geladen wird: Der Platz gehört schon dem, was kommt, damit die
   Seite nicht springt, sobald die Karten da sind. */
.weitere-sites-inhalt[aria-busy="true"] { min-height: 6rem; opacity: .6; }

/* --- Nachgeladene Seitenteile ([data-nachladen-url], panel.js) ----------
   Das Geruest hat ungefaehr die Form dessen, was kommt, damit die Seite
   beim Eintreffen nicht springt. Die Flaechen schimmern statt zu blinken:
   ein ruhiger Lichtstreifen sagt „kommt gleich", ohne zu draengeln. Der
   Satz darueber traegt die Ansage fuer Screenreader (role=status); das
   Geruest ist aria-hidden. */
.nachladen-status {
  display: flex; align-items: center; gap: .55rem;
  margin: 0 0 1rem; font-size: .9rem; color: var(--text-dim);
}
.nachladen-status .btn-spinner { color: var(--accent); }
.skelett {
  border-radius: var(--radius-sm, 6px);
  background: linear-gradient(90deg,
    var(--surface-2) 0%,
    color-mix(in srgb, var(--surface-2) 45%, var(--text) 8%) 50%,
    var(--surface-2) 100%);
  background-size: 200% 100%;
  animation: skelett-schimmer 1.4s ease-in-out infinite;
}
@keyframes skelett-schimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
.skelett-zeile { height: .9rem; margin: .7rem 0; width: 70%; }
.skelett-zeile:nth-child(odd) { width: 85%; }
.skelett-breit { width: 55%; height: 1.1rem; margin: 0 0 1rem; }
.skelett-balken { height: 12px; margin: 0 0 1.4rem; border-radius: 999px; }
.skelett-titel { height: 1rem; width: 9rem; margin-bottom: 1rem; }
.skelett-reihe { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.skelett-block { height: 3.2rem; }
@media (prefers-reduced-motion: reduce) {
  .skelett { animation: none; }
}

/* --- Ressourcen: Gruppen ------------------------------------------------ */

.res-gruppe { margin-bottom: 1.8rem; }
.res-gruppe:last-child { margin-bottom: 0; }
.res-gruppe-kopf {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .8rem; flex-wrap: wrap;
  margin-bottom: .15rem;
}
.res-gruppe-titel {
  margin: 0; font-size: .95rem; font-weight: 700; letter-spacing: -.01em;
}
.res-gruppe-zahl {
  display: inline-block; margin-left: .3rem; padding: .05rem .4rem;
  border-radius: 999px; background: var(--surface-2, rgba(127, 127, 127, .16));
  font-size: .75rem; font-weight: 600; letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.res-gruppe-summe { margin: 0; font-size: .85rem; font-variant-numeric: tabular-nums; }
.res-gruppe-hinweis { margin: 0 0 .7rem; font-size: .8rem; }

/* --- Kunden-Seite (dashboard/templates/dashboard/clients.html) ----------
   Die Seite handelt von Zugehörigkeit: wem gehört welches Projekt, und was
   kostet das. Alles hier folgt daraus — das Anlegen sitzt hinter einem
   Knopf, der Arbeitsvorrat steht oben, und Projekte sind Chips, weil man
   sie als Menge liest und nicht als Aufzählung.

   Die Sprache ist bewusst zurückhaltend: „gehört uns" ist eine Einordnung,
   kein Zustand. Deshalb gestrichelte Kanten statt Farbe — ein roter oder
   grüner Ton wäre eine Wertung, die niemand gemeint hat. */

/* Anlegen: ein <summary>, das wie ein Knopf aussieht. Aufklappen ist
   Browser-Verhalten, kein JavaScript — wie überall sonst hier. */
.k-neu { margin-bottom: 1.5rem; }
.k-neu-toggle { list-style: none; user-select: none; }
.k-neu-toggle::-webkit-details-marker { display: none; }
.k-neu-toggle svg { width: 15px; height: 15px; transition: transform var(--panel-motion-duration-fast) ease; }
.k-neu[open] .k-neu-toggle svg { transform: rotate(45deg); }
/* Derselbe Ausstieg, den .output-details > summary::before schon hat — hier
   fehlte er als Einziges. */
@media (prefers-reduced-motion: reduce) {
  .k-neu-toggle svg { transition: none; }
}
.k-neu-karte { margin-top: .8rem; max-width: 42rem; }
.k-neu-frage { margin: 0 0 .55rem; font-size: .8rem; font-weight: 600; color: var(--text-dim); }

/* Kunde oder eigene Firma: zwei gleichwertige Felder statt eines Häkchens
   mit Fließtext daneben. Es ist die erste Entscheidung und beeinflusst
   jede Summe danach — sie gehört nicht an den Rand. */
.k-typwahl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-bottom: 1.3rem; }
.k-typ {
  display: flex; gap: .65rem; align-items: flex-start;
  border: 1.5px solid var(--border); border-radius: var(--radius-md);
  padding: .8rem .85rem; cursor: pointer;
  transition: border-color var(--panel-motion-duration-fast) ease, background-color var(--panel-motion-duration-fast) ease;
}
.k-typ:hover { border-color: var(--text-dim); }
.k-typ input { margin: .15rem 0 0; flex: 0 0 auto; accent-color: var(--accent); }
.k-typ:has(input:checked) { border-color: var(--accent); background: var(--accent-glow); }
.k-typ:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.k-typ-inhalt { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.k-typ-titel { display: flex; align-items: center; gap: .45rem; font-size: .88rem; font-weight: 650; }
.k-typ-text { font-size: .78rem; line-height: 1.45; color: var(--text-dim); }
.k-neu-felder { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 .9rem; }
/* JEDES Feld so breit wie seine Spalte. `.field input` gab es schon,
   `select` nicht — und ein Auswahlfeld, das nur so breit ist wie sein
   laengster Eintrag, laesst in einem Formular aus sonst vollbreiten
   Feldern eine ausgefranste Kante stehen. Auf dem Telefon, wo alles
   untereinander steht, sind das vier verschiedene Breiten uebereinander. */
.k-neu-felder .field select,
.k-neu-felder .field input:not([type="checkbox"]) { width: 100%; }
/* Der globale Notnagel an `select` (max-width: 100vw - 5rem) haelt ein
   Auswahlfeld davon ab, ein Telefon zu sprengen, wo es in nichts steckt,
   das es begrenzt. Hier steckt es in einer Spalte, die es begrenzt — und
   die Deckelung liesse das Feld neun Pixel schmaler stehen als das
   Textfeld darueber. */
.k-neu-felder .field select { max-width: 100%; }
/* Die Beschriftung einer Feldgruppe (die Ankreuzfelder) sieht aus wie
   jedes <label> daneben: Es ist ein <span>, weil ein <label> ohne
   zugehoeriges Feld keines ist. */
.k-neu-felder .field > .field-label { display: block; font-size: .82rem; font-weight: 400; color: var(--text-dim); margin-bottom: .3rem; }
.k-neu-fuss { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; padding-top: .9rem; border-top: 1px solid var(--border); }
.k-neu-hinweis { font-size: .78rem; margin-left: auto; }
@media (max-width: 640px) {
  .k-typwahl, .k-neu-felder { grid-template-columns: minmax(0, 1fr); }
  .k-neu-hinweis { margin-left: 0; }
  /* Der Knopf nimmt die Breite, die er auf einem Telefon braucht: Ein
     kleines Ziel am linken Rand unter zehn vollbreiten Feldern ist das
     Gegenteil von dem, worauf der Daumen zielt. */
  .k-neu-fuss { flex-direction: column; align-items: stretch; gap: .6rem; }
  .k-neu-fuss .btn { width: 100%; min-height: 44px; justify-content: center; }
  .k-neu-hinweis { text-align: center; }
}

/* Kostenkacheln. Die eigene Firma bekommt eine gestrichelte Kante — das
   ist dieselbe Aussage wie das „intern"-Abzeichen, nur in Kachelform. */
.k-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .8rem; margin-bottom: .7rem; }
.k-tile { border: 1px solid var(--border-strong); border-radius: var(--radius); padding: .9rem 1rem; background: var(--surface); }
.k-tile-eigen { border-style: dashed; border-color: var(--border); }
.k-tile-label { display: block; font-size: .74rem; font-weight: 600; color: var(--text-dim); margin-bottom: .35rem; }
/* Proportionale Ziffern: Ein einzeln stehender Wert bekommt jeder 1 sonst
   die Breite einer 0, und „18 Tage" steht auseinandergezogen da.
   Tabellarisch wird erst wieder, was in einer Spalte untereinander steht. */
.k-tile-wert { display: block; font-size: 1.5rem; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: proportional-nums; }
.k-tile-wert-still { color: var(--text-dim); }
.k-tile-sub { display: block; font-size: .73rem; color: var(--text-dim); margin-top: .15rem; }
.k-kosten-note { font-size: .76rem; line-height: 1.55; max-width: var(--mass-text); margin: 0 0 1.7rem; }

/* Arbeitsvorrat: Projekte ohne Zuordnung. Steht oben, weil sie in keiner
   Kostenzeile auftauchen, solange niemand entscheidet. */
.k-tray { margin-bottom: 1.4rem; }
.k-tray-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
/* Die Warnfarbe sitzt am KOPF, nicht im SVG: Das Symbol war das einzige
   dieser Oberflaeche mit fest verdrahtetem stroke — es erbt seine Farbe
   jetzt ueber currentColor wie jedes andere, und ein Kopf, der einmal einen
   anderen Zustand traegt, nimmt es mit. */
.k-tray-head svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--warn-text); }
.k-tray-titel { margin: 0; font-size: .88rem; font-weight: 650; }
.k-tray-sub { font-size: .78rem; }
.k-zahl {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.4rem; padding: .05rem .4rem; border-radius: 999px;
  background: var(--surface-2); color: var(--text-dim);
  font-size: .74rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.k-frei { display: flex; flex-wrap: wrap; gap: .5rem; }
.k-frei-item {
  display: inline-flex; align-items: center; gap: .6rem;
  background: var(--bg-2); border: 1px solid var(--border-strong);
  border-radius: var(--radius-md); padding: .4rem .45rem .4rem .75rem;
}
.k-frei-namen { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.k-frei-slug { font-size: .8rem; font-weight: 600; }
.k-frei-domain { font-size: .7rem; color: var(--text-dim); }

/* Abschnitte: Kunden und eigene Firma sind benannt, nicht bloß markiert. */
.k-section-head { display: flex; align-items: baseline; gap: .6rem; margin: 0 0 .7rem .15rem; }
/* Satzkasten: „KUNDEN" / „EIGENE FIRMA" ist sichtbarer deutscher Text und
   faellt damit unter dieselbe Regel wie jeder andere. Die Abstufung macht
   Groesse, Gewicht und --text-dim. */
.k-section-head h2 { margin: 0; font-size: .88rem; font-weight: 700; letter-spacing: 0; color: var(--text-dim); }
.k-section-head .muted { font-size: .74rem; }
.k-liste { display: flex; flex-direction: column; gap: .75rem; margin-bottom: 1.6rem; }

.k-card {
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius); padding: 1.1rem 1.15rem;
}
.k-card-eigen { border-left: 3px dashed var(--border); }
.k-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: .85rem; }
.k-card-wer { min-width: 0; }
/* Die Kundennummer neben dem Namen („K26001"): klein, in Monospace, als
   Knopf (klicken kopiert). Leise, bis man hinfährt — sie ist eine Angabe zum
   Nachschlagen, nicht zum Überwachen. */
.k-nummer {
  font: inherit; font-family: var(--font-mono); font-size: .74rem; font-weight: 600;
  padding: .12rem .5rem; border: none; border-radius: 999px; cursor: copy;
  background: var(--surface-2); color: var(--text-dim);
  box-shadow: 0 0 0 1px var(--border) inset;
  transition: color var(--panel-motion-duration-fast) ease, box-shadow var(--panel-motion-duration-fast) ease;
}
.k-nummer:hover, .k-nummer:focus-visible { color: var(--text); box-shadow: 0 0 0 1px var(--border-strong) inset; }
.k-nummer:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Die Projektnummer im Projekt-Chip: noch eine Stufe leiser, sie steht
   neben dem Slug, der den Chip benennt. */
.k-projektnr {
  font: inherit; font-family: var(--font-mono); font-size: .7rem;
  padding: 0 .3rem; border: none; border-radius: var(--radius-sm); cursor: copy;
  background: none; color: var(--text-dim);
}
.k-projektnr:hover, .k-projektnr:focus-visible { color: var(--text); background: var(--surface-2); }
.k-projektnr:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.k-card-name { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: 0 0 .15rem; font-size: 1.05rem; font-weight: 650; letter-spacing: -.01em; }
.k-card-meta, .k-card-notiz { margin: 0; font-size: .78rem; }
.k-card-notiz { margin-top: .2rem; }
.k-card-rechts { display: flex; align-items: center; gap: .85rem; flex-shrink: 0; }
.k-card-kosten { text-align: right; }
.k-card-kosten-wert { display: block; font-size: .95rem; font-weight: 650; font-variant-numeric: tabular-nums; }
.k-card-eigen .k-card-kosten-wert { color: var(--text-dim); }
.k-card-kosten-sub { display: block; font-size: .7rem; color: var(--text-dim); }
/* Geschaefts- oder Privatkunde. Steht als kleine Zeile unter den
   Metadaten und nicht neben den Icon-Knoepfen rechts: Es ist eine
   Eigenschaft dieses Kunden und keine Aktion an ihm. */
.k-card-art { display: flex; align-items: center; gap: .4rem; margin-top: .45rem; }
.k-card-art select { font-size: .78rem; padding: .2rem .4rem; }
/* Die Kette unter den Projekt-Chips: was die Sites dieses Kunden belegen,
   und ob die wiederkehrende Einnahme das traegt. Eine Zeile, die
   umbricht, statt einer Tabelle — es sind zwei bis drei Angaben, und sie
   gehoeren nebeneinander gelesen. */
.k-kette { display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem 1.1rem; margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--border); }
.k-kette-teil { display: flex; align-items: baseline; gap: .4rem; }
.k-kette-label { font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); }
.k-kette-wert { font-size: .82rem; font-variant-numeric: tabular-nums; }
.k-kette-satz { flex-basis: 100%; font-size: .76rem; line-height: 1.5; }
.k-kette-warn { color: var(--warn-text); }
/* „Was wir berechnen" (_client_card.html): die eingehenden Verträge
   dieses Kunden als Zeilen, darunter das Schnellformular. Dieselbe
   Trennlinie wie .k-kette darunter — es ist die erste Hälfte derselben
   Rechnung (was reinkommt), die Kette die zweite (was es kostet). */
.k-abrechnung { margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: .55rem; }
.k-abrechnung-kopf { display: flex; align-items: baseline; justify-content: space-between; gap: .8rem; }
.k-abrechnung-summe { font-size: .95rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.k-abrechnung-summe .muted { font-size: .75rem; font-weight: 500; }
.k-abrechnung-liste { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
.k-abrechnung-zeile {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .15rem .9rem;
  padding: .55rem .75rem; border-radius: var(--radius-md);
  background: var(--surface-2);
}
.k-abrechnung-zeile.ist-still { opacity: .6; }
.k-abrechnung-name { font-weight: 650; font-size: .86rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.k-abrechnung-betrag { font-weight: 700; font-size: .9rem; font-variant-numeric: tabular-nums; text-align: right; }
.k-abrechnung-takt { font-size: .74rem; }
.k-abrechnung-aktionen { display: flex; gap: .35rem; justify-content: flex-end; }
.k-abrechnung-neu > summary { list-style: none; cursor: pointer; width: max-content; }
.k-abrechnung-neu > summary::-webkit-details-marker { display: none; }
.k-abrechnung-neu[open] > summary { margin-bottom: .55rem; }
.k-abrechnung-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .55rem .7rem; }
.k-abrechnung-feld { display: flex; flex-direction: column; gap: .2rem; font-size: .72rem; color: var(--text-dim); flex: 1 1 8rem; min-width: 0; }
.k-abrechnung-feld input, .k-abrechnung-feld select { font-size: .85rem; padding: .4rem .55rem; width: 100%; }
.k-abrechnung-feld-betrag { flex: 0 1 7rem; }
.k-abrechnung-form .btn { flex: none; }
@media (max-width: 640px) {
  .k-abrechnung-feld { flex-basis: calc(50% - .35rem); }
  .k-abrechnung-feld-betrag { flex: 1 1 calc(50% - .35rem); }
  .k-abrechnung-form .btn { width: 100%; justify-content: center; min-height: 44px; }
}
/* .5rem statt .2rem: Zwei identisch aussehende 32px-Icons dicht
   nebeneinander, von denen eines still saemtliche Serverkosten eines Kunden
   in die andere Summe verschiebt und das andere den Eintrag loescht — der
   Abstand ist hier kein Geschmack, sondern der Unterschied zwischen zwei
   Knoepfen und einem Fehlgriff. */
.k-card-aktionen { display: flex; gap: .5rem; }

/* Ruhige Icon-Knöpfe: Auf einer Karte, die von Zugehörigkeit handelt, sind
   Umschalten und Löschen Nebensachen — sie sollen erreichbar sein, nicht
   um Aufmerksamkeit konkurrieren. Farbe bekommt Löschen erst beim
   Darüberfahren. */
.k-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0;
  border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-dim); cursor: pointer;
  transition: background-color var(--panel-motion-duration-fast) ease, color var(--panel-motion-duration-fast) ease;
}
.k-icon-btn svg { width: 17px; height: 17px; }
.k-icon-btn:hover { background: var(--surface-2); color: var(--text); }
/* Dauerhaft gefaerbt, nicht erst beim Ueberfahren: Auf einem Telefon gibt
   es kein Ueberfahren, und „Loeschen" saehe dort aus wie „Umschalten". */
.k-icon-btn-gefahr { color: var(--bad-text); }
.k-icon-btn-gefahr:hover { background: var(--bad); color: var(--accent-ink); }
.k-icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Projekte als Chips: kurze Namen, nebeneinander gelesen. Das × sitzt im
   Chip, damit Entfernen dort passiert, wo das Ding steht. */
.k-chips { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.k-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  background: var(--surface-2); border-radius: 999px;
  padding: .28rem .3rem .28rem .7rem;
  font-size: .78rem; font-weight: 500;
}
.k-card-eigen .k-chip { background: var(--bg-2); border: 1px dashed var(--border-strong); }
.k-chip-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; padding: 0;
  border: none; border-radius: 999px;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text-dim); cursor: pointer;
  transition: background-color var(--panel-motion-duration-fast) ease, color var(--panel-motion-duration-fast) ease;
}
.k-chip-x svg { width: 11px; height: 11px; }
.k-chip-x:hover { background: var(--bad); color: var(--accent-ink); }
.k-chip-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.k-chips-leer { font-size: .78rem; }
.k-chip-add {
  display: inline-flex; align-items: center; gap: .3rem;
  background: transparent; border: 1px dashed var(--border);
  border-radius: 999px; padding: .28rem .75rem;
  font-size: .78rem; font-weight: 600; color: var(--text-dim);
  cursor: pointer; list-style: none; user-select: none;
  transition: border-color var(--panel-motion-duration-fast) ease, color var(--panel-motion-duration-fast) ease;
}
.k-chip-add::-webkit-details-marker { display: none; }
.k-chip-add svg { width: 12px; height: 12px; }
.k-chip-add:hover { border-color: var(--accent); color: var(--accent); }
.k-picker-wrap[open] > .k-chip-add { border-style: solid; border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }
.k-picker-wrap[open] > .btn { background: var(--accent-strong); }

/* Die Auswahl. Absichtlich im Fluss und nicht als Overlay: ein <details>
   ohne JavaScript kann sich nicht selbst schließen, und ein Menü, das über
   dem Inhalt hängen bleibt, wäre schlimmer als eines, das die Karte kurz
   höher macht. */
.k-picker-wrap { position: relative; }
.k-picker {
  margin-top: .5rem;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); box-shadow: var(--shadow);
  overflow: hidden; max-height: 20rem; overflow-y: auto;
  min-width: min(22rem, 100%);
}
.k-picker-head {
  padding: .5rem .7rem; background: var(--bg-2);
  border-bottom: 1px solid var(--border-strong);
  font-size: .76rem; font-weight: 700; color: var(--text-dim);
}
.k-picker-head-still { border-top: 1px solid var(--border-strong); background: transparent; }
.k-picker-row {
  display: flex; align-items: center; gap: .55rem; width: 100%;
  padding: .5rem .7rem; border: none; background: transparent;
  font: inherit; text-align: left; color: var(--text); cursor: pointer;
  transition: background-color var(--panel-motion-duration-fast) ease;
}
.k-picker-row:hover { background: var(--accent-glow); }
.k-picker-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.k-picker-plus { width: 15px; height: 15px; flex: 0 0 auto; color: var(--accent); }
.k-picker-namen { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.k-picker-slug { font-size: .8rem; font-weight: 600; }
.k-picker-domain { font-size: .7rem; color: var(--text-dim); }
.k-picker-name { font-size: .82rem; font-weight: 600; }
.k-picker-row-vergeben { cursor: default; justify-content: space-between; opacity: .55; }
.k-picker-row-vergeben:hover { background: transparent; }
.k-picker-inhaber { font-size: .7rem; color: var(--text-dim); white-space: nowrap; }
/* Text und Farbe kommen von .leer-zeile weiter oben; hier bleibt nur die
   Polsterung, die dieser eine Ort im Auswahlmenue braucht. */
.k-picker-leer { padding: .6rem .7rem; }

/* Portal-Zugänge: je Kunde selten, machen die Karte unruhig, wenn sie
   immer offen stehen. Zugeklappt, mit der Anzahl in der Zeile. */
.k-portal { margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border-strong); }
.k-portal > summary {
  display: inline-flex; align-items: center; gap: .45rem;
  font-size: .76rem; font-weight: 600; color: var(--text-dim);
  cursor: pointer; list-style: none; user-select: none;
}
.k-portal > summary::-webkit-details-marker { display: none; }
.k-portal > summary::before {
  content: ""; width: .4rem; height: .4rem; flex: 0 0 auto;
  border-right: 1.8px solid currentColor; border-bottom: 1.8px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--panel-motion-duration-fast) ease;
}
.k-portal[open] > summary::before { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .k-portal > summary::before { transition: none; }
}
.k-portal > summary:hover { color: var(--text); }
.k-portal > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.k-portal-inhalt { margin-top: .7rem; display: flex; flex-direction: column; gap: .5rem; }
.k-portal-zeile { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.k-portal-zeile code { font-size: .78rem; }
.k-portal-leer { margin: 0; font-size: .78rem; }
.k-portal-neu { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.k-portal-neu input { flex: 1 1 12rem; min-width: 0; font-size: .82rem; }

/* Das „intern"-Abzeichen — auf der Kundenseite, auf den Site-Karten der
   Übersicht und in der Typwahl dasselbe Ding, also eine Regel. */
.own-badge {
  display: inline-flex; align-items: center;
  border: 1px dashed var(--border); border-radius: 999px;
  padding: 0 .45rem; font-size: .66rem; font-weight: 650;
  color: var(--text-dim); letter-spacing: .01em; white-space: nowrap;
}
/* Voller Name statt zwölf Zeichen („Wiland Ku…"): gekürzt wird erst, wenn
   der Name breiter ist als die Karte. „intern" trägt den Akzent — auf einen
   Blick von einem Kundennamen zu unterscheiden, nicht nur an einer
   gestrichelten Kante. */
.site-owner {
  display: inline-flex; align-items: center; gap: .3rem;
  min-width: 0; max-width: 100%;
  padding: .12rem .55rem .12rem .45rem; border-radius: 999px;
  font-size: .72rem; font-weight: 650; line-height: 1.45;
  color: var(--text-dim); text-decoration: none;
  background: var(--surface-2); box-shadow: 0 0 0 1px var(--border) inset;
  transition: color var(--panel-motion-duration-fast) ease, box-shadow var(--panel-motion-duration-fast) ease;
}
.site-owner svg { width: 12px; height: 12px; flex: none; }
.site-owner span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.site-owner:hover { color: var(--text); box-shadow: 0 0 0 1px var(--border-strong) inset; }
.site-owner.is-own {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent) inset;
  text-transform: uppercase; letter-spacing: .05em; font-size: .66rem; font-weight: 750;
}
.site-owner.is-own:hover { box-shadow: 0 0 0 1px var(--accent) inset; }

/* --- Stand, Verlauf, Leads (clients.html, leads.html) -------------------
   Die drei Bauteile, die auf beiden Seiten dasselbe tun: woran man ist,
   wer sich kümmert, was besprochen wurde. Sie sitzen in derselben Karte
   wie alles andere und bekommen deshalb KEINE eigene Farbe — eine
   Leadkarte ist eine Kundenkarte, die eine Stufe früher steht, und die
   gestrichelte Kante sagt genau das (dieselbe Sprache wie „intern"). */

.k-card-lead { border-left: 3px dashed var(--accent); }

/* Stand und Zuständigkeit: zwei Ein-Feld-Formulare nebeneinander. Auf
   dem Telefon untereinander, weil zwei Auswahlfelder plus zwei Knöpfe
   in einer Zeile dort beides unlesbar machen. */
/* Art, Stand und Zuständigkeit eines Kunden: drei Endpunkte, eine Frage
   — also eine Zeile, die umbricht, statt dreier Zeilen, die wie drei
   verschiedene Vorgänge aussehen. */
.k-felder { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem 1.8rem; margin-top: .45rem; }
.k-felder > .k-card-art, .k-felder > .k-stand { margin-top: 0; }
@media (max-width: 640px) {
  /* Untereinander, und jede Zeile gleich breit: Feld waechst, Knopf
     bleibt. Nebeneinander umgebrochen standen hier drei verschieden
     breite Auswahlfelder mit drei „Merken" an drei verschiedenen
     Stellen — ein Haufen Bedienelemente statt drei Angaben. */
  .k-felder { display: grid; grid-template-columns: minmax(0, 1fr); gap: .5rem; }
  /* `.k-stand` ist die HUELLE um zwei Formulare und stapelt sie nur —
     die Zwei-Spalten-Form gehoert an die Formulare selbst, sonst landet
     das zweite Formular in der Knopfspalte des ersten. */
  .k-felder .k-stand {
    display: grid; grid-template-columns: minmax(0, 1fr); gap: .5rem;
    width: 100%; margin: 0;
  }
  .k-felder .k-card-art,
  .k-felder .k-stand-feld {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: center; gap: .5rem; width: 100%; margin: 0;
  }
  .k-felder .k-card-art select,
  .k-felder .k-stand-feld select { width: 100%; max-width: 100%; }
  /* Die Ankreuzfelder fuellen die erste Spalte und behalten ihren Knopf
     rechts — sie sind das Feld dieser Zeile. */
  .k-felder .k-wer-feld { min-width: 0; }
}
/* Zwei Formulare mit je einem „Merken" nebeneinander: Der Abstand
   dazwischen ist groesser als der innerhalb eines Formulars, sonst steht
   der zweite Knopf naeher an der fremden Auswahl als an seiner eigenen. */
.k-stand { display: flex; flex-wrap: wrap; gap: .4rem 1.8rem; margin-top: .45rem; }
.k-stand-feld { display: flex; align-items: center; gap: .4rem; }

/* Wer sich kuemmert: Ankreuzfelder, keine Auswahlliste — es koennen
   mehrere sein. Ein `select multiple` verlangt dafuer auf dem Telefon
   einen gedrueckt gehaltenen Finger und auf dem Rechner die
   Steuerungstaste; beides weiss niemand, der es nicht schon weiss.
   Das <fieldset> traegt nur die Gruppe zusammen und bringt seinen
   eigenen Rahmen und Innenabstand nicht mit. */
.k-wer-feld {
  display: flex; align-items: center; flex-wrap: wrap; gap: .2rem .7rem;
  border: none; margin: 0; padding: 0; min-width: 0;
}
/* Ueber die Gruppe adressiert (zwei Klassen) und nicht ueber die Klasse
   allein: `.field label` im Anlegeformular ist `display: block` und
   spezifischer als eine einzelne Klasse — ohne das steht das Kaestchen
   ueber seinem Namen statt daneben. */
.k-wer-feld .k-wer-name {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .78rem; cursor: pointer; margin: 0;
  min-height: 1.8rem;
}
.k-wer-feld .k-wer-name input {
  accent-color: var(--accent); margin: 0; flex: 0 0 auto;
  /* Volle Breite ist die Regel fuer jedes Feld dieses Formulars — fuer
     ein Ankreuzfeld waere sie ein handtellergrosses Kaestchen. */
  width: auto;
}
@media (hover: none) {
  /* 44px hoch auf Zeigegeraeten ohne Maus — dieselbe Groesse wie jedes
     andere Ziel dieser Oberflaeche (siehe .k-neu-toggle). */
  .k-wer-feld .k-wer-name { min-height: 44px; }
}
/* Kleiner als ein normales Auswahlfeld, aber MIT Platz für den Pfeil
   rechts: Ein `padding`-Kurzform überschreibt das padding-right von
   `select:not([multiple])` (2.25rem) und schiebt den längsten Wert
   („Warte auf Rückmeldung") sonst unter den Pfeil. Gilt für alle drei
   Felder dieser Zeile, damit sie gleich aussehen. */
.k-felder select, .k-stand-feld select { font-size: .78rem; padding: .2rem 1.9rem .2rem .45rem; }

/* „niemand zuständig" ist keine Warnung, sondern eine offene Aufgabe —
   also der Akzentton und nicht Gelb. */
.k-offen { color: var(--accent); font-weight: 600; }

/* Der Verlauf. Zugeklappt, aber nicht stumm: In der Zeile steht die
   Anzahl und die neueste Notiz mit ihrem Alter. Ein Aufklapper, hinter
   dem man erst nachsehen muss, ob etwas drinsteht, wird nicht
   aufgeklappt. */
.k-notizen { margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border-strong); }
.k-notizen > summary {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  font-size: .76rem; font-weight: 600; color: var(--text-dim);
  cursor: pointer; list-style: none; user-select: none;
}
.k-notizen > summary::-webkit-details-marker { display: none; }
.k-notizen > summary::before {
  content: ""; width: .4rem; height: .4rem; flex: 0 0 auto;
  border-right: 1.8px solid currentColor; border-bottom: 1.8px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--panel-motion-duration-fast) ease;
}
.k-notizen[open] > summary::before { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .k-notizen > summary::before { transition: none; }
}
.k-notizen > summary:hover { color: var(--text); }
.k-notizen > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.k-notizen-titel { font-weight: 650; }
/* Die neueste Notiz in der Zeile: darf schrumpfen und abschneiden, nie
   umbrechen — sonst wächst eine zugeklappte Karte an einem langen Satz. */
.k-notizen-letzte { flex: 1 1 8rem; min-width: 0; font-size: .74rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.k-notizen-inhalt { margin-top: .7rem; display: flex; flex-direction: column; gap: .6rem; }

/* Anlegen steht oben, vor den Zeilen: Wer aufklappt, will meistens etwas
   hinzufügen, und die Liste darunter kann lang werden. */
.k-notiz-neu { display: flex; flex-wrap: wrap; gap: .5rem; align-items: flex-end; }
.k-notiz-neu textarea {
  flex: 1 1 18rem; min-width: 0; resize: vertical;
  font: inherit; font-size: .82rem; line-height: 1.5;
}
.k-notiz {
  padding: .5rem .7rem; border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.k-notiz-kopf { display: flex; align-items: center; gap: .5rem; font-size: .72rem; color: var(--text-dim); }
.k-notiz-wann { font-weight: 650; }
/* Der Löschknopf ganz rechts und klein: Eine Notiz löscht man selten,
   und die Rückfrage hängt ohnehin daran (data-confirm). */
.k-notiz-weg { margin-left: auto; }
.k-notiz-weg .k-icon-btn { width: 1.5rem; height: 1.5rem; }
.k-notiz-weg .k-icon-btn svg { width: 13px; height: 13px; }
.k-notiz-text { margin: .25rem 0 0; font-size: .82rem; line-height: 1.55; white-space: normal; }

/* Der Knopf, auf den die Leadseite hinausläuft — mit der Stufe daneben,
   die der Kunde danach hat. */
.k-uebernehmen {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border-strong);
}
.k-uebernehmen-frage { font-size: .78rem; font-weight: 650; color: var(--text-dim); }
.k-uebernehmen select { font-size: .78rem; padding: .2rem .4rem; }

/* Der Hinweis auf die jeweils andere Liste und die Sortierzeile: zwei
   ruhige Sätze, kein Kasten. */
.k-drueben, .k-sortierung { margin: 0 0 .9rem .15rem; font-size: .78rem; }

/* --- Das sind wir --------------------------------------------------------
   Der eigene Eintrag hat seine eigene Karte (_eigen_card.html), weil die
   Haelfte einer Kundenkarte fuer uns nichts beantwortet: Geschaefts- oder
   Privatkunde, der Stand einer Anbahnung, wer den Kunden betreut, ein
   Portal-Zugang, ob die Einnahme die Kosten deckt.

   Was bleibt, sind drei Zahlen und die Projekte — und die duerfen
   aussehen, als gehoerten sie uns: durchgezogene Akzentkante statt der
   gestrichelten Kundenkante, ein Monogramm, und die Zahlen gross genug,
   dass man sie im Vorbeigehen liest. Ein Farbverlauf waere Dekoration;
   die Kante und das Zeichen sind die ganze Auszeichnung. */
.eigen-card {
  border: 1px solid var(--border-strong); border-left: 3px solid var(--accent);
  border-radius: var(--radius); background: var(--surface);
  padding: 1.1rem 1.2rem;
}
.eigen-kopf { display: flex; align-items: flex-start; gap: .85rem; margin-bottom: 1.1rem; }
/* Der erste Buchstabe des Namens als Zeichen — kein hochgeladenes Logo:
   Das waere eine Datei, die jemand pflegen muss; der Buchstabe ist
   immer da und immer richtig. */
.eigen-zeichen {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.4rem; height: 2.4rem; flex: 0 0 auto;
  border-radius: var(--radius-md);
  background: var(--accent); color: var(--accent-ink);
  font-size: 1.1rem; font-weight: 700; line-height: 1;
  text-transform: uppercase;
}
.eigen-titel { min-width: 0; flex: 1 1 auto; }
.eigen-name { margin: 0; font-size: 1.15rem; font-weight: 650; letter-spacing: -.01em; }
.eigen-sub { margin: .15rem 0 0; font-size: .78rem; line-height: 1.5; }
.eigen-aktionen { flex: 0 0 auto; }

/* Die drei Zahlen: Beschriftung klein darueber, Wert gross darunter,
   Zusatz noch kleiner. Proportionale Ziffern wie bei jeder einzeln
   stehenden Zahl dieser Oberflaeche. */
/* Flex und kein Raster mit `1fr`: Steht nur eine Zahl da (ein frischer
   Eintrag ohne Ressourcen-Antwort), zoege ein `1fr` sie ueber die ganze
   Breite — eine einzelne Zahl, die aussieht wie eine Ueberschrift. */
.eigen-zahlen {
  display: flex; flex-wrap: wrap; gap: .9rem 2.6rem; margin: 0 0 1.1rem;
  padding: .9rem 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.eigen-zahl { min-width: 0; flex: 0 1 auto; }
.eigen-zahl dt { font-size: .74rem; color: var(--text-dim); }
.eigen-zahl dd {
  margin: .15rem 0 0; font-size: 1.35rem; font-weight: 650;
  letter-spacing: -.02em; font-variant-numeric: proportional-nums;
}
.eigen-zahl dd.eigen-zahl-sub { font-size: .72rem; font-weight: 400; line-height: 1.45; margin-top: .1rem; }
.eigen-warn { margin: .8rem 0 0; }
@media (max-width: 640px) {
  .eigen-card { padding: .9rem 1rem; }
  .eigen-zahlen { gap: .8rem 1.6rem; }
  .eigen-zahl { flex: 1 1 8rem; }
}

/* --- Repository am Eintrag -----------------------------------------------
   Zugeklappt, aber nicht stumm — dieselbe Regel wie beim Verlauf: Steht
   eine Adresse drin, ist sie in der Zeile zu sehen. Aufgeklappt wird nur
   zum Aendern. */
.k-repo { margin-top: .85rem; }
.k-repo-kurz { font-size: .76rem; color: var(--text-dim); }
.k-repo-leer { font-size: .76rem; }
.k-repo-inhalt { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; margin-top: .6rem; }
.k-repo-link {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .8rem; text-decoration: none; color: var(--accent);
}
.k-repo-link:hover { text-decoration: underline; }
.k-repo-link svg { width: 13px; height: 13px; flex: 0 0 auto; }
.k-repo-form { display: flex; align-items: center; gap: .5rem; flex: 1 1 18rem; min-width: 0; }
.k-repo-form input { flex: 1 1 auto; min-width: 0; font-size: .78rem; padding: .3rem .5rem; }
@media (max-width: 640px) {
  .k-repo-form { flex-basis: 100%; }
}

/* „Als Team öffnen" (_vorschau_knopf.html). Ein <form> und kein Link —
   die Adresse dahinter gilt genau einmal —, und ein Formular ist ein
   Blockelement: Ohne diese Zeile spraenge der Knopf in der Tabellenzelle
   und in der Leadkarte in eine eigene Zeile, obwohl er neben die Adresse
   gehoert. */
.vorschau-oeffnen { display: inline-flex; margin: 0; }
/* „liegengeblieben" neben „noch nie": ein Hinweis, keine Warnung. Die
   Vorschau ist nicht kaputt — sie steht nur lange herum, ohne dass
   jemand hineingesehen hat. Deshalb --warn und nicht --bad. */
.vorschau-still {
  display: inline-block; margin-left: .45rem; padding: .1rem .45rem;
  border-radius: var(--radius-sm); font-size: .72rem; font-weight: 600;
  color: var(--warn-text); background: color-mix(in srgb, var(--warn) 14%, transparent);
}
.vorschau-aufraeumen { margin-top: .9rem; }
.vorschau-aufraeumen summary { cursor: pointer; font-size: .85rem; font-weight: 600; }
.vorschau-aufraeumen .note { margin: .5rem 0; }
/* In der Tabelle steht er hinter der Domain, die dort in Schreibmaschine
   gesetzt ist — der Knopf uebernimmt das nicht. */
td .vorschau-oeffnen { margin-left: .5rem; vertical-align: middle; font-family: var(--font-sans); }

/* --- Leads: Arbeitsvorrat ------------------------------------------------
   Die Leadseite ist die einzige Liste dieses Panels, die nach ZEIT
   gruppiert ist statt nach Namen (services/leadliste.py). Alles hier
   dient einer Frage: Woran sieht man von oben, was heute dran ist?

   Die Farben kommen aus denselben Tokens wie überall — Rot ist --bad
   (etwas ist vorbei), Gelb --warn (etwas wird gleich vorbei sein), der
   Akzent eine offene Aufgabe. Ein eigener Farbvorrat fuer diese Seite
   hiesse, dass „ueberfaellig" hier anders rot ist als ein Ausfall im
   Dashboard. */

/* Die Zeile ueber der Liste: keine Kacheln, sondern zwei Links. Eine
   Kachel ist ein Wert zum Ansehen; das hier ist ein Weg zum Anklicken. */
.k-arbeit { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 1.1rem; }
.k-arbeit-item {
  display: inline-flex; align-items: baseline; gap: .4rem;
  padding: .45rem .75rem; border-radius: var(--radius-md);
  border: 1px solid var(--border-strong); background: var(--surface);
  font-size: .8rem; text-decoration: none; color: var(--text);
}
.k-arbeit-item:hover { border-color: var(--accent); }
.k-arbeit-item strong { font-size: .95rem; font-variant-numeric: tabular-nums; }
.k-arbeit-item-rot { border-color: var(--bad); }
.k-arbeit-item-rot strong { color: var(--bad-text); }

/* Die Gruppenueberschrift mitten in der Liste. Sie steht da, damit
   niemand raten muss, warum ein Eintrag oben und ein anderer unten
   steht — dieselbe Abstufung wie .k-section-head, nur ohne Kasten. */
.k-gruppe {
  margin: 1.4rem 0 .1rem .15rem; font-size: .78rem; font-weight: 700;
  color: var(--text-dim);
}
.k-liste > .k-gruppe:first-child { margin-top: 0; }

/* Eine ueberfaellige Karte traegt die Kante, die sonst der Akzent haelt.
   Eine abgesagte tritt zurueck: Sie steht nur da, weil jemand
   ausdruecklich danach gefragt hat. */
.k-card-ueberfaellig { border-left-color: var(--bad); }
.k-card-abgesagt { border-left-style: dotted; border-left-color: var(--border-strong); }
.k-card-abgesagt .k-card-name { color: var(--text-dim); }

/* --- Leadkarte: Nummer, Abhaken, prominente Zuteilung, Notizfeld -------- */

/* Die laufende Nummer im Kopf — eine ruhige, runde Marke vor dem Namen,
   monospaced für gleiche Breite, kein Akzent (sie ordnet, sie warnt nicht). */
.k-card-nr {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.5rem; height: 1.5rem; padding: 0 .35rem;
  border-radius: 999px; background: var(--bg-2);
  box-shadow: 0 0 0 1px var(--border) inset;
  font-family: var(--font-mono); font-size: .74rem; font-weight: 650;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}

/* Abgehakt: die ganze Karte tritt zurück (gedämpft, Name durchgestrichen),
   bleibt aber lesbar und jederzeit wieder aufzumachen — kein Löschen. */
.k-card-erledigt { opacity: .6; }
.k-card-erledigt:focus-within, .k-card-erledigt:hover { opacity: 1; }
.k-card-erledigt .k-card-name { text-decoration: line-through; text-decoration-color: var(--text-dim); }
.k-erledigt-marke {
  display: inline-block; font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; padding: .12rem .45rem; border-radius: 999px;
  background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok-text);
}

/* Das Abhak-Häkchen im Aktionsbereich — als Chip, nicht als nacktes Kästchen. */
.k-erledigt { display: inline-flex; align-items: center; }
.k-erledigt-label {
  display: inline-flex; align-items: center; gap: .35rem; cursor: pointer;
  font-size: .76rem; color: var(--text-dim);
  padding: .3rem .55rem; border-radius: 999px;
  background: var(--bg-2); box-shadow: 0 0 0 1px var(--border) inset;
}
.k-erledigt-label:hover { color: var(--text); box-shadow: 0 0 0 1px var(--border-strong) inset; }
.k-erledigt-label input { accent-color: var(--ok); }

/* Prominente Zuteilung im Kopf: Titel plus Personen-Chips zum Ankreuzen. */
.k-wer-schnell { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; }
.k-wer-schnell-feld { display: contents; border: 0; margin: 0; padding: 0; }
.k-wer-schnell-titel { font-size: .74rem; color: var(--text-dim); padding: 0; }
.k-wer-chip {
  display: inline-flex; align-items: center; gap: .3rem; cursor: pointer;
  font-size: .78rem; padding: .25rem .55rem; border-radius: 999px;
  background: var(--bg-2); box-shadow: 0 0 0 1px var(--border) inset;
}
.k-wer-chip:hover { box-shadow: 0 0 0 1px var(--border-strong) inset; }
.k-wer-chip input { accent-color: var(--accent); }
/* Angekreuzt = zugeteilt: der Chip bekommt den Akzent, damit man auf einen
   Blick sieht, bei wem es liegt, ohne die Kästchen einzeln abzusuchen. */
.k-wer-chip:has(input:checked) {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent) inset;
  color: var(--text);
}
/* Das freie Notizfeld: unaufdringlich, wächst bei Fokus, volle Kartenbreite. */
.k-card-notizfeld { display: flex; flex-direction: column; gap: .4rem; margin-top: .55rem; }
.k-card-notizfeld textarea {
  width: 100%; resize: vertical; min-height: 2.4rem;
  font: inherit; font-size: .82rem;
}
.k-card-notizfeld .btn { align-self: flex-start; }

/* Die Nummer in der kompakten „später"-Zeile, links vor dem Datum. */
.k-spaeter-nr {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.4rem; height: 1.4rem; padding: 0 .3rem;
  border-radius: 999px; background: var(--bg-2); box-shadow: 0 0 0 1px var(--border) inset;
  font-family: var(--font-mono); font-size: .72rem; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.k-spaeter-erledigt > summary .k-spaeter-name { text-decoration: line-through; color: var(--text-dim); }

/* Die Terminzeile: Marke, Datum, Vorhaben, dann die drei Schnellknoepfe.
   Sie steht ganz oben an der Karte, weil sie der Grund ist, warum diese
   Zeile heute in der Liste steht. */
.k-termin {
  display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .6rem;
  margin-bottom: .75rem; padding: .55rem .7rem;
  border-radius: var(--radius-md); background: var(--bg-2);
  border: 1px solid var(--border);
}
.k-termin-ueberfaellig { border-color: var(--bad); }
.k-termin-heute { border-color: var(--warn); }
.k-termin-status { flex: 0 0 auto; }
.k-termin-marke {
  display: inline-block; font-size: .72rem; font-weight: 650;
  color: var(--text-dim); letter-spacing: .01em;
}
.k-termin-marke-rot { color: var(--bad-text); }
.k-termin-marke-gelb { color: var(--warn-text); }
.k-termin-marke-leer { font-weight: 600; }
.k-termin input[type="date"] { font-size: .78rem; padding: .2rem .45rem; }
/* Das Vorhaben darf wachsen, das Datum nicht: Ein Datumsfeld ist so
   breit, wie ein Datum ist, ein Satz ist so lang, wie er ist. */
.k-termin-schritt { flex: 1 1 14rem; min-width: 0; font-size: .78rem; padding: .2rem .45rem; }
/* Die drei Verschieber sehen NICHT aus wie der Knopf daneben: „Merken"
   traegt, was im Feld steht, diese drei verschieben nur. Drei gleich
   aussehende Knoepfe nebeneinander sind drei Fragen, wo eine steht. */
.k-termin-schnell { display: inline-flex; gap: 0; margin-left: auto; }
.k-termin-schnell button {
  padding: .22rem .5rem; border: 1px solid var(--border);
  background: transparent; color: var(--text-dim);
  font: inherit; font-size: .72rem; font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.k-termin-schnell button:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.k-termin-schnell button:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.k-termin-schnell button + button { border-left: none; }
.k-termin-schnell button:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }

/* Die Absagezeile steht dort, wo sonst der naechste Schritt stuende —
   sie IST das, was von diesem Eintrag bleibt. */
.k-absage {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem .6rem;
  margin: 0 0 .75rem; padding: .55rem .7rem;
  border-radius: var(--radius-md); background: var(--bg-2);
  border: 1px solid var(--border); font-size: .8rem;
}
.k-absage-grund { font-weight: 600; }
.k-absage-hinweis { flex-basis: 100%; font-size: .74rem; }

/* Die Einschaetzung: zugeklappt, aber nicht stumm — dieselbe Regel wie
   beim Verlauf darueber, und aus demselben Grund. */
.k-einschaetzung { margin-top: .85rem; }
.k-einschaetzung-kurz { font-size: .76rem; }
.k-einschaetzung-felder {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 .9rem; margin-top: .6rem;
}
.k-einschaetzung-fuss {
  grid-column: 1 / -1; display: flex; align-items: center; gap: .8rem;
  flex-wrap: wrap; font-size: .74rem;
}
@media (max-width: 640px) {
  .k-einschaetzung-felder { grid-template-columns: minmax(0, 1fr); }
  .k-termin-schnell { margin-left: 0; }
}

/* Die beiden Entscheidungen am Fuss der Karte. Untereinander und nicht
   nebeneinander: Es sind zwei Wege, und zwei Formulare in einer Zeile
   sehen aus wie eines mit zu vielen Feldern. Die Absage bekommt keine
   Signalfarbe — sie ist ein normaler Ausgang, kein Fehler. */
.k-entscheidung {
  display: flex; align-items: baseline;
  gap: .8rem 2.4rem; flex-wrap: wrap;
  margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--border-strong);
}
.k-entscheidung .k-uebernehmen { margin: 0; padding: 0; border-top: none; }
/* Zugeklappt, und das ist die Aussage: „Zu Kunde machen" ist das Ziel
   der Seite, eine Absage kommt je Lead hoechstens einmal vor. Aufgeklappt
   nimmt sie die ganze Breite — ein Grund und ein Satz dazu passen nicht
   neben einen Knopf. */
.k-absagen > summary {
  font-size: .78rem; color: var(--text-dim); cursor: pointer;
  list-style: none; user-select: none;
}
.k-absagen > summary::-webkit-details-marker { display: none; }
.k-absagen > summary:hover { color: var(--text); }
.k-absagen[open] { flex-basis: 100%; }
.k-absagen[open] > summary { font-weight: 650; color: var(--text); margin-bottom: .55rem; }
.k-absagen-felder { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.k-absagen-felder input[type="text"] { flex: 1 1 12rem; min-width: 0; font-size: .78rem; padding: .2rem .45rem; }
.k-absagen-felder select { max-width: 15rem; font-size: .78rem; padding: .2rem 1.9rem .2rem .45rem; }

/* Die Gruppe „spaeter": eine Zeile je Lead statt einer ganzen Karte.
   Datum, Name, Vorhaben, Stand — mehr will man ueber einen Termin, der
   noch nicht da ist, nicht wissen. Aufgeklappt ist es dieselbe Karte. */
.k-spaeter { border-radius: var(--radius); }
.k-spaeter > summary {
  display: grid; grid-template-columns: 3.4rem minmax(6rem, 14rem) minmax(0, 1fr) auto;
  align-items: center; gap: .9rem;
  padding: .55rem .9rem; cursor: pointer; list-style: none;
  border: 1px solid var(--border); border-left: 3px dashed var(--border-strong);
  border-radius: var(--radius); background: var(--surface);
}
.k-spaeter > summary::-webkit-details-marker { display: none; }
.k-spaeter > summary:hover { border-color: var(--border-strong); }
.k-spaeter[open] > summary { border-radius: var(--radius) var(--radius) 0 0; border-bottom: none; }
.k-spaeter[open] > .k-card { border-top-left-radius: 0; border-top-right-radius: 0; }
.k-spaeter-wann { font-size: .78rem; font-variant-numeric: tabular-nums; color: var(--text-dim); }
.k-spaeter-name { font-size: .86rem; font-weight: 600; }
.k-spaeter-schritt { font-size: .78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 640px) {
  .k-spaeter > summary { grid-template-columns: 3.4rem minmax(0, 1fr); gap: .3rem .7rem; }
  .k-spaeter-schritt, .k-spaeter > summary .status-pill { grid-column: 2; }
}

/* --- Leadauswertung ------------------------------------------------------
   Die Abschnittsueberschrift ueber einem Kachelband (die Tabellen
   darunter stehen in einer .card und haben ihre eigene .card-title). */
.k-section-titel {
  margin: 1.8rem 0 .7rem .15rem; font-size: .88rem; font-weight: 700;
  color: var(--text-dim);
}

/* EINE Zahl fuehrt die Seite an, daneben der Trichter als EIN Balken.
   Vier gleich grosse Kacheln waeren vier gleichberechtigte Fragen; hier
   gibt es eine, und die drei Zahlen daneben sind ihre Bestandteile. */
.lead-kopf {
  display: grid; grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
  gap: 1.4rem 2.4rem; align-items: center;
  margin-bottom: 1.1rem;
}
.lead-quote-wert {
  display: block; font-size: 3.4rem; line-height: 1; font-weight: 650;
  letter-spacing: -.03em;
  /* Proportionale Ziffern, ausdruecklich KEIN tabular-nums: Bei dieser
     Groesse gibt gleiche Zeichenbreite jeder 1 den Platz einer 0, und
     „11 %" steht auseinandergezogen da. Tabellarisch wird erst wieder,
     was in einer Spalte untereinander steht. */
  font-variant-numeric: proportional-nums;
}
.lead-quote-leer { color: var(--text-dim); }
.lead-quote-sub {
  margin: .5rem 0 0; font-size: .82rem; line-height: 1.5;
  max-width: 24rem; color: var(--text-dim);
}

/* Der Trichter. Die Breiten kommen aus flex-grow mit der Anzahl als
   Wert — kein Prozentrechnen in der Vorlage. Getrennt wird durch eine
   LUECKE in der Flaechenfarbe, nicht durch einen Rahmen um die
   Segmente: Ein Strich um eine Marke ist Tinte, die keine Daten traegt. */
.lead-balken {
  display: flex; gap: 2px; height: .85rem; margin-bottom: .9rem;
  border-radius: 999px; overflow: hidden;
}
.lead-balken-teil { display: block; min-width: 3px; }
/* Drei gleich breite Spalten, jede als Beschriftung / Zahl / Betrag
   untereinander. Nebeneinander in einer Reihe richteten sich die Zahlen
   nach der Laenge des Geldbetrags darueber aus — und „Noch offen" fiel
   in die naechste Zeile, obwohl daneben Platz war. */
.lead-teile {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .9rem 1.6rem; margin: 0;
}
.lead-teil { min-width: 0; }
.lead-teil dt {
  display: flex; align-items: center; gap: .4rem;
  font-size: .76rem; color: var(--text-dim);
}
.lead-teil dd { margin: .15rem 0 0; font-size: 1.15rem; font-weight: 650; }
/* `dd.` und nicht nur die Klasse: `.lead-teil dd` darueber ist
   spezifischer als eine einzelne Klasse und wuerde den Betrag sonst so
   gross und fett setzen wie die Zahl, zu der er gehoert. */
.lead-teil dd.lead-teil-geld { font-size: .72rem; font-weight: 400; line-height: 1.45; }
@media (max-width: 540px) {
  .lead-teile { grid-template-columns: minmax(0, 1fr); gap: .7rem; }
}
@media (max-width: 720px) {
  .lead-kopf { grid-template-columns: minmax(0, 1fr); gap: 1.1rem; }
  .lead-quote-wert { font-size: 2.6rem; }
}

/* Die drei Toene dieser Seite. Sie sagen einen ZUSTAND (gewonnen,
   verloren, offen) und sind deshalb die Statusfarben des Systems, nicht
   frei gewaehlte Reihenfarben. Wo sie nebeneinander stehen, traegt immer
   ein Wort daneben dieselbe Aussage: Rot und Gruen fallen bei einer
   Rot-Gruen-Schwaeche zusammen (geprueft: DeltaE 6.8), Farbe allein
   duerfte hier also nie entscheiden. */
.lead-ton-gewonnen { background: var(--ok); }
.lead-ton-verloren { background: var(--bad); }
/* „Offen" ist ausdruecklich UNBUNT: Es ist weder der gute noch der
   schlechte Ausgang, sondern der noch nicht gefallene — der neutrale
   Mittelpunkt zwischen zwei Polen. Ein dritter Farbton hier hiesse, dass
   „unentschieden" auch eine Meinung ist. `label-tertiary` und nicht
   `--surface-2`: Letzteres liegt so nah an der Flaeche, dass das Segment
   wie eine leere Schiene aussieht statt wie ein Anteil. */
.lead-ton-offen { background: var(--text-dim); }
.lead-ton-neu { background: var(--accent); }

/* Ein Balken in einer Tabellenzelle: der Anteil als Laenge, die Zahl
   daneben. Beides, weil eine Laenge sich vergleichen und eine Zahl sich
   lesen laesst — und weil ein Balken ohne Zahl auf dem Telefon, wo die
   Zelle unter ihrer Beschriftung steht, nur noch ein Strich ist. */
.k-balken {
  height: .4rem; width: 100%; max-width: 12rem; border-radius: 999px;
  background: var(--surface-2); overflow: hidden;
}
.k-balken-fuell { height: 100%; background: var(--accent); border-radius: 999px; }
.k-balken-zahl { font-size: .76rem; font-variant-numeric: tabular-nums; color: var(--text-dim); }

/* Der Monatsverlauf als drei BESCHRIFTETE Spuren, nicht als drei Saeulen
   je Monat: Nebeneinander unterschied allein die Farbe „zugesagt" von
   „abgesagt". Jede Zeile traegt jetzt ihren Namen, die Farbe bestaetigt
   nur noch — und Rot steht nicht mehr neben Gruen.

   Ein gemeinsamer Massstab ueber alle drei Zeilen: je Zeile skaliert
   saehe ein Monat mit einer Zusage so hoch aus wie einer mit acht
   Anfragen. */
.lead-spuren { display: flex; flex-direction: column; gap: .75rem; }
.lead-spur { display: grid; grid-template-columns: 8.5rem minmax(0, 1fr); gap: .8rem; align-items: end; }
.lead-spur-name {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .76rem; color: var(--text-dim); padding-bottom: .1rem;
}
/* Niedrig und dicht, mit einer Grundlinie je Spur. Hoehere Zeilen
   klingen grosszuegig und sind es nicht: Bei einem Hoechstwert von acht
   ist ein Monat mit einer Anfrage ein Strich, und je hoeher die Zeile,
   desto mehr Leere steht um ihn herum. Die Linie darunter ist der
   Unterschied zwischen „hier war null" und „hier war nichts" — eine
   durchgezogene Haarlinie, einen Schritt neben der Flaeche, nie
   gestrichelt. */
.lead-spur-balken {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: 2px; align-items: end; height: 1.9rem;
  border-bottom: 1px solid var(--border);
}
/* Der Platz ist die volle Hoehe und traegt den Hover: Die Saeule selbst
   ist an einem ruhigen Monat zwei Pixel hoch, und ein Ziel von zwei
   Pixeln trifft niemand. */
.lead-saeule-platz { display: flex; align-items: flex-end; height: 100%; }
.lead-saeule { width: 100%; min-height: 2px; border-radius: 2px 2px 0 0; }
/* Ein Monat, in dem wirklich nichts war, bekommt die Mindesthoehe nicht:
   Ein Strich am Boden sagt „eine" und meint „keine". */
.lead-saeule-leer { min-height: 0; }
.lead-spur-achse { align-items: start; margin-top: -.35rem; }
.lead-spur-achse .lead-spur-balken { height: auto; border-bottom: none; }
.lead-achse-label {
  text-align: center; font-size: .64rem; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lead-tabelle { margin-top: 1.3rem; }
.lead-tabelle .table-wrap { margin-top: .7rem; }
/* Die Fussnote gehoert zur Karte, nicht an den Aufklapper geklebt. */
.card > .lead-tabelle + .k-kosten-note { margin-top: 1rem; margin-bottom: 0; }
.k-punkt { width: .6rem; height: .6rem; border-radius: 2px; flex: 0 0 auto; }
@media (max-width: 640px) {
  /* Auf dem Telefon steht der Name UEBER seiner Spur: Eine Spalte von
     8,5rem neben zwoelf Saeulen laesst fuer die Saeulen nichts uebrig. */
  .lead-spur { grid-template-columns: minmax(0, 1fr); gap: .2rem; }
  .lead-spur-achse .lead-spur-name { display: none; }
}

/* --- Kasse der eGbR ------------------------------------------------------
   Drei Diagramme mit drei verschiedenen Aufgaben, und deshalb drei
   verschiedene Formen — nicht dreimal dieselbe, anders eingefaerbt:

     Verlauf      zwei Reihen je Monat, gruppierte Saeulen. Eine Linie
                  waere falsch: Monatswerte sind einzelne Betraege, keine
                  stetige Kurve, und eine Linie dazwischen behauptet
                  Werte, die es nicht gibt.
     Wofuer       liegende Balken, absteigend. Liegend wegen der langen
                  Beschriftungen, sortiert, weil die Frage "wofuer geht
                  das meiste weg" lautet. EINE Farbe: Die Laenge traegt
                  die Menge, eine zweite Skala ueber die Farbe waere
                  dieselbe Aussage doppelt.
     Ausgleich    Balken nach beiden Seiten um eine Mittellinie. Das ist
                  keine Menge, sondern eine Richtung, und dafuer gibt es
                  genau diese Form.

   Beide Reihen im Verlauf haengen an EINER Achse (gleiche Einheit). Eine
   zweite Achse ist die haeufigste Art, ein Finanzdiagramm zu verfaelschen.

   DIE FARBEN SIND DIE DER MARKE: Einnahmen in Tinte, Ausgaben im Rot der
   Statusschrift (--bad-text). Vorher Blau (#2a78d6) und ein eigenes Rot
   aus der dataviz-Vorgabe — das Blau steht nicht auf dem Markenblatt, und
   ein zweites Rot neben dem der Statusmeldungen war eins zu viel. Beide
   halten gegen Karte und Papier in beiden Themes mehr als 4,5:1, und
   farbenblind bleiben sie über die Helligkeit unterscheidbar: Tinte ist
   Schwarz bzw. Weiß, das Rot liegt jeweils deutlich dazwischen. */
:root {
  --kasse-ein: var(--text);
  --kasse-aus: var(--bad-text);
  --kasse-neutral: var(--border-strong);
}

.kasse-ein { color: var(--kasse-ein); }
.kasse-aus { color: var(--kasse-aus); }
.ist-plus-text { color: var(--kasse-ein); }
.ist-minus-text { color: var(--kasse-aus); }
.kasse-band .zahl-wert { font-size: 1.5rem; }
.kasse-warnung { border-color: color-mix(in srgb, var(--warn) 45%, transparent); }

/* Zwei Diagramme nebeneinander, ab genug Platz. Darunter untereinander —
   ein Saeulendiagramm in einer halben Handybreite ist keines mehr. */
.kasse-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 900px) { .kasse-grid { grid-template-columns: 3fr 2fr; } }

.kasse-legende { display: flex; gap: 1rem; align-items: center; margin: 0 0 .8rem; font-size: .8rem; color: var(--text-dim); }
.kasse-punkt { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: .3rem; vertical-align: baseline; }
.kasse-punkt-aus { background: var(--kasse-aus); }
.kasse-punkt-ein { background: var(--kasse-ein); }

/* --- Verlauf --- */
.kasse-verlauf {
  display: flex; align-items: flex-end; gap: .35rem;
  height: 150px; overflow-x: auto;
}
.kasse-monat { flex: 1 1 0; min-width: 26px; display: flex; flex-direction: column; align-items: center; gap: .3rem; height: 100%; }
.kasse-saeulen {
  flex: 1 1 auto; width: 100%;
  display: flex; align-items: flex-end; justify-content: center;
  /* 2px Abstand zwischen den beiden Saeulen eines Monats: Ohne ihn
     verschmelzen sie zu einem Balken mit einem Farbwechsel. */
  gap: 2px;
}
.kasse-saeule {
  flex: 1 1 0; min-width: 0;
  /* Abgerundet nur oben, am Boden buendig: Der Balken haengt an der
     Nulllinie, und eine Rundung dort naehme ihm den Bezug. */
  border-radius: 3px 3px 0 0;
  /* Ein Monat mit wenig darf nicht unsichtbar werden — sonst liest sich
     "wenig" wie "nichts", und das sind verschiedene Monate. */
  min-height: 2px;
}
/* Null ist nicht "wenig". Die Mindesthoehe darueber sorgt dafuer, dass
   ein kleiner Betrag sichtbar bleibt; bei einem Monat ohne Buchung waere
   derselbe Strich eine Behauptung. */
.kasse-saeule.ist-null { min-height: 0; }
.kasse-saeule-aus { background: var(--kasse-aus); }
.kasse-saeule-ein { background: var(--kasse-ein); }
.kasse-monat-label { font-size: .65rem; color: var(--text-dim); white-space: nowrap; }

/* --- Handbuch ----------------------------------------------------------
   Zugeklappt ist die Seite ein Inhaltsverzeichnis: Titel und ein Satz je
   Artikel, gruppiert nach Kapitel. Aufgeklappt ist sie Fliesstext — und
   der bekommt eine Lesebreite, waehrend die Karten selbst die volle
   Breite behalten. Eine Zeile ueber 100 Zeichen liest niemand zweimal. */
.hb-kapitel { margin-bottom: var(--space-8); }
.hb-kapitel-titel {
  margin: 0 0 .7rem; font-size: .78rem; font-weight: 700;
  color: var(--text-dim); letter-spacing: .02em;
}
.hb-artikel { margin-bottom: .6rem; }
/* Ein eigener Pfeil statt des Standard-Dreiecks: `display: grid` am
   <summary> nimmt den Marker ohnehin weg, und der Pfeil gehoert hier
   neben den Titel und nicht neben die zweizeilige Zelle. Dieselbe Form
   und dieselbe Drehung wie bei summary.aufklapper weiter oben — zwei
   Aufklapper im selben Panel sollen nicht verschieden aussehen. */
.hb-kopf {
  cursor: pointer; user-select: none; list-style: none;
  display: grid; grid-template-columns: 1fr auto; gap: .15rem .8rem;
  padding: .1rem 0;
}
.hb-kopf::-webkit-details-marker { display: none; }
.hb-kopf::after {
  content: ""; grid-row: 1 / -1; grid-column: 2; align-self: center;
  width: 7px; height: 7px; flex: none; color: var(--text-dim);
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--panel-motion-duration-base) var(--talvesa-motion-easing-apple-out);
}
.hb-artikel[open] > .hb-kopf::after { transform: rotate(45deg); }
.hb-kopf:hover .hb-titel { color: var(--accent); }
.hb-kopf:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: reduce) {
  .hb-kopf::after { transition: none; }
}
.hb-titel { font-size: .95rem; font-weight: 650; color: var(--text); }
/* Der eine Satz unter dem Titel ist der Grund, warum man den Artikel
   aufklappt oder eben nicht — er gehoert sichtbar in die zugeklappte
   Zeile und nicht in den Text darunter. */
.hb-kurz { font-size: .84rem; color: var(--text-dim); line-height: 1.5; }
.hb-artikel[open] .hb-kopf { margin-bottom: .9rem; padding-bottom: .8rem; border-bottom: 1px solid var(--border); }
.hb-text { max-width: var(--mass-text); }
.hb-text p { margin: 0 0 .8rem; font-size: .88rem; line-height: 1.65; }
.hb-abschnitt { margin: 1.2rem 0 .5rem; font-size: .85rem; font-weight: 650; color: var(--text); }
.hb-punkte { margin: 0 0 .8rem; padding-left: 1.1rem; display: flex; flex-direction: column; gap: .4rem; }
.hb-punkte li { font-size: .88rem; line-height: 1.6; }
/* Der Rueckweg: Ein Artikel erklaert etwas, das man auf einer Seite tut
   — und endet deshalb mit dem Weg dorthin. */
.hb-verweis { margin: 1rem 0 0; }
.hb-text code, .hb-kurz code {
  font-family: var(--font-mono); font-size: .84em;
  padding: .08rem .3rem; border-radius: var(--radius-sm);
  background: var(--surface-2);
}

/* --- Profil ------------------------------------------------------------
   Eine Leseseite, kein Formular. Deshalb traegt der Kopf die Identitaet
   (Namensschild, Name, Funktion) und die zwei Angaben, wegen denen es
   dieses Profil gibt — Adresse und Nummer —, und das Formular erscheint
   erst hinter „Bearbeiten" (?bearbeiten=1) an genau derselben Stelle.

   Farbe traegt genau EINE Karte: die Sicherheit. Sie ist die Antwort auf
   den Satz in der Karte daneben („dieses Konto kann Backups
   zurueckspielen") und darf deshalb lauter sein als alles andere hier. */
.profil-kopf {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0 1.1rem;
  align-items: start;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 1.3rem 1.35rem;
  margin-bottom: var(--space-7);
}
/* Das Namensschild ist das einzige Stueck Flaeche in Akzentfarbe auf
   dieser Seite ausser dem Sicherheits-Punkt: zwei Buchstaben, damit ein
   Konto ein Gesicht hat, wenn kein Foto existiert (und es existiert
   keins — dieses Panel speichert keine Bilder). */
.profil-schild {
  display: grid; place-items: center;
  width: 3.4rem; height: 3.4rem;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  font-size: 1.15rem; font-weight: 650; letter-spacing: .04em;
}
.profil-wer { min-width: 0; }
.profil-name { margin: .1rem 0 .15rem; font-size: 1.55rem; font-weight: 650; letter-spacing: -.02em; line-height: 1.15; }
.profil-funktion { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin: 0 0 .9rem; font-size: .9rem; color: var(--text-dim); }
/* „Voller Zugriff" ist keine Verzierung, sondern die Aussage der Seite —
   deshalb steht es direkt an der Person und nicht in einer Legende. */
.profil-stufe {
  padding: .12rem .5rem;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border) inset;
  color: var(--text);
  font-size: .74rem; font-weight: 600;
}
.profil-kontakt {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: .7rem 1.4rem; margin: 0;
  padding-top: .9rem; border-top: 1px solid var(--border);
}
.profil-kontakt dt { font-size: .72rem; color: var(--text-dim); margin-bottom: .1rem; }
.profil-kontakt dd { margin: 0; font-size: .9rem; }
/* Eine fehlende Nummer ist kein Strich, sondern eine Einladung. */
.profil-fehlt { color: var(--text-dim); text-decoration: underline dotted; text-underline-offset: 3px; }
/* Ein Ring, damit der Knopf auf der erhoehten Flaeche des Kopfes
   ueberhaupt als Knopf zu erkennen ist: .btn traegt sonst --surface-2,
   und das ist genau der Unterschied, den diese Karte nicht hat. */
.profil-bearbeiten { align-self: start; box-shadow: 0 0 0 1px var(--border) inset; }

.profil-form { grid-column: 2 / -1; }
.profil-form-felder { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0 1.1rem; }
.profil-form-fuss { display: flex; gap: .6rem; margin-top: .3rem; }

.profil-spalten { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: var(--space-7); align-items: start; }
.profil-spalten .card { margin-bottom: 0; }
.profil-karte-titel { display: flex; align-items: center; gap: .5rem; margin: 0 0 .5rem; font-size: 1rem; font-weight: 650; }
.profil-karte-satz { margin: 0 0 .9rem; font-size: .88rem; line-height: 1.6; color: var(--text-dim); max-width: var(--mass-text); }

/* Die eine Karte mit Farbe. Sie traegt den Zustand an der Kante, in der
   Ueberschrift und im Punkt — nicht als flaechiger Block: „nur ein
   Passwort" ist auf einem Konto, das Backups zurueckspielen kann, keine
   Randnotiz, aber auch kein Alarm, der die halbe Seite einfaerbt. Der
   Untergrund bleibt deshalb bei einer Ahnung von Farbe (6-7 %), die
   Kante macht die Aussage. */
.profil-sicherheit { border-left: 3px solid var(--border-strong); }
.profil-sicherheit[data-zustand="gut"] {
  border-left-color: var(--ok);
  background: color-mix(in srgb, var(--ok) 6%, var(--surface));
}
.profil-sicherheit[data-zustand="mittel"],
.profil-sicherheit[data-zustand="offen"] {
  border-left-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 7%, var(--surface));
}
.profil-sicherheit[data-zustand="gut"] .profil-karte-titel { color: var(--ok-text); }
.profil-sicherheit[data-zustand="mittel"] .profil-karte-titel,
.profil-sicherheit[data-zustand="offen"] .profil-karte-titel { color: var(--warn-text); }
.profil-punkt { width: .55rem; height: .55rem; border-radius: 999px; background: currentColor; flex: none; }
.profil-codes { margin: 0 0 .9rem; font-size: .82rem; color: var(--text-dim); }
.profil-codes-knapp { color: var(--warn-text); }
.profil-karte-aktionen { display: flex; flex-wrap: wrap; gap: .5rem; }

/* Was dieses Konto darf: Haken fuer das, was geht, Strich fuer das, was
   ein Flag gerade abschaltet. Die abgeschalteten Zeilen bleiben stehen —
   „was darf ich" ohne sie waere eine halbe Antwort. */
.profil-rechte { list-style: none; margin: 0 0 .9rem; padding: 0; display: flex; flex-direction: column; gap: .45rem; }
.profil-recht { display: grid; grid-template-columns: 1.1rem 1fr; gap: .55rem; align-items: baseline; font-size: .88rem; line-height: 1.45; }
.profil-recht svg { width: 1.1rem; height: 1.1rem; color: var(--ok-text); transform: translateY(.15rem); }
.profil-recht.ist-aus { color: var(--text-dim); }
.profil-recht.ist-aus svg { color: var(--text-dim); }

@media (max-width: 560px) {
  .profil-kopf { grid-template-columns: auto 1fr; }
  .profil-form, .profil-bearbeiten { grid-column: 1 / -1; }
  .profil-bearbeiten { margin-top: .9rem; justify-self: start; }
}

/* --- Wofuer --- */
.kasse-kategorien { display: flex; flex-direction: column; gap: .55rem; }
.kasse-kat { display: grid; grid-template-columns: minmax(7rem, auto) 1fr auto; gap: .7rem; align-items: center; }
.kasse-kat-name { font-size: .84rem; }
.kasse-kat-spur { height: 10px; background: var(--surface-2); border-radius: 3px; overflow: hidden; }
.kasse-kat-balken { display: block; height: 100%; border-radius: 3px; background: var(--kasse-aus); min-width: 2px; }
.kasse-kat-wert { font-size: .82rem; white-space: nowrap; }
/* "Fuer wen" daneben: Dort ist der erste Balken eine EINNAHME und traegt
   deshalb deren Farbe. Der zweite (was fuer diesen Kunden ausgegeben
   wurde) bleibt beim Standard oben — zwei Balken in einer Farbe waeren an
   dieser Stelle zwei verschiedene Aussagen, die gleich aussehen. */
.kasse-kat-balken-ein { background: var(--kasse-ein); }

/* --- Ausgleich --- */
.kasse-erklaerung { margin: 0 0 1rem; font-size: .85rem; line-height: 1.6; max-width: var(--mass-text); }
.kasse-salden { display: flex; flex-direction: column; gap: .7rem; }
.kasse-saldo { display: grid; grid-template-columns: minmax(6rem, auto) 1fr minmax(8rem, auto); gap: .7rem; align-items: center; }
.kasse-saldo-name { font-size: .88rem; font-weight: 600; }
.kasse-saldo-spur { display: flex; align-items: center; height: 14px; }
.kasse-saldo-halb { flex: 1 1 0; display: flex; height: 100%; background: var(--surface-2); }
.kasse-saldo-halb.ist-links { justify-content: flex-end; border-radius: 3px 0 0 3px; }
.kasse-saldo-halb.ist-rechts { justify-content: flex-start; border-radius: 0 3px 3px 0; }
/* Die Mittellinie ist die Null. Sie muss sichtbar sein, auch wenn auf
   beiden Seiten ein Balken anliegt. */
.kasse-saldo-mitte { width: 2px; height: 100%; background: var(--kasse-neutral); flex: 0 0 auto; }
.kasse-saldo-balken { height: 100%; }
.kasse-saldo-balken.ist-plus { background: var(--kasse-ein); border-radius: 0 3px 3px 0; }
.kasse-saldo-balken.ist-minus { background: var(--kasse-aus); border-radius: 3px 0 0 3px; }
.kasse-saldo-wert { font-size: .82rem; text-align: right; white-space: nowrap; }

.kasse-tabelle { margin-top: 1rem; }
.kasse-tabelle > summary { cursor: pointer; font-size: .82rem; color: var(--text-dim); }

/* --- Eintragen --- */
.kasse-form { display: flex; flex-wrap: wrap; gap: .7rem; align-items: flex-end; }
.kasse-form-schmal { margin-top: 1rem; }
.kasse-feld { display: flex; flex-direction: column; gap: .25rem; flex: 1 1 9rem; min-width: 0; }
.kasse-feld-breit { flex: 2 1 16rem; }
/* Satzkasten: „DATUM", „ART", „WER HAT GEZAHLT" sind Feldbeschriftungen,
   und Versalien lesen sich gerade dort messbar langsamer. Die Abstufung
   macht Groesse, Gewicht und --text-dim. */
.kasse-feld > span { font-size: .78rem; font-weight: 600; color: var(--text-dim); }
.kasse-feld input, .kasse-feld select {
  padding: .45rem .6rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--text); font: inherit; font-size: .88rem; min-width: 0;
}
/* Kein `outline: none` mehr: Diese eine Zeile schaltete den globalen
   Fokusring und die Input-Regel fuer alle neun Felder der beiden
   Kasse-Formulare ab — uebrig blieb eine 1px-Randfarbe, also Farbe als
   einziger Traeger des Fokus. Der Rand darf mitgehen, der Ring bleibt. */
.kasse-feld input:focus-visible, .kasse-feld select:focus-visible { border-color: var(--accent); }
.kasse-marke {
  margin-left: .4rem; padding: 0 .35rem; border-radius: 4px;
  font-size: .74rem; font-weight: 600;
  background: var(--surface-2); color: var(--text-dim);
}

/* --- Faellige Vertraege ------------------------------------------------
   Der einzige Block der Kasse, der etwas VERLANGT statt etwas zu berichten
   — deshalb traegt er den Warnton als Rand und nicht als Flaeche: Eine
   volle Warnfarbe hinter einer Liste von Betraegen liest sich wie ein
   Fehler, und ein faelliges Abo ist keiner. */
.kasse-faellig { border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
.kasse-faellig-liste { list-style: none; margin: .9rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
.kasse-faellig-zeile {
  display: grid; grid-template-columns: auto 1fr auto auto; gap: .7rem; align-items: center;
  padding: .45rem .6rem; border-radius: var(--radius-sm); background: var(--surface-2);
}
.kasse-faellig-datum { font-size: .8rem; color: var(--text-dim); white-space: nowrap; }
.kasse-faellig-name { font-size: .88rem; min-width: 0; }
.kasse-faellig-name .muted { font-size: .76rem; margin-left: .35rem; }
.kasse-faellig-betrag { font-size: .85rem; white-space: nowrap; }

/* --- Zahlungsvorschlaege -----------------------------------------------
   Der Pfeil ist eine Grafik und kein „->" aus Zeichen: Er traegt die
   Richtung, und die ist hier die ganze Aussage. Der Betrag steht am Rand
   (margin-left:auto) und nicht direkt hinter dem Namen — so bilden
   mehrere Zeilen eine Spalte, in der sich Betraege vergleichen lassen. */
.kasse-zahlungen { margin-top: 1.2rem; }
.kasse-zwischen { font-size: .82rem; font-weight: 600; color: var(--text-dim); margin: 0 0 .55rem; }
.kasse-zahlungsliste { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.kasse-zahlungsliste li {
  display: flex; align-items: center; gap: .55rem; flex-wrap: wrap; font-size: .88rem;
  padding: .4rem .6rem; border-radius: var(--radius-sm); background: var(--surface-2);
}
.kasse-zahlung-von, .kasse-zahlung-an { font-weight: 600; }
.kasse-pfeil { width: 24px; height: 12px; flex: none; color: var(--text-dim); }
.kasse-zahlung-betrag { margin-left: auto; font-size: .85rem; }

/* --- Segmentschalter: Art einer Buchung, Richtung eines Vertrags -------
   Zwei Moeglichkeiten, die einander ausschliessen und die ganze Eingabe
   einfaerben — dafuer ist eine Auswahlliste ein Klick zu viel. Es bleiben
   echte <input type="radio"> darunter: sichtbar nur fuer die Tastatur
   (opacity, nicht display:none — ein ausgeblendetes Feld ist nicht mehr
   fokussierbar, und dann waere der Schalter mit der Tastatur unbedienbar). */
.kasse-seg { display: flex; gap: .35rem; }
/* Der Satz, der nicht mehr auf die beiden Knoepfe passt. */
.kasse-feld > .kasse-feld-hinweis {
  /* Muss die Regel .kasse-feld > span weiter oben schlagen (dort:
     Beschriftung, halbfett) — dieser Satz ist Erklaerung, keine
     Beschriftung. Deshalb zwei Klassen im Selektor. */
  font-size: .76rem; color: var(--text-dim); font-weight: 400;
}
.kasse-seg-teil { position: relative; flex: 1 1 0; min-width: 0; }
.kasse-seg-teil > input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.kasse-seg-teil > span {
  display: block; text-align: center; padding: .45rem .6rem;
  border-radius: var(--radius-sm); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-dim);
  font-size: .85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.kasse-seg-teil > input:checked + span { color: var(--text); font-weight: 600; }
/* Die Farbe steht fuer dieselbe Richtung wie ueberall sonst auf der Seite
   (siehe --kasse-aus/--kasse-ein oben): Wer „Einnahme" waehlt, sieht die
   Farbe, in der die Zahl gleich in der Liste stehen wird. */
.kasse-seg-ausgabe > input:checked + span,
.kasse-seg-ausgehend > input:checked + span {
  border-color: var(--kasse-aus); background: color-mix(in srgb, var(--kasse-aus) 14%, transparent);
}
.kasse-seg-einnahme > input:checked + span,
.kasse-seg-eingehend > input:checked + span {
  border-color: var(--kasse-ein); background: color-mix(in srgb, var(--kasse-ein) 14%, transparent);
}
.kasse-seg-teil > input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- Betragsfeld: das Euro-Zeichen steht im Feld -----------------------
   Als ::after und nicht als Text daneben: Es gehoert zur Zahl, nicht zur
   Beschriftung. pointer-events:none, damit der Klick darauf trotzdem im
   Eingabefeld landet. */
.kasse-euro { position: relative; display: block; }
.kasse-euro > input { width: 100%; padding-right: 1.7rem; }
.kasse-euro::after {
  content: "€"; position: absolute; right: .6rem; top: 50%; transform: translateY(-50%);
  color: var(--text-dim); font-size: .85rem; pointer-events: none;
}

/* --- Vorlagen ----------------------------------------------------------
   Was zuletzt dastand, mit einem Klick wieder — gefuellt wird das Formular
   von panel.js (initKasse). Ohne JavaScript bleiben die Knoepfe sichtbar
   und wirkungslos; deshalb ist keine Angabe nur ueber sie erreichbar. */
.kasse-vorlagen { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: 0 0 1rem; }
.kasse-vorlagen-titel { font-size: .78rem; font-weight: 600; color: var(--text-dim); }
.kasse-vorlage {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem .7rem; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--text-dim); font: inherit; font-size: .8rem; cursor: pointer;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kasse-vorlage:hover { color: var(--text); border-color: var(--border-strong); }
.kasse-vorlage > .mono { font-size: .78rem; }

/* --- Formular in einer Tabellenzeile (Gesellschafter) ------------------
   Beide Felder liegen in EINER Zelle: Ein <form> kann nicht mehrere <td>
   umschliessen, und zwei Formulare fuer zwei Felder derselben Person
   waeren zwei Knoepfe fuer eine Aenderung. */
.kasse-zeilenform { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
.kasse-zeilenform input {
  padding: .35rem .5rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--text); font: inherit; font-size: .85rem; min-width: 0;
}
.kasse-zeilenform input[name="name"] { flex: 1 1 8rem; }
.kasse-prozent { display: inline-flex; align-items: center; gap: .25rem; }
.kasse-prozent > input { width: 4.6rem; text-align: right; }
.kasse-prozent::after { content: "%"; color: var(--text-dim); font-size: .8rem; }

/* --- Zeilenaktionen und die gerade bearbeitete Zeile ------------------- */
.kasse-zeilen-aktionen { display: flex; gap: .3rem; flex-wrap: wrap; align-items: center; }
.kasse-beleg { margin-left: .4rem; font-size: .75rem; }
/* Welche Zeile gerade im Formular unten liegt — sonst ist nach dem Sprung
   dorthin nicht mehr zu sehen, woher die Werte stammen. */
.table-wrap tr.ist-bearbeitet { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.kasse-karte-aktiv { border-color: color-mix(in srgb, var(--accent) 50%, transparent); }

/* --- Vertraege ---------------------------------------------------------
   Die Zustandsfarbe haengt an einem data-Attribut und nicht an einer
   Klasse: Die drei Zustaende heissen im Dienst genau so, wie sie auf der
   Seite stehen — „laeuft" mit Umlaut, „pausiert", „ausgelaufen" —, und
   „ist-laeuft" waere ein Klassenname mit Umlaut. In einem Attributwert
   ist derselbe Text dagegen eine gewoehnliche Zeichenkette. */
.vertrag-name { font-weight: 600; }
.vertrag-zeile2, .vertrag-notiz, .vertrag-jahr {
  display: block; font-size: .76rem; color: var(--text-dim); margin-top: .15rem;
}
.vertrag-notiz { font-style: italic; }
.vertrag-zustand { font-size: .84rem; }
.vertrag-zustand[data-zustand="pausiert"],
.vertrag-zustand[data-zustand="ausgelaufen"] { color: var(--text-dim); }

/* --- Kostenplan --------------------------------------------------------
   Die Postenzelle benutzt die Vertragsklassen (.vertrag-name, .vertrag-zeile2,
   .vertrag-notiz) — dieselbe Zeile in derselben Kasse soll gleich aussehen,
   und eine Kopie unter neuem Namen ist eine, die beim naechsten Umbau
   vergessen wird. Eigen ist nur, was es bei Vertraegen nicht gibt: die
   Beschreibung aus dem Katalog und die Frist-Markierung. Deren Wert haengt wie
   bei .vertrag-zustand an einem data-Attribut, weil „ueberfaellig" im Dienst
   mit Umlaut heisst und ein Klassenname das nicht sein sollte. */
.kostenplan-beschreibung {
  display: block; font-size: .8rem; color: var(--text-dim); margin-top: .25rem;
  max-width: 52ch; line-height: 1.45;
}
.kostenplan-frist { display: block; font-size: .76rem; margin-top: .15rem; font-family: var(--font-sans); }
.kostenplan-frist[data-frist="überfällig"] { color: var(--bad-text); font-weight: 600; }
.kostenplan-frist[data-frist="bald"] { color: var(--warn-text); font-weight: 600; }
.kostenplan-zeile-abgehakt .vertrag-name { color: var(--text-dim); font-weight: 500; }
/* „Bis wann" brach in einer Tabelle voller „—" auf zwei Zeilen um — die Spalte
   war schmaler als ihre Ueberschrift. */
.kostenplan-tabelle th { white-space: nowrap; }

/* --- Die Info-Seite ----------------------------------------------------
   Reine Fliesstexte in Karten. Der linke Strich an jedem Punkt ersetzt die
   Aufzaehlungszeichen einer Liste: Es sind ganze Saetze, keine Stichworte,
   und ein Punkt vor einem dreizeiligen Absatz sieht aus wie ein Fehler. */
.info-liste { display: flex; flex-direction: column; gap: .9rem; }
.info-punkt {
  margin: 0; font-size: .88rem; line-height: 1.65; max-width: var(--mass-text);
  padding-left: .9rem; border-left: 2px solid var(--border);
}
.info-punkt > strong { color: var(--text); }

.kasse-gemeinschaft { margin-top: 1rem; }
/* „+2.500,00 €" darf nicht hinter dem Euro-Zeichen umbrechen: Eine
   Betragsspalte, in der jede dritte Zahl zweizeilig ist, laesst sich
   nicht mehr ueberfliegen. */
.kasse-betrag { white-space: nowrap; }

/* --- Steuerseite -------------------------------------------------------
   Die Grenze des § 19 als Balken und nicht als zwei Zahlen nebeneinander:
   „18.400 von 25.000" beantwortet die Frage „wie nah sind wir dran" erst
   nach einer Kopfrechnung. */
.steuer-grenze { margin-bottom: 1rem; }
.steuer-grenze-kopf {
  display: flex; justify-content: space-between; gap: .8rem; flex-wrap: wrap;
  font-size: .88rem; margin-bottom: .45rem;
}
.steuer-spur { display: block; height: 12px; background: var(--surface-2); border-radius: 3px; overflow: hidden; }
.steuer-balken { display: block; height: 100%; border-radius: 3px; background: var(--kasse-ein); min-width: 2px; }
/* Voll heisst hier nicht „geschafft", sondern „Grenze erreicht" — deshalb
   die Warnfarbe und nicht dieselbe wie darunter. */
.steuer-balken.ist-voll { background: var(--warn); }

/* Die Summenzeile einer Tabelle: abgesetzt, nicht fett gedruckt — fett
   sind darin schon die Zahlen, auf die es ankommt. */
.table-wrap tr.ist-summe > td { border-top: 2px solid var(--border-strong); }

/* Vorher/Nachher im Aenderungsprotokoll untereinander: Zwei Saetze
   nebeneinander in einer Tabellenzelle liest niemand als Paar. */
.steuer-vorher, .steuer-nachher { display: block; font-size: .8rem; line-height: 1.5; }
.steuer-vorher { color: var(--text-dim); text-decoration: line-through; }
.steuer-nachher { color: var(--text); }

/* Die Pflichtenliste der Steuerseite: Sechs Stichzeilen zum Abhaken.
   Was jede bedeutet, steht im Handbuch — hier steht sie, damit man weiss,
   dass es sie gibt. Deshalb eine echte Liste mit Punkten und nicht die
   Absatzform von .info-punkt. */
.steuer-pflichten {
  margin: 0; padding-left: 1.1rem;
  display: flex; flex-direction: column; gap: .35rem;
  font-size: .88rem; line-height: 1.6; max-width: var(--mass-text);
}
.steuer-pflichten > li::marker { color: var(--text-dim); }
.steuer-pflichten strong { color: var(--text); }

/* Ein Ankreuzfeld mit Satz daneben, in der Form der uebrigen
   Kasse-Felder. */
.kasse-schalter { display: inline-flex; align-items: center; gap: .5rem; font-size: .88rem; font-weight: 400; color: var(--text); }
.kasse-schalter input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); flex: none; }

/* Die Beschriftung vor einer Knopfreihe (.filters), damit „2026 2025"
   nicht ohne Angabe dasteht, wovon es die Auswahl ist. */
.filter-titel { font-size: .78rem; font-weight: 600; color: var(--text-dim); }

@media (max-width: 640px) {
  .kasse-verlauf { height: 120px; }
  .kasse-kat { grid-template-columns: 1fr auto; }
  /* Auf dem Handy steht die Spur unter Name und Wert statt dazwischen —
     drei Spalten, von denen eine ein Balken ist, gibt es dort nicht. */
  .kasse-kat-spur { grid-column: 1 / -1; }
  .kasse-saldo { grid-template-columns: 1fr auto; }
  .kasse-saldo-spur { grid-column: 1 / -1; }
  .kasse-form .btn, .kasse-feld input, .kasse-feld select { min-height: 44px; }
  .kasse-band .zahl-wert { font-size: 1.25rem; }
  /* Die faellige Zeile und der Zahlungsvorschlag stapeln: vier Spalten,
     von denen eine ein Knopf ist, gibt es auf einem Telefon nicht. */
  .kasse-faellig-zeile { grid-template-columns: 1fr auto; }
  .kasse-faellig-name { grid-column: 1 / -1; order: 3; }
  .kasse-faellig-zeile form { grid-column: 1 / -1; order: 4; }
  .kasse-faellig-zeile .btn { width: 100%; min-height: 44px; }
  .kasse-zahlung-betrag { margin-left: 0; width: 100%; }
  .kasse-seg-teil > span, .kasse-vorlage, .kasse-zeilenform input { min-height: 44px; }
  .kasse-seg-teil > span { display: flex; align-items: center; justify-content: center; }
  .kasse-zeilenform .btn { min-height: 44px; }
  .kasse-schalter { min-height: 44px; }
}
