/* ============================================================================
 * app.css — Listen-App-spezifisches Styling (on top of head.css).
 * Design-Referenzen: projectdata/*.png
 * ========================================================================== */

/* ===========================================================================
 * Schrift — Bricolage Grotesque, selbst gehostet (SIL OFL 1.1)
 * ---------------------------------------------------------------------------
 * 'system-ui' hiess: auf jedem Gerät eine andere Schrift, auf keinem eine
 * Entscheidung. Diese Familie ist variabel im Gewicht (400-700), sodass Titel
 * und Eintragstext aus einer Datei kommen.
 *
 * Nur die Gewichtsachse, bewusst ohne 'wdth' und 'opsz': die drei Achsen
 * zusammen wiegen 131 KB, das Gewicht allein 41 KB. Ein schmaler Titel war die
 * Idee, 90 KB dafür auf jedem Kaltstart sind es nicht wert.
 *
 * Die Versionsnummer steckt im Dateinamen. bump.mjs stempelt nur Verweise in
 * index.html, und diese hier stehen im CSS — ein neuer Schnitt bekommt also
 * einen neuen Namen, sonst hielte der Browser die alte Datei für gültig.
 * ======================================================================== */
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../assets/fonts/bricolage-latin-v9.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../assets/fonts/bricolage-latin-ext-v9.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Alle drei Selektoren, weil head.css '--accent-ink' auch unter
   ':root[data-theme="dark"]' setzt — ein blosses ':root' käme dagegen nicht an.
   Dort steht im Dunkelmodus '#0e1a0e', also Fast-Schwarz. Das passte zum alten
   hellgrünen Akzent; die vier wählbaren Farben sind aber allesamt satt und
   dunkel, weisse Schrift ist auf jeder von ihnen die richtige. Sichtbar wurde
   es am Knopf im Aktualisierungs-Toast: schwarz auf Tintenblau. */
:root,
:root[data-theme="light"],
:root[data-theme="dark"] {
  --accent-ink: #ffffff;
}

:root {
  /* Überschreibt --font aus head.css (dort system-ui). */
  --font: 'Bricolage Grotesque', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* Zusätzliche Tokens, unabhängig von Theme */
  --bar-h: 56px;
  /* Die aktive Pille und der Teilen-Knopf folgen jetzt der gewählten
     Akzentfarbe. Vorher standen hier zwei feste Blautöne — wer im Menü auf
     Grün stellte, bekam trotzdem eine blaue Pille. */
  --accent-pill: var(--accent, #2f4bd8);
  --danger: #e11d48;
  --danger-ink: #ffffff;
  --share: var(--accent, #2f4bd8);
  --share-ink: #ffffff;
  --overlay-bg: rgba(20, 20, 24, .55);
}

/* Hintergrund-Layer: entweder Bild oder Solid --bg */
#bg-layer {
  position: fixed; inset: 0; z-index: -2;
  background-color: var(--bg);
  background-size: cover; background-position: center; background-repeat: no-repeat;
  pointer-events: none;
}
#bg-tint {
  position: fixed; inset: 0; z-index: -1;
  background: transparent;
  pointer-events: none;
  transition: background .2s;
}
/* Bei aktivem Bild: Scrim für Lesbarkeit. Liegt nur über dem oberen Bereich,
   wo Titel und die ersten Items sitzen, und läuft nach unten aus — so bleibt
   das Motiv in der unteren Bildhälfte sichtbar. */
body[data-bg-mode="image"] #bg-tint {
  background: linear-gradient(180deg,
    rgba(10,12,18,.72) 0%,
    rgba(10,12,18,.58) 26%,
    rgba(10,12,18,.22) 56%,
    rgba(10,12,18,0)   82%);
}
/* Im hellen Modus derselbe Verlauf, nur aufgehellt statt abgedunkelt. Der
   dunkle Scrim liegt HINTER den durchsichtigen Bändern und färbte sie mit:
   die obersten Zeilen kamen grau heraus, die unteren fast weiss — eine Liste
   mit Schatten von oben, ohne dass etwas Schatten wirft.

   Schwächer als der dunkle Verlauf, weil die Liste ihre Lesbarkeit inzwischen
   selbst mitbringt (Scheibe weiter unten) — der Verlauf muss hier nur noch
   die Fläche neben und unter der Liste beruhigen. */
:root[data-theme="light"] body[data-bg-mode="image"] #bg-tint {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--bg) 46%, transparent) 0%,
    color-mix(in srgb, var(--bg) 36%, transparent) 26%,
    color-mix(in srgb, var(--bg) 14%, transparent) 56%,
    transparent 82%);
}

/* Das Install-Banner aus install.js sitzt auf 'bottom: safe-area + 12px' —
   also genau dort, wo unsere Bottombar liegt. Es verschwand halb dahinter und
   der Installieren-Knopf war kaum zu treffen. Eine Leistenhöhe höher setzen.
   '!important' ist nötig, weil install.js sein <style> zur Laufzeit einhängt
   und damit nach dieser Datei greift. */
#install-banner {
  bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 12px) !important;
}
/* Es liegt mit z-index 9998 über allem, auch über den Overlays — im Fly-in
   verdeckte es die unteren Pillen, im Menü die letzte Bildreihe. Solange ein
   Overlay offen ist, hat es nichts zu melden. Dasselbe gilt neben dem
   Aktualisierungs-Toast: zwei Aufforderungen übereinander, und die neue
   Version ist die dringendere. Das Banner kommt beim nächsten Start wieder.
   Während der Tour ebenso: sie erklärt gerade die Bottombar, und das Banner
   deckt sie zu (driver.js setzt 'driver-active' auf <html>). */
body:has(.fs-overlay[data-open]) #install-banner,
body:has(#update-toast) #install-banner,
.driver-active #install-banner { display: none !important; }

/* Der Toast rechnet mit '--nav-h' (64px) — das gehört zur <app-bottom-nav> des
   Templates, die es hier nicht gibt. Unsere Leiste ist '--bar-h' (56px). */
.update-toast {
  bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 16px);
}

/* Base-CSS reserviert padding-bottom für <app-bottom-nav> — wir haben aber
   <list-bottombar>. Also selbst regeln. */
body {
  padding-top: 0;
  padding-bottom: calc(var(--bar-h) + env(safe-area-inset-bottom,0px));
}
body:has(list-topbar) {
  padding-top: calc(var(--bar-h) + env(safe-area-inset-top,0px));
}

main#app-main {
  max-width: 720px; margin: 0 auto;
  /* Kein Seitenrand: die Item-Bänder laufen bis an die Kante, sonst steht die
     Liste als schwebendes Rechteck über dem Hintergrundbild. Oben kein Abstand:
     der erste Eintrag soll unter dem Verlauf der Leiste hervorkommen, nicht
     nach einer Lücke beginnen. */
  padding: 0 0 8px;
  min-height: calc(100dvh - var(--bar-h)*2 - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px));
}

/* =========================================================================
 * Topbar (fixed top): trash-left, title-center, check-right
 * ========================================================================= */
/* Über Bildern wird der Hintergrund hinter der Leiste weichgezeichnet — der
   leichte Ton darüber hält den weissen Titel auch auf hellen Motiven lesbar. */
/* Die Leiste selbst ist durchsichtig — Hintergrund UND Weichzeichner sitzen in
 * '::before'. Nur so lässt sich beides nach unten auslaufen lassen: eine
 * Maske auf dem Pseudoelement nimmt den Weichzeichner mit weg, und die
 * Trennkante zwischen Leiste und Liste verschwindet. Vorher endete die Leiste
 * hart, und darunter klaffte der Abstand zum ersten Eintrag.
 *
 * Das Pseudoelement reicht über die Leiste hinaus ('bottom: -22px'), damit die
 * Übergangszone nicht von der Beschriftung abgeht. 'z-index: -1' hält es hinter
 * dem Inhalt der Leiste; sie hat mit 'z-index: 40' einen eigenen Stapelkontext,
 * das Pseudoelement bleibt also trotzdem über der Seite. */
list-topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  display: flex; align-items: center; gap: 8px;
  height: calc(var(--bar-h) + env(safe-area-inset-top,0px));
  padding: env(safe-area-inset-top,0px) 12px 0;
  color: #fff;
  background: transparent;

  &::before {
    content: "";
    position: absolute;
    left: 0; right: 0; top: 0; bottom: -22px;
    z-index: -1;
    pointer-events: none;
    -webkit-backdrop-filter: blur(16px) saturate(115%);
    backdrop-filter: blur(16px) saturate(115%);
    background: linear-gradient(to bottom,
      rgba(10,12,18,.42) 0%,
      rgba(10,12,18,.34) 58%,
      rgba(10,12,18,0) 100%);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 100%);
  }

  /* Der Listenname ist die einzige Stelle mit vollem Gewicht — dafür ist die
     Schrift da. Enger gesetzt, damit auch lange Namen ohne Kürzung durchgehen. */
  & .tb-title {
    flex: 1; text-align: center; font-weight: 700; font-size: 19px;
    letter-spacing: -.015em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    cursor: pointer; min-width: 0;

    &[hidden] { display: none; }
  }
  /* Eingabefeld sitzt exakt auf dem Titel — Umbenennen ohne Sprung im Layout. */
  & .tb-title-input {
    background: color-mix(in srgb, currentColor 12%, transparent);
    border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
    border-radius: 10px;
    color: inherit; font: inherit; font-weight: 600; font-size: 17px;
    text-align: center; padding: 4px 10px;

    &:focus { outline: none; border-color: currentColor; }
  }
  & .tb-btn {
    width: 40px; height: 40px; display: grid; place-items: center;
    background: transparent; border: none; color: inherit; cursor: pointer;
    border-radius: 12px;

    &:active { transform: scale(.92); }
    /* Abgeblendet, wenn es nichts zu tun gibt (Besen ohne abgehakte Einträge).
       'pointer-events: none' zusätzlich zu 'disabled', damit auch das
       Antipp-Feedback ausbleibt — sonst wippt ein Knopf, der nichts auslöst. */
    &:disabled {
      opacity: .3;
      cursor: default;
      pointer-events: none;
    }
    &.tb-spinning svg[data-icon="sync"] { animation: tb-rot 1s linear infinite; }
  }
  & svg { width: 22px; height: 22px; stroke-width: 1.8; }
  & [data-icon="off"] { color: var(--muted); }
}

/* Ohne Foto trägt die Leiste die Themefarbe statt Weiss. Steht ausserhalb des
   Blocks, weil die Bedingung am <body> hängt und nicht an der Leiste. */
body:not([data-bg-mode="image"]) list-topbar {
  color: var(--text);

  &::before {
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    background: linear-gradient(to bottom,
      color-mix(in srgb, var(--bg) 94%, transparent) 0%,
      color-mix(in srgb, var(--bg) 82%, transparent) 58%,
      transparent 100%);
  }
}
/* Auch über einem Foto: im hellen Modus ein heller Schleier mit dunkler
   Schrift. Eine dunkle Leiste über einer hell getönten Liste war der einzige
   dunkle Balken im Bild und las sich wie ein Rest aus dem anderen Theme.
   Dieselbe Rechnung wie bei den Bändern — ein Schleier auf 72% ist auch über
   einem dunklen Motiv hell genug, um dunkle Schrift zu tragen. */
:root[data-theme="light"] body[data-bg-mode="image"] list-topbar {
  color: var(--text);

  &::before {
    background: linear-gradient(to bottom,
      color-mix(in srgb, var(--bg) 78%, transparent) 0%,
      color-mix(in srgb, var(--bg) 66%, transparent) 58%,
      transparent 100%);
  }
}
@keyframes tb-rot { to { transform: rotate(360deg); } }

/* =========================================================================
 * Bottombar (fixed bottom): menu-left, input, custom-right
 * ========================================================================= */
list-bottombar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
  display: flex; align-items: center; gap: 8px;
  height: calc(var(--bar-h) + env(safe-area-inset-bottom,0px));
  padding: 0 10px env(safe-area-inset-bottom,0px);
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  /* Mit Präfix, sonst bleibt die Leiste auf iOS 16.5 ein flacher Vollton —
     unprefixed kennt Safari erst ab 18. Genau der fehlende Weichzeichner
     machte sie unten so hell. */
  -webkit-backdrop-filter: blur(14px) saturate(115%);
  backdrop-filter: blur(14px) saturate(115%);
  border-top: 1px solid var(--line);

  & .bb-btn {
    width: 40px; height: 40px; display: grid; place-items: center;
    background: transparent; border: none; color: var(--text); cursor: pointer;
    border-radius: 12px; flex-shrink: 0;

    &:active { transform: scale(.92); }
  }
  & svg { width: 22px; height: 22px; stroke-width: 1.8; }
  & .bb-input {
    flex: 1; height: 40px; border-radius: 999px;
    background: var(--surface-2); border: 1px solid var(--line);
    padding: 0 14px; color: var(--text); font: inherit; font-size: 15px;
    min-width: 0;

    &:focus { outline: none; border-color: var(--accent); }
  }
  & .bb-mic {
    color: var(--muted);

    &.recording { color: var(--danger); animation: pulse 1s infinite; }
    /* Whisper rechnet nach dem Stopp — sichtbar, aber nicht rot: es wird nichts
       mehr aufgenommen. Der Knopf ist derweil gesperrt, ein zweiter Tipp träfe
       sonst mitten in die Erkennung. */
    &.busy {
      color: var(--accent);
      animation: pulse 1.4s infinite;
      cursor: progress;
    }
  }
}
/* Über einem Foto war die Leiste im hellen Modus der hellste Fleck des
   Bildschirms: --bg auf 94% ist praktisch deckendes Creme, und sie steht
   direkt neben dem unteren, ungetönten Teil des Bildes. Zurückgenommen auf
   einen Schleier — das Motiv läuft sichtbar dahinter durch, die Leiste bleibt
   als Fläche trotzdem klar von der Liste getrennt. Im dunklen Modus bleibt es
   bei 94%: dort ist die Leiste dunkel und fällt ohnehin nicht auf. */
:root[data-theme="light"] body[data-bg-mode="image"] list-bottombar {
  background: color-mix(in srgb, var(--bg) 66%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(125%);
  backdrop-filter: blur(20px) saturate(125%);
  border-top-color: color-mix(in srgb, var(--text) 12%, transparent);
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }

/* =========================================================================
 * Abspielsteuerung (Vorlesen) — liegt über der Bottombar
 * ========================================================================= */
/* Gleiche Geometrie wie die Bottombar, ein z-index darüber: die Leiste
   ERSETZT sie nicht, sie deckt sie zu. Beim Vorlesen tippt man weder Einträge
   noch diktiert man, darunter ist also gerade nichts zu holen. */
list-player {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 41;
  display: none;
  align-items: center; gap: 4px;
  height: calc(var(--bar-h) + env(safe-area-inset-bottom,0px));
  padding: 0 10px env(safe-area-inset-bottom,0px);
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(115%);
  backdrop-filter: blur(14px) saturate(115%);
  border-top: 1px solid var(--line);

  &[data-open] { display: flex; }

  & .pl-btn {
    width: 40px; height: 40px; display: grid; place-items: center;
    background: transparent; border: none; color: var(--text); cursor: pointer;
    border-radius: 12px; flex-shrink: 0;
    font: inherit; font-size: 14px; font-weight: 600;

    &:active { transform: scale(.92); }
  }
  & svg { width: 22px; height: 22px; stroke-width: 1.8; }

  /* Der Abspielknopf ist der einzige, den man im Halbdunkel treffen muss. */
  & .pl-play {
    background: var(--accent); color: var(--accent-ink);
    width: 44px; height: 44px;
  }
  & .pl-stop { color: var(--danger); }
  & .pl-rate { min-width: 44px; letter-spacing: -.02em; }

  /* Steht dort, wo sonst das Eingabefeld ist, und schiebt Tempo und Stop nach
     rechts — ohne die Position weiss man beim Pausieren nicht, wo man ist. */
  & .pl-pos {
    flex: 1; min-width: 0;
    text-align: center;
    font-size: 14px; font-variant-numeric: tabular-nums;
    color: var(--muted);
  }
}

/* =========================================================================
 * List-View: Items mit Checkbox, Text, Handle
 * ========================================================================= */
list-view {
  display: block;
}
/* Der Leerzustand zeigt eine angedeutete erste Zeile statt eines Hinweises im
   Nichts — dieselbe Geometrie wie eine echte Zeile, nur zurückgenommen. Man
   sieht damit, was gleich passiert, statt es zu lesen. */
.lv-empty { padding: 8px 0 0; }
.lv-empty-row {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 16px;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  color: color-mix(in srgb, var(--text) 30%, transparent);
}
.lv-empty-check {
  width: 22px; height: 22px; border-radius: 6px;
  border: 1.5px dashed currentColor;
  flex-shrink: 0;
}
.lv-empty-text { font-size: 16px; line-height: 1.4; }
.lv-empty-hint {
  margin: 18px 0 0;
  text-align: center; font-size: 14px;
  color: color-mix(in srgb, var(--text) 55%, transparent);
}
body[data-bg-mode="image"] .lv-empty-row {
  background: rgba(12,14,20,.28);
  color: rgba(255,255,255,.42);
}
body[data-bg-mode="image"] .lv-empty-hint { color: rgba(255,255,255,.75); }

/* Der Block endete hart gegen den Untergrund — ein abgeschnittenes Rechteck,
   kein Ende. Die letzten Pixel laufen deshalb aus. */
.lv-container::after {
  content: "";
  display: block;
  height: 28px;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--text) 8%, transparent), transparent);
}
.lv-container:empty::after { display: none; }

/* Jede Zeile sitzt auf einem halbtransparenten Band. Über Fotos trägt das den
   Text auch dort, wo das Motiv hell wird. Der Trenner ist eine durchgezogene
   Haarlinie auf niedriger Deckkraft — gepunktet wirkte angedeutet statt
   gesetzt, ohne dabei ruhiger zu sein. */
.lv-item {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 13px 16px;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--text) 10%, transparent);
  color: var(--text);
}
.lv-item:last-child { border-bottom: none; }
/* Über Fotos trägt ein heller Schleier den Text nicht — je nach Motiv steht
   Weiss auf Weiss. Dunkles Band plus leichter Schatten hält den Kontrast
   unabhängig davon, was im Bild darunter liegt. */
body[data-bg-mode="image"] .lv-item {
  color: #fff;
  background: rgba(12,14,20,.46);
  border-bottom-color: rgba(255,255,255,.14);
  text-shadow: 0 1px 2px rgba(0,0,0,.55);
}
body[data-bg-mode="image"] .lv-container::after {
  background: linear-gradient(to bottom, rgba(12,14,20,.46), transparent);
}
/* Die weichenden Zeilen gleiten, die gezogene folgt dem Finger ohne Verzug. */
.lv-item { transition: transform .16s cubic-bezier(.2,.7,.3,1); }
.lv-item.dragging {
  transition: none;
  position: relative; z-index: 5;
  box-shadow: 0 10px 24px rgba(0,0,0,.35);
}
body[data-bg-mode="image"] .lv-item.dragging { background: rgba(12,14,20,.72); }

/* ---- Heller Modus über einem Foto --------------------------------------
 * Die Regeln darüber gelten für beide Themes und setzen dunkle Bänder mit
 * weisser Schrift. Damit sah der helle Modus über einem Bild aus wie der
 * dunkle — die Umschaltung im Menü änderte nur die Leiste unten, und die
 * Wahl hatte keinen Sinn mehr.
 *
 * Hier drehen sich die Bänder um: heller Schleier, Schrift in --text. Der
 * Schatten fällt weg, er war für weisse Schrift auf dunklem Grund gedacht und
 * macht dunkle Schrift auf hellem Grund nur unscharf.
 *
 * Der Schleier liegt aber nicht mehr auf jeder Zeile, sondern als EINE Scheibe
 * hinter der ganzen Liste — mit Weichzeichner. Der Grund ist die Deckkraft:
 * ein Schleier, der ohne Weichzeichner auskommen muss, braucht um die 74%,
 * sonst läuft auf unruhigen Motiven die Struktur des Bildes durch die Zeilen
 * und schluckt die Trennlinien. Bei 74% ist die Liste dann aber ein weisser
 * Kasten, und das Bild darunter hätte man sich sparen können. Weichgezeichnet
 * reichen 45%: das Foto bleibt als Farbe und Helligkeit sichtbar, seine
 * Zeichnung stört den Text nicht mehr.
 *
 * Eine Scheibe für alle Zeilen, nicht eine pro Zeile — 'backdrop-filter' pro
 * Element hiesse eine eigene Ebene je Eintrag, bei langen Listen spürbar. */
:root[data-theme="light"] body[data-bg-mode="image"] .lv-container {
  /* Eigener Stapelkontext, damit das 'z-index: -1' des Pseudoelements hier
     drin bleibt und nicht hinter dem Seitenhintergrund landet. */
  position: relative;
  z-index: 0;
}
:root[data-theme="light"] body[data-bg-mode="image"] .lv-container::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background: color-mix(in srgb, var(--surface) 50%, transparent);
  /* 'brightness' hebt dunkle Motive an, damit die Scheibe auch über einem
     Nachtwald hell genug für dunkle Schrift bleibt. Bewusst nur 1.12: bei 1.3
     lief die Liste über hellen Motiven ins Weisse — also genau der deckende
     Kasten, der hier vermieden werden soll. */
  -webkit-backdrop-filter: blur(0px) saturate(125%) brightness(1.12);
  backdrop-filter: blur(0px) saturate(125%) brightness(1.12);
}
:root[data-theme="light"] body[data-bg-mode="image"] .lv-item {
  color: var(--text);
  background: transparent;
  border-bottom-color: color-mix(in srgb, var(--text) 12%, transparent);
  text-shadow: none;
}
/* Die gezogene Zeile hebt sich von der Scheibe ab — sonst wandert nur der
   Schatten und die Zeile selbst bleibt unsichtbar. */
:root[data-theme="light"] body[data-bg-mode="image"] .lv-item.dragging {
  background: color-mix(in srgb, var(--surface) 72%, transparent);
}
/* Die auslaufende Unterkante entfällt hier. Sie stammt von den dunklen
   Bändern, wo der Block sonst als abgeschnittenes Rechteck endete — die
   Scheibe hat dagegen eine eigene Kante, und die Blende darüber legte sich als
   heller Schleier über das Foto unter der Liste. Ohne das ::after endet die
   Scheibe genau an der letzten Zeile. */
:root[data-theme="light"] body[data-bg-mode="image"] .lv-container::after {
  display: none;
}
/* Der Leerzustand steht ausserhalb der Scheibe (eigener Block neben dem
   Container) und bringt seinen Schleier selbst mit. */
:root[data-theme="light"] body[data-bg-mode="image"] .lv-empty-row {
  background: color-mix(in srgb, var(--surface) 52%, transparent);
  color: color-mix(in srgb, var(--text) 38%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(125%);
  backdrop-filter: blur(16px) saturate(125%);
}
/* Der Hinweis steht ohne Band direkt auf dem Bild. Über dem dunklen Scrim des
   dunklen Modus ist Weiss dort richtig — über dem hellen Scrim hier wäre es
   Weiss auf Weiss. */
:root[data-theme="light"] body[data-bg-mode="image"] .lv-empty-hint {
  color: color-mix(in srgb, var(--text) 80%, transparent);
}

/* Kurz gesetzt beim Loslassen, siehe list-view.js */
.lv-container.lv-no-anim .lv-item { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .lv-item { transition: none; }
}
/* Die Checkbox war mit 2px vollem Weiss der kontraststärkste Gegenstand der
   Zeile — lauter als der Eintrag selbst. In einer Einkaufsliste ist der Text
   das Wichtige; das Bedienelement muss nur auffindbar sein. Volle Deckkraft
   bekommt es erst, wenn es etwas zu melden hat: im angehakten Zustand.
   Der Versatz hält es auf der ersten Textzeile, auch wenn der Eintrag umbricht. */
/* Alles, was zu einer Zeile gehört, hängt unter '.lv-item'. Die
 * theme-abhängigen Fassungen stehen weiter oben, weil ihre Bedingung am
 * <body> hängt und nicht an der Zeile. */
.lv-item {
  /* Die Checkbox war mit 2px vollem Weiss der kontraststärkste Gegenstand der
     Zeile — lauter als der Eintrag selbst. In einer Einkaufsliste ist der Text
     das Wichtige; das Bedienelement muss nur auffindbar sein. Volle Deckkraft
     bekommt es erst, wenn es etwas zu melden hat: im angehakten Zustand.
     Der Versatz hält es auf der ersten Textzeile, auch wenn der Eintrag
     umbricht. */
  & .lv-check {
    width: 22px; height: 22px; border-radius: 6px;
    border: 1.5px solid color-mix(in srgb, currentColor 45%, transparent);
    display: grid; place-items: center; flex-shrink: 0;
    cursor: pointer; background: transparent;
    color: inherit;
    margin-top: 1px;
    transition: border-color .14s ease, background-color .14s ease;

    &.checked {
      background: currentColor;
      border-color: currentColor;

      &::after {
        content: ""; width: 10px; height: 6px;
        border: 2px solid var(--bg); border-top: 0; border-right: 0;
        transform: rotate(-45deg) translate(0px, -1px);
      }
    }
  }

  /* Textarea und Stiftstrich liegen übereinander — deshalb eine gemeinsame
     Hülle, die die Flex-Breite trägt. */
  & .lv-textwrap {
    flex: 1; min-width: 0;
    position: relative;
  }
  /* textarea: bricht um statt seitlich zu scrollen. Die Höhe kommt bevorzugt
     aus 'field-sizing: content' — nativ und ohne Messzeitpunkt. Wo das fehlt,
     springt autoGrow() in list-view.js ein. */
  & .lv-text {
    display: block; width: 100%;
    background: transparent; border: none; color: inherit; font: inherit;
    font-size: 16px; line-height: 1.4;
    padding: 3px 0;
    field-sizing: content;
    word-break: break-word;
    resize: none;
    overflow: hidden;
    /* head.css setzt textarea auf min-height:80px — für eine Listenzeile viel
       zu hoch, und autoGrow() käme dagegen nicht an. */
    min-height: 0;

    &:focus { outline: none; }
  }

  /* ---- Der Haken ist ein Stift -----------------------------------------
   * Abgehakte Einträge bekamen 'text-decoration: line-through': eine exakte,
   * haardünne, mittig sitzende Linie. So sieht kein Zettel aus, den jemand
   * abgearbeitet hat. Stattdessen ein gezeichneter Strich pro Textzeile, der
   * beim Antippen von links nach rechts läuft.
   *
   * Die Pfade baut list-view.js aus echten Zeilenkästen (Range +
   * getClientRects auf einem Messknoten), damit der Strich dort endet, wo der
   * Text endet — nicht am Rand des Kastens. 'non-scaling-stroke' hält die
   * Strichstärke unabhängig davon, wie das SVG in seinen Kasten skaliert.
   *
   * Unsichtbar, solange nichts abgehakt ist — und zwar über 'visibility',
   * nicht über die Strichlinie allein: eine runde Strichkappe wird auch dann
   * noch gezeichnet, wenn der Strich vollständig versetzt ist, und hinterliess
   * hinter jedem offenen Eintrag einen Punkt. Beim Zurücknehmen schaltet
   * 'visibility' verzögert, damit der Strich sich noch zurückziehen darf. */
  & .lv-strike {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    pointer-events: none;
    overflow: visible;
    color: inherit;
    visibility: hidden;
    transition: visibility 0s linear .19s;

    & path {
      fill: none;
      stroke: currentColor;
      stroke-width: 2;
      stroke-linecap: round;
      vector-effect: non-scaling-stroke;
      opacity: .8;
      /* Länge setzt list-view.js je Pfad; 0 = fertig gezeichnet. */
      stroke-dashoffset: var(--len, 0);
      stroke-dasharray: var(--len, 0);
      transition: stroke-dashoffset .19s cubic-bezier(.4,.1,.5,1);
      transition-delay: calc(60ms * var(--l, 0));
    }
  }
  &.done {
    & .lv-strike {
      visibility: visible;
      transition: visibility 0s linear 0s;

      & path { stroke-dashoffset: 0; }
    }
    & .lv-text { opacity: .45; }
  }

  /* Zwei dicke Balken in vollem Weiss lasen sich als Gleichheitszeichen, nicht
     als Griff — und zogen Aufmerksamkeit vom Eintrag ab. Drei dünne Linien,
     zurückgenommen, voll deckend erst beim Anfassen. */
  /* Zwischenüberschrift über der ersten abgehakten Zeile. Sie hängt AN der
     Zeile (siehe list-view.js), liegt aber absolut darüber — der Platz kommt
     aus dem margin-top der Zeile selbst.
     Beim Wischen fährt sie mit, weil sie ein Kind ist; deshalb blendet sie
     aus, sobald die Geste greift. Sichtbar mitzureisen sähe aus, als gehörte
     die Überschrift zum Eintrag. */
  & .lv-head { display: none; }
  &.lv-first-done {
    margin-top: 30px;

    & .lv-head {
      display: block;
      position: absolute;
      left: 16px; right: 16px; top: -24px;
      font-size: 12px; font-weight: 600;
      letter-spacing: .06em;
      text-transform: uppercase;
      opacity: .5;
      pointer-events: none;
      transition: opacity .12s ease;
    }
    &.lv-swipe .lv-head { opacity: 0; }
  }

  & .lv-handle {
    width: 28px; height: 28px; display: grid; place-items: center;
    cursor: grab; color: inherit; opacity: .32; flex-shrink: 0;
    touch-action: none;
    margin-top: 1px;
    transition: opacity .14s ease;
    /* Ohne Reset schlägt die native Button-Optik durch (weiße Fläche + Rahmen). */
    background: transparent; border: none; padding: 0;

    &:active { cursor: grabbing; opacity: .85; }
  }
  &.dragging .lv-handle { cursor: grabbing; opacity: .85; }
}

@media (prefers-reduced-motion: reduce) {
  .lv-item .lv-strike path { transition: none; }
}

/* Der Messknoten darf das Layout nicht anfassen und nie sichtbar werden. */
.lv-meter {
  position: absolute; top: 0; left: -9999px;
  visibility: hidden; pointer-events: none;
  white-space: pre-wrap; word-break: break-word;
}

/* =========================================================================
 * Wischen nach rechts = löschen
 * -------------------------------------------------------------------------
 * Der Hinweis hängt an der LINKEN Kante der Zeile ('right: 100%') und ist ihr
 * Kind — dasselbe transform bewegt beide. Er wird hereingezogen, nicht
 * aufgedeckt; deshalb kann er weder hinter der Zeile hervorkommen noch
 * zurückbleiben.
 * ========================================================================= */
/* 'clip' statt 'hidden': hidden machte den Container zum Scroll-Container und
   nähme die senkrechte Achse mit — die gezogene Zeile beim Sortieren wäre dann
   an der Blockkante abgeschnitten. 'clip' schneidet nur diese eine Achse.
   Nötig ist es nach rechts: eine verschobene Zeile vergrössert sonst den
   scrollbaren Bereich, und die Seite liesse sich seitlich wegziehen. */
.lv-container { overflow-x: clip; }

/* 'relative' als Bezugspunkt für den Hinweis — ohne das hinge er am
   Ansichtsfenster statt an der Zeile.
   'pan-y' ist der Kern der Geste: senkrecht scrollt der Browser weiter selbst,
   waagrecht gehört uns. Nur deshalb kommt beim Rechtswisch kein
   'pointercancel'. Der Griff behält 'touch-action: none'; der Browser bildet
   die Schnittmenge, none ∩ pan-y = none, dort ändert sich also nichts. */
.lv-item { position: relative; touch-action: pan-y; }

.lv-item .lv-hint {
  display: none;
  position: absolute;
  top: 0; bottom: 0; right: 100%;
  /* Voll breit, damit beim Weiterziehen links keine Kante auftaucht. */
  width: 100vw;
  align-items: center; justify-content: flex-end; gap: 8px;
  padding-right: 16px;
  /* Vor der Schwelle zurückgenommen, danach voll: die Farbe sagt, ob das
     Loslassen jetzt löscht. Zweiter Weg neben dem kurzen Rütteln — für alle,
     bei denen das Rütteln aus ist oder nicht ankommt. */
  background: color-mix(in srgb, var(--danger) 58%, transparent);
  color: var(--danger-ink);
  font-size: 15px; font-weight: 600; line-height: 1;
  /* Über Fotos erbt die Zeile einen Textschatten — auf Rot ist der Schmutz. */
  text-shadow: none;
  pointer-events: none;
  transition: background-color .12s ease;

  & svg { width: 20px; height: 20px; flex-shrink: 0; }
}
.lv-item.lv-swipe .lv-hint { display: flex; }
.lv-item.lv-armed .lv-hint { background: var(--danger); }

/* Solange der Finger führt, kein Übergang: die Zeile steht ohne Verzug unter
   ihm. Dieselbe Sperre, die '.dragging' beim Sortieren schon hat. */
.lv-item.lv-follow { transition: none; }

/* Hinausfliegen: eigene Kurve, weil der Übergang aus '.lv-item' fürs Ausweichen
   beim Sortieren gedacht und dafür zu kurz ist. Die Dauer muss zu FLY_MS in
   list-view.js passen. */
.lv-item.lv-fly {
  transition: transform .19s cubic-bezier(.4,.1,.5,1), opacity .19s linear;
  opacity: 0;
}

/* Mit der Maus zöge man sonst quer durch die Zeile eine Textmarkierung. */
.lv-container.lv-swiping { user-select: none; -webkit-user-select: none; }

@media (prefers-reduced-motion: reduce) {
  .lv-item.lv-fly { transition: none; }
}

/* =========================================================================
 * Fullscreen-Overlays: Fly-in, Main-Menu, Share, Scanner
 * ========================================================================= */
.fs-overlay {
  position: fixed; inset: 0; z-index: 60;
  background: var(--overlay-bg);
  backdrop-filter: blur(8px);
  display: none;
}
.fs-overlay[data-open] { display: block; }
/* Nur aktiv, solange Layout- und sichtbare Höhe auseinanderlaufen (siehe
   Viewport-Korrektur in app.js). 'fixed' hängt an der fixierten Ebene, und
   genau die zeichnet Chrome nach der Bildauswahl versetzt. 'absolute' hängt
   dagegen am Dokument und ist von dem Versatz nicht betroffen — der Seiten-
   scroll ist gesperrt, solange ein Overlay offen ist, deshalb deckt es
   trotzdem den sichtbaren Bereich. */
html[data-vv-fix] .fs-overlay {
  position: absolute;
  top: calc(var(--vv-scroll, 0px) + var(--vv-top, 0px));
  left: 0; right: 0;
  bottom: auto;
  height: var(--vv-h, 100dvh);
}
.fs-overlay .fs-close {
  position: absolute; top: calc(env(safe-area-inset-top,0px) + 10px); right: 12px;
  width: 40px; height: 40px; display: grid; place-items: center;
  background: color-mix(in srgb, currentColor 14%, transparent);
  border: none; color: inherit; cursor: pointer;
  border-radius: 999px;
}
.fs-overlay .fs-close svg { width: 20px; height: 20px; stroke-width: 2; }

/* =========================================================================
 * List-Flyin (rechts unten, Stagger)
 * ========================================================================= */
/* Das Overlay bleibt im DOM und im Layout, geschaltet wird nur die
   Sichtbarkeit — das 'display: none' aus .fs-overlay muss dafür weg, sonst
   gäbe es beim Öffnen kein Element, an dem ein Übergang laufen könnte.
   'visibility' schaltet beim Schliessen verzögert, damit die Pillen ihren
   Abflug noch zeigen dürfen; sofortiges 'hidden' würde sie schlagartig
   verschwinden lassen. 'pointer-events' hängt nicht am Timing: der Deckel
   darf ab dem ersten Moment nichts mehr abfangen. */
list-flyin {
  &.flyin-overlay {
    background: rgba(10, 12, 18, .35);
    backdrop-filter: blur(4px);
    display: block;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    transition: opacity .22s ease, visibility 0s linear .28s;

    &[data-open] {
      visibility: visible;
      opacity: 1;
      pointer-events: auto;
      transition: opacity .22s ease, visibility 0s linear 0s;
    }
  }

  & .flyin-stack {
    position: absolute;
    right: 4px;
    bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px) + 12px);
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    max-height: calc(100dvh - var(--bar-h)*2 - 40px);
    /* Mit vier Aktionen neben der Pille wird die Zeile sonst breiter als der
       Bildschirm und das Löschen-Symbol rutscht links raus. */
    max-width: calc(100vw - 12px);
    /* Die Pills starten außerhalb (translateX 140%). Ohne horizontales Clipping
       scrollt der Stack seitlich. padding-right gleicht das kleinere 'right'
       wieder aus und lässt dem Schatten Platz. */
    overflow-x: hidden;
    overflow-y: auto;
    padding: 0 8px 4px 0;
  }

  /* '--i' zählt in Anzeigereihenfolge von oben nach unten, das Plus ganz oben
   * bekommt die 0. Die Staffelung läuft damit einmal von oben durch, ohne am
   * Ende wieder nach oben zu springen. */
  & .pi-row {
    display: flex;
    align-items: center;
    /* Ohne die Begrenzung wächst die Zeile über den Stack hinaus, sobald die
       Longpress-Aktionen dazukommen — die linken Symbole lägen ausserhalb des
       Bildschirms. Die Pille gibt stattdessen nach, ihr Text kürzt sich. */
    max-width: 100%;
    visibility: hidden;
    opacity: 0;
    transform: translateX(140%);
    /* Abflug: alle gleichzeitig (kein Delay), und 'visibility' erst danach. */
    transition: transform .24s cubic-bezier(.4,0,.6,1) 0s,
                opacity .2s ease 0s,
                visibility 0s linear .24s;
    will-change: transform, opacity;

    /* Einflug gestaffelt über --i, 'visibility' sofort. 34ms Versatz statt 60:
       bei sechs Listen dauerte der Einflug sonst über eine halbe Sekunde, bis
       die unterste stand — man wartete auf die Liste, statt sie zu benutzen. */
    &.shown {
      visibility: visible;
      opacity: 1;
      transform: translateX(0);
      transition: transform .26s cubic-bezier(.22,.84,.22,1) calc(34ms * var(--i, 0)),
                  opacity .22s ease calc(34ms * var(--i, 0)),
                  visibility 0s linear 0s;
    }
  }
  /* Nur die Plus-Zeile hat zwei Kinder mit Abstand (Namensfeld + Knopf). Die
     Pillen-Zeilen bekommen ihren Abstand aus dem Aktions-Container, sonst
     klaffte dort auch im eingeklappten Zustand eine Lücke. */
  & .pi-row-plus { gap: 8px; }

  & .pi-pill {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 14px;
    min-height: 40px;
    border-radius: 999px;
    background: #f5f3ee; color: #1a1916;
    border: none;
    font: inherit; font-size: 15px; font-weight: 500;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0,0,0,.18);
    white-space: nowrap;
    max-width: min(70vw, 320px);
    min-width: 0;
    overflow: hidden;

    & .pi-txt { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
    & .pi-ico { width: 18px; height: 18px; flex-shrink: 0; opacity: .75; }

    &.active {
      background: var(--accent-pill);
      color: #fff;

      & .pi-ico { opacity: 1; }
    }
    &:active { transform: scale(.96); }
  }

  /* Plus */
  & .pi-plus {
    width: 46px; height: 46px;
    display: grid; place-items: center;
    border-radius: 999px;
    border: none;
    background: var(--danger); color: var(--danger-ink);
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(225,29,72,.35);

    & svg { width: 22px; height: 22px; }
    &:active { transform: scale(.94); }
  }

  /* Longpress-Actions (links neben der gedrückten Pille) -------------------
   * Die Knöpfe stecken immer im DOM. Der Container klappt die Breite auf, die
   * Knöpfe blenden gestaffelt ein. Breite statt 'display', weil nur so ein
   * Übergang läuft — und ohne Messerei: jede Aktion ist 40px breit plus 8px
   * Abstand, --n steht im Markup, also ist die Zielbreite ausrechenbar.
   * Eingeklappt ist die Breite 0, das padding-right verschwindet mit. */
  & .pi-actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    padding-right: 8px;
    box-sizing: border-box;
    overflow: hidden;
    width: 0;
    visibility: hidden;
    opacity: 0;
    transition: width .2s ease, opacity .16s ease, visibility 0s linear .2s;
  }

  & .pi-action {
    flex: 0 0 auto;
    width: 40px; height: 40px;
    display: grid; place-items: center;
    border-radius: 999px;
    border: none;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0,0,0,.25);
    color: #fff;
    opacity: 0;
    /* Von rechts hereinkommen, aus der Richtung der Pille — nicht bloss
       aufploppen. Beim Einklappen fliegen sie ohne Staffel wieder dorthin. */
    transform: translateX(14px) scale(.7);
    transition: transform .18s ease, opacity .14s ease;

    & svg { width: 18px; height: 18px; }
    &.pi-del { background: var(--danger); }
    &.pi-share { background: var(--share); }
    &.pi-speak { background: #059669; }
    &.pi-rename { background: #b45309; }
  }

  /* Aufgeklappt: Container auf Zielbreite, Knöpfe gestaffelt herein.
     --a zählt in Leserichtung, das Markup setzt es: Löschen 0, Vorlesen 1, … */
  & .pi-row.actions {
    & .pi-actions {
      width: calc(var(--n, 4) * 48px);
      visibility: visible;
      opacity: 1;
      transition: width .2s ease, opacity .16s ease, visibility 0s linear 0s;
    }
    & .pi-action {
      opacity: 1;
      transform: none;
      transition-delay: calc(45ms * var(--a, 0));

      /* Muss die Regel darüber überbieten, sonst frisst die das Tap-Feedback. */
      &:active { transform: scale(.94); }
    }
  }
}

/* Alles, was auf Longpress oder Ziehen reagiert, darf beim Halten weder Text
   markieren noch das Kontextmenü des Systems aufziehen. Steht ausserhalb der
   Komponentenblöcke, weil die Regel zwei davon betrifft. */
list-flyin .pi-pill,
list-flyin .pi-action,
list-flyin .pi-plus,
.lv-item .lv-handle,
.lv-item .lv-check {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* Textfeld-Grundgestalt für die Overlays. Lag vorher nur im Scanner; mit dem
   zweiten Feld im Menü wären daraus zwei Kopien geworden, die mit der Zeit
   auseinanderlaufen. Gemischt wird gegen currentColor, damit es in beiden
   Themes und auch über einem Foto sitzt.

   Als 'input.app-field' geschrieben, nicht als blosse Klasse: head.css setzt
   'input[type=text] {…}', und dagegen käme eine einzelne Klasse nicht an —
   das Feld behielte den Vollton aus dem Template. Gleiche Spezifität,
   app.css steht später, also gewinnt diese Regel. */
input.app-field {
  width: 100%;
  padding: 10px 14px;
  background: color-mix(in srgb, currentColor 10%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  border-radius: 10px;
  color: inherit; font: inherit;
}
input.app-field:focus { outline: none; border-color: var(--accent); }

/* =========================================================================
 * Main-Menu (Fullscreen)
 * ========================================================================= */
/* Das Overlay selbst scrollt NICHT. Kopfzeile und Inhaltsspalte sitzen als
   Flex-Zeilen darin, gescrollt wird nur .mm-scroll. Vorher war das fixe
   Overlay selbst der Scroller mit sticky Kopfzeile — auf Android kam die
   Kombination aus der Bildauswahl mit verschobenem Paint bei unverändertem
   Hit-Test zurück (schwarzer Streifen oben, ✕ traf noch an alter Stelle). */
/* Farben kommen aus den Theme-Tokens, damit der Hell/Dunkel-Wechsel auch am
   offenen Menü sofort greift. Alles Weitere darin mischt gegen currentColor
   statt gegen festes Weiss. */
main-menu {
  &.mm-overlay {
    background: var(--bg);
    color: var(--text);
    overflow: hidden;
    padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px);

    /* '.mm-overlay' muss mit in den Selektor: '.fs-overlay[data-open]' setzt
       display:block und hätte sonst die höhere Spezifität. */
    &[data-open] {
      display: flex;
      flex-direction: column;
      animation: mm-in .24s cubic-bezier(.22,.84,.22,1) both;
    }
  }

  /* Inhalt kommt einen Tick nach der Fläche — das Menü baut sich auf, statt
     schlagartig da zu sein. */
  &[data-open] .mm-scroll {
    animation: mm-content-in .3s cubic-bezier(.22,.84,.22,1) .06s both;
  }
  /* Schliessen: main-menu.js hält das Overlay noch 180ms mit 'data-closing'. */
  &[data-open][data-closing] {
    animation: mm-out .18s ease-in both;
    pointer-events: none;   /* keine Klicks mehr während des Ausblendens */

    & .mm-scroll { animation: mm-content-out .18s ease-in both; }
  }

  /* Feste Kopfzeile ausserhalb des Scrollbereichs. Das Menü ist deutlich höher
     als ein Telefon-Viewport (Hintergrund-Grid allein ~900px) — scrollte das ✕
     mit, liesse sich das Menü beim "Eigenes Bild"-Knopf nicht mehr schliessen. */
  & .mm-top {
    flex: 0 0 auto;
    position: relative;
    display: flex; align-items: center; justify-content: space-between;
    /* Gleicher Abstand oben wie unten — vorher 12/10, und die gefüllten Kreise
       wirkten dadurch, als klebten sie am Trennstrich. */
    padding: 14px 12px;
    border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent);
  }
  /* Beide Schaltflächen gleich gross und rund. Der Theme-Knopf war eine 56x30
     Pille und damit das einzige Element dieser Form im ganzen Menü. */
  & .mm-theme {
    width: 40px; height: 40px; border-radius: 999px;
    background: color-mix(in srgb, currentColor 12%, transparent); border: none; color: inherit;
    display: grid; place-items: center; cursor: pointer;
    flex: 0 0 auto;

    & svg { width: 18px; height: 18px; }
  }
  /* Verankert die Kopfzeile — der Theme-Knopf stand vorher ohne Bezug oben
     links. Absolut mittig statt im Flex-Fluss: Theme-Knopf (56px) und ✕ (40px)
     sind unterschiedlich breit, im Fluss läge der Titel also aus der Mitte
     versetzt. 'max-width' hält ihn von den beiden Schaltflächen fern, statt
     darunter zu laufen. */
  & .mm-title {
    position: absolute;
    left: 50%; transform: translateX(-50%);
    max-width: calc(100% - 150px);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 17px; font-weight: 600;
    letter-spacing: -.01em;
    pointer-events: none;
  }
  & .fs-close {
    position: static; /* overrides .fs-overlay .fs-close absolute */
    background: color-mix(in srgb, currentColor 12%, transparent); color: inherit;
  }

  & .mm-scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    width: 100%;
    padding: 20px 16px 40px;
    max-width: 720px; margin: 0 auto;
  }

  & .mm-section {
    margin-bottom: 32px;

    /* Hairline zwischen den Blöcken statt bloßem Abstand. */
    & + .mm-section {
      border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
      padding-top: 32px;
    }
    & h3 {
      font-size: 20px; font-weight: 500; margin: 0 0 16px;
      color: inherit;
    }
  }
  & .mm-hint {
    color: var(--muted); font-size: 14px;
    line-height: 1.5; margin-bottom: 16px;
  }

  /* Background-Grid */
  & .mm-bg-grid {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
    margin-bottom: 16px;
  }
  & .mm-bg-tile {
    /* block, nicht das Voreingestellte inline-block: als Inline-Element hängt
       unter dem Knopf noch die Unterlängen-Zeile, und im Slot machte die die
       Kachel 6px kürzer als ihr Feld. */
    display: block;
    aspect-ratio: 3/4;
    border-radius: 6px; border: 2px solid transparent;
    background-color: color-mix(in srgb, currentColor 14%, transparent);
    background-size: cover; background-position: center;
    cursor: pointer;
    transition: border-color .15s, transform .15s;
    padding: 0;

    &:active { transform: scale(.97); }
    &.active { border-color: currentColor; }
  }
  /* Eigenes Bild: Kachel plus Löschknopf. Der Slot ist das Rasterfeld, die
     Kachel füllt ihn aus — ein Knopf im Knopf wäre ungültiges Markup. */
  & .mm-bg-slot {
    position: relative;
    aspect-ratio: 3/4;

    & .mm-bg-tile { width: 100%; height: 100%; aspect-ratio: auto; }

    & .mm-bg-del {
      position: absolute; top: 4px; right: 4px;
      width: 26px; height: 26px;
      display: grid; place-items: center;
      border: none; border-radius: 999px;
      /* Eigener dunkler Grund statt Theme-Farbe: die Kachel darunter kann jedes
         Motiv sein, ein durchscheinendes X wäre auf hellen Bildern unsichtbar. */
      background: rgba(12,14,20,.62);
      color: #fff;
      cursor: pointer;
      padding: 0;
      -webkit-backdrop-filter: blur(4px);
      backdrop-filter: blur(4px);

      & svg { width: 14px; height: 14px; }
      &:active { transform: scale(.9); }
    }
  }

  & .mm-bg-none {
    display: grid; place-content: center; justify-items: center; gap: 8px;
    color: var(--muted);

    & svg { width: 26px; height: 26px; }
  }
  & .mm-bg-label { font-size: 13px; }
  & .mm-bg-actions { display: flex; justify-content: center; }

  & .mm-btn-ghost {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 18px;
    background: color-mix(in srgb, currentColor 10%, transparent); color: inherit;
    border: 1px solid color-mix(in srgb, currentColor 18%, transparent); border-radius: 999px;
    cursor: pointer; font: inherit; font-size: 14px;

    & svg { width: 18px; height: 18px; }
  }

  /* Farbschema */
  & .mm-colors { display: flex; gap: 14px; flex-wrap: wrap; }
  & .mm-color {
    width: 42px; height: 42px; border-radius: 50%;
    border: 3px solid transparent; cursor: pointer;
    padding: 0; box-shadow: 0 2px 6px rgba(0,0,0,.2);

    &.active { border-color: currentColor; }
  }

  /* Backup */
  & .mm-backup-actions {
    display: flex; gap: 10px; margin-top: 8px;
    flex-wrap: wrap;
  }
  & .mm-btn-outline {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 16px;
    background: transparent; color: inherit;
    border: 1px solid color-mix(in srgb, currentColor 38%, transparent); border-radius: 999px;
    cursor: pointer; font: inherit; font-size: 14px;

    & svg { width: 16px; height: 16px; }
    &:active { background: color-mix(in srgb, currentColor 8%, transparent); }
  }
}

@keyframes mm-in {
  from { opacity: 0; }
}
@keyframes mm-content-in {
  from { opacity: 0; transform: translateY(14px); }
}
@keyframes mm-out {
  to { opacity: 0; }
}
@keyframes mm-content-out {
  to { opacity: 0; transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
  main-menu[data-open],
  main-menu[data-open] .mm-scroll,
  main-menu[data-open][data-closing],
  main-menu[data-open][data-closing] .mm-scroll { animation: none; }
}

/* =========================================================================
 * Share-Modal
 * ========================================================================= */
share-modal {
  &.share-overlay {
    background: var(--bg);
    color: var(--text);
  }
  &[data-open] { display: block; }

  & .share-body {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    min-height: 100dvh;
    padding: env(safe-area-inset-top, 0px) 24px calc(env(safe-area-inset-bottom, 0px) + 32px);
    max-width: 480px; margin: 0 auto;
    gap: 28px;
  }
  & .share-qr {
    background: #fff;
    padding: 12px;
    border-radius: 6px;
    width: 240px; height: 240px;
    display: grid; place-items: center;

    & svg { width: 100%; height: 100%; }
  }
  & .share-hint {
    color: var(--text);
    font-size: 15px; text-align: center;
    line-height: 1.5;
  }
  & .share-copy {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    cursor: pointer;
    padding: 8px 12px; border-radius: 12px;

    &:active { background: color-mix(in srgb, currentColor 8%, transparent); }
  }
  & .share-copy-label {
    font-size: 15px; color: inherit;
  }
  & .share-copy-id {
    font-family: ui-monospace, monospace;
    font-size: 13px;
    color: var(--muted);
    word-break: break-all;
    text-align: center;
  }
  & .share-err {
    color: var(--danger); font-size: 14px;
  }
}

/* =========================================================================
 * Scanner-Modal
 * ========================================================================= */
scanner-modal {
  &.scanner-overlay {
    background: var(--bg);
    color: var(--text);
  }
  &[data-open] { display: block; }

  & .scanner-body {
    display: flex; flex-direction: column;
    min-height: 100dvh;
    padding: calc(env(safe-area-inset-top, 0px) + 60px) 16px calc(env(safe-area-inset-bottom, 0px) + 24px);
    max-width: 480px; margin: 0 auto;
    gap: 24px;
  }
  & .scanner-cam {
    width: 100%; aspect-ratio: 1/1;
    background: #000; border-radius: 12px;
    overflow: hidden;

    /* html5-qrcode setzt die Masse per Inline-Stil — dagegen hilft nur
       '!important', sonst sprengt das Video den Rahmen. */
    & video { width: 100% !important; height: 100% !important; object-fit: cover; }
  }
  & .scanner-alt {
    display: flex; flex-direction: column; gap: 10px;

    & p { color: var(--muted); font-size: 14px; }
  }
  & .scanner-input-row {
    display: flex; gap: 8px;

    & input { flex: 1; }
  }
  & .scanner-err {
    color: var(--danger); font-size: 14px;
  }
}

/* ============================================================================
 * driver.js — Tour-Popover an die App angeglichen
 * ----------------------------------------------------------------------------
 * Die mitgelieferten Stile setzen 'all: unset' und danach feste Farben; darum
 * braucht fast jede Zeile hier '!important'. Das Blatt wird ausserdem erst zur
 * Laufzeit eingehängt (siehe js/onboarding.js) und steht damit nach app.css —
 * ohne '!important' gewänne es auch bei gleicher Spezifität.
 *
 * Der Grund ist deckend gehalten, nicht durchscheinend: das Popover liegt über
 * dem abgedunkelten Bildschirm, und darunter kann ein Foto liegen.
 * ========================================================================== */
.driver-popover {
  background: var(--surface) !important;
  color: var(--text) !important;
  border: 1px solid var(--line) !important;
  border-radius: 14px !important;
  box-shadow: 0 18px 42px -12px rgba(0, 0, 0, .55) !important;
  font-family: var(--font) !important;
  padding: 16px 18px !important;
  min-width: 240px; max-width: 320px;

  & * { font-family: var(--font) !important; text-shadow: none !important; }

  & .driver-popover-title {
    color: var(--text) !important;
    font-size: 16px !important; font-weight: 700 !important;
    line-height: 1.3 !important;
    margin: 0 26px 8px 0 !important;
  }
  & .driver-popover-description {
    color: var(--text) !important;
    font-size: 14px !important; line-height: 1.5 !important;
  }
  & .driver-popover-progress-text {
    color: var(--muted) !important;
    font-size: 12px !important; font-weight: 600 !important;
  }
  & .driver-popover-footer {
    margin-top: 14px !important;

    /* Der Weiter-Knopf muss INNERHALB dieser Verschachtelung stehen: als
       eigene Regel daneben wäre er eine Stelle unspezifischer als
       '.driver-popover-footer button' und bliebe farblos. */
    & button {
      background: color-mix(in srgb, currentColor 10%, transparent) !important;
      color: var(--text) !important;
      border: 1px solid var(--line) !important;
      border-radius: 10px !important;
      font-size: 13px !important; font-weight: 600 !important;
      padding: 9px 15px !important;
    }
    & .driver-popover-next-btn {
      background: var(--accent) !important;
      color: var(--accent-ink) !important;
      border-color: transparent !important;
    }
  }
  & .driver-popover-close-btn {
    color: var(--muted) !important;
    font-size: 20px !important; line-height: 1 !important;
    width: 26px !important; height: 26px !important;
    top: 8px !important; right: 8px !important;
  }
  /* Der Pfeil erbt seine Farbe aus vier festen border-Regeln je Seite — die
     alle nachzuziehen wäre mehr Aufwand als er wert ist. */
  & .driver-popover-arrow { display: none !important; }
}
.driver-overlay { opacity: .72 !important; }
