@import "tokens.css";

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--font-size-base);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.45;
  min-height: 100vh;
  min-height: 100dvh;
}

h1, h2, h3, h4 {
  line-height: 1.2;
  margin: 0 0 var(--space-3) 0;
}
h1 { font-size: var(--font-size-xl); }
h2 { font-size: var(--font-size-lg); }
p { margin: 0 0 var(--space-3) 0; }

a { color: var(--color-brand-dark); }

img { max-width: 100%; display: block; }

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

/* Sichtbarer Fokus ueberall -- Barrierefreiheit, nie entfernen */
:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
}

.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;
}

.skip-link {
  position: absolute;
  left: var(--space-2);
  top: -100px;
  background: var(--color-brand);
  color: var(--color-brand-contrast);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  z-index: 1000;
  transition: top 0.15s ease;
}
.skip-link:focus {
  top: var(--space-2);
}

/* -------------------------------------------------------------------- */
/* Layout                                                                */
/* -------------------------------------------------------------------- */
/* #app-chrome buendelt Offline-Banner + Kopfzeile in EINEM fixierten Block
   (position: fixed statt sticky), damit beide beim Scrollen sichtbar
   bleiben -- konsistent mit dem ohnehin "position: fixed" Bottom-Nav.
   #app-main gleicht die dadurch fehlende Flow-Hoehe per
   `padding-top: var(--chrome-height)` aus; dieser Wert wird in js/app.js
   (bzw. js/invite-app.js) per ResizeObserver live nachgemessen und
   gesetzt -- robust auch bei zweizeiligem Banner-Text auf sehr schmalen
   Screens oder einem langen Haushaltsnamen im Kopfzeilen-Switcher. */
#app-chrome {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
}

/* Redesign D2 "Puls": die Kopfzeile ist NICHT mehr farbig abgesetzt --
   sie liegt flach auf dem App-Hintergrund (--bg-app), abgegrenzt nur durch
   eine dezente untere Trennlinie (--border-color). Dadurch braucht sie auch
   keine eigenen Kontrast-Sonderregeln fuer .btn-ghost/.btn-secondary mehr
   (frueher noetig, weil der gruene "Markt"-Header eine eigene Hintergrund-
   farbe hatte) -- die normalen Button-Stile (unten) greifen unveraendert. */
#app-header {
  background: var(--bg-app);
  color: var(--text-ink);
  padding: var(--space-3) var(--space-4);
  padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0));
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border-color);
}

#app-header .header-row-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

#app-header .brand {
  font-weight: 700;
  font-size: var(--font-size-lg);
  color: var(--text-ink);
  text-decoration: none;
}

#app-header .header-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

/* Haushaltswechsler (Name wie eingegeben, kein "WG "-Praefix mehr -- siehe
   QA-Fix in js/components/shell.js) -- Pille (Vorgabe D2: rgba(255,255,255,.05)
   im Dark-Theme, siehe --pill-bg/--pill-text in css/tokens.css) hebt den
   Umschalter dezent vom flachen Kopf ab, ohne selbst laut zu wirken. */
#app-header select#household-switcher {
  min-height: 36px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--border-color);
  background: var(--pill-bg);
  color: var(--pill-text);
  max-width: 46vw;
}

/* Listentitel (b) und Ersparnis-/Angebotszeile (c): beide leben in der
   Kopfleiste, werden aber nur befuellt/eingeblendet, wenn eine View
   tatsaechlich einen Kontext hat -- siehe js/components/shell.js
   setHeaderContext(). Kein Platzhalter-Leerraum, wenn [hidden]. Typografie
   (Groesse/Schnitt/Zeichenabstand) kommt aus den zentralen
   --header-title-*-Tokens (css/tokens.css, Vorgabe D2: 26px/850/-.03em). */
#header-title {
  margin-top: var(--space-2);
  font-weight: var(--header-title-weight);
  font-size: var(--header-title-size);
  letter-spacing: var(--header-title-tracking);
  line-height: 1.15;
  color: var(--text-ink);
}
#header-title[hidden] {
  display: none;
}

/* Ersparnis-/Angebotszeile: Text in Limette auf der dezenten Pillen-Flaeche
   (Vorgabe D2 "Ersparnis-/Sub-Zeile" in Limette). */
#header-savings {
  display: inline-block;
  margin-top: var(--space-1);
  padding: 2px var(--space-3);
  border-radius: 999px;
  background: var(--pill-bg);
  color: var(--color-primary);
  font-size: var(--font-size-sm);
  font-weight: 700;
}
#header-savings[hidden] {
  display: none;
}

#app-main {
  max-width: 480px;
  margin: 0 auto;
  /* Bodenabstand ueber der fixierten Bottom-Nav: --content-bottom-inset
     (css/tokens.css) buendelt Nav-Hoehe + Luftraum + Safe-Area-Inset an
     EINER Stelle, damit alle Seiten mit Bottom-Nav (Listen, Haushalte,
     Zuweisungen, Profil, Listen-Detail/-Einstellungen) konsistent genug
     Platz ueber der Leiste lassen -- betrifft z.B. den vollbreiten
     "+ Artikel"-Button am Ende der Listenansicht. */
  padding: var(--space-4) var(--space-4) var(--content-bottom-inset) var(--space-4);
  /* --content-top-inset (css/tokens.css): zusaetzlicher Luftraum UNTER der
     fixierten Kopfzeile, damit der Inhalt (Listentitel, "<- Alle Listen",
     Eintraege usw.) nicht direkt am Kopf klebt. --chrome-height
     bleibt die live gemessene Header-Hoehe (verhindert Verdeckung),
     --content-top-inset kommt on top -- dieselbe Stelle gilt fuer ALLE
     Ansichten mit dieser Kopfzeile (Login, Registrieren, Haushalte,
     Listen, Zuweisungen, Profil, Impressum, Datenschutz). */
  padding-top: calc(var(--chrome-height, 56px) + var(--content-top-inset));
  width: 100%;
}

#invite-main {
  max-width: 480px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-4) var(--space-6) var(--space-4);
  /* Gast-Ansicht (/i/) teilt sich denselben fixierten Header -- derselbe
     zentrale Top-Abstand wie #app-main, siehe Kommentar dort. */
  padding-top: calc(var(--chrome-height, 56px) + var(--content-top-inset));
}

/* Vorgabe D2: Hintergrund = App-Hintergrund (nicht die Karten-Flaeche),
   Oberkante als eigener, etwas dunklerer Trennton (--nav-border-color) statt
   des allgemeinen Karten-/Formularrandes -- im Dark-Theme sichtbar, aber
   dezenter als eine helle Kartenkontur. */
nav.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  background: var(--bg-app);
  border-top: 1px solid var(--nav-border-color);
  z-index: 20;
  padding-bottom: env(safe-area-inset-bottom, 0);
}

/* Eigene "display: flex" oben wuerde sonst die native [hidden]-Regel des
   Browsers (UA-Stylesheet, niedrigere Prioritaet als Autoren-CSS)
   uebersteuern -- vor Login/fuer die Gastseite muss die Navigation aber
   wirklich verschwinden, nicht nur leer aussehen. */
nav.bottom-nav[hidden] {
  display: none;
}

/* 5 Punkte statt vormals 4 (neu: "Angebote", siehe index.html) muessen auf
   390px Breite sauber nebeneinander passen (Handy-first-Vorgabe) -- jede
   Spalte hat dadurch nur noch ~78px Breite. Kompaktere Schrift/Icon-Groesse
   + reduzierte Innenabstaende gegenueber vorher (4 Punkte), "white-space:
   nowrap" + minimale horizontale Polsterung verhindern Zeilenumbrueche bei
   den laengsten Labels ("Zuweisungen"). Tap-Ziel bleibt trotzdem ueber der
   44px-Mindesthoehe (--tap-min, WCAG 2.5.5). */
nav.bottom-nav a {
  flex: 1;
  min-width: 0;
  text-align: center;
  padding: 6px 2px;
  min-height: 56px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  text-decoration: none;
  color: var(--nav-inactive);
  font-size: 0.6875rem; /* 11px -- kompakt genug fuer 5 Spalten auf 390px */
  line-height: 1.15;
  white-space: nowrap;
}

nav.bottom-nav a .nav-icon {
  font-size: 1.15rem;
  line-height: 1;
}

nav.bottom-nav a[aria-current="page"] {
  color: var(--color-primary);
  font-weight: 700;
}

/* -------------------------------------------------------------------- */
/* Karten / Listen                                                       */
/* -------------------------------------------------------------------- */
.card {
  background: var(--card-bg);
  border: none;
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
  padding: var(--space-4);
  margin-bottom: var(--space-3);
}

.card-link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.list-plain {
  list-style: none;
  margin: 0;
  padding: 0;
}

.stack > * + * {
  margin-top: var(--space-3);
}

.row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

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

.badge {
  display: inline-block;
  font-size: var(--font-size-sm);
  padding: 2px var(--space-2);
  border-radius: 999px;
  background: var(--color-border);
  color: var(--color-text);
}

/* A11y-Fix (historisch, weiterhin gueltig): weisser/heller Text direkt auf
   --color-brand fiel unter AA -- deshalb --color-brand-dark +
   --color-brand-contrast (css/tokens.css). Betrifft die "spart X €"-Chips
   je Artikel und die Store-Ersparnis-Badge (js/components/offers.js) sowie
   die Rollen-Badge in js/views/households.js. Dunkler Text #111700 auf der
   Hover-Limette #b3e34e = 12,22:1 (einziges, dunkles Theme -- Nur-Dunkel,
   Auftraggeber-Entscheidung). */
.badge-brand {
  background: var(--color-brand-dark);
  color: var(--color-brand-contrast);
}

/* -------------------------------------------------------------------- */
/* Formulare                                                             */
/* -------------------------------------------------------------------- */
.field {
  display: block;
  margin-bottom: var(--space-3);
}

.field label,
.field-label {
  display: block;
  font-weight: 600;
  margin-bottom: var(--space-1);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}

.field textarea {
  min-height: 88px;
}

.field .hint {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  margin-top: var(--space-1);
}

.field .error-text {
  font-size: var(--font-size-sm);
  color: var(--color-danger);
  margin-top: var(--space-1);
}

fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  margin: 0 0 var(--space-3) 0;
}
legend {
  font-weight: 600;
  padding: 0 var(--space-1);
}

/* -------------------------------------------------------------------- */
/* Buttons                                                               */
/* -------------------------------------------------------------------- */
/* Primaerer Button traegt im Dark-Theme den D2-Glow (--shadow-glow-primary,
   css/tokens.css -- exakter Wert der Vorgabe, u.a. fuer den
   "＋ Artikel hinzufügen"-Button). Im Light-Theme ist derselbe Token auf
   "none" gesetzt, dort bleibt der Button ohne Zusatzschatten (der Lime-
   Schimmer passt nicht zum Markt-Gruenton) -- eine einzige Regel bedient
   beide Themes, keine Verzweigung im Markup/JS noetig. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-weight: 600;
  cursor: pointer;
  background: var(--color-brand);
  color: var(--color-brand-contrast);
  text-decoration: none;
  box-shadow: var(--shadow-glow-primary);
}
.btn:hover { background: var(--color-brand-dark); }
.btn:disabled { opacity: 0.6; cursor: not-allowed; box-shadow: none; }

.btn-secondary {
  background: var(--color-surface);
  color: var(--color-brand-dark);
  border-color: var(--color-brand-dark);
  box-shadow: none;
}
.btn-secondary:hover { background: var(--color-bg); }

/* Destruktive Aktionen: gesaettigte Flaeche + weisse Schrift
   (--color-danger-dark, css/tokens.css -- Kontrast siehe Tokendatei),
   bewusst OHNE den primaeren Lime-Glow. */
.btn-danger {
  background: var(--color-danger-dark);
  color: var(--color-danger-contrast, #fff);
  box-shadow: none;
}
.btn-danger:hover { filter: brightness(0.9); }

.btn-ghost {
  background: transparent;
  color: var(--color-text);
  border-color: var(--color-border);
  box-shadow: none;
}

.btn-block { width: 100%; }
.btn-sm { min-height: 36px; padding: var(--space-1) var(--space-3); font-size: var(--font-size-sm); }

.btn-icon {
  min-width: var(--tap-min);
  padding: var(--space-2);
}

/* Angebots-/Preis-Chip: Signalakzent, NUR fuer echte Angebots-/Preistreffer,
   mit leichtem Schatten/Glow (--color-offer-shadow). Seit M2 mit echten
   Treffern aus GET /lists/{id}/offers befuellt (docs/API.md Abschnitt 9).
   Textfarbe IMMER dunkel, nie weiss (--color-offer-contrast in
   css/tokens.css) -- A11y-Pflichtfix, siehe ausfuehrlichen Kommentar dort
   fuer die gemessenen Kontrastwerte je Theme (Dark/D2: 6,42:1, Light/Markt:
   6,26:1 -- beide ueber der AA-Schwelle von 4,5:1). */
.chip-offer {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--color-offer);
  color: var(--color-offer-contrast);
  padding: 2px var(--space-3);
  border-radius: 999px;
  font-weight: 700;
  font-size: var(--font-size-sm);
  box-shadow: var(--color-offer-shadow);
}

/* Zeile mit Chip + Testdaten-Badge unter einem Listeneintrag (M2-7:
   "Jeder Treffer zeigt Quelle und Gueltigkeit"). */
.item-offer-hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
}

/* Neutrale Badge-Variante (M2-6 "Ersparnis unbekannt" / Querschnitt 8
   Testdaten-Hinweis) -- Warnfarbtoene aus tokens.css, kein neuer Farbwert. */
.badge-warning {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
}

/* -------------------------------------------------------------------- */
/* Item-Zeilen (Einkaufsliste)                                           */
/* -------------------------------------------------------------------- */
.item-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3);
  border: none;
  border-radius: var(--card-radius);
  background: var(--card-bg);
  box-shadow: var(--card-shadow);
}

.item-row.is-purchased {
  opacity: 0.65;
}
.item-row.is-purchased .item-term {
  text-decoration: line-through;
  color: var(--text-muted);
}

.item-row.is-unavailable {
  box-shadow: 0 0 0 2px var(--color-danger) inset, var(--card-shadow);
}

/* Eckige Box (nicht rund), Radius 8px (--radius-checkbox) -- gefuellt
   Limette wenn erledigt, Haekchen-Symbol dann dunkel (--color-primary-
   contrast) statt weiss -- Limette ist eine sehr helle Farbe, weisser Text/
   Symbol darauf waere kaum lesbar (siehe A11y-Kommentar in css/tokens.css). */
.item-check {
  flex: 0 0 auto;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--radius-checkbox);
  border: 2px solid var(--checkbox-border);
  background: var(--card-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  cursor: pointer;
}
.item-check[aria-pressed="true"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-contrast);
  font-weight: 700;
}

/* Kategorie-/Namens-Kachel links in der Artikelkarte. M1 kennt keine
   Kategoriedaten (docs/API.md: Items haben nur search_term/quantity/note/
   status) -- die Kachel zeigt daher bewusst nur den Anfangsbuchstaben des
   Suchbegriffs als visuellen Anker, KEINE erfundene Kategorie. */
.item-tile {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  background: var(--color-primary-light);
  color: var(--color-primary-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--font-size-lg);
}
.item-row.is-purchased .item-tile {
  opacity: 0.7;
}

.item-body { flex: 1 1 auto; min-width: 0; }
.item-term { font-weight: 600; word-break: break-word; }
.item-meta { font-size: var(--font-size-sm); color: var(--color-text-muted); }
.item-actions { display: flex; gap: var(--space-1); flex: 0 0 auto; }

.item-status-pill {
  font-size: var(--font-size-sm);
  padding: 1px var(--space-2);
  border-radius: 999px;
}
.status-open { background: var(--color-border); }
.status-purchased { background: var(--color-success-bg); color: var(--color-success); }
.status-unavailable { background: var(--color-danger-bg); color: var(--color-danger); }

/* Vollbreiter Button zum Hinzufuegen, unten in der Liste -- oeffnet das
   Formular in einem Bottom-Sheet (dialog.sheet), statt es dominant oben
   dauerhaft anzuzeigen. */
.add-item-trigger {
  margin-top: var(--space-2);
}

/* -------------------------------------------------------------------- */
/* Meldungen                                                             */
/* -------------------------------------------------------------------- */
.alert {
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-3);
  border: 1px solid transparent;
}
.alert-error {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border-color: var(--color-danger);
}
.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-color: var(--color-success);
}
.alert-info {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  border-color: var(--color-warning-text);
}

#toast-region {
  position: fixed;
  bottom: calc(64px + env(safe-area-inset-bottom, 0) + var(--space-3));
  left: var(--space-3);
  right: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  z-index: 50;
  pointer-events: none;
}

/* Vormals eine satte Flaeche in --color-text mit hartcodiertem weissem
   Text -- funktionierte nur, solange --color-text zuverlaessig dunkel war.
   Im Dark-Theme ist --color-text (=--text-ink) jetzt HELL, "weiss auf hell"
   waere unlesbar. Toasts nutzen daher denselben getoenten Karten-Stil wie
   .alert-* (Flaeche + passender Text/Rahmen aus demselben Farbpaar) -- in
   beiden Themes automatisch korrekt, kein Sonderfall noetig. */
.toast {
  background: var(--card-bg);
  color: var(--text-ink);
  border: 1px solid var(--border-color);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-2);
  max-width: 480px;
  margin: 0 auto;
  pointer-events: auto;
}
.toast-error {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border-color: var(--color-danger);
}
.toast-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-color: var(--color-success);
}

/* PWA-Auto-Update-Hinweis (js/components/update-banner.js): dieselbe
   getoente Kartenoptik wie .toast oben, aber OHNE Timeout -- bleibt stehen,
   bis der Nutzer antippt oder die Seite tatsaechlich neu laedt. Eigener
   Limette-Akzentrand + hoeherer z-index, damit er sich von normalen,
   voruebergehenden Status-Toasts abhebt. */
.update-banner {
  position: fixed;
  left: var(--space-3);
  right: var(--space-3);
  bottom: calc(64px + env(safe-area-inset-bottom, 0) + var(--space-3));
  z-index: 60;
  max-width: 480px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  background: var(--card-bg);
  color: var(--text-ink);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-2);
  padding: var(--space-3);
}
.update-banner-text {
  flex: 1 1 160px;
  margin: 0;
  font-size: var(--font-size-sm);
}
.update-banner .btn {
  flex: 0 0 auto;
}

#offline-banner {
  display: none;
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  text-align: center;
  padding: var(--space-2);
  font-size: var(--font-size-sm);
  border-bottom: 1px solid var(--color-warning-text);
}
#offline-banner.is-visible { display: block; }

.testdata-note {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  border: 1px solid var(--color-warning-text);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-size: var(--font-size-sm);
  margin-bottom: var(--space-3);
}

/* -------------------------------------------------------------------- */
/* F1: Angebote-Übersicht (js/views/offers.js) + Läden-Präferenzen        */
/* -------------------------------------------------------------------- */
/* Aufklappbare Artikel-Karten (natives <details class="card">, siehe
   js/components/offers.js::renderHouseholdArticle) -- der native
   Aufklapp-Pfeil bleibt erhalten (Barrierefreiheit/Wiedererkennung "hier
   öffnet sich etwas"), nur seine Farbe folgt den Tokens statt dem
   Browser-Default. Ein offener Zustand trennt Kopf/Inhalt mit einer
   dezenten Linie, damit die Angebotszeilen darunter nicht am Ladennamen
   kleben. */
details.card > summary {
  color: var(--color-text);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
}
details.card > summary::marker {
  color: var(--color-text-muted);
}
details.card[open] > summary {
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

/* Client-Umschalter "nur bevorzugte Läden anzeigen" (Angebote-Tab UND
   per-Liste-Angebotsansicht) + Läden-Präferenz-Checkliste im Profil --
   beide teilen sich dasselbe größere, gut tappbare Kontrollkästchen
   (--tap-min-hohe Zeile, Limette als Haken-Farbe, konsistent mit dem
   Haupt-Akzent). */
.toggle-row {
  gap: var(--space-2);
  min-height: var(--tap-min);
}
.toggle-row input[type="checkbox"],
.store-pref-row input[type="checkbox"] {
  width: 22px;
  height: 22px;
  accent-color: var(--color-primary);
  flex: 0 0 auto;
}
.toggle-row label,
.store-pref-row label {
  cursor: pointer;
}

.store-pref-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-1) 0;
}
.store-pref-row label {
  flex: 1 1 auto;
}

/* -------------------------------------------------------------------- */
/* Modal (leichtgewichtig, per <dialog>)                                */
/* -------------------------------------------------------------------- */
dialog.sheet {
  border: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: var(--space-4);
  width: 100%;
  max-width: 480px;
  margin: auto auto 0 auto;
  box-shadow: var(--shadow-2);
  background: var(--card-bg);
  color: var(--text-ink);
}
dialog.sheet::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
dialog.sheet .sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

/* -------------------------------------------------------------------- */
/* Utility                                                               */
/* -------------------------------------------------------------------- */
.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.spinner {
  width: 20px;
  height: 20px;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-brand);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  display: inline-block;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 1.6s; }
}

.empty-state {
  text-align: center;
  padding: var(--space-6) var(--space-3);
  color: var(--color-text-muted);
  background: var(--card-bg);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}

code.token {
  display: block;
  word-break: break-all;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  font-size: var(--font-size-sm);
}
