/* === Reset & Base === */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }

:root {
  /* Colors */
  --bg:          #0f0f0f;
  --surface:     #1a1a1a;
  --surface2:    #242424;
  --card-bg:     var(--surface);
  --border:      #2e2e2e;
  --border-strong: rgba(255,255,255,0.10);
  /* Eigener, hellerer Rand NUR für Eingabefelder: --border (#2e2e2e) gegen den
     Feld-BG (--surface2) misst 1.14:1, WCAG 1.4.11 (Non-text Contrast) verlangt
     3:1 für Bedienelement-Grenzen. #6f6f6f ergibt 3.07:1 gegen --surface2 und
     3.44:1 gegen --surface. (Das im Audit vorgeschlagene #4a4a4a erreicht nur
     1.75:1 — nachgerechnet, reicht nicht.) Karten/Sektionen dürfen weiter
     dezent mit --border arbeiten, dort gilt 1.4.11 nicht. */
  --border-input: #6f6f6f;
  --text:        #e8e8e8;
  --text-muted:  #b0b0b0;
  --text-brand:  #f6f7f4;
  --text-accent-label: #d7c7a3;
  --accent:      #4caf82;
  /* --accent ist die TEXT-/Akzentlinien-Farbe (5.7–6.4:1 auf den dunklen
     Flächen). Als FLÄCHE hinter weißem Text erreicht sie nur 2.71:1.
     --accent-btn ist deshalb die einzige zulässige Weiß-auf-Grün-Fläche
     (4.79:1); .btn-primary war bereits so abgedunkelt, die Geschwister-CTAs
     nicht. --accent-btn-strong ist der dunkle Gradient-Stop (6.46:1). */
  --accent-btn:        #3a7f61;
  --accent-btn-strong: #2e6950;
  /* Schrift/Icon auf einer --accent-btn-Fläche (gefüllte Chips, Primärknopf). */
  --on-accent:   #fff;
  --danger:      #e05252;
  /* Fehler-TEXT ist nie das rohe --danger: das misst nur 4.06:1 auf
     --surface2. Aufgehellt mit --text: 5.02:1 auf --surface2, 5.63:1 auf
     --surface. Eine Quelle fuer alle Fehlertexte (Befund 13).
     --danger bleibt Flaechen-/Rahmenfarbe. */
  --danger-text: color-mix(in srgb, var(--danger) 80%, var(--text));
  --warning:     #d6aa4a;
  /* Text von .btn-decline („Ablehnen“): etwas heller als --danger-text, damit er auch auf getönten Karten AA hält
     (Saldo-Vorschlag: Fläche mit 8 % --warning, --danger-text misst dort 4,32:1; dies 4,84:1). */
  --decline-text: color-mix(in srgb, var(--danger) 70%, var(--text));
  /* Reliability tier "Aufbauend" (60-79) — see RELIABILITY_TIERS in app.js. */
  --rel-building: #4cd6c4;
  --signal-date-color:     #9ab4ff;
  --signal-progress-color: #9edfbf;

  /* Radii */
  --radius:      10px;
  --radius-xs:   8px;
  --radius-btn:  12px;
  --radius-sm:   6px;
  --radius-tile: 12px;   /* Kacheln & Kästen (.kpi-card, .snapshot-item, .sd-sum-card); Knöpfe bleiben --radius-btn */
  --radius-card: 18px;
  --radius-modal: 22px;
  --radius-pill:  999px;

  /* Sizing */
  --touch-target: 44px;
  --taxi-map-h:  220px;

  /* Modal-Größenskala (Desktop): eine Quelle der Wahrheit für Breiten-Stufen
     und EINEN gemeinsamen Höhen-Cap, damit alle Modal-Typen konsistent skalieren.
     Ausnahme: .modal-card-confirm (modal-confirm, modal-logs-range-delete)
     überschreibt gezielt auf 360px statt der --modal-w-sm-Basis — kurze
     Ja/Nein-Dialoge wirken bei 440px unnötig breit/leer. Bewusste Abweichung,
     kein unbeabsichtigter Drift. */
  --modal-w-sm: 440px;   /* Basis: Zahlung, Entscheiden, Auszahlung … */
  --modal-w-md: 540px;   /* Formulare: Neue Leihe, Ergänzung, Gegenangebot */
  --modal-w-lg: 640px;   /* Breit: Einstellungen, Statistik, Profil, Taxi */
  --modal-w-xl: 820px;   /* Extra: Log-Archiv */
  --modal-max-h: min(88dvh, 860px);
  /* D12: Die Stufen standen bis 2560px fest. Die datenreichsten Dialoge
     (Statistik mit 881px breiter Heatmap, Benutzer/Rechte-Editor,
     Rueckmeldungen-Admin) steckten auf 640px und scrollten horizontal.
     Ab 1280px staffeln die beiden oberen Stufen mit; sm/md bleiben schmal,
     kurze Dialoge und Formulare gewinnen durch Breite nichts. */

  /* Motion — vier Stufen, KEINE rohen Zwischenwerte mehr.
     xs: Mikro-Feedback (Hover-Tint, Fokusring), fast: Standard-UI-Transition,
     base: Ein-/Ausblenden, slow: Layout-/Fortschrittsbewegung. */
  --motion-xs:   0.12s;
  --motion-fast: 0.18s;
  --motion-base: 0.30s;
  --motion-slow: 0.44s;
  --shadow-card: 0 2px 8px rgba(0,0,0,0.35);
  --shadow-hover:0 18px 36px rgba(0,0,0,0.32);
  --shadow:      0 8px 30px rgba(0,0,0,0.45);
  --shadow-lg:   0 25px 60px rgba(0,0,0,0.6);
  --ease-standard: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-exit: cubic-bezier(0.36, 0, 0.66, 0);
  --ease-spring: cubic-bezier(0.18, 0.89, 0.32, 1.18);

  /* Timing constant (mirrored in app.js DURATION_*) */
  --duration-history-pulse: 2200ms;

  /* Shorthand alias used across components */
  --transition: var(--motion-base) var(--ease-standard);
  --transition-fast: var(--motion-fast) var(--ease-standard);
  --transition-slow: var(--motion-slow) var(--ease-standard);

  /* === Dim-Skala ===
     Ersetzt 21 verstreute Opazitätswerte zwischen .25 und .65. Regel:
     Für TEXT wird die FARBE gedimmt (--text-dim / --text-dim-strong), nicht
     das Element — nur dann ist der Kontrast berechenbar. --dim-* bleibt für
     ganze Container/Icons/Grafiken. */
  --dim-soft:     0.75;
  --dim-strong:   0.55;
  --dim-disabled: 0.5;
  /* Gedimmte Textfarben mit garantiertem Kontrast auf --surface/--surface2. */
  --text-dim:        #9a9a9a;  /* ≈ 5.6:1 auf --surface2 */
  --text-dim-strong: #808080;  /* ≈ 4.0:1 — nur für dekorative Sekundärzeilen */

  /* Semantic color tokens */
  --info: #6ea8fe;
  --badge-pending-text:  #e6c869;
  --badge-rejected-text: #ff7a7a;
  --badge-overdue-text:  #ff8585;

  /* === Type-Scale (Phase E) ===
     Konsolidiert die ~48 unique font-sizes auf 7 Tokens. Neue Code-Stellen
     sollen ausschließlich diese Vars verwenden; Altcode wird inkrementell
     migriert. Werte orientieren sich an den häufigsten Treffern aus dem
     Audit (0.7/0.78/0.82/0.85/0.9/1/1.15rem). */
  --text-xs:   0.7rem;
  --text-sm:   0.78rem;
  --text-base: 0.85rem;
  --text-md:   0.92rem;
  --text-lg:   1rem;
  --text-xl:   1.15rem;
  --text-2xl:  1.5rem;
  --text-3xl:  2rem;

  /* === Spacing-Scale (Phase E) ===
     4/8-Grid statt zufälliger 0.45/0.55/0.65/0.85rem-Werte. */
  --space-1:  0.25rem;
  --space-2:  0.5rem;
  --space-3:  0.75rem;
  --space-4:  1rem;
  --space-5:  1.5rem;
  --space-6:  2rem;
  --space-7:  3rem;

  /* === Erweiterte Color-Tokens (Phase E) ===
     Die häufigsten Hex-Streuungen aus dem Audit: var(--orange) (Tailwind-Orange,
     für Counter-Offer-Outlines), #2eb872 (Karten-Akzent-Variante),
     var(--map-end) (Map-Marker-Ziel) — bekommen jetzt Namen. */
  --accent-strong: #2eb872;
  --accent-soft:   color-mix(in srgb, var(--accent) 18%, transparent);
  --orange:        #f59e0b;
  --map-end:       #e25656;
  --map-start:     #2eb872;
  --map-stop:      #e8a937;
  /* Pickup and driver return had the start green (--accent-strong) and a
     second red (--danger) — "Dein Start"/"Abholung" and "Ziel"/"Rückkehr"
     looked identical on the map and in its legend. Own hues now: blue for the
     passenger's pickup (the passenger line is blue too), violet for the
     driver's return (marker and dashed return leg). */
  --map-pickup:    #4f8ef7;
  --map-return:    #a57cf0;
  /* Log filter / archive tints (used in log-filter-btn.active / logs-archive-toggle.active) */
  --log-success-text: #dff4e8;
  --log-archive-text: #c77dff;

  /* z-index scale */
  --z-card-overdue:  1;
  --z-swipe-acting:  2;
  --z-settings-head: 3;
  --z-dropdown:      5;
  /* Popups that can overlap the sticky .loan-actions bar (also --z-dropdown).
     On a tie the bar wins, being later in the DOM, and swallowed the lower
     menu entries — e.g. "Löschen" in the entry menu. Still below --z-modal. */
  --z-dropdown-over-sticky: 6;
  --z-offline-mob:   9;
  --z-header:        10;
  --z-modal-head:    10;
  --z-drag:          20;
  --z-sidebar-open:  90;
  --z-modal:         100;
  --z-confirm:       200;
  --z-autocomplete:  1100;
  --z-install:       900;
  --z-offline:       9000;
  --z-ptr:           9500;
  --z-top:           9999;

  /* Motion-Audit: `transition: var(--transition-fast)` ohne Eigenschaft ist
     ein verstecktes `transition: all` — es animierte auch padding, min-height,
     Radien und sogar scrollbar-color, sobald ein Media-Query- oder Zustands-
     wechsel sie aenderte. --transition-ui nennt die Eigenschaften, die
     Bedienelemente tatsaechlich wechseln (Farbe, Rahmen, Schatten, Press/Lift,
     Dimmen). Layout-Eigenschaften springen damit wie gewollt. */
  --transition-ui:
    background-color var(--transition-fast),
    border-color var(--transition-fast),
    color var(--transition-fast),
    box-shadow var(--transition-fast),
    opacity var(--transition-fast),
    transform var(--transition-fast),
    filter var(--transition-fast);
  /* Motion-Audit: eine Dauer fuer alle "hier bin ich"-Hervorhebungen
     (Karten-Puls nach Sprung, Zeilen-Flash, Banner-Ring) statt 1.2/1.4/2.2 s. */
  --motion-emphasis: 1.2s;

  /* === Design-System-Konsistenz ===
     --page-gutter: EINE Quelle fuer den seitlichen Rand von Header UND main,
     damit Logo/Header-Buttons buendig mit dem Inhalt darunter stehen (vorher
     20px vs 16px am Desktop, 10.4px vs 12px am Handy). Stufe fuer <=820px
     siehe @media-Block direkt unter diesem :root.
     --accent-text: --accent als TEXT auf gruen getoenter Flaeche (aktiver
     Tab, "Aktiv"-Badge, Status-Chips) misst nur 3.9–4.4:1. Mit --text
     aufgehellt: 4.8:1 auf --surface2+Toenung, 5.4:1 auf --surface+Toenung.
     --radius-segmented: Segment-Leisten (Tab-Bar, Perspektiv-Schalter,
     Statistik-Filter, Feedback-Modi) = innerer Pill-Radius (--radius) +
     4px Innenabstand — vorher 14px/16px gemischt.
     --label-*: Uppercase-Kicker/Gruppen-Labels ("EIGENE EINSTELLUNGEN",
     KPI-Labels, Log-Detail-Titel …) liefen mit 0.7–0.75rem, Gewicht 600/700
     und Laufweite 0.03–0.1em auseinander. */
  --page-gutter: 1rem;
  --accent-text: color-mix(in srgb, var(--accent) 75%, var(--text));
  --radius-segmented: 14px;
  --label-size: 0.72rem;
  --label-weight: 700;
  --label-tracking: 0.06em;

  /* Modal-System: Bewegungskurve der Bottom-Sheets (iOS-Sheet-Kurve: schneller
     Start, weiches Auslaufen ohne Überschwingen) und der Bereich, den eine
     virtuelle Tastatur unten verdeckt (app.js _updateModalKeyboardInset; nur
     wo der Layout-Viewport NICHT mitschrumpft, v. a. iOS Safari). */
  --ease-sheet: cubic-bezier(0.32, 0.72, 0, 1);
  --modal-kb-inset: 0px;
  /* Modal-System: Breite der breiten FORMULAR-Dialoge ab 1280px (unten). */
  --modal-w-lg-form: 720px;

  /* Motion R2 / Elevation: vier Hoehenstufen statt ~20 frei gewaehlter
     Schatten. 1 = ruhende Karte (= --shadow-card), 2 = angehoben (Hover,
     aufgeklappter Eintrag), 3 = schwebend (Menues, Popover, Spickzettel),
     4 = Overlay-Hinweise (Toasts, Banner). Sheets/Modals behalten ihren
     eigenen, groesseren Schatten. */
  --elev-1: var(--shadow-card);
  --elev-2: 0 10px 22px rgba(0,0,0,0.2);
  --elev-3: 0 18px 36px rgba(0,0,0,0.42), 0 4px 10px rgba(0,0,0,0.24);
  --elev-4: 0 10px 26px rgba(0,0,0,0.45), 0 2px 6px rgba(0,0,0,0.3);

  /* Touch-R4: generisches Druck-Feedback fuer tippbare Elemente OHNE eigenes
     :active (Regel "Touch-R4: Globale Touch-Basis" weiter unten). Dimmen wie
     ein nativer Text-/Icon-Button statt Transform: kein zweiter Transform,
     und anders als filter aendert opacity keinen Containing Block (absolut
     positionierte Kinder springen beim Druck nicht). */
  --press-opacity: 0.7;

  /* R6-Close: Kurve fuers Schliessen von Sheet UND Dialog (Karte und
     Abdunklung laufen auf derselben Kurve, also synchron). Startet sofort
     (Anfangssteigung ~1,7 statt 0 bei --ease-exit), beschleunigt kaum und
     laeuft weich aus. --ease-sheet (Anfangssteigung 2,25) schoss ein hohes
     Sheet im ersten Frame 110–180 px weit und nach ~150 ms aus dem Bild,
     danach blendete nur noch die Abdunklung nach. */
  --ease-close: cubic-bezier(0.3, 0.5, 0.4, 1);
  /* Runde 6 (Knöpfe): Mindest-Trefferflaeche mit der Maus (feiner Zeiger).
     Touch bleibt --touch-target (44px). Regeln: Block am Dateiende. */
  --hit-min-fine: 32px;
}
@media (max-width: 820px) {
  :root { --page-gutter: 0.75rem; }
}

/* Modal-System: registrierte Properties, damit der Backdrop (Abdunklung +
   Blur) ein- und ausblenden kann, ohne die Opacity des Overlays anzufassen —
   die würde die Karte mit ausblenden (das mobile Sheet soll beim Schließen
   nur gleiten, nicht verblassen). --modal-drag-progress ist registriert, damit
   der Backdrop nach einem abgebrochenen Swipe weich zurückblendet statt zu
   springen. Ohne @property-Support: diskreter Wechsel, sonst identisch. */
@property --modal-backdrop-k {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}
@property --modal-drag-progress {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
/* R6-Close: nur der Weichzeichner eines schliessenden VERSCHACHTELTEN Dialogs
   blendet aus (seine Abdunklung bleibt bis zum Ende, sonst blitzte die Seite
   durch) — der Dialog darunter wurde vorher im letzten Frame schlagartig scharf. */
@property --modal-blur-k {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

/* Modal-Breiten-Staffelung ab Desktop (siehe --modal-w-* im :root-Block). */
@media (min-width: 1280px) {
  :root {
    --modal-w-lg: 940px;
    --modal-w-xl: 1040px;
  }
}
@media (min-width: 1600px) {
  :root {
    --modal-w-lg: 1000px;
    --modal-w-xl: 1180px;
  }
}
/* Modal-System: D12 hat die lg-Stufe für die DATEN-Dialoge (Statistik,
   Benutzer, Rückmeldungen-Admin) auf 940/1000px angehoben — alle anderen
   lg-Dialoge sind aber Formulare bzw. kurze Listen und wuchsen unbeabsichtigt
   mit: Einstellungs-Eingabefelder über 900px lang, zwei Score-Ringe mitten in
   einer 940px-Fläche. Die bleiben bei einer Formular-Breite. */
@media (min-width: 1280px) {
  #modal-settings .modal-card,
  #modal-reliability .modal-card,
  #modal-taxi-vehicles .modal-card,
  #modal-taxi-request .modal-card,
  #modal-taxi-edit .modal-card {
    max-width: var(--modal-w-lg-form);
  }
}

html {
  /* Match header surface so iOS PWA status-bar area / notch blends with the
     sticky header instead of showing a black strip above it. */
  background: var(--surface);
  background-color: var(--surface);
  /* Allow native rubber-band overscroll — feels more "real-app" on iOS.
     html-bg is var(--surface) so the visible-during-overscroll area blends
     with the header instead of showing black. */
  min-height: 100%;
  /* Reserve the scrollbar track permanently. `html.modal-open { overflow:hidden }`
     removes the document scrollbar on every modal open, which otherwise shifts
     the whole page ~15px sideways (visible through the blurred backdrop) and
     back again on close. Same jump on tab switches that shorten the page.
     No-op on iOS (overlay scrollbars). */
  scrollbar-gutter: stable;
  /* Native dark UI for everything the browser draws itself: date/time
     pickers, <select> popups, checkboxes, autofill, the page scrollbar, the
     canvas before the stylesheet paints. Without it Android showed white
     select sheets and a light date picker over the dark app. */
  color-scheme: dark;
  /* iOS inflates text on rotation to landscape; the layout is responsive, so
     font sizes must stay what the CSS says. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* No double-tap-to-zoom anywhere (taps react instantly, rapid taps on a
     button don't zoom the page); pinch-zoom stays available. Descendants
     with their own touch-action (pan-y, none) narrow this further. */
  touch-action: manipulation;
}

body {
  background: var(--surface);
  background-color: var(--surface);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

/* Formular-Elemente erben die Schrift NICHT automatisch: ohne diese Regel
   liefen alle Buttons/Selects ohne eigenes font-family (btn-primary,
   btn-secondary, Bonus-Chips, "Schließen" …) auf Windows/Android in der
   UA-Schrift (Arial/Roboto) statt Segoe UI/System — sichtbar zwei
   Schriftarten im selben Dialog. Nur die Familie, NICHT font-size: viele
   Button-Regeln verlassen sich auf ihre eigene Groesse. Element-Selektoren
   = niedrigste Spezifitaet, jede Klassenregel (Monospace etc.) gewinnt. */
button,
input,
select,
textarea {
  font-family: inherit;
}

/* Es gibt keine globale Link-Farbe -> jedes klassenlose <a> rendert im
   UA-Blau #0000EE und misst auf --surface 1.65:1. Betrifft u.a. den
   Alt-Text "Angehaengtes Bild" bei abgelaufenen Anhaengen
   (message-images.css .conv-image-link). --accent als Text: 5.7-6.4:1. */
a {
  color: var(--accent);
  text-underline-offset: 0.18em;
}
html:not(.using-touch) a:hover { color: color-mix(in srgb, var(--accent) 75%, var(--text)); }

/* App chrome behaves like native controls: a long-press on a button, tab or
   menu entry no longer starts a text selection (blue handles over the label)
   or opens iOS's link/callout sheet. Zero specificity via :where(), so any
   component rule still wins; content (amounts, names, messages, IBANs) stays
   selectable and copyable. Inputs are deliberately not in this list — iOS
   refuses to edit a field with user-select:none. */
:where(button, [role="button"], [role="tab"], [role="menuitem"], summary, label.toggle-control, label.reminder-chip, label.nu-preset, .tab-bar, .modal-drag-handle) {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* === Touch-R4: Globale Touch-Basis ==========================================
   Ergaenzt die :where()-Regel direkt darueber (Runde 1). Alles mit
   Spezifitaet 0 — jede Komponentenregel gewinnt weiterhin.

   (1) UI-Chrome ohne Textauswahl/Callout. Die Runde-1-Liste erfasste weder
       Links noch [data-action]-Flaechen, Options-/Switch-Rollen, klickbare
       Karten oder Labels (Schalter-Beschriftungen, Rueckmeldungs-Modi,
       Feld-Labels). Ein langer Druck darauf markierte die Beschriftung bzw.
       oeffnete auf iOS die Link-Vorschau. */
:where(a[href], [role="option"], [role="switch"], [role="menuitemradio"], [role="menuitemcheckbox"], [data-action], .dashboard-card-clickable, .kpi-card-clickable, .status-pill-clickable, label) {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* Sicherheitsnetz: -webkit-user-select wird in WebKit/Blink VERERBT. Liegt
   ein Eingabefeld in einem der Container oben (Label um ein Feld,
   [data-action]-Zeile mit Inline-Edit), bliebe es auf iOS uneditierbar.
   Felder bekommen die Auswahl deshalb ausdruecklich zurueck. */
:where(input:not([type="checkbox"], [type="radio"], [type="button"], [type="submit"], [type="reset"], [type="range"], [type="color"], [type="file"]), textarea, [contenteditable]:not([contenteditable="false"])) {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

/* (2) Kein Doppeltipp-Zoom auf Bedienelementen. html hat schon
       touch-action: manipulation, WebKit setzt die wirksame touch-action an
       jedem Scroll-Container (Modal-Karte, Listen) aber neu an — schnelles
       Doppeltippen auf einen Knopf im Dialog zoomte dort die Seite. Direkt am
       Element gesetzt gilt es ueberall; touch-action ist eine Schnittmenge,
       strengere Werte der Vorfahren (pan-y, none) bleiben wirksam. */
:where(button, a[href], [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], summary, label, select, input, textarea, [data-action]) {
  touch-action: manipulation;
}

/* (3) Bilder der Oberflaeche: nicht ziehbar, kein iOS-"Bild sichern"-Callout
       (Vorschau-Kacheln im Verlauf, Anhaenge im Composer, Karten-Kacheln).
       Speichern bleibt in der Lightbox moeglich — dort ist das Bild Inhalt. */
:where(img) {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
.fb-lightbox-stage img {
  -webkit-user-drag: auto;
  -webkit-touch-callout: default;
}

/* (4) Druck-Feedback fuer alles Tippbare OHNE eigenes :active. Die Regel
       wirkt nur auf :active — auf Touch also genau solange der Finger liegt;
       der Browser nimmt :active beim Scroll-Start selbst zurueck, nach dem
       Loslassen springt es (bzw. per Komponenten-Transition) zurueck.
       Ausgenommen: Familien mit eigener Druck-Stufe (Liste unten) — sonst
       doppelte Rueckmeldung —, gesperrte/beschaeftigte Elemente und ein
       Container, in dem gerade ein ANDERES Bedienelement gedrueckt wird
       (summary mit Info-Punkt, Karte mit Knopf: :active gilt auch fuer alle
       Vorfahren). Wer einer Komponente spaeter ein eigenes :active gibt,
       traegt sie in die Ausnahme-Liste ein oder setzt dort
       --press-opacity: 1. */
:where(button, a[href], [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], summary, [data-action], .kpi-card-clickable, .status-pill-clickable, label.toggle-label, label:has(input[type="checkbox"], input[type="radio"])):where(:active):where(:not(:disabled, [aria-disabled="true"], [aria-busy="true"], .btn-primary, .btn-secondary, .btn-danger, .header-icon-btn, .header-user-pill, .btn-logout, .sync-status-pill, .btn-menu, .app-tab, .global-search-row, .notification-row, .fb-row, .fb-ov-card, .dashboard-card-clickable, .loan-actions > button, .btn-new-loan, .btn-borrower-cta, .btn-entry-menu, .entry-menu-item, .modal-close, .kw-add, .decide-payout-qr-download, .decide-payout-copy, .info-dot, .presence-star, .rel-filter-pill, .btn-counter-offer, .btn-taxi-counter-offer, .payout-callout-btn, .taxi-vehicle-move-up, .taxi-vehicle-move-down, .taxi-consumption-chip, .btn-taxi-request, .taxi-stop-handle, .taxi-stop-fav-save, .btn-taxi-fav-save, .taxi-fav-chip-edit, .taxi-map-ctl, .btn-preset, .quick-reply-chip, .toggle-control, .nu-preset, .perm-option, .reminder-chip, .tab-pill, .purpose-make-cat, .purpose-chip-label, .log-filter-btn, .logs-archive-toggle, .stats-jump-chip, .status-pill, .modal-feedback-btn, .mi-thumb-edit, .mi-thumb-remove, .toggle-label, .fb-kind, .fb-mode, .fb-context-toggle, .sd-tab, .sd-seg-opt, .sd-pair, .sd-icon-btn, .sg-chip-x, .sd-paid-btn, .sd-sec-btn)) {
  opacity: var(--press-opacity);
}
/* Getrennt, damit ein Browser ohne :has() nur diese Ausnahme verliert, nicht
   das ganze Druck-Feedback (ein ungueltiges :not(:has()) haette die Regel
   oben komplett gekippt). */
:where(button, a[href], [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], summary, [data-action], .kpi-card-clickable, .status-pill-clickable, label):where(:has(:is(button, a[href], [role="button"], [role="switch"], input, select, textarea):active)) {
  opacity: 1;
}
/* (5) Gedimmte Icon-/Text-Knoepfe (Ruhe-Opazitaet < 1, z. B. Stift/Papierkorb
       an Notizen und Zahlungen): deren eigene opacity schlaegt die Regel (4)
       mit Spezifitaet 0, ein Tipp zeigte gar nichts. Ihr Druck ist das
       Aufhellen auf volle Deckkraft — dasselbe, was die Maus beim Hover sieht. */
.btn-delete-history:not(:disabled):active,
.btn-reactivate:not(:disabled):active,
.btn-note-delete:not(:disabled):active,
.btn-note-edit:not(:disabled):active,
.profile-device-rename-btn:not(:disabled):active,
.reliability-loan-link:active,
.taxi-details > summary:active {
  opacity: 1;
}

.hidden { display: none !important; }
/* overflow:hidden prevents background scroll on all browsers.
   position:fixed + top:var(--scroll-lock-top) keeps the page at its scrolled
   position across all browsers (iOS Safari, Chrome, Firefox). JS sets
   --scroll-lock-top to -${scrollY}px on open and restores scrollY on close. */
html.modal-open { overflow: hidden; }
body.modal-open {
  overflow: hidden;
  position: fixed;
  left: 0; right: 0; width: 100%;
  top: var(--scroll-lock-top, 0);
}
/* Installed iOS app: soft lock instead (app.js _useSoftScrollLock) — the hard
   lock above made WebKit clip everything ~59pt above the screen edge. */
html.modal-soft-lock.modal-open { overflow: visible; }
html.modal-soft-lock body.modal-open {
  overflow: visible;
  position: static;
  top: auto;
  width: auto;
}

/* Global focus ring for keyboard navigation */
*:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Form controls inside modal-cards render the focus ring INSIDE the element
   (outline-offset: -2px) instead of outside it. The modal-card has
   overflow-x: hidden to prevent horizontal scroll, which clips outlines on
   full-width form controls — the negative offset keeps the visual focus
   indicator within the control's own box, so it cannot be clipped. */
.modal-card :is(input, select, textarea):focus-visible,
.modal-card-wide :is(input, select, textarea):focus-visible,
.modal-card-logs :is(input, select, textarea):focus-visible {
  outline-offset: -2px;
}

/* Explicit focus ring for tabindex-driven interactive elements */
.header-user-pill:focus-visible,
[role="button"]:focus-visible,
[tabindex="0"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Consistent placeholder color across browsers */
input::placeholder,
textarea::placeholder {
  color: var(--text-muted);
  opacity: 1;
}

input:disabled, textarea:disabled, select:disabled {
  opacity: var(--dim-disabled);
  cursor: not-allowed;
  background: var(--surface2) !important;
  color: var(--text-muted);
}

/* Buttons erben die App-Schrift. <button> nimmt sonst die UA-Schrift
   (Windows/Linux-Chrome: Arial) statt der body-Schrift — gemessen: .btn-primary,
   .tab-pill, .loan-actions-Buttons, Header-Buttons liefen in Arial, direkt
   daneben .perspective-tab/.header-user-pill (font: inherit) in Segoe UI.
   Nur die Familie, Groesse/Gewicht bleiben Sache der Komponente; wer eine
   eigene Schrift setzt (.info-dot Georgia), gewinnt per Spezifitaet. */
button { font-family: inherit; }

/* Ein einziger Disabled-Zustand fuer alle Buttons. Vorher hatten .btn-secondary
   und .btn-danger gar keine :disabled-Regel (zusammen 60+ Vorkommen im Markup,
   53 `disabled = true`-Stellen in app.js), und die 11 vorhandenen Bloecke
   nutzten vier verschiedene Opazitaeten (0.4/0.45/0.6/0.65). saturate() nimmt
   zusaetzlich die Signalfarbe heraus, damit "gesperrt" nicht allein ueber die
   Helligkeit kodiert ist. Bewusst vor allen Komponenten, damit ein
   Einzelfall-Override weiterhin moeglich bleibt. */
button:disabled,
button[aria-disabled="true"],
[role="button"][aria-disabled="true"] {
  opacity: var(--dim-disabled);
  cursor: not-allowed;
  filter: saturate(0.6);
  box-shadow: none;
  transform: none;
}

/* Allow pointer events on disabled submit buttons so JS can detect
   blocked-submit taps and show validation errors. */
.modal-actions button:disabled {
  pointer-events: auto;
  cursor: not-allowed;
}

/* Required-field indicator in labels */
/* Inline-Icons aus ICONS/icon() sitzen per Default auf der Textbasislinie und
   haengen dadurch rund 3px unter der Zeilenmitte — ueberall, wo ein Icon neben
   Text steht (Pillen, Badges, Buttons, Kartenzeilen), verrutschte die Zeile.
   Eine Grundregel statt einer Flex-Umstellung an dutzenden Containern. */
svg[viewBox="0 0 24 24"][aria-hidden="true"] {
  vertical-align: -0.16em;
  flex: 0 0 auto;
}

.req-star {
  display: inline-block;
  margin-left: 6px;
  vertical-align: middle;
  color: var(--danger-text);
  font-size: 0.9em;
  font-weight: 800;
  line-height: 1;
  /* opacity 0.82 drueckte den Kontrast von 4.56 auf 3.99:1. Das Sternchen ist
     dekorativ (aria-hidden), aber es sitzt in der Label-Zeile und soll lesbar
     bleiben -- die Zurueckhaltung kommt jetzt allein aus der Schriftgroesse. */
  flex-shrink: 0;
}

/* Error state on form inputs */
.form-group input.error,
.form-group textarea.error,
.form-group select.error {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 6%, transparent);
}

/* === Login === */
.login-body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  /* dvh: 100vh is the toolbar-collapsed height on mobile Safari/Chrome, so
     the "centred" card sat low and the page scrolled a little. */
  min-height: 100dvh;
  background:
    radial-gradient(circle at top left, color-mix(in srgb, var(--accent) 16%, transparent), transparent 28%),
    radial-gradient(circle at top right, rgba(215,199,163,0.08), transparent 24%),
    linear-gradient(180deg, #111312 0%, #0c0d0d 100%);
}

.login-view {
  width: 100%;
  padding: 1rem;
  /* Installed app draws under the (translucent) status bar and home
     indicator — keep the card clear of both on short screens. */
  padding-top: max(1rem, env(safe-area-inset-top, 0px));
  padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px));
  display: flex;
  justify-content: center;
}

.login-card {
  position: relative;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.015)),
    var(--surface);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-modal);
  padding: 2.7rem 2.15rem 2.2rem;
  width: 100%;
  max-width: 390px;
  text-align: center;
  box-shadow: 0 28px 70px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
  overflow: hidden;
  /* Entrance: same soft rise the app uses for cards (fadeInUp is defined
     further down — keyframes are hoisted file-wide). Covered by the global
     prefers-reduced-motion guard. */
  animation: fadeInUp var(--motion-slow) var(--ease-standard) both;
}

.login-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 4px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 90%, transparent), rgba(215,199,163,0.7));
  opacity: 0.9;
}

/* Wallet-Marke wie im App-Header (SVG statt 💸-Emoji) in einer Akzent-Kachel. */
.login-icon {
  display: inline-grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  margin-bottom: 0.9rem;
  border-radius: 16px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
  box-shadow: 0 8px 22px color-mix(in srgb, var(--accent) 14%, transparent);
  /* Ambient float, same rhythm as .empty-state-icon. Delayed so it starts
     after the card entrance has settled. */
  animation: emptyStateFloat 6s ease-in-out 0.6s 3;
}

.login-card h1 {
  font-size: 1.9rem;
  font-weight: 800;
  letter-spacing: -0.04em;
  margin-bottom: 0.35rem;
}
.login-subtitle {
  color: var(--text-muted);
  font-size: 0.9rem;
  margin: 0 0 1.6rem;
}
.login-body { min-height: 100dvh; min-height: var(--app-full-h, 100dvh); }
/* Installierte iPhone-App: WebKit meldet beim ersten Laden eine zu kurze
   100dvh, bis einmal gescrollt wurde. Darunter schien das hellere
   html-Grau (var(--surface)) als Streifen durch, und beim Korrigieren
   sprang die mittig gesetzte Karte – der erste Tipp ins Feld ging daneben.
   html bekommt deshalb die Endfarbe des Login-Verlaufs, die Höhe kommt in der
   App aus screen.* (login.js setzt --app-full-h, wie app.js). */
html:has(> body.login-body) { background: #0c0d0d; }
.login-card .btn-primary { margin-top: 0.35rem; }

/* Passwort anzeigen/verbergen: Augen-Button im Feld (rechts). */
.login-pw-wrap { position: relative; }
.login-pw-wrap input { padding-right: 3rem; }
.login-pw-toggle {
  position: absolute;
  top: 50%;
  right: 0.2rem;
  transform: translateY(-50%);
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
html:not(.using-touch) .login-pw-toggle:hover { color: var(--text); background: rgba(255,255,255,0.06); }
.login-pw-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.login-pw-toggle .login-pw-eye-off,
.login-pw-toggle[aria-pressed="true"] .login-pw-eye { display: none; }
.login-pw-toggle[aria-pressed="true"] .login-pw-eye-off { display: block; }
/* Mobile-Audit A2: der Augen-Knopf war 40 × 40 px – unter dem 44-px-Ziel. Die Trefferfläche wächst
   unsichtbar auf 44 × 44 (wie .info-dot), das Feld bleibt unverändert. */
@media (pointer: coarse) {
  .login-pw-toggle::after { content: ""; position: absolute; inset: -2px; }
}

/* Browser-Autofill färbte die Felder hell (Chrome: blassgelb/-blau) — auf der
   dunklen Karte ein greller Block mit unlesbarer heller Schrift. */
.login-card input:-webkit-autofill,
.login-card input:-webkit-autofill:hover,
.login-card input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--surface2) inset;
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
}

/* Querformat-Handy (≈390px hoch): die Karte war 446px hoch, der
   Anmelden-Button lag unter dem Rand. Kompakter, oben ausgerichtet. */
@media (max-height: 520px) and (orientation: landscape) {
  .login-body { align-items: flex-start; }
  .login-view { padding: 0.75rem 1rem; }
  .login-card { padding: 1.4rem 1.75rem 1.25rem; }
  .login-icon { width: 2.75rem; height: 2.75rem; margin-bottom: 0.4rem; }
  .login-icon svg { width: 24px; height: 24px; }
  .login-card h1 { font-size: 1.5rem; margin-bottom: 0.9rem; }
  .login-subtitle { display: none; }
  .login-card .form-group { margin-bottom: 0.6rem; }
}

/* === Form Groups === */
.form-group { margin-bottom: 1rem; text-align: left; }

.form-group label,
.form-group .label-text {
  display: flex;
  align-items: center;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.4rem;
}
@media (max-width: 540px) {
  /* Some labels interpolate a counterparty name ("Nachricht an <username>") —
     one long unbroken word can push this flex label past the modal edge
     without a break opportunity. */
  .form-group label,
  .form-group .label-text {
    overflow-wrap: anywhere;
  }
}

/* When an input inside the same form-group is invalid, tint the label red */
.form-group:has(input[aria-invalid="true"]) label,
.form-group:has(textarea[aria-invalid="true"]) label,
.form-group:has(select[aria-invalid="true"]) label {
  color: color-mix(in srgb, var(--danger) 55%, var(--text-muted));
}
.form-group:has(input[aria-invalid="true"]) .req-star,
.form-group:has(textarea[aria-invalid="true"]) .req-star,
.form-group:has(select[aria-invalid="true"]) .req-star {
  opacity: 1;
  color: var(--danger);
}

.form-group input,
.form-group textarea,
.form-group select {
  width: 100%;
  min-height: var(--touch-target);
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.65rem 0.9rem;
  font-size: 1rem;
  font-family: inherit;
  outline: none;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  -webkit-appearance: none;
  appearance: none;
}
/* Selects keep the custom background/border but get their native dropdown
   arrow back — appearance:none left them looking identical to a plain text
   field, and other selects in the app (.logs-select, .history-sort-select)
   never had appearance:none removed in the first place. */
.form-group select {
  -webkit-appearance: auto;
  appearance: auto;
}
/* Keyboard focus: restore outline (suppressed by outline:none above), but
   keep it inside the element so overflow-x:hidden on modal-card can't clip it. */
.form-group input:focus-visible,
.form-group textarea:focus-visible,
.form-group select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
  border-color: var(--accent);
  /* Soft focus ring; inset so overflow:hidden on modal-card can't clip it
     (same constraint as the focus-visible outline above). */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* Touch-R5: Auswahlfelder auf Touch wie native Picker. Nach dem Antippen
   oeffnet das System seinen eigenen Picker (iOS-Rad, Android-Dialog) und
   ueberdeckt das Feld ohnehin — danach blieb der Select aber fokussiert und
   trug Akzent-Rahmen + Ring (Chrome wertet einen angetippten Select als
   :focus-visible), bis man woanders hintippte. Auf Touch zeigt der Select
   deshalb keinen Fokuszustand; der gewaehlte Wert ist die Rueckmeldung.
   Ein Fehlerrahmen (aria-invalid) bleibt. Kein blur() per JS: iOS feuert
   `change` schon beim Drehen des Rads, ein blur schloesse den Picker mitten
   in der Auswahl. Tastatur/Maus (kein .using-touch) behalten den Ring. */
html.using-touch select:not([aria-invalid="true"]):focus,
html.using-touch select:not([aria-invalid="true"]):focus-visible {
  outline: none;
  box-shadow: none;
  border-color: var(--border-input);
}


.form-group textarea {
  resize: vertical;
  min-height: 80px;
}

/* html { color-scheme: dark } already draws the picker icon light; the old
   invert(0.7) (made for the black light-scheme icon) would now darken it.
   Same resulting grey, reached from the other side. */
.form-group input[type="date"]::-webkit-calendar-picker-indicator {
  opacity: 0.7;
}
/* R9-Mobile: WebKit on iOS gives date/time fields an intrinsic minimum width
   from the formatted value ("24.09.2026, 10:00") — in a narrow column that is
   more than 100 %, and the field pushed past the dialog edge. */
.form-group :is(input[type="date"], input[type="datetime-local"], input[type="time"]) {
  min-width: 0;
  max-width: 100%;
}

/* Decimal/numeric inputs: kill Firefox's spinner buttons. Webkit was already
   covered by appearance:none on the generic input rule above. */
.form-group input[inputmode="decimal"],
.form-group input[inputmode="numeric"],
.form-group input[type="number"] {
  -moz-appearance: textfield;
}

/* Inline-field validation: paint a red border + helper text when JS sets
   aria-invalid="true". Field-error is rendered next to the input by the form-
   wiring code, keyed by aria-describedby so screen readers pick it up. */
.form-group input[aria-invalid="true"],
.form-group textarea[aria-invalid="true"],
.form-group select[aria-invalid="true"] {
  border-color: color-mix(in srgb, var(--danger) 60%, transparent);
  background: color-mix(in srgb, var(--danger) 5%, var(--surface2));
}
.form-group input[aria-invalid="true"]:focus,
.form-group textarea[aria-invalid="true"]:focus,
.form-group select[aria-invalid="true"]:focus {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 5%, var(--surface2));
  /* Toast-Fehler: the error focuses its field — the green inset focus ring of
     .form-group input:focus was drawn over the red border. */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 55%, transparent);
}
/* Formular-Audit: Fehlerfelder bekommen den Fokus (showFieldError) — der
   grüne Fokusring überdeckte dann die rote Markierung. */
.form-group input[aria-invalid="true"]:focus-visible,
.form-group textarea[aria-invalid="true"]:focus-visible,
.form-group select[aria-invalid="true"]:focus-visible {
  outline-color: var(--danger);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 55%, transparent);
}
/* Toast-Fehler: the message itself is shown as the error toast (showFieldError
   in app.js). .field-error only carries it for screen readers — linked via
   aria-describedby while set — so it is visually hidden like .sr-only. */
.field-error {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.field-error:empty { display: none; }
/* Taxi audit: in the long taxi request sheet the ride time and the required
   repayment date sit far from the submit, and the toast alone did not say
   which of them was wrong once it faded — there the message also stays
   visible under the field. */
#modal-taxi-request .form-group > .field-error {
  position: static;
  width: auto;
  height: auto;
  margin: 0.3rem 0 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
  font-size: 0.78rem;
  color: var(--danger-text);
}
/* Marked fields outside a .form-group (inline chat field, note editor, taxi
   stops, category and limit rows, SMTP test address) get the same red border — without text
   under them it is the only mark on the field itself. */
:is(.conv-compose, .note-edit-form, .taxi-stop-fields, .category-row, .inline-edit-row, .lend-limit-row, .settings-test-row) :is(input, textarea, select)[aria-invalid="true"] {
  border-color: color-mix(in srgb, var(--danger) 60%, transparent);
  background: color-mix(in srgb, var(--danger) 5%, var(--surface2));
}
:is(.conv-compose, .note-edit-form, .taxi-stop-fields, .category-row, .inline-edit-row, .lend-limit-row, .settings-test-row) :is(input, textarea, select)[aria-invalid="true"]:focus {
  border-color: var(--danger);
  outline-color: var(--danger);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 55%, transparent);
}
.field-hint {
  display: block;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-top: 0.2rem;
}
.field-hint-good { color: var(--accent); }
/* Rohes --danger misst als Text 4.06:1 — gleiche Klasse wie die uebrigen
   Fehlertexte (Befund 13). */
.field-hint-bad  { color: var(--danger-text); }

.error-msg {
  color: var(--danger-text);
  font-size: 0.82rem;
  font-weight: 500;
  margin-bottom: 0.5rem;
}
.error-msg:empty {
  display: none;
}
.error-msg:not(:empty) {
  padding: 0.5rem 0.75rem;
  background: color-mix(in srgb, var(--danger) 8%, var(--surface2));
  border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
  border-radius: var(--radius-xs);
}

/* === Buttons === */
.btn-primary {
  width: 100%;
  /* Darkened from #56bb8e/#439b75 — the original stops fell to 2.36:1/3.40:1
     white-text contrast, below WCAG AA's 4.5:1 minimum. */
  background: linear-gradient(180deg, var(--accent-btn), var(--accent-btn-strong));
  color: #fff;
  border: none;
  border-radius: var(--radius-btn);
  padding: 0.75rem;
  min-height: var(--touch-target);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  transition:
    opacity var(--motion-fast) linear,
    transform var(--motion-base) var(--ease-standard),
    box-shadow var(--motion-base) var(--ease-standard),
    filter var(--motion-base) var(--ease-standard);
  margin-top: 0.5rem;
  box-shadow: none;
}

html:not(.using-touch) .btn-primary:hover {
  opacity: 0.97;
  transform: translateY(-1px);
  filter: saturate(1.04);
}

/* Avoid hover-transform overlapping the focus-ring (P3-9). */
html:not(.using-touch) .btn-primary:focus-visible:hover,
html:not(.using-touch) .btn-secondary:focus-visible:hover,
html:not(.using-touch) .btn-danger:focus-visible:hover {
  transform: none;
  filter: none;
  box-shadow: none;
}

.btn-primary:active {
  transform: translateY(0) scale(0.985);
}

/* Opazitaet/Cursor kommen aus der globalen button:disabled-Regel. */
.btn-primary:disabled { transform: none; }

.btn-secondary {
  flex: 1;
  background: rgba(255,255,255,0.015);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--text-muted);
  border-radius: var(--radius-btn);
  padding: 0.75rem;
  min-height: var(--touch-target);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    border-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard),
    background var(--motion-base) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard),
    box-shadow var(--motion-base) var(--ease-standard);
}

html:not(.using-touch) .btn-secondary:hover {
  border-color: rgba(255,255,255,0.18);
  color: var(--text);
  background: rgba(255,255,255,0.03);
  transform: translateY(-1px);
  box-shadow: var(--elev-2);
}

.btn-secondary:active {
  transform: translateY(0) scale(0.99);
}

.btn-danger {
  flex: 1;
  /* Darkened from #e45f5f/#cd4949 — the original stops fell to 3.44:1/4.52:1
     white-text contrast, below WCAG AA's 4.5:1 minimum. */
  background: linear-gradient(180deg, #bb4e4e, #a83c3c);
  color: #fff;
  border: none;
  border-radius: var(--radius-btn);
  padding: 0.75rem;
  min-height: var(--touch-target);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    opacity var(--motion-fast) linear,
    transform var(--motion-base) var(--ease-standard),
    box-shadow var(--motion-base) var(--ease-standard);
  box-shadow: none;
}

html:not(.using-touch) .btn-danger:hover {
  opacity: 0.97;
  transform: translateY(-1px);
}

.btn-danger:active {
  transform: translateY(0) scale(0.985);
}

/* Compact modifier for buttons inside dense lists (currently the vehicle rows
   in the vehicles modal). The class was already applied in the markup but had
   no rule at all, so those buttons rendered at full .btn-secondary/.btn-danger
   size. Must stay AFTER those two blocks — same specificity, later wins.
   Padding and type shrink; the 44px touch target does not, because this list
   is operated with a thumb on phones. */
.btn-sm {
  padding: 0.45rem 0.7rem;
  font-size: 0.85rem;
  min-height: var(--touch-target);
}

/* === Ablehnen: EINE Regel (B3) ================================================
   Vorher drei Sprachen: rote Kontur (Anfrage-Karten), rot gefüllt (Entscheidungs-Karten, Gegenangebot-
   Dialoge, Widerspruch), neutral (Verrechnung im Dashboard, Saldo). Jetzt gilt:
   - ablehnende SEKUNDÄR-Aktion („Ablehnen“ einer Anfrage, eines Vorschlags, einer Meldung, einer Fahrt):
     `.btn-decline` – rote Kontur (--danger), Text --decline-text (≥ 4,8:1 auf allen Flächen), transparente Fläche,
     bei Hover/Druck gefüllt wie .btn-danger (weiß auf #bb4e4e → #a83c3c, ≥ 4,9:1);
   - destruktive PRIMÄR-Aktion (Löschen, Bestätigungsdialog „Ablehnen“, Entscheidungs-Dialog): `.btn-danger`, gefüllt rot.
   `.btn-decline` ist ein Farb-Modifikator: Größe, Rundung und Schrift kommen vom Basisknopf daneben
   (.btn-danger, .btn-secondary, .btn-reject, .btn-extend-reject … je nach Umgebung), die Farbe nur von hier. */
button.btn-decline {
  background: transparent;
  color: var(--decline-text);
  border: 1px solid var(--danger);
  box-shadow: none;
}
html:not(.using-touch) button.btn-decline:not(:disabled):hover,
button.btn-decline:not(:disabled):active {
  background: linear-gradient(180deg, #bb4e4e, #a83c3c);
  border-color: #a83c3c;
  color: #fff;
  opacity: 1;
}

/* === Button-Zustaende: Druck mit der Maus, Hover im gesperrten Zustand ======
   1) Maus-Stufe fuer :active. Solange die Maus drueckt, gilt auch :hover — und
      die Hover-Regeln (html + :not(.using-touch) + Klasse + :hover) sind
      spezifischer als ein nacktes .x:active. Deren translateY(-1px) gewann,
      ein Klick zeigte mit der Maus gar keinen Druck (gemessen: transform
      waehrend mouse.down = matrix(...,-1) auf .btn-primary, .loan-actions-
      Buttons, .header-icon-btn). Gleiches Muster wie die Chip-Stufe am
      Dateiende. :not(:disabled) hebt die Spezifitaet ueber die Hover-Regel —
      damit gewinnt der Druck unabhaengig von der Position in der Datei — und
      ist zugleich die Semantik: gesperrte Buttons geben nicht nach.
   2) Header-Buttons (Profil, Suche, Glocke) hatten gar keinen Druckzustand,
      der Burger daneben schon (scale .96) — jetzt alle gleich.
   3) Gesperrt + Hover: die Hover-Regeln setzten opacity .97 / Lift / saturate
      auch auf :disabled und hoben damit button:disabled auf (gemessen: ein
      gesperrter .btn-primary sprang unter dem Cursor von opacity .5 auf .97).
      Busy-Buttons (aria-busy) behalten ihre volle Deckkraft, nur der Lift
      entfaellt. */
html:not(.using-touch) .btn-primary:not(:disabled):active,
html:not(.using-touch) .btn-danger:not(:disabled):active {
  transform: translateY(0) scale(0.985);
  filter: none;
}
/* Runde 6 (Knöpfe): 0.985 wie die Touch-Stufe (Block "Card micro-
   interactions" am Dateiende) und wie .btn-primary/.btn-danger — gemessen
   drueckte Abbrechen mit der Maus 0.99, mit dem Finger 0.985. */
html:not(.using-touch) .btn-secondary:not(:disabled):active {
  transform: translateY(0) scale(0.985);
  box-shadow: none;
}
html:not(.using-touch) .loan-actions > button:not(:disabled):active,
html:not(.using-touch) .history-entry-toggle:not(:disabled):active {
  transform: scale(0.985);
  box-shadow: none;
}
html:not(.using-touch) .btn-borrower-cta:not(:disabled):active {
  transform: translateY(0);
  filter: brightness(0.95);
}
.header-icon-btn:active,
.header-user-pill:active,
.btn-logout:not(:disabled):active {
  transform: scale(0.96);
  transition-duration: 60ms;
}
html:not(.using-touch) .header-icon-btn:not(:disabled):active,
html:not(.using-touch) .btn-logout:not(:disabled):active {
  transform: scale(0.96);
}
/* Im Burger-Menue sind die Eintraege volle Breite — dort reicht ein
   flacherer Druck; die Hover-Regel des Menues (0,5,1) setzt transform:none
   und braucht deshalb eine eigene, spezifischere Maus-Stufe. */
html.nav-collapsed .header-nav .btn-logout:not(:disabled):active,
html:not(.using-touch).nav-collapsed .header-nav .btn-logout:not(:disabled):active {
  transform: scale(0.98);
}
/* Der Profil-Knopf war der einzige Header-Button ohne Hover-Rueckmeldung. */
html:not(.using-touch) .header-user-pill:hover {
  border-color: rgba(255,255,255,0.14);
  background: rgba(255,255,255,0.045);
}
html:not(.using-touch) :is(.btn-primary, .btn-secondary, .btn-danger, .btn-borrower-cta, .btn-logout, .header-icon-btn, .loan-actions > button):disabled:hover {
  transform: none;
  box-shadow: none;
}
html:not(.using-touch) :is(.btn-primary, .btn-danger, .btn-borrower-cta):disabled:not([aria-busy="true"]):hover {
  opacity: var(--dim-disabled);
  filter: saturate(0.6);
}
html:not(.using-touch) .btn-secondary:disabled:not([aria-busy="true"]):hover {
  border-color: rgba(255,255,255,0.08);
  color: var(--text-muted);
  background: rgba(255,255,255,0.015);
}
@media (prefers-reduced-motion: reduce) {
  html:not(.using-touch) .btn-primary:not(:disabled):active,
  html:not(.using-touch) .btn-danger:not(:disabled):active,
  html:not(.using-touch) .btn-secondary:not(:disabled):active,
  html:not(.using-touch) .loan-actions > button:not(:disabled):active,
  html:not(.using-touch) .history-entry-toggle:not(:disabled):active,
  html:not(.using-touch) .header-icon-btn:not(:disabled):active,
  html:not(.using-touch) .btn-logout:not(:disabled):active,
  html.nav-collapsed .header-nav .btn-logout:not(:disabled):active,
  html:not(.using-touch).nav-collapsed .header-nav .btn-logout:not(:disabled):active,
  .header-icon-btn:active,
  .header-user-pill:active,
  .btn-logout:not(:disabled):active {
    transform: none;
  }
}

/* === Header === */
header {
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  padding-top: env(safe-area-inset-top, 0);
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  box-shadow: none;
}

/* iOS PWA standalone: paint a safe-area-sized backdrop above the sticky
   header so the status-bar / notch area never shows a black gap when the
   user scrolls (rubber-band) past the top of the viewport. We bind the
   spacer height to the actual safe-area inset so devices without a notch
   (Desktop, Android without cutout) collapse it to 0 and don't show a
   dark stripe at the top of the page. */
header::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: env(safe-area-inset-top, 0);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  pointer-events: none;
  z-index: -1;
}
/* On standalone PWA mode (where rubber-band can expose the area above the
   header) we extend the spacer enough to cover any pull-down stretch. */
@media (display-mode: standalone), (display-mode: fullscreen) {
  header::before {
    height: max(env(safe-area-inset-top, 0), 200px);
  }
}

/* Landscape notch / Dynamic Island / rounded corners (viewport-fit=cover):
   the side insets were never applied, so in landscape the dashboard cards,
   the header row and the sheet content ran under the camera cut-out. The
   in-flow page (header, offline banner, main) is inset once on body; the
   header then bleeds its background back out to the screen edge so the bar
   stays edge-to-edge like a native toolbar. env() is 0 everywhere without a
   cut-out, so desktop and portrait phones are unaffected. Fixed layers
   (modal overlays, banners, toasts) handle their own insets. */
body {
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}
header {
  margin-left: calc(-1 * env(safe-area-inset-left, 0px));
  margin-right: calc(-1 * env(safe-area-inset-right, 0px));
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* PWA-Native (QA N13): the header was 70 % opaque with a 14px blur — card
   borders, amounts and the green buttons shone through as coloured smears.
   Now a native "bar material": 92 % surface + stronger blur with saturation
   (what iOS navigation bars do), and the hairline only appears once content
   actually runs underneath (html.is-scrolled, app.js). At the top of the page
   header and page read as one surface.
   The material sits on ::after instead of on <header> itself: backdrop-filter
   on the header made it the containing block of every position:fixed
   descendant, so the menu could not become a bottom sheet (tab-bar mode). */
header {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-bottom-color: transparent;
  transition: border-color var(--transition-fast);
}
header::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  -webkit-backdrop-filter: saturate(170%) blur(24px);
  backdrop-filter: saturate(170%) blur(24px);
}
header::before {
  background: color-mix(in srgb, var(--surface) 92%, transparent);
}
/* Touch (Handy/Tablet, v. a. installierte iOS-App): die 92-%-Glasfläche liess
   gescrollten Inhalt unter Header und Statusleiste als dunklen Schleier
   durchscheinen ("oben leicht fadig"). Dort deckend und ohne Blur — auch
   günstiger beim Scrollen. */
html.using-touch header::after,
html.using-touch header::before {
  background: var(--surface);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
html.is-scrolled header {
  border-bottom-color: rgba(255, 255, 255, 0.08);
}
/* Feedback #2 (iOS 26/27, installed app): with black-translucent the system lays
   its Liquid Glass "scroll edge effect" — a dark, blurred ramp — under the
   status bar unless a fixed/sticky box with an OPAQUE background of its own
   touches the top edge; WebKit then uses that box's colour instead. The header
   is sticky at top:0 but its own background is transparent (the material sits
   on ::after, see above), so iOS drew the ramp over the header icons. An own
   opaque background changes nothing visually (same colour as ::after) and,
   unlike backdrop-filter, does not make the header a containing block for the
   fixed menu sheet. */
html.is-ios.is-standalone header {
  background: var(--surface);
}
/* Runde 4 (Native): app bar and tab bar are chrome, not content. The buttons
   in them were already unselectable (:where() rule at the top of this file),
   but a long-press on the brand, the header background or the gap between two
   tabs still selected "Money"/the tab labels and could open the iOS callout. */
header,
.app-tabbar {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.header-inner {
  /* D2 hatte das hier auf min(96vw, 1880px) entkoppelt, damit die Inline-Nav
     eines Admins (9 Buttons, ~1700px) ab ~1900px sichtbar wird statt auf jeder
     Breite bis 2560px im Burger zu verschwinden. Sichtbares Ergebnis: der
     Header war spuerbar breiter als der Content darunter — das faellt staerker
     auf als ein oefter genutztes Burger-Menue. Zurueckgedreht auf denselben Cap
     wie `main` (siehe @media (min-width: 821px) unten, wo beide zusammen
     stehen) — Header und Inhalt bleiben jetzt buendig, auch wenn dadurch mehr
     Punkte im Burger landen. */
  max-width: 1320px;
  margin: 0 auto;
  min-height: 68px;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Seitlich --page-gutter wie `main` — Logo und Header-Buttons fluchten mit
     dem Inhalt (vorher 1.25rem Header vs 1rem main = 4px Versatz). */
  padding: 0.7rem var(--page-gutter);
  gap: 1rem;
}

.header-brand {
  display: flex;
  align-items: center;
}

.header-title {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 1.6rem;
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--text-brand);
  text-shadow: 0 0 18px color-mix(in srgb, var(--accent) 12%, transparent);
  white-space: nowrap;
}

.header-title-icon { display: inline-flex; color: var(--accent); }

.header-right {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.85rem;
  color: var(--text-muted);
  position: relative;
}

.header-user-pill {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: 0.3rem;
  /* Match the visible height of the grouped desktop nav buttons. The nav
     buttons sit inside .header-nav-group, whose padding adds to the 44px
     touch target; the profile button is standalone, so it needs that height
     baked in to line up visually. */
  padding: 0.5rem 0.9rem;
  min-height: calc(var(--touch-target) + 0.4rem);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-btn);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)),
    rgba(255,255,255,0.01);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  white-space: nowrap;
  /* Lets the username truncate instead of forcing header-right to overflow
     when a long username sits next to the header-icon-btn on very narrow
     (~320px) screens — the pill itself can now shrink below its content's
     natural width. */
  min-width: 0;
  /* Button-element resets so the native <button> matches the previous <div>. */
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
  /* Runde 6 (Knöpfe): wie .header-icon-btn daneben — ohne Transition sprang
     der Profil-Knopf nach dem Druck (scale .96) hart zurück. */
  transition: var(--transition-ui);
}

.header-user-label {
  font-size: 0.78rem;
  color: var(--text-muted);
}

#header-user {
  font-size: 0.84rem;
  color: var(--text-accent-label);
  font-weight: 600;
  /* Truncate rather than push header-icon-btn/btn-menu off-screen on narrow
     viewports — requires .header-user-pill's min-width:0 above to take effect. */
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 9rem;
}

/* Icon-only header button (notifications, and any future one-off header
   actions) — same visual language as .header-user-pill but square and
   label-less so it stays cheap on mobile header width. .sync-status-pill
   used to share this look too; it's now just the dot, no box. */
.header-icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: calc(var(--touch-target) + 0.4rem);
  min-height: calc(var(--touch-target) + 0.4rem);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-btn);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)),
    rgba(255,255,255,0.01);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
  font-size: 1.05rem;
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
  transition:
    background var(--transition-fast),
    border-color var(--transition-fast),
    color var(--transition-fast),
    transform var(--transition-fast);
}
.header-icon-badge {
  position: absolute;
  /* B4: Maße in rem, aber gedeckelt – bei 200 % Schrift wurde die Zahl zu einem 34-px-Ball, der die Glocke verdeckte. */
  top: min(0.25rem, 4px);
  right: min(0.25rem, 4px);
  min-width: min(1.05rem, 18px);
  height: min(1.05rem, 18px);
  padding: 0 min(0.25rem, 4px);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--danger);
  /* Weiss auf --danger = 3.82:1. --bg auf --danger = 5.02:1.
     font-size war 0.62rem = 9.92px, unter der --text-xs-Untergrenze (11.2px). */
  color: var(--bg);
  font-size: min(var(--text-xs), 12px);
  font-weight: 700;
  line-height: 1;
  box-shadow: 0 0 0 2px var(--surface);
}
html:not(.using-touch) .header-icon-btn:hover {
  color: var(--text);
  border-color: rgba(255,255,255,0.14);
  background: rgba(255,255,255,0.045);
  transform: translateY(-1px);
}

/* War eine Pille mit Rahmen, Hintergrund und Text (Sync/Polling/Live + Zeit) —
   ist jetzt nur noch der farbige Punkt, ganz links vor dem Profil-Knopf, ohne
   eigene Box. Der volle Zustand steht weiterhin in title/aria-label und im
   Erklär-Dialog (renderSyncStatusModalBody), nur nicht mehr ausgeschrieben in
   der Leiste. 26px Klickfläche bleibt über der WCAG-2.5.8-Mindestgröße (24px),
   ohne gegen den direkt anschließenden Profil-Knopf zu stoßen. */
.sync-status-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.625rem;
  height: 1.625rem;
  flex-shrink: 0;
  border: none;
  border-radius: 50%;
  background: none;
  padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--transition-fast);
}

html:not(.using-touch) .sync-status-pill:hover {
  background: rgba(255,255,255,0.06);
}
/* Touch: sichtbar bleibt der 26px-Punkt, die Trefferflaeche waechst auf 44px
   (gemessen vorher 26x26 auf allen Handy-Viewports). Asymmetrisch: rechts nur
   6px, weil der Profil-Knopf 7px daneben beginnt; links liegt nur die nicht
   klickbare Marke. Auf Ueberlappung gewinnt ohnehin der spaeter folgende
   Profil-Knopf. */
@media (pointer: coarse) {
  .sync-status-pill { position: relative; }
  .sync-status-pill::after {
    content: "";
    position: absolute;
    inset: -9px -6px -9px -12px;
  }
}
.sync-status-pill:active .sync-status-dot { transform: scale(0.8); }
.sync-status-dot { transition: transform var(--motion-fast) var(--ease-standard); }

.sync-status-dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--text-muted);
  box-shadow: 0 0 0 3px rgba(255,255,255,0.045);
  flex: 0 0 auto;
}

.sync-status-pill[data-state="live"] .sync-status-dot {
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.sync-status-pill[data-state="polling"] .sync-status-dot {
  background: var(--info);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--info) 18%, transparent);
}
.sync-status-pill[data-state="syncing"] .sync-status-dot,
.sync-status-pill[data-state="starting"] .sync-status-dot {
  background: var(--text-muted);
  box-shadow: 0 0 0 3px rgba(255,255,255,0.07);
}
.sync-status-pill[data-state="pending"] .sync-status-dot {
  background: var(--warning);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning) 20%, transparent);
}
.sync-status-pill[data-state="offline"] .sync-status-dot,
.sync-status-pill[data-state="error"] .sync-status-dot {
  background: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 18%, transparent);
}

/* Desktop: nav is inline flex row */
.header-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.header-nav-group {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem;
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 16px;
  background: rgba(255,255,255,0.02);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
  transition: transform var(--motion-base) var(--ease-standard), border-color var(--motion-base) var(--ease-standard), box-shadow var(--motion-base) var(--ease-standard);
}

.header-nav-group-account {
  border-color: color-mix(in srgb, var(--danger) 14%, transparent);
  background: color-mix(in srgb, var(--danger) 4%, transparent);
}

.btn-logout {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  border-radius: var(--radius-btn);
  padding: 0.5rem 0.9rem;
  font-size: 0.84rem;
  font-family: inherit;
  cursor: pointer;
  touch-action: manipulation;
  transition: var(--transition-ui);
  min-height: var(--touch-target);
  white-space: nowrap;
  font-weight: 600;
}
/* Icon vor dem Label — margin statt gap, damit es nicht mit dem eigenen
   margin-left der rn-nav-badge (nach dem Label) kollidiert bzw. sich addiert. */
.btn-logout > svg { margin-right: 0.5rem; }

/* Colours skipped while this is the active view button — the hover selector
   outweighs .btn-logout.is-active, which greyed out the green "you are here"
   state as soon as the cursor touched it. */
html:not(.using-touch) .btn-logout:hover:not(.is-active) {
  border-color: rgba(255,255,255,0.08);
  color: var(--text);
  background: rgba(255,255,255,0.04);
}

/* Lift stays in both states so every nav button reacts to hover. */
html:not(.using-touch) .btn-logout:hover {
  transform: translateY(-1px);
}

.btn-logout.is-active {
  color: #f3f5f2;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--accent) 24%, transparent), color-mix(in srgb, var(--accent) 14%, transparent)),
    color-mix(in srgb, var(--accent) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}

.btn-logout-danger {
  color: #e6b4b4;
}

html:not(.using-touch) .btn-logout-danger:hover {
  color: #fff0f0;
  background: color-mix(in srgb, var(--danger) 8%, transparent);
  border-color: color-mix(in srgb, var(--danger) 16%, transparent);
}

/* Hamburger – hidden on desktop. Größe/Radius bewusst identisch zu
   .header-icon-btn (Suche/Glocke): war var(--touch-target)/var(--radius)
   statt +0.4rem/var(--radius-btn), dadurch sichtbar kleiner und eckiger als
   seine direkten Nachbarn, sobald der Burger auf breiten Screens erscheint
   (Admin mit vielen Nav-Punkten). ≤820px werden beide ohnehin auf
   var(--touch-target) zurückgestutzt (siehe media query), dort war es
   unauffällig. */
.btn-menu {
  display: none;
  flex-shrink: 0;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--text);
  border-radius: var(--radius-btn);
  width: calc(var(--touch-target) + 0.4rem);
  height: calc(var(--touch-target) + 0.4rem);
  font-size: 1.05rem;
  cursor: pointer;
  touch-action: manipulation;
  align-items: center;
  justify-content: center;
  transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
  flex-shrink: 0;
}

html:not(.using-touch) .btn-menu:hover {
  background: rgba(255,255,255,0.08);
  border-color: rgba(255,255,255,0.14);
}

.btn-menu:active { transform: scale(0.96); }

/* Die beiden Geschwister-Varianten (~ / +) deckten früher beide möglichen
   DOM-Reihenfolgen von header-nav/btn-menu ab; seit btn-menu vor header-nav
   im Markup steht (Tab-Reihenfolge, siehe index.html), matcht nur noch die
   aria-expanded-Variante — die ist ohnehin DOM-Reihenfolge-unabhängig und
   war schon vorher die eigentliche Quelle der Wahrheit. */
.btn-menu[aria-expanded="true"] {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: color-mix(in srgb, var(--accent) 32%, transparent);
  color: var(--accent);
}

/* Dezenter Hinweis "einer der Menüpunkte ist gerade die aktuelle Ansicht",
   sichtbar auch wenn das Menü zu ist — vorher stand das nur als Unterstrich
   auf dem Eintrag selbst, unsichtbar solange das Menü geschlossen war.
   setActiveHeaderButton()/clearActiveHeaderButton() in app.js pflegen die
   Klasse. Reine Farbe statt eines weiteren Badges, damit es nicht mit dem
   roten Ungelesen-Badge (.rn-menu-badge) auf demselben Knopf kollidiert. */
.btn-menu.has-active-item {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 32%, transparent);
}

/* === Burger mode ==========================================================
   Driven by html.nav-collapsed, which app.js sets whenever the inline nav no
   longer fits — NOT by a fixed breakpoint. A lender carries seven nav buttons
   (~1400px of header) versus four for a borrower (~1100px), so any single
   media query is wrong for one of them; the old 820px threshold left a wide
   band where the rightmost buttons were simply cut off with no burger in
   sight. Measuring covers both roles, long usernames and font-size changes.
   ========================================================================== */
html.nav-collapsed .header-nav {
  display: none;
  flex-direction: column;
  align-items: stretch;
  position: absolute;
  /* +18px (was 10px) gives the box-shadow blur enough room to fall away
     from the header so it doesn't bleed up through the title bar between
     the brand and the user pill. */
  top: calc(100% + 18px);
  right: 0.75rem;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01)),
    var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 16px;
  padding: 8px;
  gap: 8px;
  z-index: var(--z-sidebar-open);
  width: min(236px, calc(100vw - 1.5rem));
  max-width: calc(100vw - 1.5rem);
  /* Smaller vertical reach, slightly less opaque — keeps a soft depth cue
     without leaking up into the header. */
  box-shadow: 0 18px 34px rgba(0,0,0,0.42), 0 4px 10px rgba(0,0,0,0.24), inset 0 1px 0 rgba(255,255,255,0.05);
  transform-origin: top right;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  /* Nothing capped this before — in landscape (much less viewport height)
     the menu (up to 9 items for an admin) ran past the bottom of the screen
     with no way to scroll the rest into view, hiding Logs/Einstellungen and
     even Abmelden. ~100px covers header height + the 18px gap + margin
     across the compact paddings below; approximate, but a slightly-too-early
     scrollbar beats an unreachable item. */
  max-height: calc(100dvh - 100px - env(safe-area-inset-bottom, 0px));
  overflow-y: auto;
}
/* Sticky fade at the bottom of the scroll area, same "content sliding under
   here" cue as .modal-actions above. The negative top-margin pulls this over
   the last item instead of leaving a gap when NOT stuck — meant to stay
   invisible then, but a transparent-to-surface gradient still visibly
   smudges over that item (often "Abmelden") even with nothing scrolled
   beneath it. updateHeaderNavScrollShadow() (app.js) gates it on .is-scrollable,
   set only when the menu is actually tall enough to scroll. */
html.nav-collapsed .header-nav.is-scrollable::after {
  content: '';
  position: sticky;
  bottom: -8px;
  display: block;
  height: 20px;
  margin: -20px -8px -8px;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none;
}

html.nav-collapsed .header-nav.open {
  display: flex;
  animation: slideDown var(--motion-base) var(--ease-spring) both;
}

html.nav-collapsed .header-nav.closing {
  display: flex;
  animation: slideFadeOut var(--motion-fast) var(--ease-exit) both;
}

html.nav-collapsed .header-nav-group {
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  padding: 5px;
  border-radius: var(--radius-tile);
  background: rgba(255,255,255,0.025);
  border: 1px solid rgba(255,255,255,0.055);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.025);
}
html.nav-collapsed .header-nav-group-account {
  border-top: 1px solid color-mix(in srgb, var(--danger) 16%, transparent);
  padding-top: 5px;
  margin-top: 0;
  background: color-mix(in srgb, var(--danger) 4.5%, transparent);
}

html.nav-collapsed .header-nav.open .header-nav-group {
  animation: navItemIn var(--motion-fast) var(--ease-standard) both;
}
html.nav-collapsed .header-nav.open .header-nav-group:nth-child(2) {
  animation-delay: 0.03s;
}
html.nav-collapsed .header-nav.open .header-nav-group:nth-child(3) {
  animation-delay: 0.06s;
}

html.nav-collapsed .btn-menu { display: flex; }

/* Group labels ("Eigene" / "Admin"): burger menu only, and only when
   there is more than one group to tell apart. */
.header-nav-label { display: none; }
html.nav-collapsed .header-nav.has-rights-group .header-nav-label {
  display: block;
  padding: 0.3rem 0.6rem 0.15rem;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-muted);
}

html.nav-collapsed .header-nav .btn-logout {
  display: flex;
  /* PWA-Native (QA M26): list rows read left-aligned like a native menu —
     centred, the icons zig-zagged down the panel with the label lengths. */
  justify-content: flex-start;
  align-items: center;
  text-align: left;
  border-radius: var(--radius);
  padding: 0.72rem 0.85rem;
  font-size: 0.92rem;
  min-height: 48px;
  border-color: transparent;
  background: transparent;
  color: var(--text);
  font-weight: 650;
  letter-spacing: 0;
  transition:
    background var(--transition-fast),
    border-color var(--transition-fast),
    color var(--transition-fast),
    box-shadow var(--transition-fast),
    transform var(--transition-fast);
  white-space: normal;
  overflow-wrap: anywhere;
  -webkit-hyphens: auto;
  hyphens: auto;
}
html:not(.using-touch).nav-collapsed .header-nav .btn-logout:hover {
  background: rgba(255,255,255,0.075);
  border-color: rgba(255,255,255,0.08);
  transform: none;
  color: var(--text);
}
html.nav-collapsed .header-nav .btn-logout.is-active {
  background: color-mix(in srgb, var(--accent) 15%, rgba(255,255,255,0.035));
  color: var(--text);
  border-color: color-mix(in srgb, var(--accent) 28%, transparent);
  box-shadow: inset 0 -3px 0 var(--accent);
}
html.nav-collapsed .header-nav .btn-logout-danger {
  color: #ffd0d0;
  background: color-mix(in srgb, var(--danger) 3.5%, transparent);
}
html:not(.using-touch).nav-collapsed .header-nav .btn-logout-danger:hover {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border-color: color-mix(in srgb, var(--danger) 18%, transparent);
  color: #fff0f0;
}

/* B4: Burger-Menü im Querformat (Höhe bis 430 px, z. B. iPhone 13 quer = 390 px, SE quer = 320 px): kompaktere Zeilen –
   das Touch-Ziel bleibt 44 px, nur Polster, Lücken und Abstand zur Kopfzeile schrumpfen –, damit mehr Einträge
   ohne Scrollen sichtbar sind. */
@media (max-height: 430px) {
  html.nav-collapsed .header-nav {
    top: calc(100% + 8px);
    padding: 5px;
    gap: 4px;
    max-height: calc(100dvh - 76px - env(safe-area-inset-bottom, 0px));
  }
  html.nav-collapsed .header-nav-group { gap: 1px; padding: 3px; }
  html.nav-collapsed .header-nav .btn-logout { min-height: var(--touch-target); padding: 0.3rem 0.8rem; }
  html.nav-collapsed .header-nav.has-rights-group .header-nav-label { padding: 0.15rem 0.6rem 0.05rem; }
}

/* === PWA-Native: header stages, avatar, tab bar ===========================
   (1) html.nav-compact (app.js updateHeaderNavMode, QA M26): the stage
       between the full inline row and the burger. "Angemeldet als" goes, the
       admin tools and "Abmelden" become icon buttons — an admin's nine
       entries fit inline at 1440px instead of hiding behind the burger. */
html.nav-compact .header-user-label { display: none; }
html.nav-compact:not(.nav-collapsed) .header-nav-group-admin .btn-logout,
html.nav-compact:not(.nav-collapsed) .header-nav-group-account .btn-logout {
  position: relative;
  width: var(--touch-target);
  padding: 0;
  font-size: 0;
}
html.nav-compact:not(.nav-collapsed) .header-nav-group-admin .btn-logout > svg,
html.nav-compact:not(.nav-collapsed) .header-nav-group-account .btn-logout > svg {
  width: 18px;
  height: 18px;
  margin-right: 0;
}
html.nav-compact:not(.nav-collapsed) .header-nav-group-admin .rn-nav-badge {
  position: absolute;
  top: 0.1rem;
  right: 0.1rem;
  margin: 0;
  font-size: var(--text-xs);
}

/* (2) Avatar initial instead of a clipped name on phones (QA M5: the pill
       read "p_ad…" on an iPhone SE). data-initial comes from app.js; the
       name stays in the DOM for screen readers. */
@media (max-width: 480px) {
  .header-user-pill[data-initial] {
    width: var(--touch-target);
    min-width: var(--touch-target);
    padding: 0;
    justify-content: center;
    border-color: transparent;
    background: none;
    box-shadow: none;
    border-radius: 50%;
  }
  .header-user-pill[data-initial]::before {
    content: attr(data-initial);
    display: grid;
    place-items: center;
    width: 2.125rem;
    height: 2.125rem;
    border-radius: 50%;
    background: color-mix(in srgb, var(--accent) 22%, var(--surface2));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent);
    color: var(--accent-text);
    font-size: 0.95rem;
    font-weight: 750;
    line-height: 1;
    transition: transform var(--motion-fast) var(--ease-standard);
  }
  .header-user-pill[data-initial]:active::before { transform: scale(0.92); }
  .header-user-pill[data-initial] #header-user {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* (3) Bottom tab bar — only in the INSTALLED app on a phone (html.has-tabbar,
       app.js syncAppTabbar: standalone + ≤820px + signed in). Search,
       notifications and the menu move from the crowded header into the thumb
       zone; a dual account switches Verliehen/Geschuldet there (the in-page
       switch is hidden). Everything anchored to the bottom edge moves up by
       --app-tabbar-h: sticky card action bars, toasts (app.js), the menu
       sheet. 0 while the keyboard is up (html.kb-open) — the bar hides then. */
.app-tabbar { display: none; }
html.has-tabbar {
  --app-tabbar-h: calc(3.3rem + env(safe-area-inset-bottom, 0px));
}
html.has-tabbar.kb-open { --app-tabbar-h: 0px; }
/* iOS installed app (html.is-ios.is-standalone): while a modal locks page
   scroll, WebKit shortens the layout viewport by the status bar — fixed
   elements anchored at `bottom: 0` rose ~59px (sheet bottom far above the
   edge; tab bar jumping up and back down on close). Pin both to the full
   height measured while unlocked (app.js _syncAppFullHeight). */
html.is-ios.is-standalone .modal-overlay {
  bottom: auto;
  height: var(--app-full-h, 100dvh);
  min-height: 0;
}
html.is-ios.is-standalone.has-tabbar .app-tabbar {
  bottom: auto;
  top: calc(var(--app-full-h, 100dvh) - 3.3rem - env(safe-area-inset-bottom, 0px));
}
/* Self-heal (app.js _syncTabbarOffset): when the layout viewport is shorter
   than the screen (first launch after an update, the WebKit “short until the
   first scroll” state) the screen-pinned bar reaches past it and WebKit cut
   it off – half a tab bar (user report after 3.0.0). While that holds the bar
   hangs off the viewport bottom instead; it returns once the height is back. */
html.is-ios.is-standalone.has-tabbar.tabbar-viewport-anchored .app-tabbar {
  top: auto;
  bottom: 0;
}
html.has-tabbar .app-tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-header);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  height: calc(3.3rem + env(safe-area-inset-bottom, 0px));
  padding: 0 env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  -webkit-backdrop-filter: saturate(170%) blur(24px);
  backdrop-filter: saturate(170%) blur(24px);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  transition: transform var(--motion-base) var(--ease-sheet), visibility 0s;
}
html.has-tabbar.kb-open .app-tabbar {
  transform: translateY(100%);
  visibility: hidden;
  transition: transform var(--motion-fast) var(--ease-exit), visibility 0s var(--motion-fast);
}
.app-tab {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  min-width: 0;
  min-height: var(--touch-target);
  padding: 0.3rem 0 0.2rem;
  border: 0;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--motion-fast) var(--ease-standard);
}
.app-tab-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.4rem;
  height: 1.8rem;
  border-radius: var(--radius-pill);
  transition: background-color var(--motion-fast) var(--ease-standard), transform var(--motion-fast) var(--ease-standard);
}
.app-tab-label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Active tab: accent colour + the pill indicator behind the icon (Material 3
   navigation bar; reads fine on iOS too). */
.app-tab.is-active { color: var(--accent-text); }
.app-tab.is-active .app-tab-icon { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.app-tab:active .app-tab-icon { transform: scale(0.9); }
.app-tab:focus-visible { outline-offset: -4px; }
/* "Geschuldet" = money coming back in: the outgoing arrow, turned around. */
.app-tab.is-incoming .app-tab-icon svg { transform: rotate(180deg); }
.app-tab-badge {
  position: absolute;
  top: -0.2rem;
  left: calc(50% + 0.35rem);
  min-width: min(1.1rem, 18px);
  height: min(1.1rem, 18px);
  padding: 0 min(0.28rem, 4px);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--bg);
  font-size: min(var(--text-xs), 12px);
  font-weight: 700;
  line-height: 1;
  box-shadow: 0 0 0 2px var(--surface);
}
.app-tab-badge.is-alert { background: var(--danger); }
.app-tab-badge.hidden { display: none; }
/* R13-Anim: Zaehler-Badges (Glocke, Tab-Leiste) erschienen hart; jetzt ein
   kurzer Pop beim Erscheinen (die Animation laeuft, sobald .hidden faellt)
   und derselbe Huepfer wie .tab-count.is-bumped, wenn die Zahl steigt
   (app.js _bumpBadge). Keine Dauer-Animation. */
@keyframes badgePopIn {
  from { opacity: 0; transform: scale(0.4); }
  to   { opacity: 1; transform: scale(1); }
}
.header-icon-badge:not(.hidden),
.app-tab-badge:not(.hidden) { animation: badgePopIn var(--motion-base) var(--ease-spring) both; }
.header-icon-badge.is-bumped,
.app-tab-badge.is-bumped { animation: countBump var(--motion-slow) var(--ease-spring); }
@media (prefers-reduced-motion: reduce) {
  .header-icon-badge:not(.hidden),
  .app-tab-badge:not(.hidden),
  .header-icon-badge.is-bumped,
  .app-tab-badge.is-bumped { animation: none; }
}
/* The tab bar owns search, notifications, menu and the perspective switch. */
html.has-tabbar #btn-global-search,
html.has-tabbar #btn-notifications,
html.has-tabbar #btn-menu,
html.has-tabbar #main-content .perspective-switch { display: none; }
html.has-tabbar main { padding-bottom: calc(3rem + var(--app-tabbar-h)); }
@media (max-width: 600px) {
  /* Sticky card action bars stop ON the tab bar instead of under it; the
     home-indicator inset is the tab bar's business now. */
  /* --tabbar-offset = the tab bar's measured distance from the viewport
     bottom (app.js _syncTabbarOffset); −1px overlaps it so no hairline of
     page content shows between bar and tab bar (user report, iPhone app). */
  html.has-tabbar .loan-card .loan-actions {
    bottom: calc(var(--tabbar-offset, var(--app-tabbar-h)) - 1px);
    padding-bottom: 0.65rem;
  }
  html.has-tabbar .btn-activity-toggle.expanded {
    bottom: calc(var(--tabbar-offset, var(--app-tabbar-h)) - 1px);
    padding-bottom: 0.55rem;
  }
  html.has-tabbar .loan-card:has(.loan-actions) .btn-activity-toggle.expanded {
    bottom: calc(var(--loan-actions-h, 4.5rem) + 0.5rem + var(--tabbar-offset, var(--app-tabbar-h)));
  }
}
/* Entry menus (⋯ on payments/notes) are anchor-positioned against the
   viewport and flip upwards only when they would leave it — the tab bar was
   not part of that, so a menu near the bottom opened BEHIND it. The bottom
   margin counts in the overflow test (and moves to the top on a flip). */
html.has-tabbar .entry-menu-panel { margin-bottom: calc(var(--app-tabbar-h) + 0.3rem); }
/* The menu becomes a bottom sheet above the tab bar, over a real scrim
   element (taps on it close the menu instead of hitting the card below). */
html.has-tabbar.nav-collapsed .header-nav {
  position: fixed;
  top: auto;
  left: max(0.5rem, env(safe-area-inset-left, 0px));
  right: max(0.5rem, env(safe-area-inset-right, 0px));
  bottom: calc(var(--app-tabbar-h) + 0.5rem);
  width: auto;
  /* Phone: edge to edge minus the gutter; portrait tablet: a centred sheet
     instead of an 800px wide list. */
  max-width: 30rem;
  margin-inline: auto;
  max-height: calc(100dvh - var(--app-header-h, 68px) - var(--app-tabbar-h) - 1.5rem);
  /* Runde 4 (Native): scrolling the menu to its end no longer carries on
     into the page behind the scrim. */
  overscroll-behavior: contain;
  border-radius: 20px;
  transform-origin: bottom center;
  box-shadow: 0 -10px 34px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.05);
}
html.has-tabbar.nav-collapsed .header-nav.open {
  animation: navSheetIn var(--motion-base) var(--ease-sheet) both;
}
html.has-tabbar.nav-collapsed .header-nav.closing {
  animation: navSheetOut var(--motion-fast) var(--ease-exit) both;
}
@keyframes navSheetIn {
  from { opacity: 0; transform: translateY(1.5rem); }
}
@keyframes navSheetOut {
  to { opacity: 0; transform: translateY(1.5rem); }
}
.app-menu-scrim { display: none; }
html.has-tabbar.nav-menu-open .app-menu-scrim {
  display: block;
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-top) + 1);
  background: rgba(0, 0, 0, 0.45);
  animation: appScrimIn var(--motion-fast) var(--ease-standard) both;
}
@keyframes appScrimIn {
  from { opacity: 0; }
}
/* QA M26: toasts lay on top of the open menu. While it is open the header
   (which contains the menu) stacks above toasts and the tab bar. The rest of
   the header dims with the scrim in sheet mode. */
html.nav-menu-open header { z-index: calc(var(--z-top) + 2); }
html.has-tabbar.nav-menu-open .header-brand,
html.has-tabbar.nav-menu-open .header-right > :not(.header-nav) {
  opacity: 0.4;
  transition: opacity var(--motion-fast) var(--ease-standard);
}

/* (4) Press states for whole tappable rows. The global tap-highlight is off
   (reset at the top of this file), so a tap on a search hit, a notification
   or a feedback row gave no sign of life until the next view appeared.
   Background only — no transform (the Motion-Audit keeps transforms off list
   items: they fought the swipe gestures). Doubled with the mouse-hover
   selector, which otherwise outranks a plain :active (see header comment on
   :active specificity). */
.global-search-row:active,
html:not(.using-touch) .global-search-row:active,
.notification-row:active,
html:not(.using-touch) .notification-row:active,
.fb-row:active,
html:not(.using-touch) .fb-row:active,
.fb-ov-card:active,
html:not(.using-touch) .fb-ov-card:active {
  background: color-mix(in srgb, var(--text) 9%, var(--surface2));
  transition-duration: 0s;
}
html.nav-collapsed .header-nav .btn-logout:not(:disabled):active {
  background: rgba(255, 255, 255, 0.09);
}
@media (prefers-reduced-motion: reduce) {
  html.has-tabbar .app-tabbar,
  html.has-tabbar.kb-open .app-tabbar { transition: none; }
  html.has-tabbar.nav-collapsed .header-nav.open,
  html.has-tabbar.nav-collapsed .header-nav.closing,
  html.has-tabbar.nav-menu-open .app-menu-scrim { animation: none; }
}

/* === Main === */
main {
  /* Scaled cap: phone bleibt komfortabel-eng (Lese-Zeile ≤ 900 px), aber
     auf großen Screens darf der Content auf bis zu 1320 px wachsen, damit
     der Loan-Grid 2-3 Karten nebeneinander zeigen kann (siehe weiter unten).
     Ohne diese Skalierung wirkt die App auf 1920er-Monitoren wie eine
     zentrierte Phone-Spalte mit ~1000 px verschenktem Whitespace.
     D11: Der 900px-Floor im frueheren clamp(900px, 92vw, 1320px) machte main
     zwischen 821 und 978px BREITER als 92vw (bei 900px Viewport blieb nur die
     1rem-Innenpolsterung), ab 1024px sprang der Rand wieder auf 4% — ein
     sichtbarer Sprung. Unter 821px bleibt main volle Breite (wie bisher, der
     Cap greift dort ohnehin nicht), ab 821px durchgehend 92vw. */
  max-width: 1320px;
  margin: 0 auto;
  padding: 1.5rem var(--page-gutter) 3rem;
}
@media (min-width: 821px) {
  main,
  /* Zusammen mit main, damit die beiden Caps nie wieder auseinanderlaufen
     koennen (siehe Kommentar bei .header-inner oben). */
  .header-inner { max-width: min(92vw, 1320px); }
}

#main-content.content-swap-out {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--motion-xs) var(--ease-exit), transform var(--motion-xs) var(--ease-exit);
}

#main-content.content-swap-in {
  animation: contentSwapIn var(--motion-base) var(--ease-standard) both;
}

@keyframes contentSwapIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

.stat-profit-inline { color: color-mix(in srgb, var(--accent) 70%, white); font-weight: 500; }

/* === Sections === */
.section { margin-bottom: 2rem; }
/* Offset for the sticky app-header (~68px) when scrolled into view via JS */
#pending-section,
#pending-additions-section,
#active-section,
#history-section {
  scroll-margin-top: 80px;
}

.section-dashboard { margin-bottom: 1.5rem; }

/* D4: Die 4-Spalten-Basis + Hero span 2 ergibt 6 Zellen fuer 5 Kacheln —
   Reihe 2 liess Spalte 3+4 leer. Die 3-Spalten-Stufe galt nur bis 820px, das
   Bento-Layout erst ab 1100px, also fielen iPad Mini Landscape und iPad Pro
   Portrait (821-1099px) in genau diese Luecke. Stufen jetzt lueckenlos:
   2 Spalten < 600, 3 Spalten 600-1099, Bento ab 1100. */
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.85rem;
}
@media (min-width: 1100px) {
  .dashboard-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Lender dashboard fits 5 tiles into 4 columns by letting the hero ("Offen
   gesamt") span two columns when a 5th tile is present (Größter Schuldner).
   Without :has() support, we use a server-driven modifier class on the grid
   instead — `.dashboard-grid-5` is applied when the 5-tile layout renders.
   Reinforces visual hierarchy — the biggest number gets the biggest tile. */
.dashboard-grid-5 > .dashboard-hero {
  grid-column: span 2;
}

/* === Bento Layout (Desktop ≥ 1100px) ===
   On wide screens we promote the hero to a 2×2 tile and arrange the four
   remaining cards as a 2×2 block to its right. This trades a slim single
   row for a more deliberate, breathing layout that 2026 fintech dashboards
   typically use — asymmetry signals priority instead of "every card is
   equal". The intermediate 820–1100 range keeps the single-row variant so
   the layout never feels squeezed. */
@media (min-width: 1100px) {
  .dashboard-grid-5 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-rows: auto auto;
    /* Slightly looser gap so the bento blocks read as discrete groups, not
       a continuous strip. */
    gap: 1rem;
  }
  .dashboard-grid-5 > .dashboard-hero {
    grid-column: span 2;
    grid-row: span 2;
    /* Justify content top-aligned so the title-row sits where the eye
       lands first on a 2×2 tile; the value sits below with extra breathing
       room from the increased min-height. */
    justify-content: space-between;
    align-items: flex-start;
    text-align: left;
    padding: 1.4rem 1.5rem;
    gap: 0.45rem;
    min-height: 240px;
  }
  .dashboard-grid-5 > .dashboard-hero .dashboard-label {
    margin-bottom: 0.5rem;
  }
  .dashboard-grid-5 > .dashboard-hero .dashboard-value {
    font-size: 2.6rem;
    line-height: 1;
    margin-bottom: 0.25rem;
  }
  .dashboard-grid-5 > .dashboard-hero .dashboard-sublabel {
    font-size: 0.95rem;
    opacity: 0.85;
  }
  /* Der 2×2-Hero verteilte Titel / Betrag / Zusatz mit space-between über
     ~240px — der Betrag schwebte in der Mitte, ohne Bezug zu Titel oder
     Zusatzzeile, darüber und darunter je ~80px Leere. Jetzt Titel oben,
     Betrag + Zusatz als Block unten verankert (klassische Bento-Kachel), und
     der Betrag bekommt die Größe, die eine doppelt so hohe Kachel trägt.
     Die doppelte Klasse schlägt das Subgrid oben — der Hero spannt zwei
     Kachelreihen (= 6 Subgrid-Spuren) und ordnet seinen Inhalt selbst. */
  .dashboard-grid.dashboard-grid-5 > .dashboard-card.dashboard-hero {
    display: flex;
    justify-content: flex-start;
    grid-row: span 2;
  }
  .dashboard-grid.dashboard-grid-5 > .dashboard-card.dashboard-hero > .dashboard-value {
    align-self: flex-start;
    margin-top: auto;
    font-size: clamp(2.6rem, 3.3vw, 3.4rem);
    letter-spacing: -0.02em;
  }
  /* Mit Verteilungsliste (ab zwei Leihnehmern): Betrag direkt unter den
     Titel, die Liste füllt den Rest und schließt unten ab. */
  .dashboard-grid.dashboard-grid-5 > .dashboard-card.dashboard-hero:has(.hero-breakdown) > .dashboard-value { margin-top: 0.15rem; }
  .dashboard-grid-5 > .dashboard-hero .hero-breakdown { display: grid; }
}

/* "Wer schuldet wie viel" im Desktop-Hero (app.js renderLenderDashboard). Nur
   im Bento-Layout sichtbar; überall sonst ist der Hero eine schmale Kachel
   (dort als Subgrid mit genau drei Zeilen — ein viertes sichtbares Kind
   hätte keine eigene Spur). */
.hero-breakdown { display: none; }
.hero-breakdown {
  list-style: none;
  margin: auto 0 0;
  padding: 0.9rem 0 0;
  width: 100%;
  gap: 0.55rem;
  border-top: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
}
.hero-breakdown-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 0.75rem;
  row-gap: 0.25rem;
  align-items: baseline;
  font-size: 0.8rem;
}
.hero-breakdown-name {
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hero-breakdown-row.is-rest .hero-breakdown-name { color: var(--text-muted); font-weight: 500; }
.hero-breakdown-amount {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.hero-breakdown-bar {
  grid-column: 1 / -1;
  height: 4px;
  border-radius: var(--radius-pill);
  background: rgba(255,255,255,0.07);
  overflow: hidden;
}
.hero-breakdown-bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  opacity: 0.8;
}
.hero-breakdown-row.is-rest .hero-breakdown-bar > span { background: var(--text-muted); opacity: 0.5; }
@supports (grid-template-rows: subgrid) {
  @media (min-width: 1100px) {
    .dashboard-grid.dashboard-grid-5 > .dashboard-card.dashboard-hero { grid-row: span 6; }
  }
}

.dashboard-card {
  background:
    linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)),
    var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 1rem 1.05rem;
  box-shadow: var(--shadow-card);
  /* Centered content for visual consistency with empty-states, stat blocks
     and reliability cards. Avoids the mixed-alignment look across the page. */
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  /* Same overflow-safety as .loan-card/.snapshot-item — the "Größter
     Schuldner" card shows a free-text username instead of a number, and
     sits in the same 2-column mobile grid that already forced this fix
     elsewhere for long unbreakable strings. */
  min-width: 0;
}

.dashboard-card-strong {
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--accent) 12%, transparent), color-mix(in srgb, var(--accent) 3%, transparent)),
    var(--surface);
}

/* "Fälligkeiten" (borrower dashboard): its own content is often just one or
   two short due-list rows, much shorter than its grid row-mate "Du
   schuldest gesamt" (label + value + progress bar + tempo hint). CSS Grid
   stretches both cards to the row's tallest member, and .dashboard-card's
   shared justify-content:center then floated this card's short content in
   the middle of all that extra height instead of anchoring it under its own
   label — reads as "small and adrift" rather than as a normal stat tile. */
.dashboard-card-due {
  justify-content: flex-start;
}

.dashboard-card-alert {
  border-color: color-mix(in srgb, var(--danger) 38%, transparent);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--danger) 12%, transparent), color-mix(in srgb, var(--danger) 3%, transparent)),
    var(--surface);
}

.dashboard-card-positive {
  border-color: color-mix(in srgb, var(--accent) 28%, transparent);
}

/* Clickable dashboard cards become subtle "shortcuts" to a tab. Hover state
   signals affordance without changing layout. The chevron in the top-right
   corner is the explicit "this is a link"-cue — before it, only the cursor
   gave it away, which mobile users couldn't see. */
.dashboard-card-clickable {
  cursor: pointer;
  position: relative;
  transition: var(--transition-ui);
}
.dashboard-card-clickable::after {
  content: '›';
  /* A11y R5: der Pfeil ist Deko — ohne Alternativtext las der Screenreader
     „›“ als Teil des Kachel-Namens mit. */
  content: '›' / '';
  position: absolute;
  top: 0.5rem;
  right: 0.7rem;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--text-muted);
  opacity: 0.55;
  transition: var(--transition-ui);
}
/* Motion-Audit: an html:not(.using-touch) gekoppelt wie alle anderen Hover-
   Regeln (siehe app.js Zeile 3 ff.) — auf dem Handy blieb die Kachel nach dem
   Tippen angehoben mit gruenem Rand stehen (klebriger iOS/Android-Hover). Der
   Press dazu steht im Maus-Tier bei den Card micro-interactions. */
html:not(.using-touch) .dashboard-card-clickable:hover {
  transform: translateY(-2px);
  border-color: var(--accent, #0a7);
  box-shadow: 0 6px 18px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.05);
}
html:not(.using-touch) .dashboard-card-clickable:hover::after {
  opacity: 1;
  color: var(--accent);
  transform: translateX(2px);
}
.dashboard-card-clickable:active { transform: translateY(0); }
html:not(.using-touch) .dashboard-card-clickable:active { transform: scale(0.985); transition-duration: 60ms; }
@media (prefers-reduced-motion: reduce) {
  html:not(.using-touch) .dashboard-card-clickable:active { transform: none; }
}
.dashboard-card-clickable:focus-visible { outline: 2px solid var(--accent, #0a7); outline-offset: 2px; }
/* Touch-Audit R4: :active gilt auch fuer Vorfahren — ein Tipp auf den
   Info-Punkt IN der Kachel liess die ganze Kachel mit einsinken, als oeffne
   er sie. Nur der Punkt gibt nach (.info-dot:active). */
.dashboard-card-clickable:has(.info-dot:active),
html:not(.using-touch) .dashboard-card-clickable:has(.info-dot:active) { transform: none; }
/* N4: Zentrierte Titel mit Info-Punkt ("Zuverlässigkeit (i)") laufen auf
   schmalen Kacheln bis in die obere rechte Ecke, in der das Chevron sitzt —
   Punkt und Pfeil klebten aneinander (gemessen 3 px auf 320 px). Bei solchen
   Kacheln wandert das Chevron an den rechten Rand auf halber Höhe, wo neben
   dem zentrierten Wert immer Platz ist. */
.dashboard-card-clickable:has(> .label-with-info .info-dot)::after {
  top: 50%;
  right: 0.55rem;
  translate: 0 -50%;
}

/* Sublabel and small-value variants for cards that show name+amount instead
   of just a single number (e.g. "Größter Schuldner"). */
.dashboard-sublabel { font-size: 0.78rem; opacity: 0.7; margin-top: 0.1rem; }
/* Doppelte Klasse: die Regel stand VOR .dashboard-value (gleiche Spezifität)
   und verlor dadurch immer — der Name in "Größter Leihnehmer" stand in voller
   1.5rem-Zahlengröße und brach auf dem Handy mitten im Wort ("p_borrowe|r"). */
.dashboard-value.dashboard-value-small {
  font-size: 1.1rem;
  line-height: 1.25;
  /* Nutzernamen haben keine Silben — hyphens:auto setzte sonst Trennstriche
     in Fantasie-Silben; umbrochen wird nur, wenn der Name wirklich nicht passt. */
  -webkit-hyphens: none;
  hyphens: none;
}

.dashboard-label {
  display: block;
  color: var(--text-muted);
  font-size: 0.74rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 0.45rem;
}

.dashboard-value {
  font-size: 1.5rem;
  line-height: 1.1;
  font-weight: 800;
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

/* Befund 46: "UEBERFAELLIG 0" stand in voller weisser Fettschrift, also genauso
   laut wie "EUR 35,33" daneben — eine Null ist aber keine Information, sondern
   deren Abwesenheit. app.js setzte das bisher inline (emptyValueStyle());
   ueber die Klasse kann der Inline-Style weg (requests/app.md R6).
   --text-muted misst 7,2:1, bleibt also gut lesbar — nur nicht mehr dominant. */
.kpi-value.is-empty-value,
.dashboard-value.is-empty-value,
.snapshot-value.is-empty-value { color: var(--text-muted); }

/* === Leihgeber-Kacheln: gemeinsame Zeilen für Titel / Wert / Zusatz ===
   Jede Kachel zentrierte ihren Inhalt für sich. In einer Kachelreihe standen
   Titel und Zahl dadurch auf unterschiedlichen Höhen, sobald ein Titel
   umbrach ("Ausstehende Anfragen" zweizeilig neben "Überfällig") oder nur
   eine Kachel eine Zusatzzeile hatte (€-Betrag unter "Überfällig"). Mit
   Subgrid teilen sich alle Kacheln einer Reihe drei Spuren: Titel oben
   bündig, Werte auf einer Linie, Zusatzzeile darunter. Ohne Subgrid-Support
   bleibt es beim bisherigen zentrierten Flex-Layout. Nur das Leihgeber-
   Raster — das Leihnehmer-Raster hat Fortschrittsbalken und Listen. */
@supports (grid-template-rows: subgrid) {
  .dashboard-grid:not(.dashboard-grid-borrower) > .dashboard-card {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    align-content: start;
    row-gap: 0.2rem;
  }
  .dashboard-grid:not(.dashboard-grid-borrower) > .dashboard-card > .dashboard-label {
    align-self: start;
    margin-bottom: 0.25rem;
  }
  .dashboard-grid:not(.dashboard-grid-borrower) > .dashboard-card > .dashboard-value { align-self: center; }
  .dashboard-grid:not(.dashboard-grid-borrower) > .dashboard-card > .dashboard-sublabel {
    align-self: start;
    margin-top: 0;
  }
}

.section-title {
  font-size: var(--text-lg);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Subsection headers inside lender view (per-borrower grouping) */
.subsection-title {
  font-size: var(--text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  margin: 0.75rem 0 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.section-toggle {
  cursor: pointer;
  user-select: none;
  touch-action: manipulation;
  /* Klappbarer Sektionstitel: Trefferflaeche war nur ~21px hoch, und der
     Abstand zum Inhalt (0.5rem) wich von .section-title ohne Toggle (1rem)
     ab. 44px Mindesthoehe, per negativem margin-top optisch an derselben
     Stelle; margin-bottom so, dass Text->Inhalt wieder ~1rem ergibt. */
  min-height: var(--touch-target);
  margin-top: -0.7rem;
  margin-bottom: 0.3rem;
  transition: color var(--motion-base) var(--ease-standard), transform var(--motion-base) var(--ease-standard), opacity var(--motion-base) var(--ease-standard);
  transform-origin: left center;
}

html:not(.using-touch) .section-toggle:hover {
  color: var(--text);
  transform: translateX(2px);
}
/* Touch-Audit R4: Der klappbare Titel ("Aktive Leihen ⌄") gab beim Tippen
   kein Zeichen — erst das Zu-/Aufklappen danach. Kurzes Abdimmen wie bei
   nativen Listen-Zeilen; opacity statt transform, damit der Maus-Hover
   (translateX) nicht mit dem Druck kollidiert. Doppelt formuliert wegen der
   spezifischeren Hover-Regel (BRIEF: :active-Falle). */
.section-toggle:active,
html:not(.using-touch) .section-toggle:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  opacity: 0.6;
  transition-duration: var(--motion-xs);
}

.section-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Touch: die klickbare Ueberschrift ("Aktive Leihen ⌄") ist nur 21px hoch.
   Trefferflaeche asymmetrisch auf 44px: nach unten nur 8px, genau bis zur
   ersten Karte (margin-bottom .5rem), damit kein Tipp auf die Karte die
   Sektion zuklappt; nach oben ist Luft. */
/* Maus: 21px lagen unter der WCAG-2.5.8-Grenze von 24px -> +3px je Seite. */
.section-toggle { position: relative; }
.section-toggle::after { content: ""; position: absolute; inset: -3px 0; }
@media (pointer: coarse) {
  .section-toggle::after { inset: -15px 0 -8px; }
}

.subsection-toggle {
  cursor: pointer;
  user-select: none;
  touch-action: manipulation;
  transition: color var(--motion-base) var(--ease-standard), transform var(--motion-base) var(--ease-standard), opacity var(--motion-base) var(--ease-standard);
  transform-origin: left center;
}

html:not(.using-touch) .subsection-toggle:hover {
  color: var(--text);
  transform: translateX(2px);
}
/* Touch-Audit R4: Verlauf-Gruppenkoepfe — gleicher Druck wie .section-toggle. */
.subsection-toggle:active,
html:not(.using-touch) .subsection-toggle:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  opacity: 0.6;
  transition-duration: var(--motion-xs);
}

.subsection-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* A11y R5 (WCAG 1.3.1): <h2 role="button"> war für Screenreader keine
   Überschrift mehr (die Rolle ersetzt sie). Jetzt bleibt der Titel <h2>/<h3>
   und trägt einen echten Knopf mit aria-expanded. Der Knopf erbt die Schrift
   des Titels; Trefferfläche und Druck-Feedback bleiben am Titel (Klick darauf
   klappt weiterhin), der Fokusring umrahmt wie bisher den ganzen Titel. */
.section-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: inherit;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: inherit;
  /* Über der unsichtbaren Trefferflaeche des Titels (::after), damit ein
     Tipp auf den Text den Knopf selbst trifft. */
  position: relative;
  z-index: 1;
  /* WCAG 2.5.8: der Verlaufs-Gruppenkopf (h3) war nur 21 px hoch. */
  min-height: 24px;
}
/* Touch: wie .section-toggle::after eine größere, unsichtbare Trefferfläche
   für den Gruppenkopf (Abstände 12 px oben / 8 px unten bleiben frei). */
@media (pointer: coarse) {
  .subsection-toggle { position: relative; }
  .subsection-toggle::after { content: ""; position: absolute; inset: -10px 0 -7px; }
}
.section-toggle-btn:active { --press-opacity: 1; }
.section-toggle-btn:focus-visible { outline: none; }
.section-toggle:has(> .section-toggle-btn:focus-visible),
.subsection-toggle:has(> .section-toggle-btn:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.chevron {
  font-style: normal;
  font-size: 1rem;
  transition: transform var(--motion-base) var(--ease-standard);
  display: inline-block;
}

.count-badge {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
}

.count-badge-warn {
  background: color-mix(in srgb, var(--warning) 20%, transparent);
  border-color: var(--warning);
  color: var(--warning);
}

.borrower-pending-section {
  border-left: 3px solid var(--warning);
  padding-left: 0.75rem;
}

/* === Loan Cards === */
.loan-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 1.25rem;
  margin-bottom: 1rem;
  animation: fadeInUp var(--motion-slow) var(--ease-standard) both;
  transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition);
  /* Verhindert dass innere Grids (z.B. .loan-snapshot mit langem Purpose,
     IBAN oder Adresse) die Card-Breite sprengen — Grid-Items mit
     `minmax(0, 1fr)` schrumpfen nur, wenn der Container selbst auch
     `min-width: 0` hat. Sonst läuft das Inhalt-Grid auf Mobile manchmal
     rechts raus (nur 2 von 4 Snapshot-Boxen sichtbar). */
  min-width: 0;
}

/* Swipe-Actions on lender loan-cards (Mobile / touch only).
   - While actively swiping, suppress the standard transform-transition so
     the card follows the finger 1:1.
   - At "armed" threshold (>= 80 px), inset color hint signals which action
     will fire on release: green (payment) on right, neutral on left. */
@media (max-width: 820px) and (hover: none) {
  .loan-card-lender.swipe-acting {
    will-change: transform;
    z-index: var(--z-swipe-acting);
  }
  .loan-card-lender.swipe-right-armed {
    box-shadow:
      inset 5px 0 0 0 var(--accent),
      var(--shadow-hover);
  }
  .loan-card-lender.swipe-left-armed {
    box-shadow:
      inset -5px 0 0 0 var(--text-muted),
      var(--shadow-hover);
  }
  /* Was beim Loslassen passiert, stand bisher nirgends — die Karte glitt über
     leeren Seitenhintergrund, nur ein 5px-Innenrand verriet "scharf". Jetzt
     taucht in der freigelegten Fläche die Aktion auf: links "Zahlung"
     (Wischen nach rechts), rechts "Notiz" (nach links). Die Pseudo-Elemente
     hängen außen an der Karte und wandern mit ihr; sichtbar nur während des
     Wischens, voll deckend erst ab der Auslöse-Schwelle (armed). Vertikal
     auf Fingerhöhe (--swipe-y, gesetzt beim touchstart in app.js). */
  .loan-card-lender.swipe-acting::before,
  .loan-card-lender.swipe-acting::after {
    position: absolute;
    top: var(--swipe-y, 3rem);
    transform: translateY(-50%);
    width: 104px;
    text-align: center;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--text-muted);
    opacity: 0.55;
    pointer-events: none;
    transition: opacity var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);
  }
  .loan-card-lender.swipe-acting::before { content: 'Zahlung →'; right: calc(100% + 6px); }
  .loan-card-lender.swipe-acting::after  { content: '← Notiz';   left:  calc(100% + 6px); }
  .loan-card-lender.swipe-right-armed::before { opacity: 1; color: var(--accent); }
  .loan-card-lender.swipe-left-armed::after   { opacity: 1; color: var(--text); }
}

html:not(.using-touch) .loan-card:hover {
  border-color: #3e3e3e;
  box-shadow: var(--shadow-hover);
  transform: translateY(-3px);
}

/* Open counter-offer → same "this card wants something from you" treatment
   .loan-card-overdue gets, in the counter-offer orange (matching
   .counter-offer-badge / .btn-counter-offer). Every render path already set
   this class conditionally, but there was no rule for it, so a pending
   counter-offer was invisible in a dense list until you read the card. */
.loan-card-counter-offer {
  border-color: color-mix(in srgb, var(--orange) 45%, transparent);
  background: color-mix(in srgb, var(--orange) 4%, transparent);
  position: relative;
  box-shadow: inset 4px 0 0 0 var(--orange);
}
/* An overdue loan outranks a pending offer: money owed beats a negotiation. */
.loan-card-counter-offer.loan-card-overdue {
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
  background: color-mix(in srgb, var(--danger) 4%, transparent);
  box-shadow: inset 4px 0 0 0 var(--danger);
}

.loan-card-dim {
  /* 0.65 drückte Datum, Kicker und die Abgeschlossen-/Bezahlt-Marken auf 3,2–4,1:1 (WCAG AA verlangt 4,5:1). */
  opacity: 0.9;
}

.loan-card.history-focus {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 24%, transparent), 0 14px 36px color-mix(in srgb, var(--accent) 12%, transparent);
  animation: historyFocusPulse var(--motion-emphasis) var(--ease-standard);
}

@keyframes historyFocusPulse {
  0% { transform: translateY(0) scale(0.985); }
  35% { transform: translateY(-2px) scale(1.01); }
  100% { transform: scale(1); }
}

.loan-card.overdue-focus {
  border-color: color-mix(in srgb, var(--warning) 70%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--warning) 28%, transparent), 0 14px 36px color-mix(in srgb, var(--warning) 14%, transparent);
  animation: overdueFocusPulse var(--motion-emphasis) var(--ease-standard);
}

@keyframes overdueFocusPulse {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--warning) 0%, transparent),  0 2px 8px rgba(0,0,0,0.35); }
  35%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--warning) 32%, transparent), 0 14px 36px color-mix(in srgb, var(--warning) 16%, transparent); }
  100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--warning) 18%, transparent), 0 8px 24px color-mix(in srgb, var(--warning) 8%, transparent); }
}

.loan-card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
  flex-wrap: wrap;
}

.loan-card-heading {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  min-width: 0;
}

.loan-kicker {
  font-size: 0.74rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 700;
  /* Name + Vertrauens-/Limit-Badge als umbrechende Reihe: vorher Inline-Fluss
     mit margin-left am Badge — brach das Badge auf schmalen Karten (320-375px)
     in die zweite Zeile, stand es um genau diesen Rand eingerückt statt
     bündig unter dem Namen. gap ersetzt den Rand (siehe unten). Lange
     Nutzernamen (bis 32 Zeichen, keine Leerzeichen) brechen statt die
     Platzreserve rechts für Datum/Verlauf-Knopf zu überlaufen. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.45rem;
  overflow-wrap: anywhere;
}
.loan-kicker > .trust-badge,
.loan-kicker > .lend-limit-badge { margin-left: 0; }

/* Der Status ("Aktiv", "Ausstehend", …) ist ein Badge wie die Signal-Pillen
   darunter, wurde aber als direktes Kind der Flex-Spalte .loan-card-heading
   auf deren Breite gestreckt: ein halbkartenbreiter grüner Balken ohne Bezug
   zu irgendeiner Kante. Jetzt inhaltsbreit, bündig links wie alle Pillen. */
.loan-card-heading > .badge { align-self: flex-start; }

.loan-headline-row {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  flex-wrap: wrap;
}

.loan-amount {
  font-size: 1.55rem;
  font-weight: 800;
  color: var(--accent);
}
/* Stornierte/abgelehnte Karten: der Betrag steht durchgestrichen und darf
   nicht mehr in Accent-Gruen schreien (report_10 T5). --text-muted statt
   opacity, damit der Kontrast berechenbar bleibt (7.2:1). */
.loan-amount-void { color: var(--text-muted); }
.loan-amount-void s { text-decoration-thickness: 2px; }

.loan-borrower {
  font-size: 0.92rem;
  color: var(--text-muted);
}

.loan-date {
  font-size: 0.8rem;
  color: var(--text-muted);
  white-space: nowrap;
}

/* === Status Badges === */
.badge {
  display: inline-block;
  border-radius: var(--radius-sm);
  padding: 0.15rem 0.6rem;
  font-size: 0.75rem;
  font-weight: 700;
  /* No-op where the badge sizes to its own text (the common case). Only
     visible where a flex-column ancestor stretches it to the full row width
     (e.g. .loan-card-heading's status badge) — there it would otherwise sit
     flush left in a wide bar instead of reading as a centered status pill. */
  text-align: center;
}

.badge-compact {
  font-size: 0.7rem;
  padding: 0.12rem 0.48rem;
}

.badge-inline-middle {
  vertical-align: middle;
}

/* Status badges – higher contrast, dark-theme tuned (~22% opacity fills) */
.badge-pending,
.badge-active,
.badge-rejected,
.badge-closed,
.badge-withdrawn {
  font-weight: 700;
  letter-spacing: 0.02em;
}

.badge-pending   { background: color-mix(in srgb, var(--warning) 22%, transparent); color: var(--badge-pending-text); }
.badge-active    { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent-text); }
.badge-rejected  { background: color-mix(in srgb, var(--danger) 22%, transparent);  color: var(--badge-rejected-text); }
.badge-closed    { background: rgba(136,136,136,0.18); color: var(--text-muted); }
.badge-withdrawn { background: rgba(136,136,136,0.14); color: var(--text-muted); font-style: italic; }

/* Tabular numerals on all amount-bearing classes */
.amount,
.loan-amount,
.snapshot-value,
.snapshot-value-stacked,
.dashboard-value,
.activity-amount-main,
.activity-amount-extra {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* Color-coded direction for signed amounts (additive, optional classes) */
.amount-in {
  color: var(--accent);
}
.amount-out {
  color: var(--danger-text); /* wie „du schuldest“ im Saldo; rohes --danger misst auf --surface2 nur 4.06:1 */
}
.amount-warm {
  color: var(--warning);
}

/* Empty states with icon + optional CTA */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.5rem;
  max-width: 320px;
  margin: 1rem auto;
  padding: 2rem 1rem;
  color: var(--text-muted);
}
/* Leerzustände in Modals mittig-mittig: Karten mit Höhen-Boden (Benachrich-
   tigungen, Logs-Suche ohne Treffer, Benutzer-Filter, Rückmeldungen …) hatten
   den Leerzustand oben in einer sonst leeren Fläche (Handy/Logs: unten). Jeder
   Container zwischen Karte und Leerzustand wird, solange ein Leerzustand da
   ist, zur wachsenden Flex-Spalte; der Leerzustand zentriert sich per
   margin:auto in der freien Fläche unter Suche/Filtern. */
.modal-card :is(div, section):has(.empty-state):not([hidden]) {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
}
.modal-card .empty-state { margin: auto; }
/* R9-Mobile: the hint quotes what was typed ("Kein Nutzer passt zu „…“") —
   one long word pushed the box past the dialog edge (users filter: 180 px
   sideways at 320 px). */
.empty-state { max-width: min(320px, 100%); min-width: 0; }
.empty-state > :is(.empty-state-title, .empty-state-hint) { max-width: 100%; overflow-wrap: anywhere; }
.empty-state-icon {
  color: var(--text-muted);
  opacity: 0.55;
  width: 84px;
  height: 84px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 0.4rem;
  /* Soft radial halo behind the illustration — gives it presence without
     adding a hard background shape. */
  background: radial-gradient(
    circle at center,
    color-mix(in srgb, var(--accent) 12%, transparent),
    transparent 65%
  );
  border-radius: 50%;
  /* Befund 28: war infinite — eine Dauerschleife im Leerzustand. */
  animation: emptyStateFloat 6s ease-in-out 3;
}
/* Motion R2: Leerzustand kommt gestaffelt herein — Illustration federt auf,
   Titel/Hinweis/CTA folgen (einmalig; die Float-Schleife startet danach).
   app.js markiert Leerzustaende, die einen Live-Refresh unveraendert
   ueberleben, mit .is-mounted — sonst spielte jeder Hintergrund-Render die
   Einblendung erneut ab. */
.empty-state > * {
  animation: emptyStateIn var(--motion-slow) var(--ease-standard) backwards;
}
.empty-state > :nth-child(2) { animation-delay: 60ms; }
.empty-state > :nth-child(3) { animation-delay: 120ms; }
.empty-state > :nth-child(n+4) { animation-delay: 180ms; }
.empty-state > .empty-state-icon {
  animation:
    emptyStateIconIn var(--motion-slow) var(--ease-spring) backwards,
    emptyStateFloat 6s ease-in-out var(--motion-slow) 3;
}
.empty-state.is-mounted > *,
.empty-state.is-mounted > .empty-state-icon { animation: none; }
@keyframes emptyStateIn {
  from { opacity: 0; transform: translateY(6px); }
}
@keyframes emptyStateIconIn {
  from { opacity: 0; transform: scale(0.9); }
}
@media (prefers-reduced-motion: reduce) {
  .empty-state-icon,
  .empty-state > *,
  .empty-state > .empty-state-icon { animation: none; }
}
@keyframes emptyStateFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}
.empty-state-icon svg {
  width: 100%;
  height: 100%;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Secondary stroke inside the illustration — colored with the accent at
   reduced alpha so it doesn't compete with the muted primary stroke. */
.empty-state-icon svg .ill-accent {
  color: var(--accent);
  opacity: 0.85;
}
.empty-state-icon svg .ill-accent[fill="currentColor"] {
  fill: var(--accent);
  stroke: none;
}
.empty-state-title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
  margin: 0;
}
.empty-state-hint {
  color: var(--text-muted);
  line-height: 1.5;
  margin: 0;
  font-size: 0.9rem;
  /* Zentrierter Zweizeiler ließ ein einzelnes Wort allein in Zeile 2 stehen
     ("… erscheint sie / hier."). balance verteilt die Zeilen gleichmäßig. */
  text-wrap: balance;
}
.empty-state-cta {
  margin-top: 1rem;
}

/* Tight screens: shrink the floating illustration so the title/hint keep the
   visual lead instead of being pushed off-screen below an oversized halo. */
@media (max-width: 360px) {
  .empty-state-icon { width: 64px; height: 64px; }
  .empty-state { padding: 1.5rem 0.5rem; }
}

/* === Loan-card repayment bar ===
   The progress ring in the card head shows percentage already, but a wide bar
   below the title gives a second, scannable read for both lender and borrower
   without forcing them to parse the ring. */
.loan-progress-bar {
  position: relative;
  height: 6px;
  border-radius: var(--radius-pill);
  background: rgba(255,255,255,0.06);
  overflow: hidden;
  margin: 0.55rem 0 0.4rem;
}
.loan-progress-bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--repay-pct, 0%);
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #eedba6));
  transition: width var(--motion-slow) var(--ease-standard);
}
.loan-progress-bar-fill[data-state="full"] {
  background: linear-gradient(90deg, var(--accent), var(--accent));
}
.loan-progress-bar-fill[data-state="overdue"] {
  background: linear-gradient(90deg, var(--danger), color-mix(in srgb, var(--danger) 50%, var(--warning)));
}
@media (min-width: 540px) {
  .loan-progress-bar { height: 8px; }
}
@media (prefers-reduced-motion: reduce) {
  .loan-progress-bar-fill,
  .borrower-progress-fill { transition: none; }
}

/* Inline checkmark feedback for submit buttons */
.btn-success-flash {
  background: var(--accent) !important;
  color: var(--bg) !important;
  transform: scale(1.02);
  transition: transform var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
}
/* Der Erfolgs-Haken laeuft, waehrend der Button noch gesperrt ist (die
   Handler geben ihn erst im finally frei). Ohne diese Regel dimmte
   button:disabled (opacity .5 + saturate .6) ausgerechnet die Bestaetigung:
   gemessen opacity 0.5 auf #pay-save waehrend "Gespeichert". (0,2,1) schlaegt
   button:disabled (0,1,1). */
button.btn-success-flash:disabled {
  opacity: 1;
  filter: none;
  cursor: default;
}
.btn-success-flash svg { vertical-align: -0.15em; }

.loan-card-overdue {
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
  background: color-mix(in srgb, var(--danger) 4%, transparent);
  /* Thick left rail so an overdue card reads as overdue even when only
     partially visible (top-of-list scroll position, narrow viewport). The
     existing border-color tinting alone was too subtle on dense lists. */
  position: relative;
  box-shadow: inset 4px 0 0 0 var(--danger);
}

html:not(.using-touch) .loan-card-overdue:hover {
  border-color: color-mix(in srgb, var(--danger) 70%, transparent);
  box-shadow: inset 4px 0 0 0 var(--danger), var(--shadow-hover);
}

/* === Offline-pending card marker === */
/* A card whose action is queued (offline) and not yet sent. Warning-tinted
   left rail + slight dim, mirroring the .loan-card-overdue pattern. Uses the
   theme-aware --warning variable so it works in light and dark. */
.entry-pending {
  position: relative;
  box-shadow: inset 4px 0 0 0 var(--warning);
  opacity: 0.92;
}
.entry-pending-badge {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.6rem;
  padding: 0.3rem 0.55rem;
  border-radius: var(--radius-xs);
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--warning);
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
}
.entry-pending-spin {
  width: 0.72rem;
  height: 0.72rem;
  border: 2px solid color-mix(in srgb, var(--warning) 40%, transparent);
  border-top-color: var(--warning);
  border-radius: 50%;
  animation: entryPendingSpin 0.8s linear infinite;
  flex: 0 0 auto;
}
@keyframes entryPendingSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .entry-pending-spin { animation: none; }
}

/* === Ladezustand in Buttons — Befund 31 ===================================
   app.js:1051 setBtnBusy(btn, label) friert die Breite ein (min-width aus
   offsetWidth), setzt disabled + aria-busy="true" und ersetzt den Inhalt
   durch
     <span class="entry-pending-spin" aria-hidden="true"></span><span>Label</span>
   .entry-pending-spin war bis dahin reine Kartendekoration: --warning-Ring
   auf dunkler Kartenflaeche. In einem Button stimmt davon nichts — auf
   --accent-btn-Gruen sitzt ein gelber Ring, und die globale
   button:disabled-Regel (opacity .5 + saturate .6) dimmt ausgerechnet die
   beiden Elemente weg, die den Zustand tragen.

   Gleiche Spezifitaet wie button:disabled (0,1,1), deshalb steht dieser
   Block bewusst spaeter in der Datei. Die zweite Selektorvariante
   :disabled (0,2,1) ist fuer .modal-actions button:disabled (Zeile 299)
   noetig, sonst gewinnt dort der cursor: not-allowed. */
button[aria-busy="true"],
button[aria-busy="true"]:disabled {
  /* "arbeitet gerade" ist nicht dasselbe wie "gesperrt". */
  opacity: 1;
  filter: none;
  cursor: progress;
}
button[aria-busy="true"] .entry-pending-spin {
  /* inline-block statt display:flex am Button: setBtnBusy trifft Buttons
     beider Sorten — flex-Container (dort wird inline-block ohnehin zu einem
     Flex-Item blockifiziert) und normale inline-block-Buttons, bei denen ein
     nacktes <span> weder width noch height annimmt und der Ring auf 4px
     zusammenfaellt. So sieht es in beiden Faellen gleich aus, ohne die
     Box-Art des Buttons zu aendern (ein display:block-Button mit
     margin:0 auto wuerde sonst seine Zentrierung verlieren). */
  display: inline-block;
  align-self: center;          /* sonst streckt ein flex-Button den Kreis */
  vertical-align: -0.1em;      /* Mittellinie des Labels statt Grundlinie */
  width: 0.85em;
  height: 0.85em;
  margin-inline-end: 0.45rem;
  /* currentColor statt --warning: passt damit automatisch zu jeder
     Button-Variante — weiss auf .btn-primary/--accent-btn, --text-muted auf
     .btn-secondary, --danger auf .btn-danger. */
  border-color: color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
}
/* Unter Reduce Motion steht der Ring still (siehe Guard oben). Den Zustand
   traegt dann allein das Label ("Wird gesendet…"), das setBtnBusy immer
   mitliefert — der stehende Ring bleibt als stille Marke daneben stehen und
   behauptet nichts Falsches (anders als der Undo-Balken in Befund 29, der
   bei 50 % einfror und damit eine Restzeit vortaeuschte). */

/* Waehrend eine Fenster-Aktion laeuft (setModalBusy -> data-modal-busy), sind
   die Nachbar-Buttons der Fusszeile gesperrt: vorher blieb z. B. "Ablehnen"
   neben einem laufenden "Annehmen" voll klickbar und konnte eine
   widersprechende Entscheidung hinterherschicken. pointer-events:none macht
   sie inert, die Dimmung zeigt es; der arbeitende Button (aria-busy) und ein
   laufender Erfolgs-Haken bleiben unberuehrt. */
.modal-card[data-modal-busy="true"] .modal-actions button:not([aria-busy="true"]):not(.btn-success-flash) {
  opacity: var(--dim-disabled);
  filter: saturate(0.6);
  pointer-events: none;
}

.loan-snapshot {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.75rem;
  margin-bottom: 0.9rem;
}

.snapshot-item {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-tile);
  padding: 0.75rem 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  /* Consistent with .dashboard-card — centered content for stat boxes.
     Vertikal aber oben bündig (war center): das Raster streckt alle Kacheln
     einer Reihe auf die höchste (meist "Auszahlung / Rückzahlung" mit drei
     Zeilen) — zentriert schwammen Titel und Wert der kurzen Nachbarn auf
     halber Höhe, die Titel einer Reihe standen nie auf einer Linie. */
  text-align: center;
  align-items: center;
  justify-content: flex-start;
  /* Defensive: `minmax(0, 1fr)` schrumpft das Grid-Item auf 0 ab, aber wenn
     `.snapshot-value` einen unbreakable Token enthält (lange IBAN, langes
     "Ursprungszweck"-Wort ohne Leerzeichen, Adresse), zieht `min-content`
     des Children das Item wieder auf — und das Grid sprengt rechts aus dem
     Viewport. min-width:0 + word-break sorgen dafür, dass solche Strings
     umbrechen statt das Layout zu zerreißen. Tritt nur "manchmal" auf,
     weil es vom konkreten Loan-Content abhängt. */
  min-width: 0;
  overflow: hidden;
  /* Container-Query-Anker: `.snapshot-value` und `.snapshot-value-stacked`
     skalieren ihre font-size mit `cqi` relativ zu DIESEM Container. Dadurch
     wirkt der Auto-Resize box-lokal — schmale Tile = kleine Schrift, breite
     Tile = volle 1rem. Container-Queries sind cross-browser supported
     seit 2023 (Chrome 105, Firefox 110, Safari 16). */
  container-type: inline-size;
}
.snapshot-item .snapshot-label,
.snapshot-item .snapshot-value {
  min-width: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

.snapshot-item-primary {
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--warning) 12%, transparent), color-mix(in srgb, var(--warning) 4%, transparent)), var(--surface2);
}

.snapshot-item-action {
  background: linear-gradient(180deg, rgba(255,255,255,0.035), rgba(255,255,255,0.012)), var(--surface2);
}

.snapshot-label {
  color: var(--text-muted);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.snapshot-value {
  /* Auto-Resize: clamp() + cqi (container-query inline-size) skaliert die
     Schriftgröße proportional zur tatsächlichen Box-Breite, statt bei
     fixen 1rem zu kleben und dann zu clippen. cqi = 1 % der Inline-Größe
     des `.snapshot-item`-Containers; bei 160 px Box = 1cqi = 1.6 px.
     Formel: nie unter 0.78rem (Lesbarkeit), nie über 1rem (Hierarchie). */
  font-size: clamp(0.78rem, 5.5cqi, 1rem);
  line-height: 1.2;
  font-weight: 800;
  color: var(--text);
  /* white-space:nowrap + ellipsis raus — bei extremen Tokens (lange IBAN,
     lange Adresse) lieber umbrechen statt clippen. v2.4.8 hatte schon
     overflow-wrap:anywhere auf Children gesetzt; jetzt bricht es auch
     direkt im .snapshot-value-Element. */
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

.snapshot-value-stacked {
  display: flex;
  flex-direction: column;
  gap: 0.22rem;
  /* Auto-Resize: zwei gestapelte Zeilen ("Auszahlung Bar" / "Rückgabe Nicht
     angegeben") brauchen kleinere Mindest-Schrift als .snapshot-value, weil
     jeweils zwei Zeilen plus Gap reinpassen müssen. clamp(0.7rem, 5cqi,
     0.92rem) gibt dem Browser Raum, bei langen Methoden-Strings
     ("Auszahlungskonto: DE…") auf ~11px runterzugehen. */
  font-size: clamp(0.7rem, 5cqi, 0.92rem);
  line-height: 1.3;
  min-width: 0;
  max-width: 100%;
}
.snapshot-value-stacked > span {
  min-width: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

.loan-signals {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  min-width: 0;
  align-items: center;
}
/* Status-Badge ("Aktiv", "Abgelehnt", "2 neu", "Ergänzung ausstehend") steht
   jetzt vorn in dieser Reihe (renderLoanSignals lead) — gleiche Höhe und
   Schriftgröße wie die Pillen daneben, sonst tanzt die Grundlinie. */
.loan-signals > .badge {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.72rem;
  padding: 0.2rem 0.55rem;
  border: 1px solid transparent;
  white-space: nowrap;
}

.signal-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface2);
  font-size: 0.72rem;
  font-weight: 700;
  white-space: nowrap;
}

.signal-danger {
  color: var(--danger-text); /* 4.0:1 -> 5.2:1 auf der Rot-Toenung */
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}

.signal-warning {
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
  background: color-mix(in srgb, var(--warning) 8%, transparent);
}

.signal-date {
  color: var(--signal-date-color);
  border-color: rgba(154,180,255,0.28);
  background: rgba(154,180,255,0.08);
}

.signal-progress {
  color: var(--signal-progress-color);
  border-color: color-mix(in srgb, var(--accent) 28%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.signal-good {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.signal-muted { color: var(--text-muted); }

/* Bonus field driven by a percent chip: readOnly rather than disabled, so a
   required field stays part of the form validation. The visual cue has to be
   supplied here, since readOnly has none of its own. */
.is-pct-locked {
  opacity: .7;
  cursor: not-allowed;
}

/* Negotiation rounds inside the counter-offer timeline entry. */
.counter-rounds { margin-top: .75rem; }
.counter-rounds-title {
  color: var(--text-muted);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  margin-bottom: .4rem;
}
.counter-rounds-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.counter-round {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: .15rem .6rem;
  padding: .45rem .6rem;
  border-radius: var(--radius-xs);
  background: rgba(255, 255, 255, .04);
  font-size: .82rem;
}
.counter-round-label { color: var(--text-muted); font-size: .72rem; font-weight: 700; }
.counter-round-value { font-variant-numeric: tabular-nums; }
.counter-round-msg { grid-column: 1 / -1; color: var(--text-muted); font-size: .78rem; }
.counter-round-accepted .counter-round-label { color: var(--accent); }
.counter-round-declined .counter-round-label { color: var(--danger); }

/* Qualifier next to the card's headline figure, which now shows everything
   still open including bonus rather than the bare principal. */
.loan-amount-sub {
  color: var(--text-muted);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .02em;
  align-self: center;
}

/* Agreed return date inside the Auszahlung/Rückgabe snapshot tile — one step
   quieter than the methods above it so it reads as a qualifier, not a third
   equal-weight fact. */
.snapshot-value-due {
  color: var(--text-muted);
  font-weight: 600;
}
.snapshot-value-due .text-placeholder { font-weight: 400; }
/* "Auszahlung Nicht angegeben": Beschriftung und Wert standen beide in 800er
   Fettschrift, der fehlende Wert zusätzlich kursiv — ein unruhiger
   Fett-Kursiv-Mix, in dem man Beschriftung und Wert nicht auseinanderhielt.
   Beschriftung jetzt leise (muted, 600), echter Wert fett, fehlender Wert
   kursiv in normaler Stärke. */
.snapshot-inline-label {
  color: var(--text-muted);
  font-weight: 600;
}
.snapshot-value .text-placeholder { font-weight: 400; }

/* Inline hint under a form field (bonus preview, counter-proposal preview).
   Sized like .detail-label so it reads as secondary to the input above it. */
.form-hint {
  display: block;
  margin-top: .35rem;
  color: var(--text-muted);
  font-size: .78rem;
  line-height: 1.35;
  min-height: 1.05em;
}
.form-hint .text-placeholder { font-size: inherit; }

/* Placeholders for fields that exist but have no value. #555 on --bg #0f0f0f
   was ~2.6:1 and below WCAG AA; --text-muted keeps them readable while still
   reading as "no value" next to real content. */
.text-placeholder {
  color: var(--text-muted);
  font-style: italic;
  opacity: .95;
}

/* === Loan Actions === */
.loan-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}
/* Equal-width AND identical geometry for every button in a .loan-actions
   row. flex: 1 1 0 makes the columns equal; the explicit padding /
   font-size / line-height / min-height block ensures the visible button
   bodies are also identical regardless of which underlying button class
   set its own values (.btn-approve vs .btn-counter-offer vs .btn-reject
   etc all live in this row and originally had subtly different padding). */
.loan-actions > button {
  flex: 1 1 0;
  min-width: 100px;
  padding: 0.6rem 0.9rem;
  min-height: var(--touch-target);
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.2;
}

/* A status badge in this row is text, not a button. With the equal-width rule
   above, the buttons split whatever the badge leaves over: a lone "Fahrt
   absagen" next to "⏳ Wartet auf Bestätigung des Leihnehmers" grew to 818px on
   a 1280px viewport. Size buttons to their label when a badge shares the row.
   Below 541px the two-column wrap block further down owns this row instead. */
@media (min-width: 541px) {
  .loan-actions:has(> .counter-offer-badge) > button,
  .loan-actions:has(> .badge) > button {
    flex: 0 1 auto;
  }
}

.btn-approve, .btn-reject, .btn-add-payment, .btn-close-loan, .btn-manual-entry,
.btn-borrower-action, .btn-borrower-req-active {
  border: none;
  border-radius: var(--radius-btn);
  padding: 0.6rem 0.9rem;
  min-height: var(--touch-target);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  /* P2-17: unified token; explicit property list instead of `all` so only
     paint/composite work animates (no accidental layout transitions). */
  transition: background var(--transition-fast), border-color var(--transition-fast),
              color var(--transition-fast), opacity var(--transition-fast),
              transform var(--transition-fast), filter var(--transition-fast);
}

.btn-approve     { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
/* .btn-reject: Farbe kommt von .btn-decline (Ablehnen: eine Regel, siehe dort); hier nur die Größe oben. */
.btn-reject      { border: 1px solid transparent; }
.btn-add-payment { background: var(--surface2); border: 1px solid var(--border); color: var(--text); }
.btn-close-loan  { background: transparent; border: 1px solid var(--border); color: var(--text-muted); }
.btn-manual-entry { background: color-mix(in srgb, var(--warning) 14%, transparent); border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent); color: var(--warning); }

html:not(.using-touch) .btn-approve:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); border-color: var(--accent); }
html:not(.using-touch) .btn-add-payment:hover { border-color: var(--text-muted); color: var(--text); }
html:not(.using-touch) .btn-close-loan:hover { border-color: var(--text-muted); color: var(--text); }
html:not(.using-touch) .btn-manual-entry:hover { background: color-mix(in srgb, var(--warning) 24%, transparent); border-color: var(--warning); }

/* Tactile feedback for the whole action row: subtle lift on hover (pointer
   devices only) + quick press-down on tap, matching .btn-primary/:active. */
html:not(.using-touch) .loan-actions > button:hover { transform: translateY(-1px); }
.loan-actions > button:active {
  transform: scale(0.985);
  /* Press-Feedback: war 60ms bzw. 70ms in zwei Notationen. Jetzt --motion-xs. */
  transition-duration: var(--motion-xs);
}
/* Touch-Audit R4: Ein gesperrter Knopf ("Abschließen" bei offener
   Ergänzung) sank auf Touch trotzdem ein — :active greift per Tipp-Geste
   auch auf :disabled, obwohl kein click folgt. Die Maus-Stufe oben hatte
   schon :not(:disabled); hier dieselbe Semantik: gesperrt gibt nicht nach. */
.loan-actions > button:disabled:active { transform: none; }

/* Borrower action buttons in loan-actions bar */
.btn-borrower-action {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.btn-borrower-req-active {
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}
/* Badge + "Zurückziehen" braucht ~260px. Mit der gleichverteilten Reihe
   (flex: 1 1 0) bekam der Knopf in der Zwei-Spalten-Ansicht (Karte ~480px)
   nur 216px — "Zurückziehen" lief über den Rand in "Datum verlängern".
   Inhaltsbreite als Basis; reicht die Reihe nicht, bricht sie um (≤540px
   übernimmt ohnehin die Vollbreiten-Regel weiter unten). */
.loan-actions > .btn-borrower-req-active { flex: 1 1 max-content; }
/* Mit min-width: 100px (Reihenregel oben) ragte "Widersprechen" (~145px) neben dem
   breiten "Verlängerung angefragt · Zurückziehen" über den Knopfrand hinaus, statt
   in die nächste Zeile umzubrechen. Das längste Wort bestimmt die Mindestbreite. */
.loan-actions > .btn-borrower-action { min-width: min-content; }
html:not(.using-touch) .btn-borrower-action:hover { border-color: var(--text-muted); color: var(--text); }
html:not(.using-touch) .btn-borrower-req-active:hover { background: color-mix(in srgb, var(--warning) 18%, transparent); }

/* Borrower inline request forms */

/* Lender: extend-request notice */
.extend-request-notice {
  background: color-mix(in srgb, var(--info) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.75rem;
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.82rem;
}
.extend-request-notice-top {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.extend-request-newdate {
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.extend-request-decide {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.1rem;
}
.extend-request-decide > button { flex: 1 1 0; width: auto; min-width: 0; }
.btn-extend-approve {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: var(--radius-btn);
  /* P1-14: padding scaled to match the 44px touch-target. */
  padding: 0.55rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  min-height: var(--touch-target);
}
.btn-extend-reject {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  /* P1-14: padding scaled to match the 44px touch-target. */
  padding: 0.55rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  min-height: var(--touch-target);
}
html:not(.using-touch) .btn-extend-approve:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); border-color: var(--accent); }
html:not(.using-touch) .btn-extend-reject:hover { border-color: var(--text-muted); color: var(--text); }
/* Anfrage "Verlängerung" in der Karte der Ausstehend-Liste: wie alle anderen
   Entscheidungs-Karten dort (Zahlung, Abschluss, Raten, Taxi-Storno) gefüllt
   rot/grün — vorher als einzige grau/getönt. Die kleine Hinweiszeile auf der
   Leihkarte (.extend-request-notice) behält ihre kompakte Form. */
.decision-request-card .loan-actions > .btn-extend-approve {
  border: none; color: #fff;
}
.decision-request-card .loan-actions > .btn-extend-approve { background: linear-gradient(180deg, #3a7f61, #2e6950); }
html:not(.using-touch) .decision-request-card .loan-actions > .btn-extend-approve:hover { background: linear-gradient(180deg, #3a7f61, #2e6950); opacity: 0.97; color: #fff; border: none; }

.btn-delete-history {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 0.8rem;
  /* Same pitfall this codebase already hit and fixed elsewhere (.info-dot,
     .btn-note-edit): a <button> with no explicit color takes the UA system
     color `buttontext` — black — instead of the page's text colour, so the
     currentColor-stroked icon() SVG inside rendered near-invisible on this
     dark theme even at full opacity. */
  color: var(--text-muted);
  opacity: 0.55;
  padding: 0;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
  line-height: 1;
}
html:not(.using-touch) .btn-delete-history:hover { opacity: 1; color: var(--danger); }

.btn-reactivate {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 0.95rem;
  color: var(--text-muted);
  opacity: 0.55;
  padding: 0;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: var(--transition-ui);
  line-height: 1;
}
html:not(.using-touch) .btn-reactivate:hover { opacity: 1; color: var(--accent); }
/* Touch-Audit R4: Symbol-Knoepfe der Verlaufskarten (Reaktivieren /
   Loeschen) hatten auf Touch keinen Druck — wie die Notiz-Knoepfe. */
.btn-reactivate:active,
.btn-delete-history:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  opacity: 1;
  transform: scale(0.88);
  transition-duration: var(--motion-xs);
}

/* Icon-only in both card headers (.loan-card-header-meta on active/pending
   lender cards, .history-card-actions on history cards, next to
   .btn-reactivate/.btn-delete-history), text+icon on .pending-addition-card.
   `justify-self: start` matters on .pending-addition-card: that card is a CSS
   grid, and a grid item's outer display is blockified — without it the
   inline-flex button stretches to the full card width. In grid, justify-self is
   the INLINE (horizontal) axis; align-self only handles the block axis and is
   kept for the flex-column case, where the cross axis is horizontal. */
.btn-view-log-history {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  justify-self: start;
  align-self: start;
  gap: 0.3rem;
  margin-top: 0.6rem;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  color: var(--text-muted);
  font-size: 0.78rem;
  padding: 0.35rem 0.7rem;
  /* Matches the 44 px touch-target convention used by every neighbouring
     icon button (.btn-reactivate/.btn-delete-history/.log-delete-btn). */
  min-height: var(--touch-target);
  cursor: pointer;
  transition: var(--transition-ui);
}
.history-card-actions .btn-view-log-history,
.loan-card-header-meta .btn-view-log-history {
  margin-top: 0;
  min-width: var(--touch-target);
  padding: 0.35rem;
}
/* Reihe statt Spalte (siehe .loan-card-header-meta unten) — die base rule's
   align-self:start wuerde das Datum und den Button unterschiedlich hoch in
   der Zeile ausrichten, center gleicht beide auf die Zeilenmitte an. */
.loan-card-header-meta .btn-view-log-history { align-self: center; }
html:not(.using-touch) .btn-view-log-history:hover { color: var(--text); border-color: rgba(255,255,255,0.2); }
/* Touch-Audit R4: einziger Karten-Knopf ohne Druckzustand. */
.btn-view-log-history:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  transform: scale(0.94);
  background: rgba(255,255,255,0.06);
  transition-duration: var(--motion-xs);
}
/* On a pending addition the text button closes the card on the right, like
   the actions of the other cards (still not stretched: justify-self). */
.pending-addition-card > .btn-view-log-history { justify-self: end; }

/* War eine Spalte im normalen Textfluss (Datum ueber dem Button), rechts
   mit margin-left:auto ausgerichtet — brauchte dadurch 67px Zeilenhoehe
   (Datum 17px + Abstand + 44px Button) UND blieb trotzdem nicht zuverlaessig
   in der Kartenecke: bei einer laengeren Kopfzeile (langer Name, mehrere
   Badges) wich der Block unter die Ueberschrift aus, statt oben rechts zu
   bleiben (.loan-card-header hat flex-wrap:wrap) — dieselbe Ursache wie bei
   .history-card-actions. Jetzt absolut fixiert und als Reihe (Datum links
   neben dem Button statt darueber): 44px hoch statt 67px, und "oben rechts"
   gilt unabhaengig vom Inhalt links. .loan-card-lender braucht dafuer
   position:relative; .loan-card-heading dort eine Platzreserve rechts. */
.loan-card-lender { position: relative; }
.loan-card-lender .loan-card-heading { padding-right: 7.5rem; }
/* Rückmeldung #7: Seit dem Export-Knopf (44px) stehen im Meta-Paket bis zu zwei
   Knöpfe neben dem Datum — die 7.5rem-Reserve galt nur für einen (Verlauf), das
   Vertrauens-Badge lief ins Datum. Reserve = Datum (~75px) + Knöpfe (44px + 8px Lücke
   je Stück) + Luft. */
.loan-card-lender:has(.loan-card-header-meta .export-btn-icon) .loan-card-heading { padding-right: 8.5rem; }
.loan-card-lender:has(.loan-card-header-meta .export-btn-icon):has(.loan-card-header-meta .btn-view-log-history) .loan-card-heading { padding-right: 11.5rem; }
/* Handy: die 7.5rem-Reserve galt für den GANZEN Kopf. Bei 320-412px blieben
   dem Betrag damit 130-220px — "€127.611,11" lief in das Datum, lange Namen
   ebenso, und Signal-Pillen brachen unnötig um. Das Meta-Paket (Datum +
   44px-Knopf) belegt aber nur die erste Zeile. Reserve deshalb nur noch für
   die Namenszeile, die dafür so hoch wird wie das Meta-Paket; Betrag,
   Status und Pillen darunter nutzen die volle Kartenbreite. Nur für Köpfe,
   die tatsächlich mit der Namenszeile beginnen. */
@media (max-width: 600px) {
  .loan-card-lender .loan-card-heading:has(> .loan-kicker:first-child) { padding-right: 0; }
  .loan-card-lender .loan-card-heading > .loan-kicker:first-child {
    padding-right: 5rem;
    min-height: 1.3rem;
    /* Rückmeldung #7 (Folgefehler): mit `anywhere` (Basisregel) ist die Mindestbreite
       des Namens 0 — die Platzreserve rechts fraß dann die ganze Zeile und der Name
       stand Buchstabe für Buchstabe untereinander. `break-word` behält die Wortbreite
       als Minimum; max-width deckelt nur noch einen wirklich zu langen Namen. */
    overflow-wrap: break-word;
    max-width: 100%;
  }
  .loan-card-lender:has(.loan-card-header-meta .btn-view-log-history) .loan-card-heading > .loan-kicker:first-child {
    padding-right: 7.5rem;
    min-height: var(--touch-target);
  }
  /* Rückmeldung #7: Export-Knopf (und ggf. zusätzlich Verlauf-Knopf) neben dem Datum. */
  .loan-card-lender:has(.loan-card-header-meta .export-btn-icon) .loan-card-heading > .loan-kicker:first-child {
    padding-right: 8.5rem;
    min-height: var(--touch-target);
  }
  .loan-card-lender:has(.loan-card-header-meta .export-btn-icon):has(.loan-card-header-meta .btn-view-log-history) .loan-card-heading > .loan-kicker:first-child {
    padding-right: 11.5rem;
  }
}
/* Leihnehmer-Karte: Datum ist ein normales Flex-Geschwister des Kopfes.
   Mit inhaltsbreitem Kopf rutschte es bei vielen Pillen in eine eigene Zeile
   unter die Pillen (einmal oben rechts, einmal verloren darunter — je nach
   Karte). Der Kopf füllt jetzt den Rest der Zeile, das Datum bleibt oben
   rechts. */
.loan-card-header > .loan-card-heading { flex: 1 1 0; }
.loan-card-header > .loan-date { flex-shrink: 0; }
.loan-card-header-meta {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
}
/* B4: Schrift ab 150 % (html.text-big, app.js): die Platzreserven in rem fressen die Kartenbreite, Name und
   Datum liefen ineinander. Datum und Knöpfe stehen dann als eigene erste Zeile (wie bei den Verlaufskarten),
   der Kopf darunter bekommt die volle Breite. !important nur hier: die Reserve-Regeln oben hängen an :has()-Ketten. */
html.text-big .loan-card-header-meta {
  position: static;
  order: -1;
  flex: 1 1 100%;
  justify-content: space-between;
}
html.text-big .loan-card-lender .loan-card-heading,
html.text-big .loan-card-lender .loan-card-heading > .loan-kicker:first-child { padding-right: 0 !important; min-height: 0 !important; }
html.text-big .loan-card-header > .loan-card-heading { flex: 1 1 100%; }
html.text-big .loan-card-header-date-wrap { order: -1; flex: 1 1 100%; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
html.text-big .loan-card-header > .loan-date { order: -1; flex: 1 1 100%; }
/* R12-Raten (offline): Der Streifen „Zahlung … wird gesendet, sobald online“
   (.entry-pending-badge, oben in die Karte gesetzt) lag unter Datum und
   Verlauf-Knopf. Diese rücken um seine Höhe (JS: --pending-badge-h) + Abstand. */
.loan-card.entry-pending .loan-card-header-meta { top: calc(1.25rem + var(--pending-badge-h, 1.75rem) + 0.6rem); }

.btn-new-loan {
  /* --accent als Flaeche hinter Weiss = 2.71:1. --accent-btn = 4.79:1,
     identisch zu .btn-primary (siehe Token-Kommentar). */
  background: var(--accent-btn);
  color: #fff;
  border: none;
  border-radius: var(--radius-btn);
  padding: 0.5rem 1rem;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.btn-new-loan:active { transform: scale(0.97); }
/* "+ Neu" in den Modal-Koepfen (Benutzer, Fahrzeuge) ist 32px hoch — auf Touch
   waechst nur die Trefferflaeche auf 44px, die Kopfzeile bleibt so hoch wie das
   36px-✕ daneben. */
@media (pointer: coarse) {
  .btn-new-loan { position: relative; }
  .btn-new-loan::after { content: ""; position: absolute; inset: -6px 0; }
}

.borrower-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.btn-borrower-cta {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--touch-target);
  padding: 0.65rem 1rem;
  border-radius: var(--radius-btn);
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  transition: var(--transition-ui);
}

.btn-borrower-cta-new {
  /* War 2.71 -> 2.56:1 (schlechtester Kontrast der App). Jetzt 4.79 -> 6.46:1,
     derselbe Verlauf wie .btn-primary. */
  background: linear-gradient(135deg, var(--accent-btn), var(--accent-btn-strong));
  color: #fff;
  /* Kein grüner Glow mehr (Nutzerwunsch) – flach wie die übrigen Knöpfe. */
  box-shadow: none;
}

.btn-borrower-cta-add {
  /* 18 % Tint hob den Grund so weit an, dass --accent-Text nur 4.26:1 erreichte
     (16px/700 braucht 4.5). 10 % Tint -> 4.90:1. */
  background: color-mix(in srgb, var(--accent) 10%, var(--surface2));
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  box-shadow: none;
}

html:not(.using-touch) .btn-borrower-cta:hover { filter: brightness(1.05); transform: translateY(-1px); }
.btn-borrower-cta:active   { transform: translateY(0); filter: brightness(0.95); }
.btn-borrower-cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-borrower-cta:disabled { transform: none; }

html:not(.using-touch) .btn-new-loan:hover { opacity: 0.85; }

.btn-withdraw,
.btn-addition-withdraw {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
  border-radius: var(--radius-btn);
  padding: 0.45rem 0.9rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  transition: var(--transition-ui);
}

html:not(.using-touch) .btn-withdraw:hover,
html:not(.using-touch) .btn-addition-withdraw:hover { border-color: var(--warning); color: var(--warning); }

/* === Payment running balance === */
.payment-open-outstanding { color: var(--warning); font-weight: 700; }
.payment-open-done { color: var(--accent); font-weight: 700; }

/* === Notiz === */
.note-section {
  border-top: 1px solid var(--border);
  padding-top: 0.6rem;
  margin-top: 0.25rem;
  margin-bottom: 0.25rem;
  font-size: 0.82rem;
}

.note-history-wrapper { display: flex; flex-direction: column; gap: 0.3rem; }

.note-history-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
}

.note-history-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
}

.note-history { display: flex; flex-direction: column; gap: 0.35rem; }

.note-history-entry {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  background: var(--card-bg, rgba(255,255,255,0.04));
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.4rem 0.55rem;
}

.note-history-text { color: var(--text); line-height: 1.4; }

.note-history-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
}

.note-history-date { font-size: 0.72rem; color: var(--text-muted); }

.btn-note-delete {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  color: var(--text-muted);
  opacity: 0.5;
  padding: 0;
  min-width: var(--touch-target);
  min-height: var(--touch-target);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: var(--transition-ui);
}

html:not(.using-touch) .btn-note-delete:hover { opacity: 1; color: var(--danger); }
/* Touch-Audit R4: Notiz-Knoepfe (✎ / ✕) hatten auf Touch keinen Druck. */
.btn-note-delete:active,
.btn-note-edit:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  opacity: 1;
  transform: scale(0.88);
  transition-duration: var(--motion-xs);
}

.btn-note-edit {
  background: transparent;
  border: none;
  /* Gleiche Falle wie bei den Chips weiter unten: ohne explizite Farbe nimmt der
     Button `buttontext` (schwarz) statt der Textfarbe der Seite. */
  color: var(--text);
  cursor: pointer;
  font-size: 0.8rem;
  opacity: 0.6;
  padding: 0;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}

html:not(.using-touch) .btn-note-edit:hover { opacity: 1; }

.note-edit-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.4rem;
}

.btn-note-cancel, .btn-note-save {
  flex: 1;
  border-radius: var(--radius-btn);
  /* P1-14: padding scaled to match the 44px touch-target. */
  padding: 0.55rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid var(--border);
  min-height: var(--touch-target);
  /* P2-17: unified token; explicit property list instead of `all` so only
     paint/composite work animates (no accidental layout transitions). */
  transition: background var(--transition-fast), border-color var(--transition-fast),
              color var(--transition-fast), opacity var(--transition-fast),
              transform var(--transition-fast), filter var(--transition-fast);
}

.btn-note-cancel { background: transparent; color: var(--text-muted); }
.btn-note-save   { background: var(--accent-btn); color: #fff; border-color: var(--accent-btn); }

.close-request-msg { color: var(--text-muted); font-style: italic; flex: 1; min-width: 0; }
/* .btn-close-req-cancel hatte hier eine eigene Regel — entfernt. Die Klasse
   steht nie allein: app.js:8415 setzt je nach `req.kind` genau eine von
   `btn-close-req-cancel` / `btn-extend-req-cancel` / `btn-taxi-cancel-cancel`
   ZUSAETZLICH auf denselben Button ("Anfrage zurückziehen"), und die beiden
   anderen haben gar keine Regel. Weil diese Regel spaeter in der Datei steht
   als `.btn-secondary` (Z. 584) und dieselbe Spezifitaet hat, gewann sie —
   derselbe Button war also in der close-Variante 12px/3.2px-Padding und
   transparent, in den beiden anderen Varianten 16px/12px-Padding (gemessen
   bei 1440px). Ohne die Regel rendern alle drei identisch als .btn-secondary
   bzw. .btn-borrower-req-active (app.js:10152). */

/* Lender: close request notification */
.close-request-notice {
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.75rem;
  margin-top: 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: 0.82rem;
}

.close-request-notice .close-request-msg { color: var(--text); font-style: italic; flex: 1; }
.close-request-date { color: var(--text-muted); font-size: 0.75rem; white-space: nowrap; }
/* Formular-Audit (M8): Abschluss-Anfrage mit beiden Antworten nebeneinander –
   gleiche Anordnung und Optik wie die Verlängerungsanfrage darunter. */
.close-request-notice .close-request-decide {
  display: flex;
  gap: 0.4rem;
  flex-basis: 100%;
}
.close-request-notice .close-request-decide > button { flex: 1 1 0; width: auto; min-width: 0; }

/* === R7-Teilzahlung: Zahlungsmeldungen ======================================
   Banner an der Leihkarte (beide Rollen) für eine offene Meldung. Info-Blau wie
   die Verlängerung: eine Meldung ist eine Bitte um Bestätigung, keine Warnung. */
.payment-claim-notice {
  background: color-mix(in srgb, var(--info) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.75rem;
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-size: 0.82rem;
  min-width: 0;
}
.payment-claim-top { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.payment-claim-amount { font-size: 0.95rem; color: var(--text); font-variant-numeric: tabular-nums; }
.payment-claim-top .close-request-date { margin-left: auto; }
.payment-claim-meta { color: var(--text-muted); overflow-wrap: anywhere; }
.payment-claim-notice .close-request-msg,
.pay-claim-box .close-request-msg { color: var(--text); overflow-wrap: anywhere; }
.payment-claim-actions { display: flex; gap: 0.4rem; margin-top: 0.1rem; }
.payment-claim-actions > button { flex: 1 1 0; width: auto; min-width: 0; min-height: var(--touch-target, 44px); }
/* .btn-primary bringt margin-top: 0.5rem mit – neben „Ablehnen“ stand
   „Bestätigen“ sonst 8px tiefer. */
.payment-claim-actions > .btn-primary,
.decision-request-paymentClaim .loan-actions > .btn-primary { margin-top: 0; }

/* === Widerspruch (L-04) ======================================================
   Hinweiskarte an der Leihkarte (beide Rollen) und Zeilen/Knöpfe der Entscheidungskarte im Tab „Ausstehend“.
   Warn-Ton wie Abschluss-/Verlängerungsanfrage: eine Schuld ist bestritten, das verlangt eine Entscheidung. */
.dispute-notice {
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.75rem;
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.82rem;
  min-width: 0;
}
/* Ruhender Widerspruch (Leihe abgeschlossen, bei Abschluss noch offen): kein Warn-Ton – nichts verlangt eine Entscheidung. */
.dispute-notice--dormant {
  background: color-mix(in srgb, var(--text-muted) 8%, transparent);
  border-color: var(--border);
}
.dispute-notice-top { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.dispute-notice-top .close-request-date { margin-left: auto; }
.dispute-notice-text { margin: 0; overflow-wrap: anywhere; }
.dispute-notice-meta { margin: 0; color: var(--text-muted); font-size: 0.78rem; line-height: 1.35; }
.dispute-wait { display: flex; align-items: flex-start; gap: 0.35rem; margin: 0.1rem 0 0; color: var(--text-muted); font-size: 0.82rem; line-height: 1.35; }
/* Lange Namen/Begründungen: Beschriftung bricht nur zwischen Wörtern („Dein / Gegenvorschlag“, nie mitten im Wort), der Wert
   bricht um statt zu überlaufen. */
.dispute-notice .pending-addition-detail-label,
.decision-request-dispute .pending-addition-detail-label { white-space: normal; flex: 0 1 auto; min-width: min-content; overflow-wrap: normal; }
.dispute-notice .pending-addition-detail > span:last-child,
.decision-request-dispute .pending-addition-detail > span:last-child { flex: 1 1 0; min-width: 0; text-align: right; overflow-wrap: anywhere; -webkit-hyphens: auto; hyphens: auto; }
/* Ein bis drei Knöpfe: nebeneinander, wo der Platz reicht; auf schmalen Handys rutscht der letzte (die Hauptaktion)
   in eine eigene, volle Zeile. */
.dispute-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.1rem;
}
.dispute-actions > button { flex: 1 1 7.5rem; width: auto; min-width: 0; min-height: var(--touch-target, 44px); }
/* .btn-primary bringt margin-top: 0.5rem mit – neben „Ablehnen“ stand es sonst 8px tiefer. */
.dispute-actions > .btn-primary,
.dispute-actions > .btn-danger,
.dispute-actions > .btn-secondary { margin-top: 0; }
/* Entscheidungskarte: Aktionsleiste wie die der Nachbar-Anfragen (.loan-actions: Trennlinie, Abstand). Zwei bis drei
   Knöpfe brechen um (zwei pro Zeile, die Hauptaktion füllt die Restzeile) – die Regel „zwei Entscheidungen, eine
   Reihe“ der Entscheidungskarten gilt nur für genau zwei. */
.loan-card.decision-request-card .loan-actions.dispute-actions { flex-wrap: wrap; }
.loan-card.decision-request-card .loan-actions.dispute-actions > button { flex: 1 1 7.5rem; min-width: 0; }
.decision-request-dispute .dispute-wait { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }
.decision-request-dispute .close-blocked-hint { margin-top: 0.4rem; }
/* Dialog: .toggle-field hat keinen Abstand nach unten – die Felder des Gegenvorschlags klebten sonst am Schalter
   (Abstand wie .form-group). */
#modal-dispute .toggle-field { margin-bottom: 1rem; }

/* Zahlung-melden-Modal: Segment „Alles / Teilbetrag“ (Radio-Segmente wie
   .fb-modes, gleitender Indikator über syncSegIndicator). */
.pc-kinds {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
  padding: 4px;
  margin: 0 0 0.4rem;
  border-radius: var(--radius-segmented);
  background: rgba(255, 255, 255, 0.04);
}
.pc-kind {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target, 44px);
  min-width: 0;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  transition: color var(--transition-fast), transform var(--transition-fast);
}
.pc-kind input { position: absolute; opacity: 0; pointer-events: none; }
.pc-kind:has(input:checked) { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent-text); }
.pc-kind:has(input:disabled) { opacity: var(--dim-disabled); cursor: not-allowed; }
.pc-kind:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
html:not(.using-touch) .pc-kind:hover:not(:has(input:checked)):not(:has(input:disabled)) { color: var(--text); }
.pc-kind:not(:has(input:disabled)):active {
  --press-opacity: 1; /* eigener Druckzustand, keine generische Dimmung */
  transform: scale(0.97);
  transition-duration: var(--motion-xs);
}
.pc-kinds.has-seg-indicator::before { background-color: color-mix(in srgb, var(--accent) 18%, transparent); }
.pc-kinds.has-seg-indicator > .pc-kind:has(input:checked) { background: transparent; }
.pc-kind-hint { margin: 0 0 0.8rem; }
/* Betragsbereich klappt auf (grid-template-rows 0fr → 1fr). overflow bleibt
   nur während der Bewegung hidden (.is-settled, app.js), sonst schnitte er
   die Fokusringe der Felder ab. */
.pc-partial {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--motion-base) var(--ease-standard),
              opacity var(--motion-base) var(--ease-standard);
}
.pc-partial.is-open { grid-template-rows: 1fr; opacity: 1; }
/* R9-Mobile: im Sheet zieht das Aufklappen die Oberkante mit — mit der
   expo-out-Kurve sprang sie in zwei Frames um 105 px (375 px, Karte an der
   Höhengrenze). Kurve und Dauer des Sheets selbst (sheetUp) starten ruhiger
   (wie app.js SHEET_RESIZE_EASING). */
@media (max-width: 540px) {
  .pc-partial { transition-duration: var(--motion-slow); transition-timing-function: var(--ease-sheet); }
}
.pc-partial-inner { min-height: 0; overflow: hidden; }
.pc-partial.is-open.is-settled .pc-partial-inner { overflow: visible; }
.pc-partial.pc-instant { transition: none; }
.pc-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 0.75rem; }
.pc-row > .form-group { min-width: 0; }
@media (max-width: 420px) {
  .pc-row { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .pc-partial { transition: none; }
  .pc-kind:not(:has(input:disabled)):active { transform: none; }
}

/* Leihgeber: die Meldung oben im Zahlungs-Dialog („Zahlung bestätigen“). */
.pay-claim-box {
  background: color-mix(in srgb, var(--info) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
  border-radius: var(--radius-xs);
  padding: 0.55rem 0.75rem;
  margin: 0 0 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-size: 0.85rem;
  min-width: 0;
}
.pay-claim-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }

/* R8-Meldung: Hinweis beim Eintragen von Hand, wenn der Leihnehmer schon eine
   Zahlung gemeldet hat (Doppelbuchung). Warnton wie .lend-limit-notice: ein
   deutlicher Hinweis, keine Sperre. */
.pay-dup-hint {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin: 0 0 0.75rem;
  padding: 0.65rem 0.8rem;
  border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  font-size: 0.85rem;
  line-height: 1.4;
  min-width: 0;
}
.pay-dup-head { display: flex; gap: 0.55rem; align-items: flex-start; min-width: 0; }
.pay-dup-head > svg { flex-shrink: 0; margin-top: 0.1rem; color: var(--warning); }
.pay-dup-head > p { margin: 0; overflow-wrap: anywhere; color: var(--text-muted); }
.pay-dup-head strong { color: var(--text); font-weight: 600; }
.pay-dup-hint .pay-dup-switch { width: 100%; margin-top: 0; min-height: var(--touch-target, 44px); }
.pay-dup-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.pay-dup-list > li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  padding-top: 0.4rem;
  border-top: 1px solid color-mix(in srgb, var(--warning) 22%, transparent);
}
.pay-dup-claim { flex: 1 1 auto; min-width: 0; color: var(--text); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.pay-dup-list .pay-dup-switch { flex: 0 0 auto; width: auto; }

/* R8-Meldung: Warnzeile im Bestätigungsdialog (Leihe schließen trotz offener
   Zahlungsmeldung) — steht in .confirm-msg (white-space: pre-line). */
.confirm-warning {
  display: flex;
  gap: 0.55rem;
  align-items: flex-start;
  margin-top: 0.85rem;
  padding: 0.6rem 0.75rem;
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  white-space: normal;
  font-weight: 500;
}
.confirm-warning > svg { flex-shrink: 0; margin-top: 0.15rem; color: var(--warning); }
.confirm-warning > span { min-width: 0; overflow-wrap: anywhere; }
/* R8-Meldung: drei Antworten (#confirm-alt) passen im schmalen Dialog nicht
   nebeneinander („Abbreche|n“) — Abbrechen + Alternative oben, die Aktion
   darunter über die volle Breite. DOM- = Tab-Reihenfolge bleibt erhalten.
   Im Sheet (≤540px) stapelt die Fußleiste ohnehin. */
@media (min-width: 541px) {
  .modal-card-confirm .modal-actions.has-confirm-alt {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .modal-card-confirm .modal-actions.has-confirm-alt > #confirm-ok { grid-column: 1 / -1; }
}
/* === Ende R7-Teilzahlung === */
.btn-close-loan.btn-close-accept {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  font-weight: 600;
}
html:not(.using-touch) .btn-close-loan.btn-close-accept:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent);
}
/* Grund für ein gesperrtes „Abschließen“ – ein Tooltip allein erreicht
   Touch-Geräte nicht. */
.close-blocked-hint {
  flex-basis: 100%;
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
  margin: 0.4rem 0 0;
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-muted);
}

.entry-action-menu {
  position: relative;
  display: inline-flex;
  /* P3-8: looser gap so the menu icons don't visually clump. */
  gap: 0.6rem;
  justify-content: flex-end;
  align-items: center;
}

.btn-entry-menu {
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--text-muted);
  width: 44px;
  height: 44px;
  border-radius: var(--radius);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  line-height: 1;
  transition: var(--transition-ui);
}

.btn-entry-menu svg {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  stroke-width: 1.9;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

html:not(.using-touch) .btn-entry-menu:hover,
.btn-entry-menu:focus-visible,
.entry-action-menu.open .btn-entry-menu  {
  border-color: rgba(255,255,255,0.16);
  color: var(--text);
  background: rgba(255,255,255,0.04);
}
/* Same tactile-press idiom as .info-dot/.presence-star — every other 44px
   icon button in the app has one, this shared class (loan-card entries,
   taxi favorites/vehicles, lend-limit rows) didn't. */
.btn-entry-menu:active { transform: scale(0.92); }

.btn-entry-menu-more {
  font-size: 1rem;
  line-height: 1;
  /* gleiche abgerundet-quadratische Form wie der Bearbeiten-Button (.btn-entry-menu),
     kein Kreis/Pill. */
}

/* Inline danger button (renders when a row has only ONE non-primary action
   and it's a delete). Same square shape as .btn-entry-menu (edit) for visual
   pairing — danger hint only appears on hover/focus, so neutral state stays
   calm. */
/* Hover nur mit Maus — auf Touch blieb der rote Zustand nach dem Tippen stehen. */
html:not(.using-touch) .btn-entry-menu-danger:hover,
.btn-entry-menu-danger:focus-visible {
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}

.entry-menu-panel {
  /* Anchor positioning (anchor-name/position-anchor set inline per-instance in
     app.js's renderEntryActionMenu — a single shared anchor-name resolves
     every panel to the LAST matching anchor in the doc, not each panel's own
     trigger, verified live before relying on this). position:fixed anchored
     to the trigger also escapes .activity-table's overflow:hidden entirely,
     which the old JS flip math had to route around explicitly.
     position-try-fallbacks: flip-block automatically re-anchors above the
     trigger when there's no room below (viewport-bottom case) — replaces the
     old spaceBelow/spaceAbove measurement in toggleEntryMenu(). The gap uses
     margin (not calc(anchor()+gap)) because flip-block only recalculates
     plain anchor()/margin values, not anchor() wrapped in calc() — verified
     live, the calc() form silently failed to flip. */
  position: fixed;
  /* Three explicit candidates, not just `flip-block`: a single-axis fallback
     (flip-block alone) silently does nothing when the panel ALSO overflows
     the inline axis — verified live, this is exactly the case for a
     right-anchored panel near the left edge of a narrow viewport. Listing
     flip-inline and the combined "flip-block flip-inline" (space = one
     tactic, not two) as further candidates lets the browser pick whichever
     one actually resolves the overflow, instead of only ever flipping both
     axes together even when just one needed it. */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  top: anchor(bottom);
  right: anchor(right);
  margin-top: 0.3rem;
  min-width: 148px;
  background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.02)), var(--surface2);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-tile);
  box-shadow: var(--elev-3);
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  z-index: var(--z-dropdown-over-sticky);
  opacity: 0;
  visibility: hidden;
  transform: scale(0.96);
  /* Motion-Audit: aus der Ecke des ausloesenden Knopfs aufgehen (Panel haengt
     rechtsbuendig unter ihm), nicht aus der Panel-Mitte. */
  transform-origin: top right;
  pointer-events: none;
  transition:
    opacity var(--motion-fast) linear,
    transform var(--motion-base) var(--ease-standard),
    visibility 0s linear var(--motion-base);
}

.entry-action-menu.open .entry-menu-panel {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
  pointer-events: auto;
  transition-delay: 0s;
}

.entry-menu-item {
  background: transparent;
  border: none;
  color: var(--text);
  text-align: left;
  padding: 0.55rem 0.7rem;
  min-height: var(--touch-target);
  display: flex;
  align-items: center;
  border-radius: var(--radius-xs);
  cursor: pointer;
  font-size: 0.84rem;
  transition: var(--transition-ui);
}

html:not(.using-touch) .entry-menu-item:hover,
.entry-menu-item:focus-visible { background: rgba(255,255,255,0.05); }
html:not(.using-touch) .entry-menu-item-danger:hover,
.entry-menu-item-danger:focus-visible { background: color-mix(in srgb, var(--danger) 12%, transparent); color: #ffd2d2; }
/* Full-width row, not a standalone icon — a background press reads better
   here than the scale() used on the small round/square buttons elsewhere. */
/* Two rules per state, not one: the plain one wins on touch (no competing
   :hover there, guarded off below). On desktop the mouse is still "hovering"
   during a press, so :hover's higher-specificity compound selector would
   otherwise win over a plain :active and the darker press background would
   never show at all — verified live. The second rule matches that same
   compound shape so it ties on specificity and wins on source order instead. */
.entry-menu-item:active { background: rgba(255,255,255,0.08); }
html:not(.using-touch) .entry-menu-item:active { background: rgba(255,255,255,0.08); }
.entry-menu-item-danger:active { background: color-mix(in srgb, var(--danger) 18%, transparent); }
html:not(.using-touch) .entry-menu-item-danger:active { background: color-mix(in srgb, var(--danger) 18%, transparent); }

.method-tag {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.05rem 0.35rem;
  margin-left: 0.3rem;
  vertical-align: middle;
  white-space: nowrap;
}

/* === Modal === */
.modal-overlay {
  --modal-drag-progress: 0;
  --modal-backdrop-k: 1;
  position: fixed;
  inset: 0;
  background:
    radial-gradient(circle at top, rgba(76,175,130,calc(0.08 * var(--modal-backdrop-k))), transparent 35%),
    rgba(0,0,0,calc(0.72 * (1 - var(--modal-drag-progress)) * var(--modal-backdrop-k)));
  backdrop-filter: blur(calc(7px * var(--modal-backdrop-k) * var(--modal-blur-k)));
  /* Backdrop fades in with the card (same duration as the desktop entrance)
     and out in sync with the exit — see .modal-overlay.closing. Nested dialogs
     (.is-nested) take over the backdrop instantly, see app.js openModalFocus.
     R6-Close: --modal-drag-progress is the share of the sheet dragged below
     its rest (app.js setupSwipeToClose), so the dimming follows the sheet
     like a native scrim (was: at most -0.14 after 180 px). */
  animation: modalBackdropIn var(--motion-base) var(--ease-standard) both;
  /* Released swipe that did not close: the dimming eases back with the card
     (same duration and curve as its snap-back transition). */
  transition: --modal-drag-progress var(--motion-base) var(--ease-sheet);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: var(--z-modal);
  padding: 1rem;
  padding-top: calc(1rem + env(safe-area-inset-top, 0px));
  /* Prevent scroll events on the backdrop from leaking to the page. */
  overscroll-behavior: none;
  overflow: hidden;
}

.modal-overlay.is-stacked-below {
  pointer-events: none;
  background: transparent;
  backdrop-filter: none;
}
/* R6-Close: same duration and curve as the card's exit (.modal-card.closing),
   so dimming and card leave together. --ease-exit (an ease-in) kept the
   backdrop nearly dark while the card was already gone and cut it off at
   k≈0.2 in the last frame. A swipe exit sets --modal-close-dur/-ease inline
   (app.js closeModal) to its own, velocity-matched timing. */
.modal-overlay.closing {
  animation: modalBackdropOut var(--modal-close-dur, var(--motion-fast)) var(--modal-close-ease, var(--ease-close)) both;
}
/* Modal-System: soft keyboard on a tablet in landscape (centred dialog,
   iOS does not shrink the layout viewport): centre the dialog in the part
   the keyboard leaves free and cap it to that height. Phones: sheet block.
   R5-Tablet: gilt für jeden zentrierten Dialog, also ab 541 px (Tablet
   hochkant, Handy quer). Gleiche Grenze wie der Sheet-Block, nur umgekehrt. */
html.modal-kb-open {
  --modal-max-h: min(88dvh, 860px, calc(100dvh - var(--modal-kb-inset) - 2rem));
}
@media (min-width: 541px) {
  html.modal-kb-open .modal-overlay { padding-bottom: calc(1rem + var(--modal-kb-inset)); }
}
.modal-overlay.is-nested {
  animation: none;
}
/* R6-Close: a nested dialog keeps its dimming until finish() (the host takes
   it over in the same frame) but lets its blur go with the exit — the host
   underneath (.is-unstacking) comes into focus instead of snapping sharp
   at the very end. */
.modal-overlay.is-nested.closing {
  animation: modalBackdropBlurOut var(--modal-close-dur, var(--motion-fast)) var(--modal-close-ease, var(--ease-close)) both;
}
@keyframes modalBackdropBlurOut {
  from { --modal-blur-k: 1; }
  to   { --modal-blur-k: 0; }
}
/* While the finger drags the sheet, the dimming follows it 1:1. */
.modal-overlay:has(> .modal-card.is-dragging-close) {
  transition: none;
}
@keyframes modalBackdropIn {
  from { --modal-backdrop-k: 0; }
  to   { --modal-backdrop-k: 1; }
}
@keyframes modalBackdropOut {
  from { --modal-backdrop-k: 1; }
  to   { --modal-backdrop-k: 0; }
}

.modal-overlay.is-stacked-below .modal-card {
  opacity: 0.58;
  transform: scale(0.985);
}
/* R6-Close: the dialog on top is closing and this one will be on top again —
   it brightens and grows back WHILE the other leaves (was: only after it was
   gone, a second 300 ms step). Its dimming returns in finish(), in the same
   frame the closing dialog's dimming goes. */
.modal-overlay.is-stacked-below.is-unstacking {
  backdrop-filter: blur(7px);
}
.modal-overlay.is-stacked-below.is-unstacking .modal-card {
  opacity: 1;
  transform: none;
}

.modal-card {
  position: relative;
  /* Flat surface (no top-gradient) so the sticky drag-handle / modal-head
     never shows a darker seam against the body. */
  background: var(--surface);
  border: 1px solid rgba(255,255,255,0.09);
  /* Green accent bar: replaces the top border. Never scrolls away. */
  border-top: 4px solid color-mix(in srgb, var(--accent) 92%, transparent);
  border-radius: var(--radius-modal);
  padding: 2rem;
  padding-top: 0;
  padding-bottom: 0;
  /* Lock horizontal overflow — vertical scroll handled below. Prevents the
     "kann nach links/rechts scrollen" bug if any child has overflowing width
     (e.g. long device labels in profile-device-row). */
  overflow-x: hidden;
  width: 100%;
  max-width: var(--modal-w-sm);
  height: auto;
  min-height: 0;
  max-height: var(--modal-max-h);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.12) transparent;
  /* 8rem statt 5rem: die klebende Fußleiste ist 70–120 px hoch (gemessen 70/96/120 px bei 1024×500 / 1280×720 / 375×667);
     bei 5rem blieb ein per Tab fokussiertes letztes Feld (#ml-message) 31 px hinter ihr stehen. */
  scroll-padding-bottom: 8rem;
  /* fill-mode backwards (not both): a finished entrance must not keep pinning
     opacity/transform — with `both` the end keyframe overrode the dimmed
     .is-stacked-below state (opacity .58, scale .985), so a dialog under a
     nested one never actually receded. */
  animation: modalIn var(--motion-base) var(--ease-spring) backwards;
  box-shadow: 0 30px 80px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
  will-change: transform;
  /* min-height here specifically: _resyncModalHeightFloorOnInput (app.js)
     re-syncs it on every keystroke so a field whose content shrinks (e.g.
     the "+ Merken" quick-reply button disappearing again) settles smoothly
     instead of leaving the card stuck at its tallest point for the rest of
     the session. opacity: the stacked-below dim eases in/out. */
  transition: transform var(--motion-base) var(--ease-standard), box-shadow var(--motion-base) var(--ease-standard), min-height var(--motion-base) var(--ease-standard), opacity var(--motion-base) var(--ease-standard);
}
/* Modal-System: while app.js tweens the card height (_animateModalCardHeight)
   the content is briefly taller/shorter than the box — clip instead of
   flashing a scrollbar for those ~280 ms. */
.modal-card.is-resizing,
.modal-card.is-resizing .modal-body {
  overflow-y: hidden;
}
.modal-card::-webkit-scrollbar { width: 4px; }
.modal-card::-webkit-scrollbar-track { background: transparent; }
.modal-card::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
.modal-card::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }

.modal-card-form {
  max-width: var(--modal-w-md);
}

.modal-drag-handle {
  display: none;
}

/* Sticky header zone: drag handle + title row. Stays at top of sheet while
   content scrolls underneath. Swipe-to-close is detected anywhere in here. */
.modal-sticky-header {
  position: sticky;
  top: 0;
  z-index: var(--z-dropdown);
  background: var(--surface);
  margin: 0 -2rem;
  padding: 1.5rem 2rem 0;
  border-bottom: none;
}
/* Bewusst KEIN Scroll-Separator (weder Border noch Schatten): jede Linie am
   Sticky-Header wurde als heller bzw. dunkler "Strich" beim Scrollen sichtbar.
   Der deckende Header reicht als Abgrenzung; Inhalt scrollt sauber darunter. */

/* Flex row: title (left) + close button (right) */
.history-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* 1.25rem instead of 1rem: the title's own 1rem margin-bottom is gone
     (below), this padding now carries the whole title-to-body gap. */
  padding-bottom: 1.25rem;
}
.history-modal-header > *:first-child { flex: 1; min-width: 0; }
/* Modal-System: the generic `.modal-card h2 { margin-bottom: 1rem }` also hit
   the header title. Inside this flex row that margin made the title block
   16px taller than it looks, so the close/feedback buttons (align-items:
   center) sat ~8px below the title's centre on every dialog without a kicker,
   and the gap under the header differed between dialogs with and without a
   kicker. The row's padding owns the spacing now. */
.modal-card .history-modal-header h2 { margin-bottom: 0; }
/* Formular-Audit (Fokus): Dialoge ohne sinnvolles erstes Feld fokussieren
   beim Öffnen ihren Titel (focusModalOnOpen). Er ist nur Ankerpunkt für
   Screenreader und die Tab-Reihenfolge, kein Bedienelement – ohne Ring. */
.modal-card h2[tabindex="-1"]:focus { outline: none; }

/* Close button in modal header */
.modal-close {
  position: relative;
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: var(--radius);
  background: rgba(255,255,255,0.06);
  color: var(--text-muted);
  font-size: 1rem;
  cursor: pointer;
  transition: var(--transition-ui);
  margin-left: 0.75rem;
}
html:not(.using-touch) .modal-close:hover {
  background: rgba(255,255,255,0.12);
  color: var(--text);
}
.modal-close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Touch: enlarge the hit area to the 44px touch-target without growing the
   visible 36px circle (same technique as .info-dot::after). */
@media (pointer: coarse) {
  /* inset rechnet ab der Padding-Box (36px minus 1px-Rand je Seite):
     -4px ergab gemessen nur 42px, -5px die vollen 44px. */
  .modal-close::after { content: ""; position: absolute; inset: -5px; }
}
/* Druck-Rueckmeldung wie beim Burger (.btn-menu:active) — das ✕ war der
   einzige Header-Knopf ohne. Kein Hover-Transform, also keine Maus-Stufe noetig. */
.modal-close:active { transform: scale(0.94); }
@media (prefers-reduced-motion: reduce) {
  .modal-close:active { transform: none; }
}
/* R6-Close: --ease-close instead of --ease-exit — the ease-in showed no
   change for the first ~2 frames after the tap and ended at its fastest:
   the last frame still had the card at 20–40 % opacity before it vanished. */
.modal-card.closing {
  animation: modalOut var(--motion-fast) var(--ease-close) both;
}

.modal-card.closing-success {
  animation: modalSuccessOut var(--motion-fast) var(--ease-close) both;
}

.modal-card.is-dragging-close {
  box-shadow: 0 34px 90px rgba(0,0,0,0.48), 0 0 0 1px color-mix(in srgb, var(--accent) 16%, transparent), inset 0 1px 0 rgba(255,255,255,0.06);
}


.modal-card h2 {
  /* P2-16: unified default margin; modal-top / modal-top-stacked overrides
     below keep their intentional kicker-layout spacings. */
  margin-bottom: 1rem;
  font-size: 1.34rem;
  font-weight: 800;
  letter-spacing: -0.03em;
}
@media (max-width: 540px) {
  /* Several modal titles interpolate a counterparty name (Neue Anfrage bei
     <username>, Gegenangebot fuer <username>, …). Usernames have no spaces,
     so without this a long one just keeps going past the modal edge instead
     of wrapping — .modal-card's overflow-x:hidden only hides the symptom
     (scrollWidth still exceeds clientWidth, which real mobile browsers can
     still touch-scroll through despite the hidden). */
  .modal-card h2 { overflow-wrap: anywhere; }
  /* Modal-System: long German compounds ("Benachrichtigungsverlauf",
     "Rückzahlungsdatum") otherwise hit the emergency break above and split
     mid-syllable without a hyphen ("…verlauf" → "…v / erlauf"). lang="de"
     gives the browser proper hyphenation points; usernames (no dictionary
     words) still fall back to overflow-wrap. */
  .modal-card h2 { -webkit-hyphens: auto; hyphens: auto; }
}

.modal-kicker {
  font-size: var(--label-size);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-accent-label);
  margin-bottom: 0.35rem;
}

.modal-card-wide {
  max-width: var(--modal-w-lg);
  height: auto;
  min-height: 0;
  max-height: var(--modal-max-h);
}

.modal-card-logs {
  max-width: var(--modal-w-xl);
  /* Log-BROWSER: feste (gecappte) Höhe, damit der innere #logs-body (flex:1)
     immer einen großen Scrollbereich bekommt — content-getrieben würde den
     Scrollbereich klein machen. Bewusste Ausnahme zu den hug-content-Modals. */
  height: var(--modal-max-h);
  max-height: var(--modal-max-h);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.modal-card-confirm {
  height: auto;
  min-height: 0;
  padding: 1.5rem 1.75rem 1.75rem;
  scroll-padding-top: 0;
}

#reliability-modal-body {
  padding-bottom: 1.5rem;
}

.modal-section-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  margin: 1.35rem 0 0.7rem;
}

.stats-overdue-banner {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 28%, transparent);
  border-radius: var(--radius);
  padding: 0.6rem 0.85rem;
  font-size: var(--text-base);
  color: var(--danger-text);
  font-weight: 600;
  margin-top: 0.75rem;
}
.stats-overdue-icon {
  font-size: 0.9rem;
  flex-shrink: 0;
}

.modal-actions {
  display: flex;
  gap: 0.75rem;
  margin-top: auto;
  padding-top: 0.75rem;
  /* Sticky footer: buttons stay pinned while modal content scrolls.
     Negative side margins + matching padding bleed the background flush
     with the card edges. The card has padding-bottom: 0 so this bar
     owns the entire bottom area — no negative margin needed. */
  position: sticky;
  bottom: 0;
  /* Rückmeldung #8: ohne z-index lag die Leiste unter klebenden Köpfen im
     Inhalt (.rn-head[aria-expanded] z 2, .settings-section-head z 3) — ein
     aufgeklappter Release-Notes-Eintrag zeichnete sich beim Scrollen über
     den Schließen-Knopf. 4 = über diesen Köpfen, unter .modal-sticky-header
     (5) und den Dropdowns (--z-dropdown 5). */
  z-index: 4;
  /* Plain solid fallback — always present so buttons never sit on see-through
     background. The "content sliding under here" treatment below only
     applies .is-floating-over-content (app.js: updateStickyModalFooter),
     i.e. while there's genuinely more scrollable content beneath the bar. */
  background: var(--surface);
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  padding-bottom: 1.75rem;
  margin-bottom: 0;
  margin-left: -2rem;
  margin-right: -2rem;
  padding-left: 2rem;
  padding-right: 2rem;
}
/* Mobile audit: on short landscape viewports a required field right above
   this bar was only half-visible on initial open, with nothing suggesting
   there was more to scroll to — the old fade went from 0% to fully opaque
   within the first 35% of the bar's own ~90px height, too quick a
   transition and too subtle a color shift to read as "content sliding under
   here". Slower gradient + a shadow "lip" make the overlap obvious at a
   glance — but unconditionally, on every modal, even ones short enough to
   never scroll at all, that cue was shown over nothing. Now only applied
   while updateStickyModalFooter() (app.js) confirms real content is hidden
   beneath the bar. */
.modal-actions.is-floating-over-content {
  background: linear-gradient(
    to bottom,
    transparent 0%,
    color-mix(in srgb, var(--surface) 55%, transparent) 30%,
    var(--surface) 55%,
    var(--surface) 100%
  );
  box-shadow: 0 -10px 16px -8px rgba(0,0,0,0.4);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.modal-actions-spaced { margin-top: auto; padding-top: 2rem; }
/* Modal-System: EINE Fußleisten-Höhe für alle Dialoge (Desktop). Bisher
   85px (0.75rem oben) oder 105px (.modal-actions-spaced, 2rem oben) je
   nachdem, welche Klasse der Dialog zufällig trägt — beim Wechsel zwischen
   zwei Dialogen sprang die Button-Leiste sichtbar. Mobile hat bereits eine
   einheitliche Leiste (Sheet-Block). R5-Tablet: auch Tablet hochkant. */
@media (min-width: 541px) {
  .modal-card:not(.modal-card-confirm) > .modal-actions,
  .modal-card:not(.modal-card-confirm) > .modal-actions.modal-actions-spaced {
    padding-top: 1.25rem;
  }
}

/* Every button in a modal action bar gets equal column width. The earlier
   rule only flexed .btn-primary which left .btn-secondary/.btn-danger sized
   to their content — Annehmen and Ablehnen ended up visibly different
   widths on desktop. Single-child rule is now redundant but kept for
   clarity (it's the same effective declaration). */
/* Long labels wrap instead of being silently clipped. With `flex: 1 1 0` every
   button gets exactly half the row regardless of its label, so "Trotzdem
   buchen" (156px of text in a 144px column) lost its tail to the ellipsis with
   no visual hint that text was missing. Wrapping grows the row instead. */
.modal-actions > button { flex: 1 1 0; width: auto; min-width: 0; margin-top: 0; white-space: normal; overflow-wrap: break-word; }
.modal-actions > button:only-child { flex: 0 0 auto; min-width: min(12rem, 100%); margin-top: 0; }
.modal-actions:has(> button:only-child) { justify-content: center; }
/* Ab vier sichtbaren Knoepfen (Gegenangebot erhalten: Später · Ablehnen ·
   Gegenvorschlag · Annehmen) blieben in der 540px-Fusszeile je ~95px — die
   Labels brachen mitten im Wort um ("Gegenvors|chlag", "Wird übernomm|en…").
   Zwei Reihen à zwei: gleiche Breiten, ganze Woerter, Primaer bleibt unten
   rechts. Im Sheet stapelt die Fusszeile ohnehin (siehe Mobile-Block);
   R5-Tablet: Tablet hochkant zeigt wie der Desktop einen Dialog. */
@media (min-width: 541px) {
  .modal-actions:has(> button:not([hidden]):not(.hidden) ~ button:not([hidden]):not(.hidden) ~ button:not([hidden]):not(.hidden) ~ button:not([hidden]):not(.hidden)) {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.pw-hint {
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 12%, transparent), color-mix(in srgb, var(--accent) 7%, transparent));
  border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
  color: var(--accent);
  border-radius: var(--radius-tile);
  padding: 0.7rem 0.9rem;
  font-size: 0.85rem;
  margin-bottom: 1rem;
}

/* Icon in einer eigenen Spalte links, vertikal mittig zum gesamten Textblock.
   Als Inline-Emoji würde es nur den ersten Satzanfang einrücken und der Text
   liefe beim Umbruch darunter zurück an den Rand — so bleibt die Textspalte
   über alle Zeilen bündig. */
.pw-hint-with-icon { display: flex; align-items: center; gap: 0.6rem; }
.pw-hint-icon { font-size: 1.1rem; line-height: 1; flex: 0 0 auto; }
/* Rückfrage statt Fehler: der Vorschlag ist zulässig, nur ungewöhnlich — daher
   Warnfarbe und nicht das Rot der error-msg, die eine Blockade signalisiert. */
.pw-hint-warn {
  background: linear-gradient(180deg, color-mix(in srgb, var(--orange, #f59e0b) 16%, transparent), color-mix(in srgb, var(--orange, #f59e0b) 9%, transparent));
  border-color: color-mix(in srgb, var(--orange, #f59e0b) 40%, transparent);
  color: var(--orange, #f59e0b);
}

.muted { color: var(--text-muted); }
.modal-info-text { margin-bottom: var(--space-4); font-size: var(--text-md); line-height: 1.45; }
@media (max-width: 540px) {
  /* Shared info-text line used by payout/borrower-req/settlement-propose/…
     modals for dynamic sentences that can embed a username or amount as one
     unbroken word — without this it just runs past the modal edge. */
  .modal-info-text { overflow-wrap: anywhere; }
}
.form-group-grow { flex: 1; }
.settings-pass-status { margin: 0; font-size: 0.9rem; }
.settings-pass-help { color: var(--text-muted); }
.temp-user-name { font-weight: 700; font-size: 1rem; padding: 0.4rem 0; }
/* War eine einzelne Reihe (Datum + 3 Icon-Buttons nebeneinander) ohne
   margin-left:auto/flex-shrink:0 — brauchte dadurch mehr Breite als das
   Geschwister .loan-card-header-meta (Aktiv-Karte, Datum ÜBER einem
   einzelnen Button) und wich schon auf mittelbreiten Karten unter die
   Überschrift aus (.loan-card-header hat flex-wrap:wrap), statt oben rechts
   zu bleiben. Jetzt dieselbe Spalten-Anordnung: Datum oben, Buttons darunter
   in einer eigenen Reihe (.history-card-actions-buttons) — braucht nur noch
   so viel Breite wie die 3 Buttons selbst (gemessen 148px). Reicht bei einer
   vollen Kopfzeile (Betrag+Name+Bonus-Tag+Status) auf schmalen Karten immer
   noch nicht: .loan-card-header (flex-wrap:wrap) schob den Block dann unter
   die Überschrift statt daneben zu bleiben. Absolut statt im Fluss löst das
   endgültig — "oben rechts in der Karte" gilt dann unabhängig davon, wie
   breit die Überschrift wird. .history-card braucht dafür position:relative
   (direkt bei .history-card, nicht am gemeinsamen .loan-card, um aktive
   Karten unangetastet zu lassen) und .loan-card-heading eine Platzreserve
   rechts, sonst läuft der Text unter den Buttons durch. */
.history-card { position: relative; }
.history-card .loan-card-heading { padding-right: 10.5rem; }
.history-card-actions {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.35rem;
}
.history-card-actions-buttons { display: flex; align-items: center; gap: 0.5rem; }
/* Handy: die 10.5rem-Reserve rechts ließ dem Kopf auf 320-412px nur
   80-170px — Betrag, Name und "kein Bonus"-Tag brachen je in eigene Zeilen,
   ein vierstelliger Betrag lief in die Knöpfe. Datum + Knöpfe stehen dort
   jetzt als eigene erste Zeile (Datum links, Knöpfe rechts) über dem Kopf,
   der darunter die volle Breite bekommt. .loan-card-header ist flex-wrap. */
@media (max-width: 600px) {
  .history-card .loan-card-heading { padding-right: 0; flex-basis: 100%; }
  .history-card-actions {
    position: static;
    order: -1;
    flex: 1 1 100%;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    min-height: var(--touch-target);
  }
}

.admin-summary-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
/* 320–360px: "LEIHGEB/ER", "LEIHNEH/MER" brachen mitten im Wort um —
   2×2 statt vier schmaler Kacheln (wie die Log-Kacheln). */
@media (max-width: 360px) {
  .modal-card .admin-summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.admin-summary-card {
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius);
  border: 1px solid rgba(255,255,255,0.07);
  background: rgba(255,255,255,0.015);
  text-align: center;
}

.admin-summary-label {
  display: block;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  margin-bottom: 0.15rem;
}

.admin-summary-value {
  display: block;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.admin-section-card {
  padding: 1rem 1rem 0.95rem;
  border-radius: var(--radius-card);
  border: 1px solid rgba(255,255,255,0.07);
  background: rgba(255,255,255,0.015);
  margin-bottom: 0.9rem;
}

.settings-help-text {
  display: block;
  margin-top: 0.45rem;
  /* Most callers wrap this in .form-group, whose own margin-bottom:1rem
     already separates it from whatever follows (margins collapse to the
     larger one, no double gap). Where it sits bare as the last modal-body
     child right above .modal-actions' border-top — e.g. #tv-edit-hint in
     the taxi vehicle form — nothing else provided that space, so the text
     sat flush against the divider line. */
  margin-bottom: 0.6rem;
  color: var(--text-muted);
  font-size: 0.78rem;
  line-height: 1.4;
}

.settings-save-row {
  margin-top: 0.35rem;
}

.settings-save-hint {
  color: var(--text-muted);
  font-size: 0.82rem;
  margin-bottom: 0.65rem;
}

#stats-modal-body,
#reliability-modal-body,
#users-modal-body,
#notifications-modal-body {
  flex: 1;
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  overflow-y: auto;
  /* Verhindert dass die Heatmap (intern scrollbar) oder das Time-Series-SVG
     den ganzen Modal-Body horizontal verschieben. Modal-Card hat zwar schon
     overflow-x:hidden, aber der Body als eigener Scroll-Container braucht
     den Schutz redundant. */
  overflow-x: hidden;
  /* Reserve the scrollbar so filtering down to a short, non-scrolling list
     doesn't widen the content and shove it sideways — the users modal did this
     on every keystroke in the search field. */
  scrollbar-gutter: stable;
  padding-right: 0.15rem;
}

/* Notification history is a browser, not a hug-content dialog: give the card a
   fixed (capped) height so switching filter chips never resizes the frame. A
   min-height on the body alone was only a floor — between that floor and
   max-height the card still grew and shrank with the entry count. Same
   deliberate exception as .modal-card-logs. */
#modal-notifications .modal-card {
  height: var(--modal-max-h);
  max-height: var(--modal-max-h);
  /* #notifications-modal-body is the scroller now (flex:1) — without this the
     card would keep its own overflow-y:auto and you'd get two scrollbars. */
  overflow: hidden;
}

/* Same fixed-frame fix as #modal-notifications above, same reason: switching
   between the "Alle" overview (just a few short score cards) and a single
   borrower's detail (score ring + trend + breakdown) used to visibly resize
   the card — small on first open, grown once a detail was checked, and
   (being a modal-card, see _engageModalHeightFloor in app.js) stuck at that
   larger size even switching back to "Alle" afterwards. 50rem/800px is sized
   to a typical detail view (measured ~792px total card height) rather than
   the much taller --modal-max-h cap, which would leave the "Alle" overview
   looking mostly empty on top of the small-screens fallback the min() still
   provides. */
#modal-reliability .modal-card {
  height: min(50rem, var(--modal-max-h));
  max-height: min(50rem, var(--modal-max-h));
  overflow: hidden;
}

/* Statistik: #stats-modal-body ist der Scroller. Die Karte selbst scrollte
   aber ebenfalls — der (versteckte) Diagramm-Tooltip .ts-tooltip ist
   position:fixed, hängt wegen will-change:transform an der Karte und lag
   unterhalb des Inhalts. Gemessen 412px: Karte 847px hoch, scrollHeight
   1079px — man konnte ~230px in leere Fläche scrollen, der Footer rutschte
   dabei nach oben. Ein Scroller reicht. */
#modal-stats .modal-card {
  overflow: hidden;
}
/* Ungerade Kachelzahl in der 2er-Reihe (z. B. + "Verlängerungsquote"): die
   letzte Kachel stand allein in der linken Hälfte. Volle Breite statt Lücke. */
@media (min-width: 361px) and (max-width: 639px) {
  .kpi-grid-secondary > .kpi-card:last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }
}

.history-entry-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.8rem;
  padding: 0.72rem 0.85rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  color: var(--text-muted);
  font-size: 0.84rem;
  font-weight: 700;
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  transition:
    border-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard),
    background var(--motion-base) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard),
    box-shadow var(--motion-base) var(--ease-standard);
}

html:not(.using-touch) .history-entry-toggle:hover {
  border-color: var(--text-muted);
  color: var(--text);
  transform: translateY(-1px);
  box-shadow: var(--elev-2);
}
/* Touch-Audit R4: Es gab nur die Maus-Stufe (Block "Button-Zustaende" oben),
   auf Touch gab "Details" beim Tippen nicht nach. */
.history-entry-toggle:not(:disabled):active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  transform: scale(0.985);
  transition-duration: var(--motion-xs);
}

.history-entry-toggle.expanded {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
}

.history-entry-body {
  margin-top: 0.75rem;
  transform-origin: top center;
  /* Dauerhafter Block-Formatting-Context — gleiche Begründung wie bei
     .loan-activity-content (CHANGELOG 2.9.2): animateCollapsible() misst
     scrollHeight unter `overflow: hidden`, was einen BFC erzwingt und die
     Margins der Kinder einschließt. Räumt cleanup() das overflow wieder weg,
     verschwindet der BFC, die Margin kollabiert nach außen und der eben
     geöffnete Bereich schrumpft sichtbar. Gemessen: 353,98px → 343,22px im
     selben Frame wie transitionend. */
  display: flow-root;
}

/* === Verlauf-Tab: KPI-Leiste, Filter/Sort-Controls, Gruppierung === */
.history-kpi-strip {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.6rem;
  margin-bottom: 0.85rem;
}
.history-kpi {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.7rem 0.85rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.history-kpi-value {
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--text);
}
.history-kpi-bonus .history-kpi-value { color: var(--accent); }
.history-kpi-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
}

.history-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  margin-bottom: 1rem;
}
.history-filter-pills { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.history-filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.7rem;
  font-size: 0.82rem;
  color: var(--text-muted);
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  cursor: pointer;
  transition: background var(--motion-fast), color var(--motion-fast), border-color var(--motion-fast);
}
/* :not(.is-active) — the hover selector outweighs .history-filter-pill.is-active
   and would repaint the selected pill's white-on-accent look while hovered. */
html:not(.using-touch) .history-filter-pill:hover:not(.is-active) { color: var(--text); border-color: var(--border-strong); }
.history-filter-pill.is-active {
  color: #fff;
  background: var(--accent-btn);
  border-color: var(--accent-btn);
}
.history-filter-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Touch-only min-height bump, same idiom as .taxi-stop-handle etc.: stays
   compact in the desktop filter row, reaches --touch-target on a device that
   actually needs the bigger tap target. */
@media (hover: none) and (pointer: coarse) {
  .history-filter-pill, .history-group-btn { min-height: var(--touch-target); }
}
.history-filter-count {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 12%, transparent);
}
.history-filter-pill.is-active .history-filter-count { background: rgba(0,0,0,0.22); }

.history-controls-right { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.history-group-toggle {
  display: inline-flex;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2px;
}
.history-group-btn {
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  color: var(--text-muted);
  background: transparent;
  border: none;
  border-radius: var(--radius-xs);
  cursor: pointer;
  transition: background var(--motion-fast), color var(--motion-fast);
}
.history-group-btn.is-active { color: var(--text); background: var(--surface); }
.history-group-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Touch-Audit R4: Verlauf-Filter und Gruppierung hatten keinen Druckzustand —
   gleiche Physik wie die Tab-Pillen ("Filter-pill interaction" am
   Dateiende): schnell hinein, federnd zurueck. Die Hover-Regel oben setzt
   nur Farbe/Rahmen, ein schlichtes :active gewinnt also auch mit der Maus. */
.history-filter-pill,
.history-group-btn {
  transition:
    background var(--motion-fast), color var(--motion-fast), border-color var(--motion-fast),
    transform var(--motion-fast) var(--ease-spring);
}
.history-filter-pill:active,
.history-group-btn:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  transform: scale(0.93);
  transition-duration: var(--motion-xs);
}
/* Touch-Audit R4: alle neuen Skalier-Drucke der Hauptansicht unter
   "Bewegung reduzieren" ohne Skalierung (wie .tab-pill:active dort);
   Farb-/Deckkraft-Rueckmeldung bleibt. */
@media (prefers-reduced-motion: reduce) {
  .history-filter-pill:active,
  .history-group-btn:active,
  .perspective-tab:active,
  .btn-view-log-history:active,
  .btn-note-edit:active,
  .btn-note-delete:active,
  .btn-approve-addition:not(:disabled):active,
  .btn-reject-addition:not(:disabled):active,
  .btn-reactivate:active,
  .btn-delete-history:active,
  .history-entry-toggle:not(:disabled):active,
  .sw-update-banner-btn:not(:disabled):active,
  .sw-update-banner-close:active {
    transform: none;
  }
}
.history-sort-select {
  padding: 0.35rem 0.6rem;
  font-size: 0.8rem;
  font-family: inherit;
  color: var(--text);
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-btn);
  cursor: pointer;
}
.history-sort-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.history-filter-empty {
  padding: 1.5rem 0.5rem;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.9rem;
}

/* Zeit-Gruppierung: Monats-Überschrift (nicht einklappbar) */
.history-time-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.75rem 0 0.5rem;
  text-transform: capitalize;
}
.history-time-body { display: flex; flex-direction: column; gap: 0.6rem; }

/* Karten-Verfeinerung: farbiger Status-Akzent an der linken Kante */
.history-card { border-left: 3px solid var(--border-strong); }
.history-card-closed    { border-left-color: var(--accent); }
.history-card-rejected  { border-left-color: var(--danger); }
.history-card-withdrawn { border-left-color: var(--warning); }

@media (max-width: 540px) {
  .history-kpi-strip { grid-template-columns: 1fr 1fr; }
  .history-kpi-bonus { grid-column: 1 / -1; }
  .history-controls { flex-direction: column; align-items: stretch; }
  .history-controls-right { justify-content: space-between; }
  .history-sort-select { flex: 1; }
}

.collapsible-opening,
.collapsible-closing,
.note-edit-form {
  overflow: hidden;
  transform-origin: top center;
}
/* Motion-Audit: will-change nur waehrend animateCollapsible() laeuft — auf
   .note-edit-form lag es dauerhaft (offenes Notizformular = permanente
   Compositor-Ebene); `height` ist dort ohnehin wirkungslos. */
.collapsible-opening,
.collapsible-closing {
  will-change: opacity, transform;
}
/* .note-history-wrapper above uses gap: 0.3rem between its own header and
   list/empty-state — this form is a sibling of that wrapper, not one of its
   gap children, so without its own margin it sat flush against the list
   (0px) while every other gap nearby was 0.3rem+.
   Kept OUT of the shared rule above on purpose: .collapsible-opening/
   .collapsible-closing are generic markers animateCollapsible() (app.js)
   puts on ANY element it animates, not just this form. Grouping margin-top
   in there leaked an extra 8px top margin onto every other collapsible in
   the app for the animation's duration only — the margin vanished the
   instant the animation finished and those classes were removed, so e.g.
   the admin Users modal's inline-edit panels (Rechte, Zuletzt angemeldet)
   visibly popped up by 8px right after they'd already finished opening. */
.note-edit-form {
  margin-top: 0.5rem;
}

.note-textarea {
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.65rem 0.9rem;
  font-size: 0.95rem;
  font-family: inherit;
  outline: none;
  resize: vertical;
  min-height: 80px;
  transition: border-color var(--transition-fast);
}

.note-textarea:focus { border-color: var(--accent); }

/* === Interest by Year === */
.year-table {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.year-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.6rem 1rem;
  border-top: 1px solid var(--border);
  font-size: 0.9rem;
}

.year-row:first-child { border-top: none; }
.year-label { color: var(--text-muted); }
.year-value {
  font-weight: 700;
  color: var(--accent);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.3rem;
}
.stats-sub-value { color: var(--text-muted); font-weight: 400; }

/* Chip row: sub-values as individual pill-like spans that wrap naturally. */
.stats-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  justify-content: flex-end;
}
.stats-chip {
  display: inline-block;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.55rem;
  font-size: 0.76rem;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
}
.stats-chip.stats-chip-empty { color: var(--text-muted); font-style: italic; }
.stats-chip.stat-reliability-good { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
.stats-chip.stat-reliability-building { color: var(--rel-building); border-color: color-mix(in srgb, var(--rel-building) 30%, transparent); }
.stats-chip.stat-reliability-shaky { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 30%, transparent); }
.stats-chip.stat-reliability-bad  { color: var(--danger-text);    border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
.stats-chip.stat-profit-inline    { color: var(--accent);         border-color: color-mix(in srgb, var(--accent) 30%, transparent); }

/* Stats lists on mobile: switch to card-stack layout, chips left-aligned. */
@media (max-width: 540px) {
  .year-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.25rem;
    padding: 0.65rem 0.85rem;
  }
  .year-row .year-label {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .year-row .year-value {
    font-size: 0.95rem;
    line-height: 1.3;
    width: 100%;
    align-items: flex-start;
  }
  .year-row .stats-chips {
    justify-content: flex-start;
  }
  .year-row .stats-sub-value {
    display: block;
    font-size: 0.8rem;
    margin-top: 0.15rem;
  }
}

/* === Loan Timeline (Borrower View) === */
/* Die Activity-Table wird der einzige Grid-Owner — Header, Rows und
   Row-Groups erben das Spalten-Template via `subgrid`. Vorher hatte jede
   `.activity-row` ihren eigenen Grid mit `minmax(0, 1fr)`, und diese
   schrumpften pro Row je nach Content unabhängig voneinander. Folge:
   vertikale Trennstriche zwischen den Spalten verrutschten in einzelnen
   Reihen, weil die Spalten nicht mehr aligned waren.
   Subgrid sorgt dafür, dass alle Children der Table dieselben Track-
   Breiten teilen — die Linien laufen jetzt sauber von oben bis unten. */
.activity-table {
  background: var(--surface2);
  border-radius: var(--radius);
  overflow: hidden;
  margin: 0.85rem 0 0.4rem;
  border: 1px solid var(--border);
  position: relative;
  display: grid;
  grid-template-columns: minmax(100px, 130px) minmax(0, 1fr) minmax(0, 1.2fr);
}
.activity-table--editable {
  grid-template-columns: minmax(100px, 130px) minmax(0, 1fr) minmax(0, 1.2fr) auto;
}

.activity-header {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  gap: 0;
  padding: 0;
  background: var(--border);
  color: var(--text-muted);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
}

.activity-header > span {
  padding: 0.55rem 0.75rem;
  border-right: 1px solid rgba(255,255,255,0.07);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.activity-header > span:last-child,
.activity-header > span:nth-child(3) { border-right: none; }

.activity-row-group {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  border-top: 1px solid rgba(46,46,46,0.6);
}
.activity-row-group:first-child {
  border-top: none;
}
/* Brief green wash on a just-added/edited Ergänzung so the eye finds it after
   a render — matches the loan-card history-focus accent. */
.activity-row-group.entry-focus-flash {
  animation: entryFocusFlash var(--motion-emphasis) var(--ease-standard);
}
@keyframes entryFocusFlash {
  0%   { background: color-mix(in srgb, var(--accent) 28%, transparent); }
  100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .activity-row-group.entry-focus-flash { animation: none; }
}

/* Taxi ride rendered as a timeline row. The group is a subgrid, so the ride
   block must span all inherited columns instead of being squeezed into the
   first one. Small inset padding lines it up with the date/amount/info rows. */
.activity-row-group--taxi {
  padding: 0.4rem 0.6rem;
}
.activity-row-group--taxi > .taxi-loan-block {
  grid-column: 1 / -1;
  margin: 0;
}

/* Visuelle Gruppierung: Counter-Offer-Einträge teilen `data-link-target`
   mit der zugehörigen Loan-Request- oder Addition-Row. Wenn beide das
   gleiche data-link-target haben, bekommen sie einen Akzent-Border-Left
   + leicht getintetes Background — so sieht man auf einen Blick, dass
   "Gegenangebot angenommen" zur Ergänzung darunter/darüber gehört.

   `data-link-color="0..3"` rotiert durch 4 Akzentfarben, damit mehrere
   nebeneinander stehende Counter-Gruppen visuell unterscheidbar sind.
   Reihenfolge ist chronologisch absteigend (siehe linkColorMap-Logik in
   renderLoanActivityTable). */
.activity-row-group[data-link-target] {
  --link-color: var(--orange);
  box-shadow: inset 3px 0 0 0 var(--link-color);
  background: color-mix(in srgb, var(--link-color) 5%, transparent);
}
.activity-row-group[data-link-color="0"] { --link-color: var(--orange); }
.activity-row-group[data-link-color="1"] { --link-color: #a78bfa; }
.activity-row-group[data-link-color="2"] { --link-color: var(--rel-building); }
.activity-row-group[data-link-color="3"] { --link-color: #f87fb8; }

.activity-row-group[data-link-target] > .activity-row,
.activity-row-group[data-link-target] > .timeline-entry {
  padding-left: 0.3rem;
}

/* Auch der bestehende Border-Left vom .timeline-counter-detail soll der
   Group-Farbe folgen — sonst widerspricht das fixe Orange dem rotierten
   Group-Akzent. */
.activity-row-group[data-link-target] .timeline-counter-detail {
  border-left-color: var(--link-color);
}

.activity-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  gap: 0;
  padding: 0;
  align-items: stretch;
}

/* Note-Row + Counter-Offer-Detail-Wrapper sollen die volle Breite spannen,
   damit die vertikalen Trennstriche der Datum/Betrag/Info-Spalten dort
   nicht "in der Mitte" auftauchen. */
.activity-note-row,
.activity-row-group > .timeline-entry,
.activity-row-group > details {
  grid-column: 1 / -1;
}

.activity-row > * {
  padding: 0.65rem 0.75rem;
  border-right: 1px solid rgba(46,46,46,0.6);
  min-width: 0;
}

.activity-row > *:last-child,
.activity-row > *:nth-last-child(2):is(.activity-meta-cell) { border-right: none; }

.activity-row-rejected .activity-date-main,
.activity-row-rejected .activity-date-sub,
.activity-row-rejected .activity-amount-cell,
.activity-row-rejected .activity-meta-cell,
.activity-note-row-rejected {
  opacity: 0.58;
}

.activity-date-cell {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.14rem;
  min-width: 0;
  max-width: 100%;
}

/* Vorher: nowrap + ellipsis → langer Text wurde mit "…" abgeschnitten.
   Jetzt: normaler Umbruch innerhalb der Zelle. Durch das Subgrid + die
   `align-items: stretch` auf .activity-row wachsen alle Schwester-Zellen
   der Zeile mit, sodass die Trennstriche weiterhin gleich hoch sind und
   die Zeile als Block höher wird. */
.activity-date-main {
  font-weight: 700;
  color: var(--text);
  max-width: 100%;
  /* Only ever holds fmtDate()'s fixed "DD.MM.YYYY" output (app.js), never the
     free-form long text the overflow-wrap/hyphens above were meant for — on
     narrow screens they instead broke a date apart mid-number
     ("19.09.20" / "26"), which reads worse than the ellipsis they were
     added to avoid. A date is short and fixed-width; it just needs to not
     wrap. */
  white-space: nowrap;
}

.activity-date-sub {
  font-size: 0.74rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

.activity-amount-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.14rem;
  font-weight: 700;
  color: var(--text);
}

.activity-amount-main {
  display: block;
  max-width: 100%;
  /* Beträge sind kurz (€1.234,56) und sollten nicht mitten im Wert
     umbrechen. Falls aber ein wirklich extremer Wert nicht passt, lieber
     umbrechen als clippen. */
  overflow-wrap: anywhere;
}

.activity-amount-extra {
  display: block;
  max-width: 100%;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-muted);
  white-space: normal;
  overflow-wrap: anywhere;
}

.activity-meta-cell {
  color: var(--text-muted);
  font-size: 0.82rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
  /* overflow:hidden entfernt — vorher wurde Content geclippt, der über
     die geschätzte Zellenhöhe hinausging. Jetzt darf die Zelle wachsen,
     und das Subgrid stretcht die Geschwister-Zellen mit. */
  min-width: 0;
}

.activity-open-after {
  color: var(--warning);
  font-weight: 700;
}

.activity-actions-cell {
  display: flex;
  justify-content: center;
  align-items: center;
}

.activity-note-row {
  padding: 0 0.75rem 0.6rem;
  color: var(--text-muted);
  font-size: 0.8rem;
  white-space: pre-wrap;
  /* `anywhere` (statt `break-word`) ist entscheidend für Tokens ohne
     Word-Boundaries wie IBANs (DE28370190001010593617) oder lange URLs.
     `break-word` darf nur an Wort-Grenzen brechen — ein einzelnes Token
     ohne Spaces bricht damit nie und läuft horizontal aus der
     Note-Row (die via grid-column: 1 / -1 die volle Breite spannt) hinaus,
     überschreibt visuell den Spalten-Trennstrich der Info-Spalte. */
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
  /* Defense gegen "long-string sprengt das Grid": Note-Row darf nicht
     breiter als ihr Subgrid-Track werden. */
  min-width: 0;
  max-width: 100%;
}


/* === Interest Percentage Buttons === */
.interest-pct-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.4rem;
}

.btn-pct {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
  border-radius: var(--radius-btn);
  padding: 0.4rem 0.9rem;
  min-height: var(--touch-target);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: var(--transition-ui);
}
.btn-pct:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* :not(.active) matters for legibility here: the active button is white text on
   an accent fill, and the hover rule (higher specificity) recoloured that text
   to the accent itself — accent on accent, effectively invisible. */
html:not(.using-touch) .btn-pct:hover:not(.active) { border-color: var(--accent); color: var(--accent); }
.btn-pct.active { background: var(--accent-btn); border-color: var(--accent-btn); color: #fff; }
/* Momentary "this was applied" flash for controls (e.g. the rights-preset
   buttons in renderPermissionEditor) that reuse .btn-pct without a lasting
   selected state — JS adds/removes this class around a click. */
.btn-pct.is-applied { background: var(--accent-btn); border-color: var(--accent-btn); color: #fff; }
/* R4 Modals/Touch: Bonus-/Anteil-Chips (Zahlung „Voll/Hälfte“, Zinsen,
   Rechte-Vorlagen) hatten keinen Druck-Zustand — gleicher Einwärts-Druck wie
   .quick-reply-chip. Nur transform: die Auswahl (.active) bleibt unberührt. */
.btn-pct:not(:disabled):active { transform: scale(0.96); transition-duration: var(--motion-xs); }
@media (prefers-reduced-motion: reduce) { .btn-pct:not(:disabled):active { transform: none; } }

.pending-addition-actions {
  display: flex;
  gap: 0.4rem;
  flex-shrink: 0;
}

.btn-approve-addition {
  background: var(--accent-btn);
  color: #fff;
  border: none;
  border-radius: var(--radius-btn);
  padding: 0.5rem 0.9rem;
  min-height: var(--touch-target);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity var(--transition-fast);
}

html:not(.using-touch) .btn-approve-addition:hover { opacity: 0.85; }

.btn-reject-addition {
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
  border-radius: var(--radius-btn);
  padding: 0.5rem 0.9rem;
  min-height: var(--touch-target);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity var(--transition-fast);
}

html:not(.using-touch) .btn-reject-addition:hover { opacity: 0.7; }
/* Touch-Audit R4: Annehmen/Ablehnen einer Ergaenzung gaben beim Tippen kein
   Zeichen (nur opacity-Hover fuer die Maus) — gleicher Druck wie die
   Aktionsleiste (.loan-actions > button). */
.btn-approve-addition,
.btn-reject-addition { transition: opacity var(--transition-fast), transform var(--transition-fast); }
.btn-approve-addition:not(:disabled):active,
.btn-reject-addition:not(:disabled):active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  transform: scale(0.97);
  transition-duration: var(--motion-xs);
}

.addition-interest { color: var(--text-muted); font-weight: 400; }

/* === User Management === */
.user-list { display: flex; flex-direction: column; gap: 0.5rem; }

.user-card {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 0.75rem; /* Match settings-section rhythm */
}
.user-card:last-child { margin-bottom: 0; }

.user-card-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.user-edit-row {
  display: flex;
  gap: 0.5rem;
}

.user-username-input {
  flex: 1;
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.5rem 0.75rem;
  font-size: 0.9rem;
  font-family: inherit;
  outline: none;
  transition: border-color var(--transition-fast);
}

.user-username-input:focus { border-color: var(--accent); }

.user-edit-label {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}

.btn-delete-user-inline {
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger-text); /* 4.34:1 -> >= 5:1 */
  border-radius: var(--radius-btn);
  padding: 0.4rem 0.8rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: var(--transition-ui);
}

html:not(.using-touch) .btn-delete-user-inline:hover { background: var(--danger); color: #fff; }

.temp-pass-card {
  margin-bottom: 0.85rem;
}
.temp-user-name { overflow-wrap: anywhere; }
/* Zustell-Status statt Passwort-Box (openTempPw): normaler Fließtext mit
   farbiger Randleiste wie .ml-send-status — die grüne Monospace-Box mit
   Sperrschrift las sich wie ein Passwort, obwohl ein Satz darin stand. */
.temp-pw-status {
  margin: 0.15rem 0 0;
  padding: 0.6rem 0.8rem;
  border-radius: var(--radius-xs);
  border-left: 3px solid var(--border-input);
  background: var(--surface2);
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text);
}
.temp-pw-status.is-ok {
  border-left-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.temp-pw-status.is-warn {
  border-left-color: var(--warning);
  background: color-mix(in srgb, var(--warning) 10%, transparent);
}
.temp-pass-card .temp-pw-note { margin: 0.25rem 0 0; }

/* === Logs === */
.modal-card-logs #logs-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  overflow-x: hidden;
  /* Reserve the scrollbar space so filtering to a short (non-scrolling) list
     doesn't widen the content and reflow the stats/list sideways. No-op with
     iOS overlay scrollbars; fixes the desktop horizontal jump. */
  scrollbar-gutter: stable;
  margin-bottom: 0.75rem;
}

/* Inside the container box, entries share the border visually — remove their outer frame */
.modal-card-logs .log-entry {
  border-left: none;
  border-right: none;
  border-radius: 0;
  margin-bottom: 0;
}
.modal-card-logs .log-entry:first-child { border-top: none; }
.modal-card-logs .log-entry:last-child { border-bottom: none; }

.logs-filters-bar {
  margin-bottom: 0.5rem;
  /* Clearance below the sticky header. It is opaque (background: var(--surface))
     and sits at z-index 5, so the chips' hover lift used to slide their top edge
     underneath it and vanish against the identical colour — reading as "cut off
     at the top". Not an overflow clip; a paint-order overlap. */
  padding-top: 0.35rem;
}

.logs-head-actions {
  display: flex;
  gap: 0.4rem;
  flex-shrink: 0;
}

.logs-archive-toggle.active {
  border-color: rgba(155,89,182,0.45);
  color: var(--log-archive-text);
  background: rgba(155,89,182,0.1);
}

.logs-toolbar {
  display: grid;
  /* Feste Spalten ab 821px: bei 130/130/140px schnitt das 1rem-Select
     "Alle Benutzer"/"Alle Zeiträume" mitten im Wort ab (gemessen 1280 px). */
  grid-template-columns: minmax(0, 1fr) 170px 150px 180px;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
  align-items: center;
}

.logs-daterange {
  display: flex;
  gap: 0.6rem;
  margin-bottom: 0.75rem;
}

.logs-daterange.hidden { display: none; }

.logs-daterange-label {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}

.logs-date-input {
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.45rem 0.65rem;
  font-size: 0.85rem;
  font-family: inherit;
  outline: none;
  color-scheme: dark;
}

.logs-date-input:focus { border-color: var(--accent); }

.btn-logs-subtle {
  width: auto;
  margin-top: 0;
  padding: 0.55rem 0.75rem;
  border-radius: var(--radius);
  border: 1px dashed rgba(255,255,255,0.12);
  background: transparent;
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: var(--transition-ui);
}

/* Same specificity trap as .log-filter-btn: without :not(.active) this masked
   the archive toggle's purple "on" state while the cursor rested on it. */
html:not(.using-touch) .btn-logs-subtle:hover:not(.active) {
  border-color: rgba(255,255,255,0.22);
  color: var(--text);
  background: rgba(255,255,255,0.03);
}
/* Formular-Audit (M17): „Löschen“ sah genauso aus wie „Archiv“ daneben, löscht
   aber endgültig — Gefahrenfarbe wie jede andere destruktive Aktion. */
.btn-logs-subtle.btn-logs-danger {
  color: var(--danger-text);
  border-color: color-mix(in srgb, var(--danger) 40%, transparent);
}
html:not(.using-touch) .btn-logs-subtle.btn-logs-danger:hover:not(.active) {
  color: var(--danger-text);
  border-color: color-mix(in srgb, var(--danger) 65%, transparent);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}
/* Touch-Mindestgroesse fuer die Log-Werkzeugleiste (gemessen: "Archiv"/
   "Löschen" 36px, Filter-Chips 29px, "Zur Leihe" 29px, Papierkorb 36px).
   Die Filter-Chips sitzen mobil in einem overflow-x-Streifen mit
   overflow-y:hidden und 0.3rem Innenabstand — eine ::after-Flaeche darf dort
   nur bis in dieses Padding reichen: 36px Chip (34px Padding-Box) + 2x5px
   = 44px Treffer. */
@media (pointer: coarse) {
  .btn-logs-subtle,
  .log-goto-loan { min-height: var(--touch-target); }
  .log-filter-btn { min-height: 36px; }
  .log-filter-btn::after { content: ""; position: absolute; inset: -5px 0; }
  .log-delete-btn { position: relative; }
  .log-delete-btn::after { content: ""; position: absolute; inset: -5px; }
}

.logs-search-input,
.logs-select {
  /* Wie .form-group input: 1rem (unter 16px zoomt iOS Safari beim Fokus),
     Radius --radius-xs, 44px Hoehe — vorher 0.9rem/10px/41px. */
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.6rem 0.8rem;
  font-size: 1rem;
  font-family: inherit;
  outline: none;
  min-height: var(--touch-target);
}

.logs-search-input:focus,
.logs-select:focus {
  border-color: var(--accent);
}
/* A11y-R5 (Fokus sichtbar): Diese Filterfelder setzen `outline: none` und
   zeigten den Tastaturfokus nur als 1px-Rahmenfarbe — im dunklen Theme kaum
   von „nicht fokussiert" zu unterscheiden. Jetzt derselbe Ring wie die
   Formularfelder (.form-group input:focus-visible), nach innen, damit das
   overflow der Modal-Karte ihn nicht abschneidet. Touch-Selects bleiben ohne
   Ring (Touch-R5-Regel hat die höhere Spezifität). */
.logs-search-input:focus-visible,
.logs-select:focus-visible,
.logs-date-input:focus-visible,
.users-filter:focus-visible,
.settings-test-row input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.log-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-bottom: 0.9rem;
}

.log-filter-btn {
  position: relative;
  background:
    linear-gradient(180deg, rgba(255,255,255,0.025), rgba(255,255,255,0.01)),
    rgba(255,255,255,0.01);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--text-muted);
  border-radius: var(--radius-xs);
  padding: 0.4rem 0.82rem;
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: var(--transition-ui);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
}

/* Colours only while NOT selected. This selector carries three classes plus a
   type (html + :not() + .log-filter-btn + :hover) and therefore outweighs
   .log-filter-btn.active — so a chip you just clicked stayed grey under the
   cursor and only turned green once the pointer left. */
html:not(.using-touch) .log-filter-btn:hover:not(.active) {
  border-color: rgba(255,255,255,0.18);
  color: var(--text);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.02)),
    rgba(255,255,255,0.02);
}

/* The lift applies in both states, so an active chip still reacts to hover. */
html:not(.using-touch) .log-filter-btn:hover {
  transform: translateY(-1px);
}

.log-filter-btn.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 34%, transparent);
  color: var(--log-success-text);
  box-shadow: none;
}

.logs-summary-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  /* Sticky am oberen Rand des #logs-body-Scroll-Containers, damit die
     Severity-Counts (Kritisch/Warnung/Leihe-Zhlg./Auth) beim Scrollen
     durch die Log-Liste sichtbar bleiben. Ohne den Background würden
     Log-Entries durchschimmern. */
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
  /* Negative inline margins gleichen das Border des #logs-body aus, damit
     die Karten bündig mit der Border-Linie liegen statt 1 px versetzt. */
  margin: 0 -1px 0.75rem;
  padding: 0.5rem 0.5rem 0;
}
/* Schmaler Trenner unter der Sticky-Bar, damit man beim Scrollen sieht,
   dass darunter Content beginnt. */
.logs-summary-grid::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.5rem;
  height: 0.5rem;
  background: linear-gradient(180deg, var(--surface), transparent);
  pointer-events: none;
}

.logs-summary-card {
  background: rgba(255,255,255,0.015);
  border: 1px solid rgba(255,255,255,0.07);
  border-radius: var(--radius);
  padding: 0.45rem 0.6rem;
  text-align: center;
}

.logs-summary-card-critical {
  border-color: color-mix(in srgb, var(--danger) 32%, transparent);
  background: color-mix(in srgb, var(--danger) 6%, transparent);
}

.logs-summary-card-warning {
  border-color: color-mix(in srgb, var(--warning) 32%, transparent);
  background: color-mix(in srgb, var(--warning) 6%, transparent);
}

.logs-summary-label {
  display: block;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  margin-bottom: 0.15rem;
}

.logs-summary-value {
  font-size: 0.95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Schmale Phones: drei Selects nebeneinander schnitten ihren Text mitten im
   Wort ab (gemessen 412px: "Alle Benutz", "Alle Zeiträu"; 320px: "Alle Be").
   Benutzer + Stufe teilen sich eine Zeile, der Zeitraum bekommt eine eigene.
   Zwei Klassen, damit das die spätere 3-Spalten-Regel im 820px-Block schlägt. */
@media (max-width: 440px) {
  .modal-card-logs .logs-toolbar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .modal-card-logs .logs-toolbar #logs-period { grid-column: 1 / -1; }
}
/* Eigener Zeitraum auf Mobil/Tablet: "Von"/"Bis" standen untereinander mit
   unterschiedlich breiten, nicht fluchtenden Datumsfeldern. Zwei gleich
   breite Spalten, Label über dem Feld. */
@media (max-width: 820px) {
  .modal-card-logs .logs-daterange {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.4rem;
  }
  .modal-card-logs .logs-daterange.hidden { display: none; }
  .modal-card-logs .logs-daterange-label {
    flex-direction: column;
    align-items: stretch;
    gap: 0.25rem;
    min-width: 0;
  }
  .modal-card-logs .logs-date-input { width: 100%; min-width: 0; }
}
/* 320–360px: vier Kacheln nebeneinander brachen ihre Labels mitten im Wort
   um ("KRITISC/H", "WARNU/NG"). 2×2 statt 4×1. */
@media (max-width: 360px) {
  .modal-card-logs .logs-summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .modal-card-logs .logs-summary-card { padding: 0.3rem 0.45rem; }
}

/* Archive search: old results stay (dimmed) until the new page arrives. */
#logs-list.is-refreshing {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity var(--motion-base) var(--ease-standard);
}

.logs-list-wrap {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  border-radius: var(--radius-btn);
  border: 1px solid var(--border);
  padding: 0.8rem;
  background: rgba(255,255,255,0.015);
}

.logs-day-group + .logs-day-group {
  margin-top: 1rem;
}

.logs-day-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.6rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid rgba(255,255,255,0.06);
  cursor: pointer;
  user-select: none;
}

html:not(.using-touch) .logs-day-header:hover .logs-day-title { color: var(--text); }

.logs-day-chevron {
  display: inline-flex;
  align-items: center;
  font-style: normal;
  color: var(--text-muted);
  font-size: 0.75rem;
  transition: transform var(--motion-fast) var(--ease-standard);
}

.logs-day-group.collapsed .logs-day-chevron { transform: rotate(-90deg); }
.logs-day-group.collapsed .logs-day-entries { display: none; }

.logs-day-title {
  font-size: 0.88rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.logs-day-meta {
  font-size: 0.78rem;
  color: var(--text-muted);
}

.log-entry {
  /* Listenzeilen-Rezept wie .user-card / .rn-item (--radius, 0.75rem Abstand);
     vorher 14px / 0.65rem. */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 0.75rem;
  overflow: hidden;
  transition: border-color var(--motion-base) var(--ease-standard), box-shadow var(--motion-base) var(--ease-standard), transform var(--motion-base) var(--ease-standard);
}

.log-entry[open] {
  border-color: rgba(255,255,255,0.12);
  box-shadow: var(--elev-2);
  transform: translateY(-1px);
}

.log-entry-summary {
  list-style: none;
  cursor: pointer;
  padding: 0.9rem 1rem;
  transition: background var(--motion-base) var(--ease-standard), transform var(--motion-base) var(--ease-standard);
}

.log-entry-summary::-webkit-details-marker { display: none; }

/* Das <summary> fuellt die .log-entry komplett aus (nur die 1px-Border
   dazwischen), und .log-entry hat `overflow: hidden` (Z. 4777). Der globale
   Fokusring (Z. 240: 2px solid, outline-offset: 2px) liegt damit ausserhalb
   der Clip-Box des Eltern-<details> und wurde komplett weggeschnitten — per
   Tastatur fokussierte Log-Eintraege hatten sichtbar GAR KEINEN Fokusring,
   waehrend das baugleiche .settings-section-<summary> (kein overflow:hidden)
   seinen Ring zeigt. Negativer Offset zeichnet den Ring nach innen, der
   Radius haelt ihn parallel zur 14px-Ecke der Karte. */
.log-entry-summary:focus-visible {
  outline-offset: -3px;
  border-radius: var(--radius-tile);
}

html:not(.using-touch) .log-entry-summary:hover {
  background: rgba(255,255,255,0.02);
  transform: translateX(2px);
}

.log-entry-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0.45rem;
}

.log-entry-time {
  margin-left: auto;
  color: var(--text-muted);
  font-size: 0.76rem;
  white-space: nowrap;
}

.log-delete-btn {
  background: transparent;
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--text-muted);
  border-radius: var(--radius-xs);
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: var(--transition-ui);
}

html:not(.using-touch) .log-delete-btn:hover {
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}

.log-entry-title {
  font-size: 0.96rem;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 0.18rem;
}

.log-entry-summary-text {
  color: var(--text-muted);
  font-size: var(--text-base);
  line-height: 1.45;
}
@media (max-width: 540px) {
  /* Log title/summary text is built from the raw event message (error text,
     device labels, …) and can contain one long unbroken token — .log-entry
     itself clips (overflow:hidden) so this only hides text rather than
     scrolling the page, but the underlying overflow is still real and best
     fixed at the source. */
  .log-entry-title,
  .log-entry-summary-text {
    overflow-wrap: anywhere;
  }
}

.log-severity-badge {
  border: 1px solid transparent;
}

.log-severity-badge-critical {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border-color: color-mix(in srgb, var(--danger) 25%, transparent);
}

.log-severity-badge-warning {
  color: var(--warning);
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  border-color: color-mix(in srgb, var(--warning) 25%, transparent);
}

.log-severity-badge-info {
  color: var(--text-muted);
  background: rgba(255,255,255,0.06);
  border-color: rgba(255,255,255,0.08);
}

.log-money-chip {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.18rem 0.5rem;
}

.log-entry-body {
  border-top: 1px solid rgba(255,255,255,0.06);
  padding: 0.85rem 1rem 1rem;
}

.log-entry[open] .log-entry-body {
  animation: contentSwapIn var(--motion-base) var(--ease-standard) both;
}

/* The dashed border alone was not a reliable marker: inside the logs modal
   .modal-card-logs .log-entry strips left/right borders, and the
   :first-child/:last-child rules drop one of the remaining horizontal lines —
   a cluster that is the only entry of its day ended up with no border at all
   and looked exactly like a plain row. The accent left edge survives all of
   those overrides, so it carries the "this expands" signal instead. */
.log-entry-cluster {
  border-style: dashed;
}

.modal-card-logs .log-entry.log-entry-cluster,
.log-entry-cluster {
  border-left: 3px solid color-mix(in srgb, var(--accent) 45%, transparent);
}

/* Count pill in a cluster header ("4×") — makes the collapsed state readable
   at a glance without relying on the explanatory line below the title. */
.log-cluster-count {
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--text-muted);
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: var(--radius-sm);
  padding: 0.14rem 0.42rem;
}

/* Nested entries: a 1.5% overlay was effectively invisible. The inset left
   rail plus a slightly stronger surface makes the containment obvious even
   when the cluster header has scrolled out of view. */
.log-cluster-body {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0.6rem 0.6rem 0.6rem 0.85rem;
  background: rgba(255,255,255,0.04);
  border-left: 2px solid rgba(255,255,255,0.12);
  margin-left: 0.5rem;
}

.log-cluster-body .log-entry {
  margin-bottom: 0.4rem;
}

.log-cluster-body .log-entry:last-child {
  margin-bottom: 0;
}

/* Readable label for the loan-scoped filter a "Verlauf"-button applies —
   the search box itself only ever shows a raw UUID. */
.logs-scope-chip {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.6rem;
  padding: 0.4rem 0.55rem 0.4rem 0.75rem;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
  /* Matches .tab-bar. A pill radius belongs on short, content-sized chips; on
     this full-width box it rendered as a stadium and stood out against every
     other wide container in the app. */
  border-radius: var(--radius-xs);
  font-size: 0.8rem;
  color: var(--text);
}

.logs-scope-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.logs-scope-clear {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
  min-width: var(--touch-target);
  min-height: var(--touch-target);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  transition: var(--transition-ui);
}

html:not(.using-touch) .logs-scope-clear:hover { color: var(--text); background: rgba(255,255,255,0.08); }

.log-audit-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem 1rem;
  margin-bottom: 0.75rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}

.log-raw-message {
  font-size: 0.84rem;
  line-height: 1.45;
  margin-bottom: 0.75rem;
  color: var(--text);
}
@media (max-width: 540px) {
  /* Original event message, shown verbatim inside the expanded entry —
     same long-token risk as .log-entry-title above. */
  .log-raw-message { overflow-wrap: anywhere; }
}

.log-details-section {
  margin-bottom: 0.5rem;
}

.log-details-section-title {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  /* Farbe statt opacity dimmen (Regel der Dim-Skala): opacity 0.55 auf
     --text-muted ergab 3.3:1, --text-dim haelt 5.5:1. */
  color: var(--text-dim);
  margin-bottom: 0.25rem;
  padding-left: 0.1rem;
}

.log-details-grid {
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--radius);
  overflow: hidden;
}

.log-goto-loan {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  font-size: 0.8rem;
  line-height: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-radius: var(--radius-xs);
  padding: 0.45rem 0.7rem;
  cursor: pointer;
  transition: var(--transition-ui);
  margin-top: 0.5rem;
}

html:not(.using-touch) .log-goto-loan:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }

.logs-load-more {
  display: flex;
  justify-content: center;
  padding: 1rem 0 0.5rem;
}

.btn-logs-load-more {
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 0.84rem;
  font-weight: 600;
  padding: 0.55rem 1.25rem;
  cursor: pointer;
  transition: var(--transition-ui);
}

html:not(.using-touch) .btn-logs-load-more:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.18); color: var(--text); }

.logs-count-hint {
  text-align: center;
  font-size: 0.78rem;
  color: var(--text-muted);
  padding: 0.4rem 0 0.75rem;
}

.log-detail-row {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.42rem 0.75rem;
  font-size: 0.84rem;
  border-top: 1px solid rgba(255,255,255,0.04);
}

.log-detail-row:first-child { border-top: none; }

.log-detail-key {
  flex-shrink: 0;
  width: 9rem;
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 500;
}
@media (max-width: 540px) {
  /* Fixed-width label column: a longer German compound word (e.g.
     "Rückzahlungsmethode") has no space to wrap at and can be wider than the
     9rem column itself, overflowing past it. Wrapping the word onto a
     second line here is an acceptable trade-off for staying inside the
     modal. */
  .log-detail-key { overflow-wrap: anywhere; }
}

.log-detail-val {
  color: var(--text);
  word-break: break-word;
  line-height: 1.4;
  min-width: 0;
}

.log-no-details {
  color: var(--text-muted);
  font-size: 0.82rem;
  font-style: italic;
}

/* Badge-TEXT auf 20%-Toenung braucht >=4.5:1: Auth (3.9:1) und E-Mail (3.0:1)
   mit --text aufgehellt, Fehler-Varianten auf --badge-rejected-text (wie
   .badge-rejected), Leihe auf --accent-text (wie .badge-active). */
.log-badge-auth     { background: rgba(91,141,217,0.2);  color: color-mix(in srgb, #5b8dd9 80%, var(--text)); }
.log-badge-loan     { background: color-mix(in srgb, var(--accent) 20%, transparent);  color: var(--accent-text); }
.log-badge-payment  { background: rgba(76,175,170,0.2);  color: #4cafa8; }
.log-badge-email    { background: rgba(155,89,182,0.2);  color: color-mix(in srgb, #9b59b6 62%, var(--text)); }
.log-badge-user     { background: color-mix(in srgb, var(--warning) 20%, transparent);  color: var(--warning); }
.log-badge-error    { background: color-mix(in srgb, var(--danger) 20%, transparent);   color: var(--badge-rejected-text); }
.log-badge-push     { background: rgba(110,168,254,0.2); color: var(--info); }
.log-badge-push-error { background: color-mix(in srgb, var(--danger) 20%, transparent); color: var(--badge-rejected-text); }
.log-badge-email-error { background: color-mix(in srgb, var(--danger) 20%, transparent); color: var(--badge-rejected-text); }
.log-badge-settings { background: rgba(136,136,136,0.15);color: var(--text-muted); }
/* Typen ohne eigene Farbe standen als nackter weißer Text ohne Fläche in der
   Badge-Reihe (z. B. "Rückmeldung" aus feedback.js). */
.log-badge-feedback { background: color-mix(in srgb, var(--text-accent-label) 16%, transparent); color: var(--text-accent-label); }
.log-badge-reminder { background: color-mix(in srgb, var(--rel-building) 16%, transparent); color: var(--rel-building); }
.log-badge-info     { background: rgba(136,136,136,0.15); color: var(--text-muted); }
.log-badge-warn     { background: color-mix(in srgb, var(--warning) 16%, transparent); color: var(--warning); }

.log-severity-critical {
  border-left: 3px solid color-mix(in srgb, var(--danger) 70%, transparent);
}

.log-severity-warning {
  border-left: 3px solid color-mix(in srgb, var(--warning) 70%, transparent);
}

/* === Settings === */
.settings-row {
  display: flex;
  gap: 0.75rem;
  align-items: stretch;
  flex-wrap: nowrap;
}

.settings-secure-group {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  min-width: 0;
  flex: 1;
}
/* Host | Port | SSL/TLS: eine Zeile, sobald Platz ist; sonst bricht der
   Host (flex-basis 14rem) auf eine eigene Zeile um. */
.settings-row-server { flex-wrap: wrap; column-gap: 0.75rem; row-gap: 0.6rem; }
.settings-row-server > .settings-host-group { flex: 1 1 14rem; min-width: 0; }
.settings-row-server > .settings-port-group { flex: 0 0 7.5rem; }
.settings-row-server > .settings-secure-group { flex: 0 0 auto; }

/* Formular-Raster in Einstellungen/Profil: Handy einspaltig (Label über
   Feld), ab 641px zwei bzw. drei Spalten für kurze Felder. */
.settings-field-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.6rem 0.75rem;
  align-items: start;
}
.settings-field-grid > .form-group { margin-bottom: 0; min-width: 0; }
@media (min-width: 641px) {
  .settings-field-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .settings-field-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Hinweis links, Speichern rechts in einer Zeile (wie eine Formular-
   Fußzeile) statt Text-Zeile + zentriertem Button darunter. */
@media (min-width: 641px) {
  .settings-save-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
  }
  .settings-save-row .settings-save-hint { margin-bottom: 0; }
  .settings-save-row .modal-actions { margin-top: 0; flex-shrink: 0; }
}

.toggle-label {
  display: flex;
  align-items: center;
  cursor: pointer;
  padding: 0.55rem 0;
  user-select: none;
}

.toggle-label-inline {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  column-gap: 0.9rem;
  width: 100%;
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius);
  border: 1px solid rgba(255,255,255,0.06);
  background: rgba(255,255,255,0.025);
  align-items: center;
}

.form-group label.toggle-label-inline {
  display: grid;
  align-items: center;
  margin-bottom: 0;
}

.toggle-label-inline > .toggle-copy {
  justify-self: start;
}

.toggle-label-inline > .toggle-control {
  justify-self: end;
}

#ml-send-email-row .toggle-label-inline,
#pay-notify-group .toggle-label-inline {
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-sizing: border-box;
}

#ml-send-email-row .toggle-label-inline > .toggle-copy,
#pay-notify-group .toggle-label-inline > .toggle-copy {
  flex: 1 1 auto;
  min-width: 0;
}

#ml-send-email-row .toggle-label-inline > .toggle-control,
#pay-notify-group .toggle-label-inline > .toggle-control {
  flex: 0 0 44px;
  margin-left: auto;
}

.toggle-track {
  width: 44px;
  height: 28px;
  /* Aus-Zustand: gedaempfte Flaeche + sichtbare Grenze (WCAG 1.4.11, der
     Schalter ist ein Bedienelement). --border allein war gegen die Karte
     praktisch unsichtbar. */
  background: var(--surface2);
  box-shadow: inset 0 0 0 1px var(--border-input);
  /* 14px war exakt die halbe Hoehe = eine Pille. Jetzt auch so benannt. */
  border-radius: var(--radius-pill);
  position: relative;
  transition: background var(--transition-fast), box-shadow var(--transition-fast);
  flex: 0 0 auto;
}

.toggle-control {
  position: relative;
  width: 44px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}

/* Touch: 44px width already meets the touch-target minimum, only height
   (28px) falls short — enlarge the hit area vertically only (same technique
   as .modal-close::after). */
@media (pointer: coarse) {
  .toggle-control::after { content: ""; position: absolute; inset: -8px 0; }
}

.toggle-copy {
  display: flex;
  flex-direction: column;
  gap: 0.08rem;
  color: var(--text);
  line-height: 1.2;
  min-width: 0;
}

.toggle-copy strong {
  font-size: 0.9rem;
  font-weight: 700;
}

.toggle-copy small {
  color: var(--text-muted);
  font-size: 0.76rem;
}

html:not(.using-touch) .toggle-label-inline:hover {
  border-color: rgba(255,255,255,0.1);
  background: rgba(255,255,255,0.04);
}

.toggle-label--disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.toggle-control input[type="checkbox"]:checked + .toggle-track {
  background: var(--accent);
  box-shadow: none;
}
.toggle-control input[type="checkbox"]:checked + .toggle-track .toggle-thumb { background: #fff; }

/* The checkbox itself is opacity:0 (see below), so the global *:focus-visible
   outline lands on an invisible element — redirect it onto the visible track. */
.toggle-control input[type="checkbox"]:focus-visible { outline: none; }
.toggle-control input[type="checkbox"]:focus-visible + .toggle-track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-pill);
}

.toggle-thumb {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 20px;
  height: 20px;
  /* Der ausgeschaltete Knopf war reinweiss und damit heller/auffaelliger als
     der eingeschaltete Zustand (accent-gruen) — er las sich als "an".
     Aus = gedaempft, An = weiss. */
  background: var(--text-muted);
  border-radius: 50%;
  transition: transform var(--transition-fast), background var(--transition-fast);
}

.toggle-control input[type="checkbox"]:checked + .toggle-track .toggle-thumb { transform: translateX(20px); }
.toggle-control input[type="checkbox"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
  margin: 0;
}

.settings-test-row {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
  min-width: 0;
}

/* Equal-width action buttons in Erinnerungen section */
/* Verwendungszweck-Kategorien-Editor (Stichwort-Chips) */
.categories-list {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  margin: 0.5rem 0 0.6rem;
}
.category-row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.6rem;
  border-radius: var(--radius);
  background: var(--surface-2, rgba(255,255,255,0.03));
  border: 1px solid var(--border, rgba(255,255,255,0.08));
}
.category-row-head {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}
.category-label {
  /* Eingabefeld: selbes Rezept wie .form-group input (siehe .users-filter). */
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.65rem;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-input);
  background: var(--surface2);
  color: var(--text);
  font-size: 1rem;
  min-height: var(--touch-target);
}
.category-del {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  /* ≥44px tap target for touch */
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--radius-xs);
}
html:not(.using-touch) .category-del:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
/* Runde 6 (Knöpfe): Die ✕-/↵-Knöpfe der Kategorien waren die einzigen
   Icon-Knöpfe ohne Transition (Hover sprang) und ✕ nur generisch gedimmt,
   waehrend ↵ daneben einsinkt. Jetzt wie die übrigen Icon-Knöpfe
   (.btn-entry-menu, .modal-close): weiche Farbe, Druck = Einsinken. */
.category-del,
.kw-chip-del,
.kw-add { transition: var(--transition-ui); }
.category-del:not(:disabled):active,
.kw-chip-del:not(:disabled):active,
.logs-scope-clear:active {
  --press-opacity: 1;
  transform: scale(0.92);
  transition-duration: var(--motion-xs);
}
@media (prefers-reduced-motion: reduce) {
  .category-del:not(:disabled):active,
  .kw-chip-del:not(:disabled):active,
  .logs-scope-clear:active { transform: none; }
}

/* Chip-Box: chips + inline input, wraps freely */
.kw-box {
  /* Verhaelt sich wie ein Eingabefeld -> gleicher Rand/Flaeche wie Inputs. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  min-height: 44px;
  padding: 0.35rem 0.45rem;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-input);
  background: var(--surface2);
  cursor: text;
}
.kw-box.error { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 5%, transparent); }
.kw-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.22rem 0.3rem 0.22rem 0.6rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--text);
  font-size: 0.82rem;
  line-height: 1.3;
  max-width: 100%;
  overflow-wrap: anywhere;
}
.kw-chip-del {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.85rem;
  /* generous tap target on touch without bloating the chip visually */
  min-width: 26px;
  min-height: 26px;
  border-radius: var(--radius-sm);
}
html:not(.using-touch) .kw-chip-del:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 18%, transparent); }
.kw-input {
  flex: 1;
  min-width: 6ch;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 1rem;
  padding: 0.35rem 0.2rem;
  outline: none;
}
.kw-input::placeholder { color: var(--text-muted); opacity: 0.8; }
/* A11y-R5 (Fokus sichtbar): das Stichwort-Feld ist randlos (outline: none) —
   der Tastaturfokus war unsichtbar. Der Ring liegt auf der Chip-Box, wie bei
   der Suchleiste (.loan-search-bar:has(...)). */
.kw-box:has(.kw-input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* "↵" add-button next to the chip input */
.kw-add {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  min-height: 36px;
  padding: 0 0.5rem;
  border-radius: var(--radius-xs);
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}
html:not(.using-touch) .kw-add:hover { background: color-mix(in srgb, var(--accent) 24%, transparent); }
.kw-add:active { transform: scale(0.95); }
/* Touch: "↵" 36px -> 44px. Das ✕ im Stichwort-Chip (26px) bekommt nur eine
   um 5px groessere Trefferflaeche (36px) — mehr nicht, weil es ein
   Loeschknopf ist und eine groessere Flaeche in den Nachbar-Chip ragen und
   dort versehentlich das falsche Stichwort entfernen wuerde. */
@media (pointer: coarse) {
  .kw-add { min-width: var(--touch-target); min-height: var(--touch-target); }
  .kw-chip-del { position: relative; }
  /* B4: 44 × 44 px Trefferfläche für das ✕ (26 px). Sie wächst nach links in den eigenen Chip-Text (dort ist nichts
     antippbar) und nach rechts nur um das Chip-Polster – nie in den Nachbar-Chip, damit kein falsches Stichwort
     gelöscht wird; senkrecht je 9 px, dafür bekommen die Chip-Zeilen auf Touch etwas mehr Zeilenabstand. */
  .kw-chip-del::after { content: ""; position: absolute; inset: -9px -5px -9px -13px; }
  .kw-box { row-gap: 0.55rem; }
}
/* Inline-Fehler: gleiche Groesse und Farbe wie .field-error (Befund 13). */
.category-row-error {
  font-size: 0.75rem;
  color: var(--danger-text);
}

.settings-actions-row {
  display: flex;
  gap: 0.5rem;
}
.settings-actions-row > button {
  flex: 1;
  min-width: 0;
  justify-content: center;
  white-space: nowrap;
  margin-top: 0;
}
/* On narrow phones the two side-by-side buttons can't fit their long German
   labels (e.g. "Auf Standard zurücksetzen") at flex:1 + nowrap, so the text
   overflows to the right. Stack them full-width below the mobile breakpoint. */
@media (max-width: 540px) {
  .settings-actions-row {
    flex-direction: column;
  }
}

.settings-test-row input {
  /* Wie .form-group input (vorher 0.9rem/38px, iOS-Zoom beim Fokus). */
  flex: 1;
  min-width: 0;
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.65rem 0.9rem;
  font-size: 1rem;
  font-family: inherit;
  outline: none;
  transition: border-color var(--transition-fast);
  min-height: var(--touch-target);
}
.settings-test-row input:focus { border-color: var(--accent); }

.settings-test-row #btn-test-smtp {
  flex: 0 0 auto;
  min-width: 0;
  white-space: nowrap;
  padding: 0.5rem 0.9rem;
  font-size: 0.85rem;
  min-height: 0;
}
/* Gleiche Feldhöhe/-schrift wie die übrigen Felder der Sektion (44px, 1rem)
   statt eines sichtbar niedrigeren Sonderfelds. */
.settings-test-row input { min-height: var(--touch-target); font-size: 1rem; padding: 0.65rem 0.9rem; }
.settings-test-row #btn-test-smtp { min-height: var(--touch-target); }

.pay-notify-msg {
  margin-top: 0.55rem;
  width: 100%;
  box-sizing: border-box;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.5rem 0.75rem;
  font-size: 0.9rem;
  font-family: inherit;
  resize: vertical;
  outline: none;
  transition: border-color var(--transition-fast);
}
.pay-notify-msg:focus { border-color: var(--accent); }

/* Outstanding-Block oberhalb des Betrags-Inputs im Payment-Modal. Soll auf
   einen Blick zeigen, was noch zurückgezahlt werden muss — das ist der
   eigentliche Anchor für die Eingabe. */
.payment-outstanding {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  margin: 0 0 1rem;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-radius: var(--radius-tile);
}
.payment-outstanding-label  { font-size: 0.85rem; color: var(--text-muted); }
.payment-outstanding-amount { font-size: 1.35rem; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }

/* Schnellauswahl-Pills "Voll" / "Hälfte" direkt unter dem Amount-Input.
   Verwendet die bestehende .btn-pct-Klasse für visuelle Konsistenz. */
.pay-quick-row {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

/* Time-Series-Chart (Outstanding über 12 Monate) und Cashflow. Die SVG wird
   in ihrer echten Pixelbreite gebaut (_fitStatsCharts in app.js), die Höhe
   folgt aus der viewBox (180–240 px je nach Breite). Vorher streckte
   preserveAspectRatio="none" Texte und Punkte auf dem Desktop in die Breite. */
.ts-chart-wrapper {
  position: relative;
  width: 100%;
  margin: var(--space-2) 0 var(--space-4);
  background: var(--surface2);
  border-radius: var(--radius-card);
  padding: var(--space-3);
  border: 1px solid var(--border);
}
.ts-chart {
  width: 100%;
  height: auto;
  display: block;
  touch-action: pan-y;
}
.ts-line { stroke: var(--accent); stroke-width: 2; }
.ts-area { fill: color-mix(in srgb, var(--accent) 14%, transparent); }
.ts-dot  { fill: var(--accent); }
.ts-axis-label {
  fill: var(--text-muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.ts-axis-year {
  fill: var(--text);
  font-size: 11px;
  font-weight: 600;
}
.ts-grid {
  stroke: rgba(255,255,255,0.05);
  stroke-width: 1;
  stroke-dasharray: 2 2;
}
.ts-grid-baseline {
  stroke: rgba(255,255,255,0.12);
  stroke-dasharray: none;
}
.ts-tick {
  stroke: var(--text-muted);
  stroke-width: 1;
  opacity: 0.5;
}
.ts-hit { fill: transparent; cursor: crosshair; }
.ts-hover-line {
  stroke: var(--text-muted);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.7;
  pointer-events: none;
}
.ts-hover-dot {
  fill: var(--accent);
  stroke: var(--surface);
  stroke-width: 2;
  pointer-events: none;
}
/* Invisible 1x1 point, moved via `transform: translate(x, y)` in
   wireTsChartHover() to track the hovered data point. The tooltip below
   anchors to it via position-anchor — cheaper per pointermove than the old
   approach (recompute left/top from scratch every move): a transform alone
   doesn't trigger layout, only the anchor recalculation. */
.ts-tooltip-anchor {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  anchor-name: --ts-chart-anchor;
  pointer-events: none;
}
/* Tooltip: anchor-positioned on .ts-tooltip-anchor (justify-self:anchor-center
   handles horizontal centering on the point, position-try-fallbacks flips
   below when there's no room above — e.g. the top-left corner of the chart).
   A single anchor-name is safe here (unlike the entry-menu/route-menu panels)
   because exactly one chart of this kind exists in the DOM at a time.
   Horizontal viewport-edge clamping (first/last data point) still needs a
   small JS nudge afterward — anchor-center has no built-in equivalent to
   flip-inline for a self-alignment property — see wireTsChartHover(). */
.ts-tooltip {
  position: fixed;
  position-anchor: --ts-chart-anchor;
  position-try-fallbacks: flip-block;
  bottom: anchor(top);
  margin-bottom: 8px;
  justify-self: anchor-center;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 0.35rem 0.6rem;
  font-size: var(--text-xs);
  white-space: nowrap;
  box-shadow: 0 4px 12px rgba(0,0,0,0.4);
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  opacity: 0;
  transition: opacity var(--transition-fast);
  z-index: 2;
}
.ts-tooltip.is-visible { opacity: 1; }
/* R10-Tooltip: Fallback ohne CSS Anchor Positioning (iOS vor Safari 26;
   Test-Hook window.__forceNoAnchor). wireTsChartHover() setzt left/top in px
   (mittig über dem Punkt, bei Platzmangel darunter); die Anker-Angaben oben
   dürfen dann nicht mitspielen — in Browsern, die sie kennen (erzwungener
   Fallback), würden sie die JS-Lage sonst überschreiben. Optik und Pfeil
   bleiben dieselben. */
.ts-tooltip.ts-tooltip--js {
  position-anchor: auto;
  position-try-fallbacks: none;
  right: auto;
  bottom: auto;
  margin: 0;
  justify-self: auto;
}
.ts-tooltip-label  { color: var(--text-muted); }
.ts-tooltip-amount { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
.ts-tooltip::after {
  content: '';
  position: absolute;
  /* Follows the data point, not the box centre — the box gets clamped at the
     chart edges, the arrow must stay on the value it describes. */
  left: var(--ts-arrow-x, 50%);
  bottom: -5px;
  transform: translateX(-50%) rotate(45deg);
  width: 8px;
  height: 8px;
  background: var(--surface);
  border-right: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border-strong);
}
/* Flipped below the point when there is no room above (top data rows). */
.ts-tooltip--below::after {
  bottom: auto;
  top: -5px;
  border-right: none;
  border-bottom: none;
  border-left: 1px solid var(--border-strong);
  border-top: 1px solid var(--border-strong);
}

/* KPI-Trend-Chip: kleiner Indikator unterhalb der KPI-Zahl, vergleicht
   gegen Snapshot von vor 30 Tagen. Bewusst klein (--text-xs) — Trend ist
   Sekundärinformation, der absolute Wert bleibt visuell dominant. */
.kpi-trend {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-top: 0.25rem;
  padding: 0.1rem 0.4rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
.kpi-trend-up   { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent-text); }
.kpi-trend-down { background: color-mix(in srgb, var(--danger) 18%, transparent); color: color-mix(in srgb, var(--danger) 60%, var(--text)); }
.kpi-trend-arrow { font-size: var(--text-xs); line-height: 1; }

.stats-inline-info {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  margin: 0 0 0.55rem;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 700;
}

/* Anfrage-Detail-Liste im Decide-Modal. dt/dd-Paare als zweispaltiges Grid
   damit Labels rechtsbündig und Werte links lesbar sind. */
.decide-details {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.4rem 0.9rem;
  margin: 0 0 1rem;
  padding: 0.75rem 0.9rem;
  background: var(--surface2);
  border-radius: var(--radius);
  font-size: 0.9rem;
}
.decide-details dt { color: var(--text-muted); font-weight: 500; }
.decide-details dd { margin: 0; color: var(--text); font-weight: 600; word-break: break-word; }

/* Digital-payout helper (GiroCode + copyable details) inside modal-decide. */
.decide-payout {
  margin: 0 0 1rem;
  padding: 0.85rem 0.9rem;
  background: var(--surface2);
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
}
.decide-payout-head {
  font-weight: 700;
  margin-bottom: 0.6rem;
  color: var(--text);
}
.decide-payout-qr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin: 0.25rem auto 0.5rem;
  padding: 0.5rem;
  background: #fff;          /* QR needs a light quiet zone to scan reliably */
  border-radius: var(--radius-xs);
  width: max-content;
  -webkit-touch-callout: none;   /* suppress iOS's own "save image" long-press menu */
  -webkit-user-select: none;
  user-select: none;
}
.decide-payout-qr svg {
  display: block;
  width: 200px; height: 200px;
  max-width: 60vmin; max-height: 60vmin;   /* vmin: auch im Landscape kein Höhen-Überlauf */
}
/* Placed below the code, never over it — overlapping the quiet zone or
   finder patterns can break scanning in some banking apps. */
.decide-payout-qr-download {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.4rem 0.9rem;
  border: none;
  border-radius: var(--radius-btn);
  background: var(--accent-btn);
  color: #fff;
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
html:not(.using-touch) .decide-payout-qr-download:hover { filter: brightness(1.1); }
.decide-payout-qr-download:active { transform: scale(0.96); }
.decide-payout-qr-download:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.decide-payout-hint {
  font-size: 0.8rem;
  color: var(--text-muted);
  text-align: center;
  margin: 0 0 0.6rem;
}
.decide-payout-row {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  gap: 0.2rem 0.7rem;
  padding: 0.4rem 0;
  border-top: 1px solid var(--border, rgba(128,128,128,0.18));
}
.decide-payout-row:first-of-type { border-top: none; }
.decide-payout-label { color: var(--text-muted); font-size: 0.85rem; }
.decide-payout-value { color: var(--text); font-weight: 600; word-break: break-all; font-variant-numeric: tabular-nums; }
/* B4: IBAN in Vierergruppen – ein Block bricht nie in der Mitte, nur zwischen den Blöcken (bei 390 px brach die IBAN vor der letzten Gruppe um). */
.decide-payout-value-groups { word-break: normal; overflow-wrap: normal; }
.payout-group { display: inline-block; white-space: nowrap; }
.decide-payout-copy {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-height: 2rem;
  padding: 0.35rem 0.65rem;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
  border-radius: var(--radius-btn);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 7%, transparent);
  transition: transform var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard), background-color var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);
}
.decide-payout-copy::before {
  content: "⧉";
  font-size: 0.9rem;
  line-height: 1;
}
/* Motion-Audit: Hover an using-touch gekoppelt (sonst klebt der Lift nach dem
   Tippen), Press auch im Maus-Tier, damit er gegen den Hover gewinnt. */
html:not(.using-touch) .decide-payout-copy:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: color-mix(in srgb, var(--accent) 16%, var(--surface));
}
.decide-payout-copy:active,
html:not(.using-touch) .decide-payout-copy:active {
  transform: translateY(0) scale(0.97);
}
.decide-payout-copy:focus-visible {
  /* Motion R2: einheitlicher Fokusring (volle Akzentfarbe wie *:focus-visible). */
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.decide-payout-copy.is-copied {
  border-color: color-mix(in srgb, var(--accent-strong) 60%, var(--border));
  background: color-mix(in srgb, var(--accent-strong) 18%, var(--surface));
  color: var(--accent-strong);
}
.decide-payout-copy.is-copied::before {
  content: "✓";
}
.decide-payout-open {
  display: inline-block; margin-bottom: 0.6rem;
  /* Touch-Audit R4: "PayPal öffnen" ist ein Knopf in Link-Form — ein
     Long-Press zeigte das iOS-Link-Callout (Vorschau/Kopieren) und markierte
     die Beschriftung, wie bei einem Textlink. Die :where()-Regel oben am
     Dateianfang deckt nur button/[role=button] ab. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* PWA-Native: one tap hands all payout details to the system share sheet
   (only rendered where navigator.share exists). */
.decide-payout-share {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  width: auto;
  margin: 0.4rem 0 0.6rem;
}
.decide-payout-missing { color: var(--warning); text-align: left; margin: 0.2rem 0 0; }

/* On narrow screens stack the row: label+value on one line, the copy button
   full-width below, so a long IBAN never collides with the button. */
@media (max-width: 540px) {
  .decide-payout-row {
    grid-template-columns: max-content 1fr;
    column-gap: 0.6rem;
  }
  .decide-payout-copy {
    grid-column: 1 / -1;
    justify-self: stretch;
    width: 100%;
  }
}

/* Secondary decision action inside the decide-modal footer. It shares the
   same column sizing as Abbrechen/Genehmigen, so the mobile footer stays even. */
.decide-secondary-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0.7rem 0.8rem;
  min-height: var(--touch-target);
  background: linear-gradient(135deg, var(--orange), #fb923c);
  border: 1px solid color-mix(in srgb, var(--orange) 55%, transparent);
  /* A11y-R5 (Kontrast): Weiß auf Amber hatte nur 2,15:1 (gemessen). Dunkles
     Braun wie .counter-offer-badge — über 7:1, der Knopf bleibt orange. */
  color: #3d2600;
  border-radius: var(--radius-btn);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  text-align: center;
  transition: var(--transition-ui);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html:not(.using-touch) .decide-secondary-link:hover {
  border-color: var(--accent);
  color: #3d2600;
  filter: brightness(1.08);
}

.decide-actions > button {
  flex-basis: 0;
}

.settings-test-result {
  font-size: 0.85rem;
  margin-top: 0.4rem;
  padding: 0.75rem 0.9rem;
  border-radius: var(--radius-tile);
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.06);
}

/* === Custom Confirm Dialog === */
.modal-card-confirm {
  /* Intentionally narrower than --modal-w-sm (440px) — see :root comment.
     This overrides the inherited .modal-card max-width, not a stray value.
     400 rather than 360: two equal columns then leave ~164px of label width,
     enough for the longest OK labels in use ("Trotzdem buchen" 156px,
     "Zweite Leihe anlegen") to stay on one line. */
  max-width: 400px;
  text-align: left;
}

.modal-card-confirm .modal-sticky-header {
  position: static;
  margin: 0 0 0.9rem;
  padding: 0;
  min-height: 0;
  flex-shrink: 0; /* R9-Mobile: see the sheet variant below */
  width: auto;
  background: transparent;
  border: none;
  border-radius: 0;
}

.modal-card-confirm .modal-drag-handle {
  display: none;
}

.modal-card-confirm .history-modal-header {
  justify-content: space-between;
  padding-bottom: 0;
  gap: 0.75rem;
}

.modal-card-confirm .history-modal-header h2 {
  margin: 0;
  text-align: left;
  font-size: 1.05rem;
  line-height: 1.2;
}

.modal-card-confirm .modal-close {
  display: grid;
  margin-left: auto;
}

#modal-confirm {
  z-index: var(--z-confirm);
}

.confirm-msg {
  font-size: 0.92rem;
  font-weight: 400;
  line-height: 1.45;
  margin: 0 0 1.25rem;
  color: var(--text);
  text-align: left;
  white-space: pre-line;
}

.confirm-msg:empty {
  display: none;
}

/* Confirm dialog is too small to ever scroll, so the sticky-footer
   gradient/bleed that makes sense for full-height modals looks off here.
   Reset it to a plain static row. */
.modal-card-confirm .modal-actions {
  position: static;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-top: none;
  padding-bottom: 0;
  margin-bottom: 0;
  margin-left: 0;
  margin-right: 0;
  padding-left: 0;
  padding-right: 0;
}

.log-range-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.65rem;
  margin-bottom: 1rem;
}

.log-range-btn {
  min-width: 0;
}

/* === Toast Notifications === */
#toast-container {
  position: fixed;
  bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-top);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: center;
  pointer-events: none;
  padding: 0 1rem;
  /* Landscape notch: long toasts otherwise reach under the camera cut-out. */
  padding-left: max(1rem, env(safe-area-inset-left, 0px));
  padding-right: max(1rem, env(safe-area-inset-right, 0px));
  max-width: 100%;
  width: max-content;
}

.toast {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 240px;
  max-width: min(90vw, 480px);
  padding: 0.7rem 1rem 0.7rem 0.85rem;
  background: rgba(26, 26, 26, 0.96);
  color: var(--text);
  border-radius: var(--radius-tile);
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 8px 24px rgba(0,0,0,0.45), 0 2px 6px rgba(0,0,0,0.3);
  font-size: 0.88rem;
  line-height: 1.35;
  pointer-events: auto;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  /* Motion-Audit: fill-mode `backwards`, nicht `both` — eine beendete
     Animation mit forwards-Fill haelt transform/opacity fest und uebertrumpft
     damit das Inline-transform des Wisch-Gesture (showToast in app.js): der
     Toast blieb beim Ziehen stehen (gemessen 0 px bei 40 px Fingerweg). Die
     Transition laesst ihn nach kurzem Zug weich zurueckfedern bzw. beim
     Wegwischen hinausgleiten statt zu springen. */
  animation: toastInSoft var(--motion-base) var(--ease-spring) backwards;
  transition: transform var(--motion-fast) var(--ease-standard), opacity var(--motion-fast) var(--ease-standard);
  position: relative;
  overflow: hidden;
  /* PWA-Native: swipe sideways OR flick down to dismiss (showToast in
     app.js locks the axis) — the page must not scroll under a toast drag.
     Was pan-y, which turned every downward flick into a page scroll. */
  touch-action: none;
  user-select: none;
}
/* Tap-to-close affordance on normal toasts (the undo-toast keeps its button). */
.toast:not(.toast-undo) { cursor: pointer; }
/* Touch-Audit R4: Tippen schliesst den Toast — beim Aufsetzen hellt er kurz
   auf, damit der Finger merkt, dass er "getroffen" hat (kein transform: das
   gehoert der Wisch-Geste, showToast in app.js). */
.toast:not(.toast-undo):active { background: rgba(40, 40, 40, 0.97); --press-opacity: 1; }

.toast::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--accent);
}

.toast-success::before { background: var(--accent); }
.toast-error::before   { background: var(--danger, #e05252); }
.toast-info::before    { background: var(--info); }
.toast-warn::before    { background: var(--warning); }
.toast-undo::before    { background: var(--warning); }
/* R7-Undo: deferAction() — same toast, but the action is still to come (not
   yet sent), so it reads as a success in waiting: accent rail + accent strip
   instead of the warning tone of an already executed delete. */
.toast-deferred::before { background: var(--accent); }
.toast-deferred .toast-undo-bar-fill { background: var(--accent); }
/* R8-Undo: the countdown is over. While the request is on the wire the
   button and the strip fade (the text still says "… wird gesendet"); once it
   went through the same toast says the final state with a check mark. */
.toast-undo.is-sending .toast-undo-btn,
.toast-undo.is-done .toast-undo-btn {
  opacity: 0; pointer-events: none;
  transition: opacity var(--motion-fast) var(--ease-standard);
}
.toast-undo.is-sending .toast-undo-bar,
.toast-undo.is-done .toast-undo-bar { opacity: 0; transition: opacity var(--motion-fast) var(--ease-standard); }
.toast-undo.is-done .toast-undo-btn { display: none; }

/* Undo-Variant: action button on the right + countdown progress bar at the
   bottom. 5-second window; the bar provides clear visual urgency. */
.toast-undo {
  position: relative;
  padding-right: 0.75rem;
  padding-bottom: 0.9rem;
}
/* B4: Tastaturhinweis (Strg+Z / ⌘Z) im Rückgängig-Toast – nur mit Maus/Tastatur sichtbar, nicht auf dem Handy. */
.toast-undo-key { display: none; flex-shrink: 0; align-self: center; margin-left: 0.5rem; padding: 0.1rem 0.4rem; font: inherit; font-size: 0.72rem; color: var(--text-muted); border: 1px solid rgba(255,255,255,0.14); border-radius: var(--radius-xs); }
@media (hover: hover) and (pointer: fine) and (min-width: 541px) { .toast-undo-key { display: inline-block; } }

.toast-undo-btn {
  margin-left: 0.6rem;
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.14);
  color: var(--text);
  font-weight: 600;
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast);
  flex-shrink: 0;
}
html:not(.using-touch) .toast-undo-btn:hover,
.toast-undo-btn:focus-visible {
  background: rgba(255,255,255,0.16);
  border-color: rgba(255,255,255,0.24);
}
/* Touch-Audit R4: "Rueckgaengig" hatte keinen Druckzustand. */
.toast-undo-btn:active,
html:not(.using-touch) .toast-undo-btn:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  background: rgba(255,255,255,0.22);
  border-color: rgba(255,255,255,0.3);
  transition-duration: 0s;
}
.toast-undo-bar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: rgba(255,255,255,0.06);
  border-radius: 0 0 8px 8px;
  overflow: hidden;
}
.toast-undo-bar-fill {
  height: 100%;
  width: 100%;
  background: var(--warning);
  transform-origin: left center;
  animation-name: toastUndoCountdown;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}
@keyframes toastUndoCountdown {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}
@media (prefers-reduced-motion: reduce) {
  /* Befund 29: Der Balken fror bei 50% ein und behauptete damit dauerhaft
     "halbe Zeit ist noch da" — die Restzeit war ausschliesslich ueber die
     abgeschaltete Animation kodiert. Ein Balken, der nichts misst, ist
     schlechter als kein Balken. Der numerische Zaehler im Label muss aus
     app.js kommen (siehe requests/css.md). */
  .toast-undo-bar { display: none; }
  .toast-undo-bar-fill { animation: none; }
}

.toast-icon {
  flex-shrink: 0;
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  font-size: 0.85rem;
  font-weight: 600;
}

.toast-success .toast-icon { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.toast-error   .toast-icon { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--badge-overdue-text); }
.toast-info    .toast-icon { background: color-mix(in srgb, var(--info) 18%, transparent); color: var(--info); }
.toast-warn    .toast-icon { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--warning); }
/* R7-Undo: both countdown toasts get the tinted icon disc of the others. */
.toast-undo    .toast-icon { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--warning); }
.toast-deferred .toast-icon { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }

.toast-msg { flex: 1; min-width: 0; word-wrap: break-word; }
/* Dedupe counter badge: shown when the same toast fires repeatedly */
.toast-count {
  flex: 0 0 auto;
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1;
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.12);
  color: var(--text-muted);
}

.toast.is-leaving,
.toast.toast-out {
  animation: toastOutSoft var(--motion-fast) var(--ease-exit, cubic-bezier(0.4,0,1,1)) both;
  /* Waehrend der Exit-Animation blieb der "Rueckgaengig"-Button 220ms lang
     klickbar, obwohl der Toast optisch schon weg war -> doppelte Buchung.
     Der Toast ist ab dem Moment des Schliessens tot. */
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .toast,
  .toast.is-leaving,
  .toast.toast-out { animation: none; }
}

@media (max-width: 540px) {
  #toast-container { width: 100%; }
  .toast { width: 100%; max-width: 100%; }
}

/* === Animations === */
@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(16px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Desktop dialog: rises + scales up from 97 % while fading in, leaves the
   same way (smaller distance, faster) — the classic centred-dialog motion. */
@keyframes modalIn {
  from { opacity: 0; transform: translateY(14px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* R6-Close: a little more scale than drift — the dialog recedes in place. */
@keyframes modalOut {
  to   { opacity: 0; transform: translateY(4px) scale(0.965); }
}

@keyframes modalSuccessOut {
  to   { opacity: 0; transform: translateY(8px) scale(0.99); }
}

/* Mobile sheet: one continuous glide on --ease-sheet. The former 70 %
   keyframe (-8px) on top of the overshooting --ease-spring bounced twice
   and read as wobbly rather than physical. */
@keyframes sheetUp {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* Simple, physical exit: the sheet slides straight off the bottom edge.
   No anticipation bump, no opacity fade — it leaves the viewport entirely,
   so fading would only make it look like it dissolves mid-slide.
   --sheet-drag-y is set by the swipe-to-close JS so the animation continues
   from wherever the finger released the card instead of snapping to 0. */
/* Starts exactly where the finger let go: offset AND the slight drag scale
   (both set by setupSwipeToClose), so the release frame does not jump. */
/* R6-Close: 100 % + 3rem instead of 105 %: the card's drop shadow reaches
   ~50 px above its top edge — a short sheet (confirm) left it as a dark
   seam on the bottom edge that vanished only with display:none. */
@keyframes sheetDown {
  from { transform: translateY(var(--sheet-drag-y, 0px)) scale(var(--sheet-drag-scale, 1)); }
  to   { transform: translateY(calc(100% + 3rem)) scale(var(--sheet-drag-scale, 1)); }
}

@keyframes sheetSuccessDown {
  from { transform: translateY(var(--sheet-drag-y, 0px)); opacity: 1; }
  to   { transform: translateY(18%); opacity: 0; }
}

@keyframes slideDown {
  from { opacity: 0; transform: translateY(-10px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes slideFadeOut {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(-8px) scale(0.985); }
}

@keyframes navItemIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* === Responsive === */

/* iPad-Portrait (≥ 600 ≤ 820 px): 3 statt 2 Spalten für das Dashboard, dann
   wirken die Tiles weniger geblockt + bleibt Platz für die KPI-Aktionen.
   Auf wirklichem Mobile (< 600 px) bleibt 2-spaltig erhalten.
   Selector specificity is doubled (.dashboard-grid.dashboard-grid) so this
   wins over the later, same-range-overlapping `@media (max-width:820px)`
   2-column rule regardless of source order — the two rules previously had
   identical specificity and the later one silently won across the whole
   600-820px overlap, making this block dead code. */
@media (min-width: 600px) and (max-width: 1099px) {
  .dashboard-grid.dashboard-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .dashboard-grid-5 > .dashboard-hero.dashboard-hero { grid-column: span 2; }
}

@media (max-width: 820px) {
  /* D5: Die grosse Bodenpolsterung (4.5rem) gehoert zur sticky
     .loan-actions-Bar. Die gibt es ab 601px nicht mehr (siehe D7), hier blieb
     sonst toter Raum. Die Phone-Stufe steht direkt darunter. */
  main { padding: 1rem var(--page-gutter) 2.5rem; }


  .dashboard-grid,
  .loan-snapshot {
    grid-template-columns: 1fr 1fr;
  }

  /* On 2-column mobile, the hero spanning the full row pairs the remaining
     4 tiles into a clean 2×2 below it. Avoids a lonely orphan card in the
     last row when there are 5 tiles total. */
  .dashboard-grid-5 > .dashboard-hero {
    grid-column: 1 / -1;
  }

  /* Tighter density for loan-snapshot in 540-820px range */
  .loan-snapshot {
    gap: 0.5rem;
  }

  /* Header: hamburger menu */
  /* Burger-mode styling now lives in the html.nav-collapsed block near the
     base header rules, because it must also kick in on WIDE viewports once the
     inline nav runs out of room. These two declarations stay here purely as a
     pre-JS guard: app.js is deferred, so without them a narrow viewport would
     briefly paint the inline nav before the class lands. */
  .header-nav:not(.open):not(.closing) { display: none; }
  /* .header-icon-btn schrumpft hier drunter auf var(--touch-target) (siehe
     unten) — ohne diese Zeile blieb .btn-menu bei der groesseren
     Desktop-Groesse (+0.4rem) haengen und war auf dem Handy sichtbar groesser
     als Suche/Glocke daneben, statt wie auf dem Desktop identisch. */
  .btn-menu {
    display: flex;
    width: var(--touch-target);
    height: var(--touch-target);
    border-radius: var(--radius);
  }

  .header-user-pill {
    flex-direction: row;
    gap: 0.3rem;
    /* touch-target compliant — was hardcoded to match the burger button's
       old 40px, but the burger button itself uses --touch-target (44px). */
    padding: 0 0.7rem;
    min-height: var(--touch-target);
    border-radius: var(--radius);
  }
  .header-user-label { display: none; }
  #header-user { font-size: 0.82rem; }
  .header-icon-btn {
    width: var(--touch-target);
    min-height: var(--touch-target);
    border-radius: var(--radius);
    font-size: 1rem;
  }
  /* The header row is a fixed shopping list: brand + user pill + bell + sync
     pill + burger. Below ~430px the sum exceeds the viewport, and because
     `body` is `overflow-x: clip` the surplus is silently cut off — the burger
     simply left the screen instead of producing a scrollbar. The status word
     made it worse ("Polling" is wider than "Live"), which is how it was
     noticed, but even "Live" overflowed.
     Nothing here may shrink except the username. */
  .btn-menu,
  .header-icon-btn,
  .sync-status-pill { flex-shrink: 0; }
  .header-user-pill { min-width: 0; }
  #header-user {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* .header-right is itself a flex container, so its own automatic min-width
     defaults to the min-content of ALL its children combined — including the
     three flex-shrink:0 buttons above — not just to the shrinkable user pill.
     Without this, the browser never actually shrinks the pill down to let the
     ellipsis above kick in; it only does once .header-right itself is allowed
     to shrink below its content width. Found via a longer username
     ("testlender", 10 chars) that still overflowed ~7px past the viewport
     despite all the rules above — shorter names like "testuser" (8 chars)
     happened to fit and masked this. */
  .header-right { min-width: 0; }

  /* Reclaim the missing ~55px: tighter header padding and gaps.
     Seitenrand = --page-gutter (0.75rem), exakt wie main — vorher 0.65rem,
     dadurch stand das Logo 1.6px links vom Inhalt. Der Nutzername-Pill
     schrumpft per Ellipsis (siehe oben), der Header bleibt auch bei 320px
     ohne Overflow. */
  .header-inner { padding: 0.55rem var(--page-gutter); }
  .header-right { gap: 0.4rem; }

  /* Dropdown button styling also moved to html.nav-collapsed — see above. */
}

/* Modals as bottom sheet.
   R5-Tablet: nur noch auf dem Handy hochkant (bis 540 px Breite). Vorher bis
   820 px — Tablet hochkant (768×1024, 820×1180) bekam Sheets über die volle
   Breite, und ein Handy quer unter 821 px (667×375, 740×360 — mit freigegebener
   Drehung auf Android häufig) ein schwebendes Sheet mit eckigen Unterkanten
   (D1). Ab 541 px gibt es jetzt überall den Dialog: zentriert, rundum
   abgerundet, mit Schatten, Scale/Fade statt Slide, ohne Griff und ohne
   Wisch-Geste (setupSwipeToClose fragt die Sichtbarkeit des Griffs ab).
   Tablet und Handy quer trennt die Höhe: bis 500 px greift die kompakte
   Runde-1-Variante (orientation: landscape and max-height: 500px), wie
   bisher schon bei 844×390. Gegenstück: @media (min-width: 541px). */
@media (max-width: 540px) {
  .modal-overlay {
    align-items: flex-end;
    padding: 0;
    padding-top: max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem));
    /* Use dynamic viewport height to avoid layout jumps on iOS Safari URL-bar show/hide */
    min-height: 100dvh;
    touch-action: none;
    /* Modal-System: iOS keeps the layout viewport full-height under the soft
       keyboard, so a bottom sheet would sit (with its submit row) behind it.
       app.js measures the covered strip via visualViewport; 0 everywhere else
       (Android resizes the layout viewport itself, see index.html). */
    padding-bottom: var(--modal-kb-inset);
  }
  /* R6-Close: the sheet's own duration (see .modal-card.closing below);
     a success exit (.is-closing-success) is the short one — the backdrop
     ran 300 ms against its 180 ms and vanished at ~60 % dimming. */
  .modal-overlay.closing {
    animation-duration: var(--modal-close-dur, var(--motion-base));
  }
  .modal-overlay.closing.is-closing-success {
    animation-duration: var(--motion-fast);
  }

  .modal-card {
    max-width: 100%;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
    border-bottom: none;
    /* No border-top on mobile: the opaque drag-handle bar is the top edge. A
       1px border here got shaved by the card's overflow clip (which runs at the
       padding box, inside the border), flattening the rounded top corners —
       the "top content slightly cut off" effect. The inset top highlight in
       box-shadow already gives the edge a subtle line. */
    border-top: none;
    box-shadow: 0 30px 80px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
    /* padding-bottom: 0 — the sticky .modal-actions owns the bottom area
       including safe-area-inset-bottom. Otherwise the modal-card has a
       padding strip BELOW the sticky button that the scrolling content
       shows through. */
    padding: 0 1.25rem 0 1.25rem;
    height: auto;
    min-height: 0;
    /* P0-5: subtract bottom safe-area so iOS keyboard / home-indicator does not
       hide the submit row. */
    max-height: min(88dvh, calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - env(safe-area-inset-bottom, 0px) - 0.5rem - var(--modal-kb-inset)));
    /* backwards, not both — see the base .modal-card rule. */
    animation: sheetUp var(--motion-slow) var(--ease-sheet) backwards;
  }

  .modal-card-confirm {
    height: auto;
    min-height: 0;
    /* top padding 0 — the drag-handle bar (enabled below) owns the top edge,
       just like the other mobile sheets. */
    padding: 0 1.25rem calc(1.25rem + env(safe-area-inset-bottom, 0px));
  }

  /* Confirm/info modals get the same drag-to-dismiss sheet top as the other
     mobile modals: a visible drag-handle bar + a sticky header that bleeds to
     the card edges. Overrides the compact static base layout (≈line 4476).
     Swipe-to-close is already wired (setupSwipeToClose on #modal-confirm). */
  .modal-card-confirm .modal-drag-handle { display: flex; }
  .modal-card-confirm .modal-sticky-header {
    /* R9-Mobile: the base rule's min-height: 0 let this flex item shrink as
       soon as the sheet had to scroll (keyboard open in "Leihe abschließen"):
       the header collapsed to 12 px and the title sat on top of the message
       text scrolling under it. */
    flex-shrink: 0;
    position: sticky;
    top: 0;
    width: calc(100% + 2 * 1.25rem);
    margin: 0 -1.25rem 0.4rem;
    padding: 0 1.25rem 0.75rem;
    background: var(--surface);
    border: none;
    border-radius: var(--radius-card) var(--radius-card) 0 0;
  }

  /* Drag handle */
  .modal-card::before {
    display: none;
  }

  /* Drag handle is now inside .modal-sticky-header which owns sticky positioning */
  .modal-drag-handle {
    display: flex;
    width: calc(100% + 2 * 1.25rem);
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    margin-top: 0;
    margin-bottom: 0.5rem;
    height: min(3rem, calc(env(safe-area-inset-top, 0px) + 2.5rem));
    flex-shrink: 0;
    touch-action: none;
    background: var(--surface);
    border-radius: var(--radius-card) var(--radius-card) 0 0;
    align-items: center;
    justify-content: center;
  }
  .modal-sticky-header {
    position: sticky;
    top: 0;
    z-index: var(--z-dropdown);
    background: var(--surface);
    border-radius: var(--radius-card) var(--radius-card) 0 0;
    /* Bleed to card's inner border edge — width = content + both paddings */
    width: calc(100% + 2 * 1.25rem);
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    margin-top: 0;
    /* Clearance unter dem Header als OPAKES padding (nicht margin): ein
       margin-bottom wäre ein transparentes Band, durch das der unter dem
       Sticky-Header durchscrollende Inhalt sichtbar wird ("weißer Strich" /
       Durchglitchen über den Stats-Tabs). Padding gehört zur Sticky-Box und
       deckt die Zone deckend ab. */
    margin-bottom: 0;
    padding: 0 1.25rem 1.15rem;
    border-bottom: none;
  }
  /* Kein Scroll-Separator auf Mobile (siehe Desktop): keine Trennlinie. */
  .history-modal-header {
    padding-bottom: 0;
  }

  /* Subtle drag-pill so users see the swipe-down affordance without it
     reading as a "schwarzer Streifen". Lighter, smaller, brighter alpha. */
  .modal-drag-handle::after {
    content: '';
    display: block;
    width: 38px;
    height: 4px;
    background: rgba(255, 255, 255, 0.32);
    border-radius: var(--radius-pill);
    margin: 0 auto;
  }

  .modal-card.closing {
    /* 0.28s instead of 0.38s — dismissal should feel quicker than entry
       (iOS sheets use ~0.25s). Must stay AFTER any desktop .closing rule
       in the cascade, see the note at the Motion-System block. */
    /* --ease-sheet (fast start, soft stop) instead of an ease-in: after a
       swipe the sheet keeps the finger's momentum instead of first stalling
       at the release point and then accelerating away. */
    /* R6-Close: --ease-close (see :root) — --ease-sheet threw a tall sheet
       110–180 px per frame and off screen after ~150 ms, the remaining
       150 ms only the backdrop faded. Now it glides visibly for ~2/3 of the
       time, the dimming on the same curve. */
    animation: sheetDown var(--sheet-close-dur, var(--motion-base)) var(--ease-close) both;
  }

  .modal-card.closing-success {
    animation: sheetSuccessDown var(--motion-fast) var(--ease-close) both;
  }

  /* Wide modals (logs, settings, stats) take more space */
  .modal-card-wide,
  .modal-card-logs { max-width: 100%; }

  .modal-card-wide {
    /* Content-getrieben: wächst mit dem Inhalt bis zum Cap, statt sich bei
       wenig Inhalt künstlich auf eine Mindesthöhe aufzublähen. */
    max-height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
  }

  .modal-card-logs {
    /* Log-BROWSER auf Mobile: volle verfügbare Höhe füllen, damit der
       #logs-body-Scrollbereich groß ist (Liste zum Durchsehen). */
    height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
    max-height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
    padding-left: 1.25rem;
    padding-right: 1.25rem;
    /* padding-bottom: 0 — the .modal-actions footer already owns the bottom
       area including safe-area-inset-bottom (same as standard .modal-card).
       Earlier value (1rem + safe-area) added a strip below the footer that
       made the logs bar visually larger than the other modals. */
    padding-bottom: 0;
    overflow: hidden;
  }
  /* Same treatment for the notification history sheet — the desktop rule uses
     --modal-max-h, which ignores the safe-area math the mobile sheet needs. */
  #modal-notifications .modal-card {
    height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
    max-height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
  }
  /* Rerender-Stabilität: Einstellungen und Profil bestehen aus auf-/
     zuklappbaren Sektionen. Als inhaltsgetriebenes Blatt wanderte die
     Oberkante bei jedem Auf- und Zuklappen mit — samt dem Auslöser unter dem
     Finger (412 px: Profil "Konto" zuklappen 279→447 px, Einstellungen
     "SMTP" 243→565 px). Rückmeldungen-Admin ist ein Listen-Browser mit
     Filtern, deren Trefferzahl die Höhe bestimmte. Auf dem Handy daher feste,
     volle Blatthöhe wie beim Benachrichtigungsverlauf und der Suche; der
     Inhalt scrollt darin. Desktop bleibt inhaltsgetrieben — dort hält
     _pinModalCardTop (app.js) die Oberkante. */
  #modal-settings .modal-card,
  #modal-profile .modal-card,
  #modal-feedback-admin .modal-card {
    height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
  }
  .modal-card-logs #logs-body {
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
  }

  /* Mehr Höhe für #logs-body auf Mobile: den redundanten Footer-„Schließen"
     (es gibt ✕ im Header + Swipe-to-close) und den Kicker „Protokoll" ausblenden. */
  .modal-card-logs > .modal-actions { display: none; }
  .modal-card-logs .modal-kicker { display: none; }

  .modal-card-logs .modal-drag-handle {
    margin-bottom: 0;
  }

  /* P3-7 (companion to P0-5): keep the submit row visible above the Android /
     iOS soft keyboard so users can confirm without scrolling. */
  .modal-actions {
    position: sticky;
    bottom: 0;
    /* margin-top: auto pushes the actions to the bottom of the flex-column
       modal-card even when content is short (no scroll). Combined with
       position: sticky bottom: 0 the close button is ALWAYS at the bottom
       edge — clinging when content scrolls, anchored when it doesn't. */
    margin-top: auto;
    /* Reset desktop padding-bottom/margin-bottom absorption: mobile card has
       no bottom padding so the -2rem desktop trick is not needed here. */
    margin-bottom: 0;
    /* Solid fallback; the "content sliding under here" gradient+blur only
       applies while .is-floating-over-content confirms it's true — see the
       desktop rule's comment above for why this used to be unconditional. */
    background: var(--surface);
    border-top: 1px solid rgba(255, 255, 255, 0.06);
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    padding: 0.75rem 1.25rem calc(0.7rem + env(safe-area-inset-bottom, 0px));
  }
  /* Soft keyboard open (iOS): it covers the home indicator, so the safe-area
     strip under the buttons would only push them further from the keys. */
  html.modal-kb-open .modal-card:not(.modal-card-confirm) > .modal-actions { padding-bottom: 0.7rem; }
  html.modal-kb-open .modal-card-confirm { padding-bottom: 1.25rem; }
  /* R10-Rückfrage: mit offener Tastatur (iOS) lagen die gestapelten Knöpfe
     der Rückfrage (#modal-confirm, bis zu drei mit #confirm-alt) unter der
     Tastatur — die kurze Karte scrollt dann, die Fußleiste war statisch und
     nur per Scrollen im Blatt erreichbar (320×568: alle drei verdeckt).
     Jetzt klebt sie wie bei den anderen Sheets unten am sichtbaren Teil des
     Blatts (die Karte endet dank --modal-kb-inset über der Tastatur); der
     Inhalt scrollt darunter durch und bleibt ganz erreichbar
     (_keepFocusedModalFieldVisible rechnet die Leiste heraus). Kompakter:
     zwei Knöpfe nebeneinander, bei dreien Abbrechen · Alternative oben und
     die Aktion darunter über die volle Breite (wie der Desktop-Dialog).
     Tippflächen bleiben 44 px (.modal-actions > button). */
  html.modal-kb-open .modal-card-confirm { padding-bottom: 0; }
  html.modal-kb-open .modal-card-confirm > .modal-actions {
    position: sticky;
    bottom: 0;
    z-index: 1;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
    margin: 0.25rem -1.25rem 0;
    padding: 0.6rem 1.25rem 0.75rem;
    border-top: 1px solid rgba(255, 255, 255, 0.06);
  }
  html.modal-kb-open .modal-card-confirm > .modal-actions:not(.is-floating-over-content) { background: var(--surface); }
  html.modal-kb-open .modal-card-confirm > .modal-actions.has-confirm-alt > #confirm-ok { grid-column: 1 / -1; }
  html.modal-kb-open .modal-card-confirm > .modal-actions > button:only-child { grid-column: 1 / -1; justify-self: center; }
  html.modal-kb-open .modal-card-confirm > .modal-actions > button {
    padding: 0.4rem 0.5rem;
    line-height: 1.2;
  }
  /* Sehr wenig Platz über der Tastatur (html.modal-kb-tight, app.js: sichtbare
     Höhe < 400 px, z. B. 320×568): Kopf und Leiste zusammen ließen für das
     fokussierte Feld nichts übrig. Der Kopf scrollt dann mit dem Inhalt weg,
     die Knöpfe bleiben stehen. */
  html.modal-kb-tight .modal-card-confirm .modal-sticky-header { position: static; }
  @media (max-width: 320px) {
    html.modal-kb-open .modal-card-confirm > .modal-actions {
      margin-left: -0.85rem;
      margin-right: -0.85rem;
      padding-left: 0.85rem;
      padding-right: 0.85rem;
    }
  }
  .modal-actions.is-floating-over-content {
    background: linear-gradient(
      180deg,
      color-mix(in srgb, var(--surface) 0%,  transparent) 0%,
      color-mix(in srgb, var(--surface) 80%, transparent) 40%,
      var(--surface) 60%,
      var(--surface) 100%
    );
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
  }
  /* Two-button bars stack vertically on phones. DOM order is preserved (column)
     so Abbrechen/secondary is always on top across all modals — consistent
     position regardless of whether a danger button is present. Single-button
     bars stay untouched (:only-child rule above stretches them full-width). */
  .modal-actions:has(> button + button) {
    flex-direction: column;
    gap: 0.5rem;
  }
  /* V4: `flex: 1 1 0` (Basis 0) war in der Spalte nur wegen min-height 44 px nicht 0 hoch – bei größerer Schrift (150 %/200 %) schrumpfte
     der Knopf auf Polster + 0 und der Text ragte unten heraus. Jetzt: Höhe nach Inhalt (mindestens 44 px, mehrzeilige Beschriftung wächst). */
  .modal-actions:has(> button + button) > button { flex: 0 0 auto; }
  #modal-decide .decide-actions:has(> button + button) {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    flex-direction: row;
    gap: 0.5rem;
  }
  .modal-actions > button {
    width: 100%;
    min-height: 44px;
  }
  /* Bei Ergaenzungs-Entscheidungen ist "Gegenangebot" ausgeblendet — im
     3-Spalten-Raster blieben dann zwei Knoepfe links und eine leere Spalte
     rechts stehen. */
  #modal-decide .decide-actions:has(> #decide-switch-to-counter.hidden) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  #modal-decide .decide-actions > button {
    /* 44px wie jede andere Fusszeile (vorher 42px, gemessen). */
    min-height: var(--touch-target);
    padding-left: 0.45rem;
    padding-right: 0.45rem;
    font-size: clamp(0.72rem, 3.15vw, 0.9rem);
  }
  /* Leihkarten R2 (M7): "Gegenangebot erhalten" stapelte vier Knöpfe in
     voller Breite (224 px Fußleiste auf dem iPhone SE). Jetzt: Gegenvorschlag
     als Zeile darüber, Ablehnen · Annehmen nebeneinander — die eigentliche
     Entscheidung steht unten, wo der Daumen ist. Im Vorschlagsmodus bleibt
     der Umschalter (jetzt "Zurück") in seiner Zeile stehen und "Senden" nimmt
     die untere Zeile ein — Rerender-Audit: vorher rückten Zurück · Senden in
     eine Reihe zusammen, der eben getippte Knopf sprang 53 px nach unten und
     die Fußleiste wurde bei jedem Umschalten eine Zeile kürzer/länger. */
  #modal-counter-offer-decide .counter-decide-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    flex-direction: row;
    gap: 0.5rem;
  }
  #modal-counter-offer-decide .counter-decide-actions > #counter-propose-toggle { grid-column: 1 / -1; order: -1; min-height: 40px; }
  #modal-counter-offer-decide .counter-decide-actions.is-proposing > #counter-propose-send { grid-column: 1 / -1; }
  .modal-actions > button:only-child {
    width: auto;
    align-self: center;
    min-width: min(12rem, 100%);
  }
  /* scroll-padding-bottom on the card so focus/scrollIntoView calls leave
     space for the sticky actions and don't land items beneath the button.
     Tuned to the slim 1.1rem/0.7rem footer + 44px button height. */
  .modal-card { scroll-padding-bottom: 8rem; }

  .modal-card > h2:first-of-type {
    margin-bottom: 0.4rem;
    font-size: 1.15rem;
  }
}

/* Ende des Sheet-Blocks (R5-Tablet); ab hier wieder alles bis 820 px. */
@media (max-width: 820px) {
  .logs-search-input {
    grid-column: 1 / -1;
  }

  .logs-head-actions {
    gap: 0.3rem;
  }

  .logs-head-actions .btn-logs-subtle {
    padding: 0.45rem 0.55rem;
    font-size: 0.78rem;
  }

  .logs-daterange {
    flex-direction: column;
  }

  .admin-summary-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.35rem;
  }
  .admin-summary-card { padding: 0.4rem 0.45rem; }
  /* "Leihnehmer"/"Leihgeber" are single German words (no space to wrap at) —
     in the narrow quarter-width card they can be ~8px wider than the card
     itself even at this reduced size. break-word lets it wrap onto a second
     line as a last resort instead of quietly overflowing the card. */
  .admin-summary-label { font-size: var(--text-xs); letter-spacing: 0.04em; overflow-wrap: break-word; }
  .admin-summary-value { font-size: 0.88rem; }

  .activity-header {
    display: none;
  }

  .activity-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "date amount"
      "meta meta";
    align-items: start;
    gap: 0;
    padding: 0;
    border-top: 1px solid rgba(46,46,46,0.6);
  }

  .activity-table--editable .activity-row {
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas:
      "date amount actions"
      "meta meta meta";
  }

  .activity-row > * {
    border-right: none;
    padding: 0;
  }

  .activity-date-cell {
    grid-area: date;
    padding: 0.7rem 0.55rem 0.4rem 0.7rem;
  }

  .activity-amount-cell {
    grid-area: amount;
    align-items: flex-end;
    text-align: right;
    padding: 0.7rem 0.55rem 0.4rem;
  }

  .activity-amount-main {
    font-size: 0.95rem;
  }

  .activity-amount-extra {
    max-width: 7.5rem;
  }

  .activity-actions-cell {
    grid-area: actions;
    justify-self: end;
    align-self: start;
    padding: 0.55rem 0.7rem 0.4rem 0.3rem;
  }

  .activity-meta-cell {
    grid-area: meta;
    font-size: 0.79rem;
    gap: 0.28rem;
    padding: 0 0.7rem 0.65rem;
  }

  .btn-entry-menu {
    width: 44px;
    height: 44px;
    margin: -4px;
  }
  /* Der negative Rand oben frisst 8px vom Abstand zweier Nachbarknöpfe
     (Bearbeiten/Entfernen lagen auf dem Handy nur 2px auseinander, am
     Desktop 10px). Hier ausgleichen, damit der sichtbare Abstand überall
     gleich ist. */
  .entry-action-menu { gap: calc(0.6rem + 8px); }

  .activity-note-row {
    padding: 0 0.7rem 0.75rem;
  }

  /* Log table: keep type visible and tighten spacing on mobile */
  .modal-card-logs #logs-body {
    padding: 0 0 0.5rem;
  }

  .log-filters {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: nowrap;
    overflow-x: auto;
    /* `overflow-y: hidden` clips hard at the top edge, and there was no top
       padding at all — so a chip's hover lift / press feedback / focus ring got
       sliced off along the upper border. Give the strip the same breathing room
       it has at the bottom. */
    overflow-y: hidden;
    padding-top: 0.3rem;
    padding-bottom: 0.3rem;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Mobile audit: on the narrowest viewports the last chip just ends at the
       card edge with nothing to suggest it's a swipeable strip rather than a
       hard cut-off list — fades it out instead so "there's more" reads at a
       glance. Right-edge only (not scroll-position aware): the strip is
       nearly always viewed from its start, where a left fade would be noise. */
    mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
  }

  .log-filters::-webkit-scrollbar {
    display: none;
  }

  .log-filter-btn {
    flex: 0 0 auto;
    white-space: nowrap;
  }

  /* Suche volle Breite, darunter die 3 Selects nebeneinander in EINER Zeile
     (statt 2+1) → spart eine Reihe und gibt #logs-body mehr Höhe. */
  .logs-toolbar {
    grid-template-columns: repeat(3, 1fr);
    gap: 0.4rem;
    margin-bottom: 0.4rem;
  }
  .logs-toolbar .logs-search-input { grid-column: 1 / -1; }
  .logs-toolbar .logs-select { min-width: 0; }

  /* Filterleiste kompakter halten → mehr Höhe für den #logs-body-Scrollbereich. */
  .logs-filters-bar { margin-bottom: 0.35rem; }
  .log-filters { margin-bottom: 0.4rem; }

  .logs-toolbar .btn-logs-subtle {
    grid-column: 1 / -1;
    justify-self: center;
  }

  .settings-test-row {
    flex-direction: column;
  }

  .settings-test-row #btn-test-smtp {
    width: 100%;
    white-space: normal;
  }

  .log-range-grid {
    grid-template-columns: 1fr;
  }

  .logs-summary-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.35rem;
    margin-bottom: 0.6rem;
  }

  .logs-summary-card { padding: 0.4rem 0.45rem; }
  /* Same fix as .admin-summary-label: "Leihe/Zhlg." has no space to wrap at
     and can be a few px wider than its quarter-width card. */
  .logs-summary-label { font-size: var(--text-xs); letter-spacing: 0.04em; overflow-wrap: break-word; }
  .logs-summary-value { font-size: 0.88rem; }

  .logs-list-wrap {
    padding: 0.6rem;
    flex: 0 0 auto;
    min-height: auto;
    overflow: visible;
    border-radius: var(--radius-btn);
  }

  .log-entry-summary,
  .log-entry-body {
    padding-left: 0.8rem;
    padding-right: 0.8rem;
  }

  /* Zeit + Papierkorb bleiben rechts in der Badge-Zeile (wie am Desktop).
     Vorher stand die Zeit mit width:100% auf einer eigenen Zeile und der
     Papierkorb darunter auf noch einer — ein Eintrag war so ~150px hoch,
     auf dem Handy passten kaum drei auf den Schirm. Bei vielen Badges
     bricht das Paar per flex-wrap geschlossen rechtsbündig um. */
  .log-entry-time {
    margin-left: auto;
  }

  .log-detail-key {
    width: 7rem;
  }

  /* Die alte width:100%/justify-content:flex-end-Regel hier war für den
     frueheren Flex-Im-Fluss-Zustand gedacht (rechtsbuendig auf der eigenen
     Zeile, wenn der Header umbrach). Seit .history-card-actions
     position:absolute ist (siehe Basisregel), waere width:100% hier relativ
     zur ganzen Karte aufgeblaeht (gemessen: fast Kartenbreite statt 148px)
     und hätte den Status-Badge darunter verdeckt. Nichts mehr zu tun — die
     Basisregel positioniert schon fix oben rechts, auf jeder Breite. */
}

@media (max-width: 600px) {
  main { padding-bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px)); }
}

/* Kennzahl-Kacheln (Benutzer-/Log-Uebersicht) auf Phones: vier Spalten a
   ~80px brachen die Uppercase-Labels MITTEN im Wort ("LEIHNEHME / R",
   "LEIHE/ZHL / G."). 2x2 mit Label links / Zahl rechts: jedes Label passt
   in eine Zeile, die Gesamthoehe bleibt etwa gleich (2 flache statt 1 hohe
   Reihe) — wichtig fuer die sticky Log-Uebersicht. */
@media (max-width: 480px) {
  .admin-summary-grid,
  .logs-summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .admin-summary-card,
  .logs-summary-card {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    text-align: left;
    padding: 0.45rem 0.65rem;
  }
  .admin-summary-label,
  .logs-summary-label {
    margin-bottom: 0;
    min-width: 0;
    overflow-wrap: normal;
    letter-spacing: var(--label-tracking);
    font-size: var(--label-size);
  }
  /* Log-Filter: drei Selects nebeneinander schnitten die Optionen hart ab
     ("Alle Benut", "Alle Zeiträ"). Benutzer-Filter (laengste Namen) ueber die
     volle Breite, Stufe + Zeitraum teilen sich die Zeile darunter. */
  .logs-toolbar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .logs-toolbar #logs-user-filter { grid-column: 1 / -1; }
}

/* Below ~375px even a fully collapsed username pill (0 width, see the
   .header-right min-width comment above) isn't enough — the "💸 Money"
   wordmark alone measured ~123px wide at 360px viewport width, well over the
   ~31px still missing there (mobile audit, Galaxy S23 360×780: burger button
   pushed off-screen, unreachable). Drop the text, keep the emoji as a
   compact brand mark.
   Threshold is 420px, not just past the 375px where it's strictly required:
   at 393–412px (iPhone 15 / Pixel 8 portrait) the row already fit without
   the burger being pushed off, but "fit" meant the username pill itself was
   squeezed to ~11px wide — functionally an empty, unreadable pill (mobile
   audit finding). Freeing the wordmark's ~90px at these widths too lets the
   username actually show a few characters instead of just being the one
   flex item allowed to shrink. Kept as its own block instead of folded into
   the 820px one above — that block is large and shared by many unrelated
   rules; a narrower nested query for one small tweak stays easier to find
   and to remove later than reopening it. */
@media (max-width: 420px) {
  /* Der Schriftzug war hier ausgeblendet, weil der Platz nicht reichte. Er
     kommt zurueck, indem er kleiner wird und der Sync-Status seine Box
     verliert: als reiner Punkt braucht er 28 statt 44px und liest sich auch
     nicht mehr wie ein Knopf. 28x44 bleibt ueber der Mindest-Zielgroesse
     (WCAG 2.5.8 verlangt 24px). */
  .header-title { font-size: 1.15rem; }
  .header-title-icon svg { width: 21px; height: 21px; }
}

@media (max-width: 374px) {
  /* Darunter gewinnt der Nutzername: der Schriftzug ist Zierde, der Name ist
     Information — und auf einem Geraet, das sich mehrere teilen, ist "wer ist
     gerade angemeldet" die wichtigere Auskunft. Gemessen bei 360px: mit
     Schriftzug bleiben dem Namen 15 von 60px, also knapp ein Zeichen. Ab
     375px (iPhone SE aufwaerts) reicht es fuer beides. */
  .header-title-text { display: none; }
}

/* Mobile-Audit A2: grosse Systemschrift (Android „Schriftgröße“ 150–200 %, rem wächst mit).
   Der Schriftzug und das Avatar-Initial wuchsen mit, die festen 44-px-Knöpfe nicht – Menü-Knopf
   rechts ausserhalb des Bildschirms (gemessen: iPhone 13 ab 150 %, Pixel 7 ab 150 %, SE ab 170 %,
   bis 98 px zu breit). Jetzt gibt der Schriftzug nach (Auslassungspunkte), die Knöpfe bleiben stehen,
   und das Initial bleibt im 44-px-Kreis. Bei 100 % unverändert (2,125 rem = 34 px < 44 − 6,4 px). */
@media (max-width: 820px) {
  .header-brand { flex: 1 1 0; min-width: 0; overflow: hidden; }
  .header-title { max-width: 100%; }
  /* Die Abstände zwischen den 44-px-Knöpfen sind rem-basiert und verdoppeln sich bei 200 % – gedeckelt
     auf den 100-%-Wert (0,4 rem = 6,4 px bzw. 1 rem = 16 px), bei normaler Schrift also unverändert. */
  .header-right { gap: min(0.4rem, 6.4px); }
  .header-inner { gap: min(1rem, 16px); }
  .header-title-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .header-user-pill[data-initial]::before {
    width: min(2.125rem, calc(var(--touch-target) - 0.4rem));
    height: min(2.125rem, calc(var(--touch-target) - 0.4rem));
  }
}

/* Natives <details> klappt ohne Uebergang auf — die Nachrichten auf der
   Leihkarte waren damit der einzige Aufklappbereich der App, der springt;
   alle anderen laufen ueber animateCollapsible(). ::details-content kann
   height:auto animieren, sobald interpolate-size erlaubt ist. Wo der Browser
   das nicht kennt (Safari vor 18.4), bleibt es beim bisherigen Sofort-
   Aufklappen — kein Fallback noetig, nur kein Effekt. */
@supports selector(::details-content) {
  .loan-conversation,
  .taxi-details,
  .perm-history,
  .reliability-how,
  .settings-section,
  .sync-modal-details,
  .timeline-counter-detail,
  .user-more-menu /* R13-Anim: "Weitere Aktionen" (Benutzer) klappte hart auf */ { interpolate-size: allow-keywords; }

  .loan-conversation::details-content,
  .taxi-details::details-content,
  .perm-history::details-content,
  .reliability-how::details-content,
  .settings-section::details-content,
  .sync-modal-details::details-content,
  .timeline-counter-detail::details-content,
  .user-more-menu::details-content {
    height: 0;
    opacity: 0;
    overflow: hidden;
    transition:
      height var(--motion-base) var(--ease-standard),
      opacity var(--motion-base) var(--ease-standard),
      content-visibility var(--motion-base) allow-discrete;
  }
  .loan-conversation[open]::details-content,
  .taxi-details[open]::details-content,
  .perm-history[open]::details-content,
  .reliability-how[open]::details-content,
  .settings-section[open]::details-content,
  .sync-modal-details[open]::details-content,
  .timeline-counter-detail[open]::details-content,
  .user-more-menu[open]::details-content {
    height: auto;
    opacity: 1;
  }
  /* Rerender-Audit R3: render() baut die Karten neu und stellt offene
     Nachrichten/Fahrtdetails per `open = true` wieder her — ohne diese Sperre
     klappten sie nach JEDEM Live-Update sichtbar neu auf (0 → volle Höhe) und
     schoben alles darunter, samt angetipptem Button, um bis zu 153 px weg. */
  .loan-conversation.is-restoring::details-content,
  .taxi-details.is-restoring::details-content { transition: none; }

  /* Rerender-Stabilität: ein nach einem Live-Neuaufbau wieder geöffneter
     Bereich (app.js, data-no-anim) ist schon offen — kein erneutes Aufklappen,
     keine kurz zu kurze Liste, die den Scroll-Offset kappt. */
  details[data-no-anim]::details-content { transition: none; }


  /* Die globale Reduce-Motion-Regel deckt `*, *::before, *::after` ab —
     ::details-content ist keins von beiden und lief sonst trotzdem. */
  @media (prefers-reduced-motion: reduce) {
    .loan-conversation::details-content,
    .taxi-details::details-content,
    .perm-history::details-content,
    .reliability-how::details-content,
    .settings-section::details-content,
    .sync-modal-details::details-content,
    .timeline-counter-detail::details-content,
    .user-more-menu::details-content {
      transition: none;
      height: auto;
      opacity: 1;
    }
  }
}

/* === Loading Skeleton === */
/* Motion-Audit: Der Shimmer lief als Endlosschleife auf background-position —
   das zwingt den Renderer, jede Skelett-Zeile in JEDEM Frame neu zu malen
   (bis zu 12 gleichzeitig im Start-Skelett). Jetzt gleitet nur ein
   Glanz-Pseudo-Element per transform darueber: Compositor, kein Repaint.
   Gleiche Farben (0.04 Grund + 0.06 Glanz = 0.10 Spitze), Richtung, Tempo. */
@keyframes skeleton-shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

.skeleton {
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.04);
  border-radius: var(--radius-sm, 6px);
  display: block;
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
  transform: translateX(-100%);
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
  pointer-events: none;
}

/* Stand-in for a stat/summary block. Was used in four places (profile,
   settings, reliability, notifications) with no rule at all, so it rendered
   0px high — every skeleton was shorter than its real content and the swap
   jumped. */
.skeleton-stat { height: 3.5rem; margin-bottom: 0.6rem; border-radius: var(--radius-tile); }

.skeleton-line { height: 0.85rem; margin-bottom: 0.5rem; }
.skeleton-line.short  { width: 40%; }
.skeleton-line.medium { width: 65%; }
.skeleton-line.long   { width: 90%; }
.skeleton-line.full   { width: 100%; }

.skeleton-card {
  padding: 1rem;
  border-radius: var(--radius-card, 18px);
  background: var(--surface, #1a1a1a);
  margin-bottom: 0.75rem;
  border: 1px solid var(--border, transparent);
}

.skeleton-row {
  height: 2rem;
  /* No margin-bottom: .skeleton-list already provides the gap, and having both
     produced 1.1rem between rows plus a dead 0.5rem strip at the end. */
  border-radius: var(--radius-sm);
}

/* Stand-in for a score ring (.rel-ring-large is 120px). */
.skeleton-ring {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  margin: 0.5rem auto 0.9rem;
  flex-shrink: 0;
}

/* KPI row: three stat blocks side by side, like the real breakdown header. */
.skeleton-stat-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.skeleton-stat-row .skeleton-stat { margin-bottom: 0; }

/* Shared loading veil for the settings and profile modals. Both previously
   built this inline; the profile one had no CSS class at all, so a change to
   one silently skipped the other. Sits BELOW .modal-sticky-header (z-index 5)
   so the close button stays clickable while data loads. */
.settings-skeleton-overlay,
.profile-skeleton-overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: var(--surface);
  border-radius: inherit;
  padding: 2rem 1.25rem;
  overflow: hidden;
}

.skeleton-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; display: none; }
}

/* === Install Banner ===
   War position:fixed unten mittig (z-index 900). Auf dem Handy lag die Karte
   damit dauerhaft ueber dem Inhalt: Ueberfaellig-Hinweis, Tabs und vor allem
   die klebenden .loan-actions-Leisten der Leihkarten (bottom:0) verschwanden
   darunter, dazu stapelten sich Toasts auf derselben Stelle. Jetzt steht der
   Hinweis im Fluss direkt unter dem Header (index.html) und scrollt mit weg
   wie ein Smart-App-Banner — er verdeckt nie etwas, braucht keinen
   Safe-Area-Abstand nach unten und keinen Abstand zu Bottom-Elementen.
   Breite/Rand folgen exakt main (Innenabstand 1rem bzw. 0.75rem <=820px,
   Deckel min(92vw, 1320px) ab 821px), damit er buendig ueber den Kacheln sitzt. */
.install-banner {
  position: relative;
  box-sizing: border-box;
  margin: 1rem max(0.75rem, env(safe-area-inset-right, 0px)) 0 max(0.75rem, env(safe-area-inset-left, 0px));
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--info) 9%, transparent), color-mix(in srgb, var(--info) 3%, transparent)),
    var(--surface2);
  border: 1px solid color-mix(in srgb, var(--info) 28%, var(--border));
  border-radius: var(--radius-card);
  padding: 0.8rem 0.9rem;
  display: flex;
  align-items: center;
  gap: 0.6rem 1rem;
  flex-wrap: wrap;
  animation: fadeInUp var(--motion-base) var(--ease-standard);
}
@media (min-width: 821px) {
  .install-banner {
    width: calc(min(92vw, 1320px) - 2rem);
    margin: 1.5rem auto 0;
    padding: 0.85rem 1.1rem;
  }
  /* main bringt oben selbst 1.5rem mit — ohne das doppelte sich der Abstand. */
  .install-banner:not(.hidden) ~ main { padding-top: 1rem; }
}
@media (max-width: 820px) {
  .install-banner:not(.hidden) ~ main { padding-top: 0.75rem; }
}

.install-banner.hidden { display: none; }

.install-banner-copy {
  /* 16rem Basis: wird es enger, rutschen die Knoepfe in eine eigene Zeile
     (rechtsbuendig) statt den Text auf 5-6 Zeilen zu quetschen. */
  flex: 1 1 16rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
}

.install-banner-copy strong { color: var(--text); font-size: 0.9rem; }
.install-banner-copy span  { color: var(--text-muted); }

.install-banner-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
  margin-left: auto;
}

.install-banner-actions .btn-primary,
.install-banner-actions .btn-secondary {
  padding: 0.4rem 0.85rem;
  min-height: var(--touch-target);
  font-size: 0.82rem;
  margin: 0;
}

.modal-body {
  flex: 1;
}

/* Gleiche Box und Typo wie .toggle-label-inline (Einstellungen, Zahlung,
   Taxi): vorher hatten Profil/Onboarding-Schalter ein engeres Padding und
   einen kleineren, dünneren Titel (0.85rem/600 statt 0.9rem/700). */
.toggle-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem;
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius);
  border: 1px solid rgba(255,255,255,0.06);
  background: rgba(255,255,255,0.025);
}

.toggle-label-text {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--text);
}

.toggle-desc {
  font-size: 0.76rem;
  color: var(--text-muted);
  margin-top: 0.1rem;
}

.profile-btn-full {
  width: 100%;
  justify-content: center;
}

.profile-push-status {
  font-size: 0.8rem;
  margin-top: 0.4rem;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-sm);
}

.profile-push-status:empty { display: none; }
.profile-push-unsupported { color: var(--text-muted); background: var(--surface2); }
.profile-push-blocked { color: var(--warning); background: color-mix(in srgb, var(--warning) 10%, transparent); }
.profile-push-install-required { color: var(--text-muted); background: var(--surface2); }

/* Visually separates "Limit pro Person" from "Globales Limit" above — same
   concept (a limit) but a different scope (one person vs. everyone), so the
   two blocks shouldn't read as one continuous list of fields. */
.lend-limit-per-borrower-group {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.lend-limit-per-borrower-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
/* Compact stand-in for the app's usual .empty-state — that component's 84px
   floating icon and generous padding are sized for a whole section (e.g.
   "Noch keine aktiven Leihen"), not a two-line list inside a settings card. */
.lend-limit-empty {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: var(--space-2) 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}
.lend-limit-empty svg { flex-shrink: 0; opacity: 0.7; }
.lend-limit-row {
  display: flex;
  align-items: center;
  /* app.js rendert den Auslastungsbalken als dritte, volle Zeile der
     Personen-Limit-Zeile (requests/app.md R5) — ohne flex-wrap quetscht er
     sich neben Name und Betrag. */
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  background: var(--surface2);
}
.lend-limit-row .lend-limit-usage { flex-basis: 100%; }
/* Inline-Eingabefeld beim Bearbeiten eines Personen-Limits.
   Live nachgemessen: das Feld hing komplett ohne App-Stil im Layout — WEISSER
   UA-Default mitten in einer dunklen Zeile, weil die gesamte Feldgestaltung an
   `.form-group input` haengt und diese Zeile kein .form-group ist. Deshalb hier
   dieselben Werte noch einmal, statt nur width. */
.lend-limit-edit-amount {
  width: 7rem;
  flex: 0 0 auto;
  min-height: var(--touch-target);
  /* --surface statt --surface2: die Zeile selbst ist schon --surface2, das
     Feld muss sich davon absetzen. */
  background: var(--surface);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.45rem 0.6rem;
  font-family: inherit;
  /* exakt 16px — alles darunter loest den iOS-Auto-Zoom beim Fokus aus. */
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
  -moz-appearance: textfield;   /* Firefox-Spinner weg, wie bei den Formularfeldern */
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.lend-limit-edit-amount:focus {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}
.lend-limit-edit-amount:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* .btn-secondary traegt `flex: 1`, .btn-primary `width: 100%` + margin-top —
   beides ist fuer Modal-Footer gedacht. In einer Listenzeile dehnte sich
   "Rueckgaengig" dadurch auf 240px und "Uebernehmen" sprang in eine eigene
   volle Zeile unter das Eingabefeld. In Zeilen sind das normale Inline-
   Buttons. */
.lend-limit-row > .btn-primary,
.lend-limit-row > .btn-secondary {
  flex: 0 0 auto;
  width: auto;
  margin-top: 0;
  padding: 0.5rem var(--space-3);
  font-size: var(--text-base);
}
/* flex-basis 7rem statt 0: mit `flex: 1` hatte der Name die Basis 0 und bekam
   nur den Rest, den die Betragsspalte uebrig liess. Auf 340px waren das 44px —
   "testuser" brach zusammen mit overflow-wrap:anywhere in "test/use/r".
   Jetzt konkurrieren beide Spalten beim Schrumpfen, die Auslastungszeile
   bricht zuerst um. */
/* Slightly smaller than the Users-modal default (34px) — this row is denser
   (name + amounts + action menu all share one line), the full size crowded it. */
.lend-limit-row-avatar { width: 28px; height: 28px; font-size: 0.8rem; flex-shrink: 0; }
/* Bearbeiten/Entfernen: derselbe Abstand wie jedes andere .entry-action-menu
   (0.6rem). Die frühere Sonderbreite 0.9rem riss die beiden Knöpfe sichtbar
   auseinander (Nutzer-Rückmeldung) – 44px-Knöpfe brauchen keinen Extra-Abstand. */
.lend-limit-row-name { flex: 1 1 7rem; min-width: 0; overflow-wrap: anywhere; }
.lend-limit-row-amounts { display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; min-width: 0; }
.lend-limit-row-amount { font-variant-numeric: tabular-nums; color: var(--text-muted); font-size: var(--text-sm); }
.lend-limit-row-amount-usage { font-variant-numeric: tabular-nums; font-size: var(--text-xs); color: var(--text-muted); }
.lend-limit-row-amount-usage[data-state="warn"] { color: var(--warning); }
.lend-limit-row-amount-usage[data-state="over"] { color: var(--danger); }
/* Select gets its own full-width row — at typical profile-modal widths it was
   the field most likely to force an awkward wrap when sharing a row with the
   amount input and button. Amount + button stay paired since both are short. */
.lend-limit-add-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.lend-limit-add-row select { width: 100%; }
.lend-limit-add-row-amount-row {
  display: flex;
  gap: var(--space-2);
}
.lend-limit-add-row-amount-row input { flex: 1 1 auto; min-width: 0; }
/* D8: flex-shrink:0 allein reichte nicht — der Label "+ Hinzufuegen" brach
   bei ALLEN Breiten 768-2560 auf zwei Zeilen und machte den Button hoeher als
   das Betragsfeld daneben. */
.lend-limit-add-row-amount-row button { flex-shrink: 0; white-space: nowrap; }

.profile-bank-status {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.1rem 0 0.15rem;
  padding-bottom: 0.05rem;
  overflow: visible;
}

.profile-bank-status:empty { display: none; }

.profile-bank-pill {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 0.3rem 0.55rem;
  border-radius: var(--radius-sm);
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1.25;
  background: rgba(255,255,255,0.06);
  color: var(--text-muted);
}

.profile-bank-pill[data-state="ok"] {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
}

.profile-bank-pill[data-state="warn"] {
  background: color-mix(in srgb, var(--warning) 18%, transparent);
  color: var(--warning);
}

.profile-bank-warning {
  flex-basis: 100%;
  margin: 0.1rem 0 0;
  color: var(--warning);
  font-size: 0.85rem;
}

.profile-bank-tip {
  flex-basis: 100%;
  margin: 0.1rem 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

.ob-push-status {
  font-size: 0.8rem;
  margin-top: 0.4rem;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-sm);
}
.ob-push-status:empty { display: none; }
.ob-push-unsupported { color: var(--text-muted); background: var(--surface2); }
.ob-push-install-required { color: var(--text-muted); background: var(--surface2); }

.link-btn {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font-size: inherit;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.profile-email-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.profile-email-input {
  flex: 1;
  min-width: 0;
}

.profile-session-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.82rem;
  padding: 0.2rem 0;
  gap: 0.75rem;
}

.profile-session-label { color: var(--text-muted); white-space: nowrap; }
.profile-session-value { color: var(--text); text-align: right; }

/* Header-User-Pill als Button */
#btn-open-profile {
  cursor: pointer;
  user-select: none;
}

html:not(.using-touch) #btn-open-profile:hover {
  border-color: rgba(255,255,255,0.18);
  background: rgba(255,255,255,0.06);
}

/* Pull-to-refresh indicator.
   PWA-Native: was a text pill ("Zum Aktualisieren ziehen") that dropped in
   from above the status bar and only reached ~20 % of its travel when the
   refresh armed (curve and threshold did not match). Now the native spinner:
   a round sheet slides out from UNDER the header (it sits one layer below
   it), its arc fills with the pull and turns, it pops once when armed (plus a
   haptic tick on Android), then spins in place while loading and ends on a
   check mark. The label stays in the DOM for app.js but is not shown — native
   spinners carry no text. Travel/progress are set by app.js via transform and
   --ptr-progress. */
.ptr-indicator {
  position: fixed;
  top: var(--app-header-h, calc(68px + env(safe-area-inset-top, 0px)));
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
  width: 2.5rem;
  height: 2.5rem;
  z-index: calc(var(--z-header) - 1);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface2);
  border: 1px solid var(--border-strong);
  box-shadow: 0 6px 16px rgba(0,0,0,0.45), 0 1px 3px rgba(0,0,0,0.3);
  color: var(--text-muted);
  transform: translate3d(0, -52px, 0);
  opacity: 0;
  pointer-events: none;
  will-change: transform;
  transition: opacity var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard);
}
.ptr-indicator.ptr-snap-back {
  transition:
    transform var(--motion-base) var(--ease-sheet),
    opacity var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}
.ptr-indicator:not([data-state="idle"]) { opacity: 1; }
.ptr-label { display: none; }
.ptr-spinner {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  transition: transform var(--motion-fast) var(--ease-spring);
}
.ptr-spinner-icon {
  width: 24px;
  height: 24px;
  display: block;
  overflow: visible;
  /* The arc starts at 12 o'clock and turns with the pull. */
  transform: rotate(calc(var(--ptr-progress, 0) * 270deg - 90deg));
}
.ptr-spinner-arc {
  /* pathLength=100 in index.html: the arc grows to 80 % of the circle. */
  stroke-dasharray: calc(var(--ptr-progress, 0) * 80) 100;
  transition: opacity var(--motion-fast) var(--ease-standard);
}
.ptr-spinner-check {
  opacity: 0;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.ptr-indicator[data-state="triggered"],
.ptr-indicator[data-state="refreshing"],
.ptr-indicator[data-state="done"] { color: var(--accent); }
.ptr-indicator[data-state="triggered"] .ptr-spinner { transform: scale(1.14); }
.ptr-indicator[data-state="refreshing"] .ptr-spinner-icon {
  animation: ptrSpin 0.9s linear infinite;
}
.ptr-indicator[data-state="refreshing"] .ptr-spinner-arc {
  stroke-dasharray: 60 100;
  animation: ptrArcBreath 1.4s var(--ease-standard) infinite alternate;
}
.ptr-indicator[data-state="done"] .ptr-spinner-icon { transform: none; }
.ptr-indicator[data-state="done"] .ptr-spinner-arc { opacity: 0; }
.ptr-indicator[data-state="done"] .ptr-spinner-check {
  opacity: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--motion-base) var(--ease-standard);
}
.ptr-indicator.ptr-leaving .ptr-spinner { transform: scale(0.4); }
.ptr-indicator.ptr-leaving { opacity: 0; }
@keyframes ptrSpin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
@keyframes ptrArcBreath {
  from { stroke-dasharray: 22 100; }
  to   { stroke-dasharray: 70 100; }
}

@media (prefers-reduced-motion: reduce) {
  .ptr-indicator,
  .ptr-indicator.ptr-snap-back { transition: opacity var(--motion-fast) linear; }
  .ptr-indicator[data-state="refreshing"] .ptr-spinner-icon,
  .ptr-indicator[data-state="refreshing"] .ptr-spinner-arc { animation: none; }
  .ptr-spinner,
  .ptr-spinner-check { transition: none; }
}

/* === Reminder Chips === */
.reminder-field {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

.reminder-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.reminder-chip {
  display: flex;
  align-items: center;
  gap: 0;
  cursor: pointer;
}

.reminder-chip input[type=checkbox] {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.reminder-chip span {
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.75rem;
  border-radius: var(--radius-xs);
  border: 1.5px solid var(--border);
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--text-muted);
  background: transparent;
  transition: var(--transition-ui);
  user-select: none;
}

.reminder-chip input:checked + span {
  background: var(--accent-btn);
  border-color: var(--accent-btn);
  color: #fff;
}

/* Same problem as .toggle-control: the checkbox is opacity:0/zero-size, so
   the global *:focus-visible outline needs redirecting onto the visible chip. */
.reminder-chip input[type=checkbox]:focus-visible + span {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

html:not(.using-touch) .reminder-chip:hover span {
  border-color: var(--accent);
}

.reminder-field--system-off {
  opacity: 0.45;
  pointer-events: none;
  position: relative;
}

.reminder-field--system-off::after {
  content: 'Systemweit deaktiviert';
  display: block;
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: 0.4rem;
  font-style: italic;
}

.settings-select {
  width: 100%;
  min-height: var(--touch-target);
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.65rem 0.9rem;
  font-size: 1rem;
  font-family: inherit;
  outline: none;
  margin-top: 0.6rem;
  transition: border-color var(--transition-fast);
  /* Native dropdown arrow (see .form-group select) instead of appearance:none. */
  -webkit-appearance: auto;
  appearance: auto;
}
.settings-select:focus { border-color: var(--accent); }
.settings-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* === Onboarding Modal === */
.onboarding-card {
  max-width: var(--modal-w-sm); /* an die sm-Stufe angeglichen (war 460px-Ausreißer) */
  text-align: center;
  /* Restore top padding removed by mobile sheet override; onboarding has no
     drag handle so the card needs its own breathing room at the top. */
  padding-top: 1.5rem;
}

#modal-onboarding .modal-sticky-header {
  margin-top: -1.5rem;
  padding-top: 1.5rem;
  padding-bottom: 0.9rem;
  background: var(--surface);
}
/* Nicht wegwischbar (NON_DISMISSIBLE_MODAL_IDS) — ein Griff würde etwas
   versprechen, das nicht geht. Der Kopf trägt stattdessen die Schritt-Punkte. */
#modal-onboarding .modal-drag-handle { display: none; }
#modal-onboarding .onboarding-steps { margin: 0; }

/* Mobile: lift the onboarding modal away from the viewport bottom edge.
   The global bottom-sheet rule (align-items: flex-end) is great for action
   modals but feels cramped for the welcome flow — each step has little
   content and the card ends up hugging the screen bottom. Center the
   onboarding overlay vertically so it reads as a classic welcome dialog. */
@media (max-width: 540px) {
  #modal-onboarding {
    align-items: center;
    /* On very small viewports (e.g. 375px) center+no-padding would clip the
       card — keep a small safe-area gap at top and bottom. */
    padding: calc(env(safe-area-inset-top, 0px) + 0.75rem) 0 calc(env(safe-area-inset-bottom, 0px) + 0.75rem);
  }
  #modal-onboarding .onboarding-card {
    /* Counteract the global mobile .modal-card border-radius (top corners
       only, flat bottom). Centered onboarding wants full rounded card. */
    border-radius: 18px;
    border-bottom: 1px solid rgba(255,255,255,0.09);
    /* Pull the card a touch in from the screen edges so it visually sits
       inside the safe-area rather than flush. */
    margin: 0 0.75rem;
    max-width: calc(100% - 1.5rem);
    /* On mobile the global sheet override zeroes all padding; restore it. */
    padding: 1.25rem 1.25rem 0;
    /* Allow the card to scroll when step 6 taxi-details are fully expanded. */
    max-height: calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 1.5rem);
    overflow-y: auto;
  }
  #modal-onboarding .modal-sticky-header {
    margin-top: -1.25rem;
    padding-top: 1.25rem;
  }
}

.onboarding-steps {
  display: flex;
  justify-content: center;
  gap: 0.4rem;
  margin: 0.1rem 0 1.4rem;
  padding: 0.15rem 0;
  overflow: visible;
}

.onboarding-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
  transition: background var(--motion-fast), transform var(--motion-fast);
}

.onboarding-dot-active {
  background: var(--accent);
  transform: scale(1.25);
}

.onboarding-dot-done {
  background: var(--accent);
  opacity: 0.45;
}

.onboarding-step { display: flex; flex-direction: column; align-items: center; }
/* Die Karte ist auf den längsten Schritt fixiert (_lockOnboardingCardHeight),
   der Schritt selbst war aber nur so hoch wie sein Inhalt: kurze Schritte
   klebten oben, die Buttons standen mitten in der Karte, darunter bis zu
   500px leere Fläche. Jetzt füllt der Schritt die Karte, der Inhalt sitzt
   mittig (auto-Margin am Icon) und die Buttons unten (margin-top:auto an
   .onboarding-actions) — bei jedem "Weiter" an derselben Stelle. */
.onboarding-step:not(.hidden) { flex: 1 0 auto; }
.onboarding-step > .onboarding-icon { margin-top: auto; }

.onboarding-icon {
  display: grid;
  place-items: center;
  width: 4.25rem;
  height: 4.25rem;
  margin-bottom: 1rem;
  border-radius: 50%;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
  line-height: 1;
  flex-shrink: 0;
}
.onboarding-icon svg { width: 34px; height: 34px; }

.onboarding-title {
  font-size: 1.35rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
  color: var(--text);
}

.onboarding-desc {
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1.5;
  max-width: 320px;
  margin-bottom: 1.4rem;
}

.onboarding-form-group {
  width: 100%;
  margin-bottom: 1.2rem;
}

.onboarding-form-group input {
  width: 100%;
  text-align: center;
  background: var(--surface2);
  border-color: var(--border-input);
  color: var(--text);
  -webkit-text-fill-color: var(--text);
  caret-color: var(--accent);
}

/* Mit sichtbarem Label (Auszahlungsdaten, Taxi-Aufschlag) linksbündig wie
   das Label darüber; zentriert bleibt nur das label-lose E-Mail-Feld. */
.onboarding-form-group:has(> label) input { text-align: left; }

.onboarding-form-group input:-webkit-autofill,
.onboarding-form-group input:-webkit-autofill:hover,
.onboarding-form-group input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--surface2) inset;
  -webkit-text-fill-color: var(--text);
  border-color: var(--border);
}

.onboarding-toggles {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-bottom: 1.2rem;
  text-align: left;
}

.onboarding-reminder-chips {
  justify-content: center;
  margin-bottom: 1.4rem;
}

.onboarding-actions {
  display: flex;
  gap: 0.6rem;
  justify-content: center;
  width: 100%;
  /* Keep buttons pinned at the bottom of the scrollable card so they never
     scroll out of view on content-heavy steps (e.g. step 6 with taxi details
     fully expanded on small phones). */
  position: sticky;
  bottom: 0;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--surface) 0%,  transparent) 0%,
    color-mix(in srgb, var(--surface) 45%, transparent) 45%,
    var(--surface) 70%,
    var(--surface) 100%
  );
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  padding-top: 0.9rem;
  padding-bottom: 1.25rem;
  padding-left: 2rem;
  padding-right: 2rem;
  margin-top: auto;
  margin-left: -2rem;
  margin-right: -2rem;
  margin-bottom: -1.25rem;
}

@media (max-width: 540px) {
  .onboarding-actions {
    /* Override desktop bleed (2rem card padding) with mobile card padding (1.25rem). */
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }
}

.onboarding-actions .btn-primary,
.onboarding-actions .btn-secondary {
  min-width: 0;
  min-height: var(--touch-target);
  /* Normalize the two button variants so they render identically in a row:
     .btn-primary's base `width:100%` + `margin-top:0.5rem` otherwise made the
     primary button narrower AND 8px shorter (its top margin ate into the
     stretched height), leaving it misaligned next to the secondaries. */
  width: auto;
  margin-top: 0;
  padding: 0.75rem 0.9rem;
}

/* 320–360px: drei Knöpfe (Zurück | Später | Weiter) in ~256px — die Labels
   liefen über die Knopfkanten ("Weiter" abgeschnitten). */
@media (max-width: 360px) {
  .onboarding-actions { gap: 0.4rem; }
  .onboarding-actions .btn-primary,
  .onboarding-actions .btn-secondary {
    padding-left: 0.35rem;
    padding-right: 0.35rem;
    font-size: 0.9rem;
  }
}

/* Single button: centered, natural width with a comfortable minimum. */
.onboarding-actions:has(> button:only-child) > button {
  flex: 0 0 auto;
  min-width: 160px;
}

/* Multi-button rows (2 or 3 buttons): every button shares the row equally, so
   they all have the same width and height regardless of label length. */
.onboarding-actions:has(> button + button) > button {
  flex: 1 1 0;
}

.onboarding-taxi-panel {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0;
  margin-bottom: 1.2rem;
  text-align: left;
  border: 1px solid var(--border, rgba(255,255,255,0.1));
  border-radius: var(--radius-btn);
  overflow: hidden;
  background: rgba(255,255,255,0.02);
}
.onboarding-taxi-panel > .toggle-field {
  padding: 0.8rem 1rem;
  /* Das Panel hat schon Rahmen + Fläche — kein zweiter Kasten im Kasten. */
  border: 0;
  border-radius: 0;
  background: transparent;
}
.onboarding-taxi-panel > .toggle-field:not(:last-child) {
  border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
}

.onboarding-taxi-details {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-top: 1px solid var(--border, rgba(255,255,255,0.08));
  background: rgba(255,255,255,0.02);
}

.onboarding-taxi-night-fields {
  gap: 0.5rem;
  width: 100%;
  margin: 0;
}

.onboarding-taxi-night-fields .form-group {
  flex: 1 1 80px;
  margin-bottom: 0;
}

.taxi-number-marker {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent, #0a7);
  color: #fff;
  border: 2.5px solid #fff;
  box-shadow: 0 3px 10px rgba(0,0,0,0.45);
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}
/* Route-position variants: green for the route start (pickup),
   red for the final destination, amber for intermediate stops.
   Improves at-a-glance readability when scanning the route on the map. */
.taxi-number-marker--start { background: var(--map-start); }
.taxi-number-marker--pickup { background: var(--map-pickup); }
.taxi-number-marker--end   { background: var(--map-end); }
.taxi-number-marker--stop  { background: var(--map-stop); }
.taxi-number-marker--return { background: var(--map-return); }
.taxi-number-marker.is-active {
  width: 34px;
  height: 34px;
  box-shadow: 0 0 0 4px rgba(255,255,255,0.22), 0 6px 18px rgba(0,0,0,0.52);
  transform: translateZ(0);
}

/* === Dashboard Actions === */
.dashboard-actions {
  margin-top: 0.75rem;
  display: flex;
  justify-content: flex-end;
}

.btn-create-loan {
  /* War 0.85rem — aus der Zeit, als der Knopf klein rechts sass. Heute ist er
     die volle Breite einnehmende Hauptaktion des Dashboards und stand mit kleinerer
     Schrift neben dem gleichrangigen "Leihe anfragen" (.btn-borrower-cta,
     1rem) und jedem Modal-Primaerknopf (1rem). Flex zentriert das Plus-Icon
     auf der Textmitte statt auf der Grundlinie. */
  font-size: 1rem;
  padding: 0.45rem 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
}

/* === Manual Entry: Message Field === */
.ml-message-row {
  margin-top: 0.6rem;
}

.ml-message-row .note-textarea {
  width: 100%;
}


/* Offline banner – shown by offline.js when navigator.onLine is false
   or while items are pending in the offline queue. */
.offline-banner {
  position: sticky;
  top: 0;
  z-index: var(--z-offline);
  width: 100%;
  /* Only fixed/sticky top-of-viewport element without a safe-area-inset-top
     allowance — it's the very first body child, so there's no header padding
     "in front of" it to absorb the notch/Dynamic-Island area on iOS. */
  padding: calc(0.55rem + env(safe-area-inset-top, 0px)) 1rem 0.55rem;
  /* PWA-Native: amber like the in-app strip (styles below) — green read as
     "all good" (login.html uses this base rule). */
  background: color-mix(in srgb, var(--warning) 20%, var(--surface));
  color: var(--text);
  font-size: 0.9rem;
  font-weight: 500;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
/* While a modal is open, body is position:fixed (scroll-lock) — the sticky
   banner then renders pinned at the viewport top regardless of scroll
   position, visually on top of the modal despite --z-offline being far above
   --z-modal. Drop it below the modal stack (mirrors the mobile fix a few
   breakpoints down, which uses the same reasoning for --z-offline-mob). */
body.modal-open .offline-banner {
  z-index: var(--z-header);
}

.offline-banner.hidden {
  display: none;
}
/* Title + detail (offline.js syncBanner). Also on login.html. */
.offline-banner-title { font-weight: 700; }
.offline-banner-detail::before { content: ' · '; }

/* PWA-Native (QA H4): in the app the strip sits directly UNDER the header
   (index.html order) and sticks there — like the "Keine Verbindung" strip of
   a native app. The old mobile rule parked it at top:68px with z-index 9,
   i.e. behind the 68px + safe-area header: invisible on every phone.
   --app-header-h is measured live by app.js. Amber for offline (green said
   "all good"), neutral for a draining queue, green for the short
   "Wieder online" hand-off. */
header + .offline-banner {
  position: sticky;
  top: var(--app-header-h, calc(68px + env(safe-area-inset-top, 0px)));
  z-index: calc(var(--z-header) - 1);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0 0.3rem;
  width: auto;
  margin-left: calc(-1 * env(safe-area-inset-left, 0px));
  margin-right: calc(-1 * env(safe-area-inset-right, 0px));
  padding: 0.42rem max(var(--page-gutter), env(safe-area-inset-right, 0px)) 0.42rem max(var(--page-gutter), env(safe-area-inset-left, 0px));
  font-size: 0.8rem;
  line-height: 1.3;
  font-weight: 500;
  color: var(--text);
  background: color-mix(in srgb, var(--warning) 20%, var(--surface));
  border-bottom: 1px solid color-mix(in srgb, var(--warning) 34%, transparent);
  box-shadow: none;
  animation: offlineStripIn var(--motion-base) var(--ease-standard) both;
}
header + .offline-banner .offline-banner-detail { color: var(--text-muted); }
header + .offline-banner .offline-banner-detail::before { content: none; }
header + .offline-banner::before {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  margin-right: 0.15rem;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--warning);
}
header + .offline-banner[data-mode="pending"] {
  background: color-mix(in srgb, var(--info) 14%, var(--surface));
  border-bottom-color: color-mix(in srgb, var(--info) 28%, transparent);
}
header + .offline-banner[data-mode="pending"]::before { background: var(--info); }
header + .offline-banner[data-mode="back-online"] {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface));
  border-bottom-color: color-mix(in srgb, var(--accent) 32%, transparent);
}
header + .offline-banner[data-mode="back-online"]::before { background: var(--accent); }
@keyframes offlineStripIn {
  from { opacity: 0; transform: translateY(-100%); }
}
@media (prefers-reduced-motion: reduce) {
  header + .offline-banner { animation: none; }
}

/* === Dashboard hero (slightly larger balance, but card-bound) === */
.dashboard-hero { min-width: 0; }
.dashboard-hero .dashboard-value {
  font-size: 1.7rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  line-height: 1.15;
  max-width: 100%;
  overflow-wrap: break-word;
  word-break: break-word;
}
@media (min-width: 1024px) {
  .dashboard-hero .dashboard-value { font-size: 1.9rem; }
}

.dashboard-grid-borrower {
  grid-template-columns: 1fr;
}
.dashboard-grid-borrower:has(.dashboard-card:nth-child(2)) {
  grid-template-columns: 1fr 1fr;
}
/* At the narrow end of mobile (2-column tiles ~150-170px), .borrower-progress-label's
   single-line ellipsis truncation cut the "Gesamt geliehen" summary ("€X
   zurückgezahlt · Y Bonus · Z abg.") down to an unreadable fragment with no
   "€" and no visible "…". The hero's own progress-label lines are short
   enough that wrapping never triggers for them, so this only changes
   anything for lines that were already too long to read anyway. */
@media (max-width: 600px) {
  .dashboard-grid-borrower .borrower-progress-label {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
  }
}

.borrower-progress-bar {
  height: 4px;
  background: rgba(255,255,255,0.1);
  border-radius: var(--radius-pill);
  margin: 0.65rem 0 0.3rem;
  overflow: hidden;
}
.borrower-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: var(--radius-pill);
  transition: width var(--motion-slow) var(--ease-standard);
}
.borrower-progress-label {
  font-size: 0.72rem;
  color: var(--text-muted);
  display: block;
  margin-top: 0.1rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.borrower-payment-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.borrower-payment-row {
  display: grid;
  grid-template-columns: 6rem 1fr auto;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.84rem;
  padding: 0.55rem 0.75rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.borrower-payment-date {
  color: var(--text-muted);
  font-size: 0.78rem;
  white-space: nowrap;
}
.borrower-payment-label {
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.borrower-payment-amount {
  font-weight: 700;
  color: var(--accent);
  white-space: nowrap;
  text-align: right;
}

@media (max-width: 480px) {
  .borrower-payment-row {
    grid-template-columns: 5rem 1fr auto;
    gap: 0.4rem;
  }
}

/* === Borrower hero extras === */
.borrower-tempo-hint {
  display: block;
  font-size: 0.72rem;
  color: var(--warning);
  margin-top: 0.35rem;
}
.due-list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin-top: 0.2rem;
  width: 100%;
}
.due-list-row {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}
.due-list-date {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--fg);
  line-height: 1.2;
}
.due-list-label {
  font-size: var(--text-xs);
  /* War rgba(255,255,255,.45) = 4.47:1. Gedimmte FARBE statt gedimmtem
     Element -> der Kontrast ist berechenbar (5.5:1 auf --surface2). */
  color: var(--text-dim);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.due-list-row--overdue .due-list-date {
  color: var(--danger, #d65a5a);
}
.due-list-row + .due-list-row {
  padding-top: 0.2rem;
  border-top: 1px solid rgba(255,255,255,0.07);
  margin-top: 0.1rem;
}

/* === Action banner (lender dashboard) === */
.action-banner {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.85rem 1rem;
  /* 1.5rem = Abstand von .section-dashboard: das Banner sitzt zwischen
     Dashboard und Suche, darueber und darunter soll derselbe Rhythmus gelten
     (vorher 24px oben / 20px unten). */
  margin: 0 0 1.5rem;
  border-radius: var(--radius-card);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  box-shadow: var(--shadow-card);
  animation: fadeInUp var(--motion-slow) var(--ease-standard) both;
}

/* Ein Banner, das den innerHTML-Swap "ueberlebt" hat (gleicher Text vorher wie
   nachher), bekommt die Eintrittsanimation nicht erneut. Ein Klick erzeugt zwei
   Renders — optimistischer Stand und Server-Wahrheit —, dadurch sprang es
   zweimal. render() setzt die Klasse, siehe _actionBannerKey in app.js. */
.action-banner.is-mounted { animation: none; }

.action-banner-accent {
  border-left-color: var(--accent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, transparent), color-mix(in srgb, var(--accent) 2%, transparent)), var(--surface);
}

.action-banner-warning {
  border-left-color: var(--warning);
  background: linear-gradient(180deg, color-mix(in srgb, var(--warning) 10%, transparent), color-mix(in srgb, var(--warning) 2%, transparent)), var(--surface);
}

/* "Alles erledigt" success banner: dezent + kompakt — keine fette Card. */
.action-banner.action-banner-success {
  padding: 0.45rem 0.75rem;
  margin: 0 0 0.75rem;
  gap: 0.5rem;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  box-shadow: none;
  font-size: 0.85rem;
}
.action-banner.action-banner-success .action-banner-icon {
  font-size: 0.95rem;
  width: auto;
  height: auto;
  background: transparent;
  color: var(--accent);
}
.action-banner.action-banner-success .action-banner-text strong {
  font-size: 0.85rem;
  font-weight: 700;
}
.action-banner.action-banner-success .action-banner-text span {
  font-size: 0.78rem;
}

.action-banner-icon {
  font-size: 1.4rem;
  line-height: 1;
}

.action-banner-text {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: 1;
  min-width: 0;
}

.action-banner-text strong {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}

.action-banner-text span {
  font-size: 0.8rem;
  color: var(--text-muted);
}

.action-banner-cta {
  flex-shrink: 0;
  width: auto;
  margin-top: 0;
}
/* Aktions-Hierarchie: im Ueberfaellig-Banner stand ein zweiter, vollflaechig
   gruener Primaer-Knopf ("Ansehen") direkt unter "Neue Leihe anlegen" —
   zwei gleich laute Hauptaktionen in einem Blick, und Gruen ("alles gut") auf
   einer Warnung. Tonal in der Bannerfarbe traegt er die Warnung weiter und
   laesst die eine Hauptaktion stehen. Gleiche Tonal-Form wie .btn-manual-entry. */
.action-banner-warning .action-banner-cta {
  background: color-mix(in srgb, var(--warning) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
  color: var(--warning);
}
html:not(.using-touch) .action-banner-warning .action-banner-cta:hover {
  background: color-mix(in srgb, var(--warning) 24%, transparent);
  border-color: var(--warning);
}

@media (max-width: 540px) {
  .action-banner {
    flex-wrap: wrap;
  }
  .action-banner-cta {
    width: 100%;
  }
}

/* Pulse highlight after scroll-to-section. */
.action-banner-pulse {
  animation: actionBannerPulse var(--motion-emphasis) var(--ease-standard);
}

@keyframes actionBannerPulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
  30%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 22%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
}

/* === Loan-card collapsible activity table === */
.loan-activity-collapsible {
  margin-top: 0.75rem;
}

/* flow-root: permanent block-formatting-context so .activity-table's own
   margin (0.85rem/0.4rem) stays contained regardless of the JS animation's
   temporary overflow:hidden toggle. Without this, animateCollapsible()
   measures scrollHeight while overflow:hidden traps that margin inside the
   box, then clears overflow on cleanup — the margin collapses out and the
   just-opened content visibly shrinks by ~20px right after the animation. */
.loan-activity-content {
  display: flow-root;
}

.btn-activity-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  width: 100%;
  padding: 0.55rem 0.75rem;
  margin-top: 0.25rem;
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: var(--radius-btn);
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  transition: border-color var(--transition), color var(--transition), background var(--transition);
}

html:not(.using-touch) .btn-activity-toggle:hover {
  border-color: var(--text-muted);
  color: var(--text);
  background: rgba(255,255,255,0.02);
}

/* Sticky (expanded) state needs an opaque hover background — the default
   hover above is a near-transparent white wash, fine over a normal card
   background but see-through once this button is floating over scrolling
   content underneath it. */
html:not(.using-touch) .btn-activity-toggle.expanded:hover {
  background: var(--surface);
}
/* Touch-Audit R4: Timeline-Umschalter ("Verlauf anzeigen" / "Weniger
   anzeigen") hatte keinen Druckzustand. Deckende Toenung, weil der
   ausgeklappte Knopf klebend ueber scrollendem Inhalt schwebt; doppelt
   formuliert gegen die spezifischeren Maus-Hover-Regeln oben. */
.btn-activity-toggle:active,
.btn-activity-toggle.expanded:active,
html:not(.using-touch) .btn-activity-toggle:active,
html:not(.using-touch) .btn-activity-toggle.expanded:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  background: color-mix(in srgb, var(--text) 7%, var(--surface));
  color: var(--text);
  transition-duration: 0s;
}

.btn-activity-toggle .chevron {
  display: inline-block;
  transition: transform var(--transition);
}

.btn-activity-toggle[aria-expanded="true"] .chevron,
.btn-activity-toggle.expanded .chevron {
  transform: rotate(90deg);
}

/* Sticky close button: once expanded, "Weniger anzeigen" follows scroll and
   stays reachable near the bottom of the viewport instead of requiring a
   scroll past the whole timeline to close it again. Sticks only within its
   own parent's box (.loan-activity-collapsible), so it settles back into its
   normal in-flow spot once that box's end scrolls into view — no JS needed.
   Applies on mobile and desktop alike (unlike the mobile-only .loan-actions
   bar above), since a long timeline can outgrow the viewport either way. */
.btn-activity-toggle.expanded {
  position: sticky;
  /* Flush to the viewport edge — no gap for scrolling content to peek
     through underneath. The safe-area is absorbed as padding (like the
     .loan-actions bar below) so the label stays clear of the home
     indicator instead of being pushed off-screen by an outer offset. */
  bottom: 0;
  padding-bottom: calc(0.55rem + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-dropdown);
  background: var(--surface);
}

/* Shadow only while actually pinned away from its natural resting spot
   (app.js toggles .is-stuck via a rect comparison, no IntersectionObserver/
   sentinel needed). Without this gate every expanded toggle would carry the
   shadow permanently, including short timelines that never leave their
   normal in-flow position — a floating-over-nothing look. */
.btn-activity-toggle.expanded.is-stuck {
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}

/* Active loans also show the sticky .loan-actions bar (Ergänzung/Zahlung/
   Abschließen) flush to the card's bottom edge on mobile — see "Sticky
   action bar on mobile loan-cards" above. Both are position:sticky with a
   bottom offset, so without an offset here the toggle would sit at the same
   viewport slot and get covered by the bar (later in DOM/paint order). Push
   it up to clear the bar instead. Desktop never renders that bar as sticky
   (mobile-only media query), so the flush-to-edge default above is correct
   there. --loan-actions-h is measured live by a ResizeObserver in app.js
   (_wireLoanActionsHeight) so this tracks the bar's actual rendered height
   (varies by loan state/button count) instead of a guessed constant; the
   4.5rem fallback only applies for the brief window before the observer's
   first callback fires (or if ResizeObserver is unavailable). */
@media (max-width: 600px) {
  .loan-card:has(.loan-actions) .btn-activity-toggle.expanded {
    bottom: calc(var(--loan-actions-h, 4.5rem) + 0.5rem);
    padding-bottom: 0.55rem; /* not touching the physical edge here — no safe-area needed */
  }
  /* Opaque tail below the toggle so nothing shows through in the clearance
     above: a small buffer past the measured bar height covers any timing
     gap between a resize and the observer's next callback. */
  .loan-card:has(.loan-actions) .btn-activity-toggle.expanded::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: calc(var(--loan-actions-h, 4.5rem) + 1.5rem);
    background: var(--surface);
    pointer-events: none;
  }
}

/* === Sticky action bar — NUR auf Phones ===
   D7: Bei 768x1024 (iPad Mini Portrait) stehen mehrere Loan-Cards untereinander
   im Viewport. Jede Karte bringt ihre eigene sticky .loan-actions-Bar mit, und
   die Bar der NAECHSTEN Karte verdeckte deren eigene Kopfzeile (Betrag +
   Fortschrittsring), waehrend die aktuelle Bar noch inline darueber stand —
   zwei gestapelte Balken. Auf einem Phone ist immer hoechstens eine Karte
   sichtbar, dort ist das Muster richtig. 600px ist die Phone/Tablet-Grenze,
   die auch das Dashboard-Raster benutzt (D4). D5 (die alte Luecke 769-820px
   zwischen Bar- und Padding-Breakpoint) erledigt sich damit mit. */
@media (max-width: 600px) {
  .loan-card {
    /* Generous bottom padding so the sticky action bar floats above content
       without overlapping or feeling cramped against the last row. */
    padding-bottom: 2rem;
  }
  /* Cards that contain a sticky action bar drop their bottom corner-radius:
     the bar bleeds to the very bottom of the card and the corner mismatch
     between the rounded card border and the rectangular bar would otherwise
     show a clipped sliver of the (often coloured, e.g. red for overdue)
     card border at each bottom corner — the user-perceived "abgeschnittene
     Ecke" bug. With flat bottom corners on cards-with-bar the bar sits
     perfectly flush. Top corners stay rounded so the card still reads as a
     card; the rectangular bottom looks intentional like a card with footer. */
  .loan-card:has(> .loan-actions) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }
  .loan-card .loan-actions {
    position: sticky;
    /* Anchor flush to the viewport bottom edge. The home-indicator clearance
       is absorbed as padding-bottom INSIDE the bar (iOS best practice for
       bottom action bars): the bar background extends all the way down to
       the screen edge, the buttons sit safely above the home indicator.
       This avoids the previous "bar hovers 34 px above the bottom" look. */
    bottom: 0;
    /* Min-height so a 2-button row like pending (Approve/Reject) reads as a
       proper action area, not a thin dark strip. */
    min-height: 3rem;
    display: flex;
    gap: 0.5rem;
    align-items: center;
    padding: 0.65rem 0.75rem calc(0.65rem + env(safe-area-inset-bottom, 0px));
    /* Bleed to the card edge: negative horizontal margin = card horizontal
       padding (1.25rem); negative bottom margin = card bottom padding (2rem
       on mobile, override above) + 1 px to overlap the card border so the
       border line doesn't poke through under the bar. */
    margin: 1rem -1.25rem calc(-2rem - 1px);
    background: linear-gradient(
      to top,
      var(--surface) 70%,
      color-mix(in srgb, var(--surface) 50%, transparent) 90%,
      transparent
    );
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    /* Must exceed any composited child z-index (e.g. Leaflet tiles) within
       sibling cards — keep below modals (--z-modal: 100). */
    z-index: var(--z-dropdown);
  }
  /* Buttons in the sticky bar should stretch to fill — equal-share row.
     :where() keeps this selector at (0,1,1) so the later ≤540px block can win
     inside a loan card too. At (0,2,1) it silently beat that block's
     `flex: 1 1 calc(50% - .2rem)`, so the intended two-column wrap never
     applied in cards and buttons sharing the row with a status badge
     collapsed to 27-39px with clipped labels. */
  :where(.loan-card) .loan-actions > button { flex: 1; }
}

/* Entscheidungs-Reihe einer Anfrage (Ablehnen · ⇄ Gegenangebot · Genehmigen):
   3 Buttons passen auf schmalen Phones nicht nebeneinander — "Gegenangebot" lief
   sonst aus dem Button. Erst komplett vertikal gestapelt (volle Breite, voller
   Text), das sah aber neben der Aktiv-Tab-Bar direkt darunter (Ergänzung ·
   Zahlung eintragen · Abschließen) inkonsistent aus. Jetzt dasselbe 2-Zeilen-
   Grid wie dort: die beiden häufigeren/primären Entscheidungen (Ablehnen ·
   Genehmigen) nebeneinander, die selteneren Gegenangebot-Alternative darunter
   über die volle Breite — gleiches Muster, gleiche Zeilenzahl. Taxi-Anfragen
   rendern .btn-taxi-counter-offer statt .btn-counter-offer und haben dieselben
   drei Buttons — sie waren hier ursprünglich als "2 kurze Buttons" eingestuft
   und fielen deshalb aus der Regel; gemessen lief "⇄ Gegenangebot" dort ab
   390px aus dem Button. */
@media (max-width: 540px) {
  .loan-card .loan-actions:has(.btn-counter-offer),
  .loan-card .loan-actions:has(.btn-taxi-counter-offer) {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "reject approve"
      "counter counter";
    align-items: stretch;
    min-height: 0;
  }
  .loan-card .loan-actions:has(.btn-counter-offer) .btn-reject,
  .loan-card .loan-actions:has(.btn-taxi-counter-offer) .btn-reject { grid-area: reject; }
  .loan-card .loan-actions:has(.btn-counter-offer) .btn-approve,
  .loan-card .loan-actions:has(.btn-taxi-counter-offer) .btn-approve { grid-area: approve; }
  .loan-card .loan-actions:has(.btn-counter-offer) .btn-counter-offer,
  .loan-card .loan-actions:has(.btn-taxi-counter-offer) .btn-taxi-counter-offer { grid-area: counter; }
  /* Leihnehmer-Karte (Zurückziehen + Gegenangebot prüfen) trifft dasselbe
     Raster, hat aber weder Ablehnen noch Genehmigen: "Zurückziehen" landete
     halb breit in der reject-Zelle, rechts daneben eine leere Zelle. */
  .loan-card .loan-actions:has(.btn-counter-offer) :is(.btn-withdraw, .btn-addition-withdraw),
  .loan-card .loan-actions:has(.btn-taxi-counter-offer) :is(.btn-withdraw, .btn-addition-withdraw) { grid-column: 1 / -1; }
  .loan-card .loan-actions:has(.btn-counter-offer) > button,
  .loan-card .loan-actions:has(.btn-taxi-counter-offer) > button {
    min-width: 0;
    overflow-wrap: break-word;
    font-size: 0.85rem;
  }

  /* Aktiv-Tab Bottom-Bar (+ Ergänzung · Zahlung eintragen · Abschließen): drei
     gleich breite Buttons in einer Flex-Reihe brauchten auf schmalen Phones
     mehr Höhe als eine Karte dafür hergeben sollte. 2-Zeilen-Grid statt einer
     Reihe: Ergänzung und Zahlung nebeneinander (je 50%), Abschließen darunter
     über die volle Breite — eine Zeile weniger als drei gestapelte Buttons,
     ohne das lange "Zahlung eintragen"-Label zu quetschen. Immer exakt diese
     drei Buttons in dieser Reihe (kein Badge, keine bedingte Auslassung), die
     festen grid-areas sind daher sicher. */
  .loan-card .loan-actions:has(.btn-add-payment) {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "add   pay"
      "close close";
    align-items: stretch;
    min-height: 0;
    padding-top: 0.5rem;
    padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
  }
  .loan-card .loan-actions:has(.btn-add-payment) .btn-manual-entry { grid-area: add; }
  .loan-card .loan-actions:has(.btn-add-payment) .btn-add-payment  { grid-area: pay; }
  .loan-card .loan-actions:has(.btn-add-payment) .btn-close-loan   { grid-area: close; }
  .loan-card .loan-actions:has(.btn-add-payment) > button {
    min-width: 0;
    /* break-word (not anywhere): breaks between words first, only falling
       back to a mid-word break as a last resort — "anywhere" was breaking
       "Ergänzung"/"Abschließen" mid-word even at widths where a smaller
       font would have let them fit as whole words (see font-size below). */
    overflow-wrap: break-word;
    font-size: 0.85rem;
  }
}

/* Prominent pending-additions section on the lender dashboard. */
.pending-additions-section {
  margin: 1rem 0 1.5rem;
}
.pending-additions-section .section-title {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 1rem; font-weight: 700;
  margin: 0 0 0.75rem 0.5rem;
  letter-spacing: var(--label-tracking);
}
.section-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.5rem; height: 1.5rem; padding: 0 0.4rem;
  border-radius: var(--radius-sm);
  background: var(--warning);
  color: var(--bg, #0f0f0f);
  font-size: 0.75rem; font-weight: 800;
}
.pending-additions-list {
  display: grid; gap: 0.75rem;
}
.pending-addition-card {
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--warning) 25%, transparent);
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-card, 18px);
  padding: 1rem;
  display: grid; gap: 0.5rem;
  /* minmax(0, 1fr) statt der impliziten auto-Spalte: ein langer Nutzername
     (32 Zeichen, keine Leerzeichen) im Kopf setzte die Mindestbreite der
     Spalte — auf 320px ragte die ganze Karte 23px über den Bildschirmrand,
     rechts abgeschnitten samt Knöpfen. */
  grid-template-columns: minmax(0, 1fr);
}
.pending-addition-head {
  display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
}
/* A11y R5: der Kartenkopf ist ein <header> und erbte die Regeln der App-Leiste
   (`header { position: sticky; top: 0; z-index … }` samt Glas-::after) — beim
   Scrollen klebte „P_BORROWER · Anfrage · Taxi-Ergänzung“ über der App-Leiste
   und verdeckte darunter fokussierte Elemente (WCAG 2.4.11). */
header.pending-addition-head {
  position: static;
  z-index: auto;
  margin: 0;
  padding: 0;
  border-bottom: 0;
  -webkit-user-select: auto;
  user-select: auto;
  -webkit-touch-callout: default;
}
header.pending-addition-head::before,
header.pending-addition-head::after { content: none; }
.pending-addition-head .kicker {
  font-size: var(--label-size); font-weight: var(--label-weight); text-transform: uppercase;
  letter-spacing: var(--label-tracking); color: var(--text-muted);
  min-width: 0;
  overflow-wrap: anywhere;
}
.pending-addition-badges { flex-shrink: 0; }
.pending-addition-badges {
  display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; justify-content: flex-end;
}
.pending-addition-amount {
  font-size: 1.4rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.pending-addition-meta {
  font-size: 0.85rem; color: var(--text-muted);
}
.pending-addition-context {
  font-size: 0.8rem; color: var(--text-muted);
  padding-top: 0.25rem;
  border-top: 1px solid rgba(255,255,255,0.05);
}
/* Disbursement / payout-account / return-method details on a pending addition.
   Compact key→value rows so the lender can see HOW the borrower wants the
   money delivered (IBAN!) without opening the loan card first. */
.pending-addition-details {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.82rem;
  padding: 0.5rem 0.75rem;
  background: rgba(255,255,255,0.025);
  border-radius: var(--radius-xs);
  /* Auf Breitbild (Karte ~1140px) standen Bezeichnung und Wert 1000px auseinander
     und das Auge verlor die Zeile; die Liste bleibt lesbar schmal. */
  max-width: 46rem;
}
.pending-addition-detail {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.6rem;
  min-width: 0;
}
.pending-addition-detail-label {
  color: var(--text-muted);
  font-weight: var(--label-weight);
  font-size: var(--label-size);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  white-space: nowrap;
}
.pending-addition-detail-iban {
  font-family: 'SF Mono', Menlo, monospace;
  font-size: 0.78rem;
  word-break: break-all;
  text-align: right;
}
.pending-additions-section .pending-addition-actions {
  display: flex; gap: 0.5rem; margin-top: 0.25rem;
}
/* Equal column width AND identical box geometry. The three buttons that
   live here (.btn-approve-addition primary, .btn-counter-offer warning,
   .btn-reject-addition secondary) come from different style clusters with
   different padding / font-size — flex: 1 alone aligns the columns but
   the visible button bodies still look uneven because their content boxes
   are different sizes. Force a single padding + font-size + min-height so
   the three buttons read as one consistent action row. */
.pending-additions-section .pending-addition-actions button {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  margin-top: 0;
  padding: 0.55rem 0.7rem;
  min-height: var(--touch-target);
  font-size: 0.82rem;
  font-weight: 600;
  /* Tokens instead of the previous hardcoded 8px / 44px: these sit next to
     .loan-actions buttons (12px) and read as a different button shape. */
  border-radius: var(--radius-btn);
  line-height: 1.2;
  /* Same reasoning as .modal-actions > button: wrap rather than clip, so a
     long label grows the button instead of losing its tail to an ellipsis. */
  white-space: normal;
  overflow-wrap: break-word;
}
@media (max-width: 540px) {
  .pending-additions-section .pending-addition-actions {
    flex-direction: column;
  }
  .pending-additions-section .pending-addition-actions button {
    white-space: normal;
  }
}

/* Segment tabs (pill switcher) for the lender view. */
/* Global loan-search bar above the tab-bar in the lender view. Live-filter
   across borrower, notes, amount, date, addition purpose, payment note. */
.loan-search-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--surface);
  /* Die Bar ist die sichtbare Grenze des Suchfelds -> --border-input (3:1). */
  border: 1px solid var(--border-input);
  border-radius: var(--radius-tile);
  /* D10: Die vertikale Polsterung sass frueher auf der Bar. Ein Tap darauf
     landete im Container, nicht im Feld, und fokussierte nicht. Sie liegt
     jetzt im Input selbst (siehe .loan-search-input), damit die komplette
     Bar-Hoehe Trefferflaeche des Felds ist. */
  padding: 0 0.75rem;
  margin: 0 0 0.6rem 0;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  /* iOS 26 / iPhone 17 Pro WebKit: explicit min-height prevents collapse
     to ~1px stripe when the flex children compute weird intrinsic sizes. */
  min-height: 44px;
  width: 100%;
  box-sizing: border-box;
}
.loan-search-bar:focus-within {
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
/* N15: Das Feld selbst hat outline:none, die Leiste zeigte beim Fokus nur
   einen 18-%-Schimmer (Kontrast ~1.3:1) — mit der Tastatur war nicht zu sehen,
   wo der Fokus steht. Jetzt derselbe 2-px-Akzentring wie überall. */
.loan-search-bar:has(.loan-search-input:focus-visible) {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* In a modal the search bar sits right under the opaque sticky header, which
   painted over the top of the outer ring (2px offset + 3px glow — "Markierung
   oben abgeschnitten", globale Suche). Inside cards the ring is drawn inward,
   like every other form field there (.modal-card :is(input…):focus-visible). */
.modal-card .loan-search-bar:focus-within {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}
.modal-card .loan-search-bar:has(.loan-search-input:focus-visible) {
  outline-offset: -2px;
}
.loan-search-icon {
  font-size: 0.95rem;
  opacity: 0.7;
  pointer-events: none;
}
.loan-search-input {
  flex: 1;
  min-width: 0;
  /* D10: Das Feld war nur 28 px hoch, die Bar wirkt 44 px. align-self:stretch
     + die von der Bar hierher verschobene Polsterung machen das Input so hoch
     wie die Bar — ohne die Bar wachsen zu lassen. */
  align-self: stretch;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  /* 16px exact on mobile prevents iOS Safari from zooming in on focus.
     Anything < 16px triggers the iOS auto-zoom. */
  font-size: 16px;
  font-family: inherit;
  padding: 0;
  /* Mobile audit: on narrow viewports the placeholder ("Suchen: Name, Zweck,
     Betrag, Datum…") got clipped mid-word at the field's edge — an empty
     text input has no built-in truncation, it just hard-clips. Ellipsis
     reads as "there's more text, not a typo/cut-off word". Only affects the
     placeholder in practice: typed queries are short and this field never
     shows a value longer than what fits while focused. */
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Nur mit Maus: Landscape-Phones (844px) fielen sonst unter 16px -> iOS-Zoom. */
@media (min-width: 821px) and (hover: hover) {
  .loan-search-input { font-size: 0.95rem; }
}
.loan-search-input::placeholder { color: var(--text-muted); opacity: 0.7; }
.loan-search-input::-webkit-search-cancel-button { display: none; }
/* Befund 21: Zwei optisch identische ✕ mit gegensaetzlicher Wirkung standen
   nebeneinander — "Modal schliessen" (40px, gefuellt) und "Suchfeld leeren"
   (36px, gefuellt). Der Clear-Button ist die harmlosere Aktion und wird
   rahmen-/flaechenlos, damit die beiden nicht mehr verwechselbar sind. */
.loan-search-clear {
  background: transparent;
  border: none;
  color: var(--text-muted);
  /* Visual size stays small (26 px), but the touch-area is bumped via padding
     to satisfy WCAG 2.5.5 minimum touch-target on mobile. */
  min-width: var(--touch-target);
  min-height: var(--touch-target);
  padding: 9px;
  border-radius: var(--radius-xs);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background var(--transition-fast), color var(--transition-fast);
}
html:not(.using-touch) .loan-search-clear:hover,
.loan-search-clear:focus-visible {
  background: rgba(255,255,255,0.14);
  color: var(--text);
}
.loan-search-clear.hidden { display: none; }
.loan-search-empty {
  color: var(--text-muted);
  font-size: 0.9rem;
  text-align: center;
  padding: 1.5rem 1rem;
  margin: 0;
  /* CSS animations (unlike transitions) auto-play on insertion, so this needs
     no JS change even though applySearchFilter() (app.js) just appendChild()s
     this element fresh on every "no matches" state. */
  animation: loanSearchEmptyIn var(--motion-fast) var(--ease-standard);
}
@keyframes loanSearchEmptyIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

.global-search-results {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Befund 45: 60vh, waehrend die App sonst durchgehend dvh nutzt — auf
     Mobil-Browsern mit einfahrender URL-Leiste weicht das sichtbar ab.
     vh zuerst als Fallback fuer UAs ohne dvh. */
  max-height: 60vh;
  max-height: 60dvh;
  overflow-y: auto;
  /* Scroll-Fade: die dritte Trefferzeile wurde hart abgeschnitten, ohne dass
     etwas "es geht weiter" signalisierte. Die zwei `local`-Ebenen decken den
     Schatten ab, solange der jeweilige Rand erreicht ist — der Fade erscheint
     also automatisch nur dann, wenn wirklich gescrollt werden kann. */
  background:
    linear-gradient(var(--surface) 30%, rgba(26,26,26,0)) top    / 100% 20px no-repeat local,
    linear-gradient(rgba(26,26,26,0), var(--surface) 70%) bottom / 100% 20px no-repeat local,
    linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0))      top    / 100% 12px no-repeat scroll,
    linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.35))      bottom / 100% 12px no-repeat scroll;
}
.global-search-hint {
  color: var(--text-muted);
  font-size: var(--text-base);
  text-align: center;
  padding: var(--space-5) var(--space-4);
  margin: 0;
}
/* Trefferzahl klebte direkt unter dem Suchfeld (≈5px) und stand gleich
   groß wie die Ergebnis-Titel. */
.global-search-count {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
}
/* Suche als feste Fläche statt inhaltsgetrieben: bei jedem Tastendruck
   änderte sich die Trefferzahl und damit die Kartenhöhe — das Bottom-Sheet
   wuchs/schrumpfte nach oben, am Desktop sprang das zentrierte Fenster samt
   Suchfeld auf und ab. Mobil: volle Sheet-Höhe, Liste füllt den Rest.
   Desktop: oben verankert (Command-Palette), wächst nur nach unten.
   R5-Tablet: Sheet nur auf dem Handy, Tablet hochkant wie Desktop. */
@media (max-width: 540px) {
  #modal-global-search .modal-card {
    /* minus --modal-kb-inset: with the iOS keyboard open the overlay is
       padded up by the inset — without subtracting it here the full-height
       card pushed itself to the very top edge (2nd open after the keyboard
       had been shown/hidden). */
    height: calc(100dvh - max(3.75rem, calc(env(safe-area-inset-top, 0px) + 2.75rem)) - 0.5rem - var(--modal-kb-inset));
  }
  #modal-global-search .global-search-results {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  }
}
@media (min-width: 541px) and (min-height: 501px) {
  #modal-global-search { align-items: flex-start; padding-top: max(10vh, 2rem); }
}
/* Die Suche hat keine Fußleiste: ohne eigenen Abstand lief die letzte
   Trefferzeile bündig in die Kartenkante (Rahmen unten abgeschnitten). */
@media (min-width: 541px) {
  #modal-global-search .global-search-results { padding-bottom: 1.5rem; }
}
@media (max-width: 540px) {
  /* "Keine Treffer fuer „<query>“" echoes the typed search text — a long
     query without spaces (or a pasted URL) otherwise overflows the modal. */
  .global-search-hint { overflow-wrap: anywhere; }
}
.global-search-group h3 {
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 var(--space-2);
}
.global-search-group {
  display: flex;
  flex-direction: column;
}
.global-search-group > h3 + .global-search-row,
.global-search-row + .global-search-row {
  margin-top: var(--space-2);
}
.global-search-row {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  background: var(--surface2);
  color: var(--text);
  cursor: pointer;
  min-height: var(--touch-target);
  justify-content: center;
}
html:not(.using-touch) .global-search-row:hover,
.global-search-row:focus-visible {
  border-color: var(--accent);
}
.global-search-row-kicker {
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.global-search-row-title {
  font-size: var(--text-base);
  font-weight: 600;
  overflow-wrap: anywhere;
}
.global-search-row-meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.global-search-highlight {
  background: color-mix(in srgb, var(--accent) 35%, transparent);
  color: var(--text);
  border-radius: 0.2em;
  padding: 0 0.1em;
}

.tab-bar {
  /* Defensive layout: flex over grid (Safari grid-auto-columns bugs), no
     999px pill radius on container (rendered as full-height stripe on some
     iOS versions when children collapsed). Subtle rounded rectangle. */
  display: flex;
  gap: 4px;
  padding: 4px;
  background: rgba(255, 255, 255, 0.04);
  border-radius: var(--radius-segmented);
  margin: 0 0 1rem;
  width: 100%;
  box-sizing: border-box;
  min-height: 44px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-bar > .tab-pill {
  /* Schrumpfen erlaubt (war 1 0 7rem): drei Tabs à 7rem + Abstände = 352px,
     auf 320px blieben 288px — die Leiste scrollte und schnitt "Ausstehend"
     links an ("tehend"), sobald ein anderer Tab aktiv war. min-width:
     max-content bleibt die Untergrenze, gescrollt wird nur noch, wenn der
     Text wirklich nicht passt. */
  flex: 1 1 7rem;
  width: auto;
  min-width: max-content;
  min-height: var(--touch-target);
  box-sizing: border-box;
  scroll-snap-align: center;
}
.tab-pill {
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.9rem;
  padding: 0.55rem 0.75rem;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: var(--touch-target, 44px);
  flex-shrink: 0;
}
/* Excluded while selected — otherwise this outweighs .tab-pill.is-active and
   repaints the accent-green label as plain text under the cursor. */
html:not(.using-touch) .tab-pill:hover:not(.is-active):not([aria-selected="true"]) {
  color: var(--text);
}
.tab-pill.is-active,
.tab-pill[aria-selected="true"] {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  /* --accent-text statt --accent: auf der Gruen-Toenung 4.3:1 -> 5.2:1 (AA). */
  color: var(--accent-text);
  /* Auswahl als Unterstrich, NICHT als outline: outline ist global als
     Fokusring belegt (*:focus-visible, 2px solid var(--accent)). Vorher trugen
     "ausgewaehlt" und "fokussiert" exakt denselben Ring — Tastaturnutzer
     konnten "wo bin ich" und "was ist gewaehlt" nicht unterscheiden.
     box-shadow statt border-bottom, damit die Pillen-Hoehe gleich bleibt. */
  box-shadow: inset 0 -3px 0 0 var(--accent);
  /* Nest the accent ring concentrically in the tab-bar corners instead of
     inheriting the full 999px pill radius, which read as too round against the
     14px/4px-padding container — most visible on the outer tabs. */
  border-radius: var(--radius);
}
/* === Motion R2: gleitender Segment-Indikator ===
   app.js syncSegIndicator() setzt .has-seg-indicator + --seg-x/y/w/h auf der
   Leiste; EIN ::before wandert dann vom alten zum neuen Segment, statt dass
   zwei Hintergruende hart wechseln. Die Segmente selbst werden transparent,
   ihre Textfarbe bleibt der Zustandstraeger. Ohne JS: alter Look (Fallback).
   isolation + z-index -1: Indikator liegt ueber dem Leistengrund, unter den
   Segment-Labels. Liegt in scrollbaren Leisten im Scroll-Inhalt (scrollt mit). */
#main-content .tab-bar,
#main-content .perspective-switch,
.fb-modes {
  position: relative;
  isolation: isolate;
}
.has-seg-indicator::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: var(--seg-w, 0px);
  height: var(--seg-h, 0px);
  transform: translate(var(--seg-x, 0px), var(--seg-y, 0px));
  border-radius: var(--radius);
  z-index: -1;
  pointer-events: none;
  transition:
    transform var(--motion-base) var(--ease-standard),
    width var(--motion-base) var(--ease-standard),
    height var(--motion-base) var(--ease-standard),
    background-color var(--transition-fast),
    box-shadow var(--transition-fast);
}
.has-seg-indicator.seg-instant::before { transition: none; }
.tab-bar.has-seg-indicator::before {
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
  box-shadow: inset 0 -3px 0 0 var(--accent);
}
.tab-bar.has-seg-indicator > .tab-pill.is-active,
.tab-bar.has-seg-indicator > .tab-pill[aria-selected="true"] {
  background: transparent;
  box-shadow: none;
}
.perspective-switch.has-seg-indicator::before { background-color: var(--accent); }
.perspective-switch.has-seg-indicator > .perspective-tab.is-active { background: transparent; }
/* R13-Anim: Rückmeldungs-Reiter und Karte/Stopps der Taxi-Details bekommen
   denselben gleitenden Indikator (app.js SEG_BAR_SELECTOR). */
.fb-tabs,
.taxi-detail-tabs {
  position: relative;
  isolation: isolate;
}
.taxi-detail-tabs.has-seg-indicator::before {
  border-radius: var(--radius-sm);
  background-color: color-mix(in srgb, var(--accent) 18%, rgba(255,255,255,0.05));
}
.taxi-detail-tabs.has-seg-indicator > .taxi-detail-tab.is-active:not(:active) { background: transparent; }
.taxi-detail-tab { transition: color var(--transition-fast), background-color var(--transition-fast); }
/* Runde T1: Personenfilter in Statistik/Zuverlässigkeit und der Heatmap-Schalter gleiten wie die Reiter. */
.stats-filter-pills,
.heatmap-mode-toggle {
  position: relative;
  isolation: isolate;
}
.stats-filter-pills.has-seg-indicator::before {
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
  box-shadow: inset 0 -3px 0 0 var(--accent);
}
.stats-filter-pills.has-seg-indicator > .tab-pill.is-active { background: transparent; box-shadow: none; }
.heatmap-mode-toggle.has-seg-indicator::before {
  border-radius: var(--radius-xs);
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
}
.heatmap-mode-toggle.has-seg-indicator > .heatmap-mode-btn.is-active { background: transparent; }
@media (prefers-reduced-motion: reduce) {
  .has-seg-indicator::before { transition: none; }
}
.tab-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.35rem;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.08);
  font-size: 0.7rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.tab-pill.is-active .tab-count,
.tab-pill[aria-selected="true"] .tab-count {
  background: var(--accent);
  color: var(--bg);
}
/* Motion-Audit: Zaehler-Aenderung (neue Anfrage, Leihe abgeschlossen …) —
   ein einzelner weicher Pop statt stillem Umspringen beim Hintergrund-
   Refresh. _applyPostRenderAnimations() (app.js) setzt .is-bumped nur, wenn
   sich der Wert gegenueber dem vorigen Render derselben Ansicht geaendert
   hat; unter Reduce Motion gar nicht. Die Zahl selbst bleibt die Information. */
.tab-count.is-bumped,
.perspective-count.is-bumped {
  animation: countBump var(--motion-slow) var(--ease-spring);
}
@keyframes countBump {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.22); }
  100% { transform: scale(1); }
}
.tab-panel-hidden {
  display: none !important;
}

/* Slide-fade animation when switching between tab-panels (and stats-filter
   detail panes). The active panel briefly enters from the left or right side
   and fades in. Honours prefers-reduced-motion. */
[data-tab]:not(.tab-pill),
.tab-panel-animated {
  transition:
    opacity var(--motion-base) var(--ease-standard, ease),
    transform var(--motion-base) var(--ease-standard, ease);
  /* Motion-Audit: kein dauerhaftes will-change mehr. Es hielt jedes Panel
     (samt aller Leihkarten) permanent als eigene Compositor-Ebene und machte
     es zum Containing Block fuer die position:fixed Eintrag-Menues
     (.entry-menu-panel) — deren Anchor-Flip entschied dann gegen die Panel-
     statt gegen die Viewport-Kante. Die 300-ms-Transition wird vom Browser
     auch ohne Hinweis auf dem Compositor gefahren. */
}
.tab-panel-enter-right { opacity: 0; transform: translateX(16px); }
.tab-panel-enter-left  { opacity: 0; transform: translateX(-16px); }
@media (prefers-reduced-motion: reduce) {
  [data-tab]:not(.tab-pill),
  .tab-panel-animated {
    transition: none;
  }
  .tab-panel-enter-right,
  .tab-panel-enter-left {
    opacity: 1;
    transform: none;
  }
}

/* =========================================================================
   Modal UX refresh – Settings, Profile, Stats
   ========================================================================= */

/* ---- Settings collapsible section (details/summary) ---- */
.settings-section {
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.07);
  border-radius: var(--radius-card);
  margin-bottom: 0.75rem; /* Closer to profile-section (1rem) for consistency */
  transition: background var(--transition), border-color var(--transition);
}
.settings-section[open],
.settings-section.is-closing {
  background: rgba(255,255,255,0.035);
  border-color: rgba(255,255,255,0.10);
}
.settings-section.is-closing {
  background: rgba(255,255,255,0.02);
  border-color: rgba(255,255,255,0.07);
}

.settings-section-head {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem 1rem;
  cursor: pointer;
  user-select: none;
  min-height: 44px;
}
.settings-section-head::-webkit-details-marker { display: none; }

/* Sticky: only open sections get a sticky head, so at most one header competes
   for top at any time (the earlier "all heads sticky" caused glitches because
   all closed-section heads stacked at top:0 simultaneously).
   top: -1px instead of 0 so IntersectionObserver can detect "stuck" state:
   when stuck, the top 1px is outside the scroll-container → intersectionRatio < 1.
   JS adds .is-stuck at that point to square off the top corners, preventing
   content from scrolling through the rounded corner cutouts. */
.settings-section[open] > .settings-section-head,
.settings-section.is-closing > .settings-section-head {
  position: sticky;
  top: -1px;
  padding-top: calc(0.75rem + 1px); /* compensate the -1px top offset */
  z-index: 2;
  /* color-mix matches the section's open background (rgba(255,255,255,0.035)
     composited over --surface) so the head doesn't look darker when sticky. */
  background: color-mix(in srgb, white 3.5%, var(--surface));
  border-radius: 16px 16px 0 0;
}

/* When the head is actually stuck (JS detects via IntersectionObserver),
   square the top corners so no content bleeds through the rounded cutouts. */
.settings-section[open] > .settings-section-head.is-stuck,
.settings-section.is-closing > .settings-section-head.is-stuck {
  border-radius: 0;
}

.settings-section-icon {
  font-size: 1.05rem;
  line-height: 1;
}
.settings-section-title {
  flex: 1;
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text);
}
.settings-section-status {
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.18rem 0.55rem;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.06);
  color: var(--text-muted);
  white-space: nowrap;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.settings-section-status[data-state="ok"] {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
}
.settings-section-status[data-state="warn"] {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--warning);
}

.settings-section-chevron {
  color: var(--text-muted);
  font-size: 0.9rem;
  transition: transform var(--motion-base) var(--ease-standard, cubic-bezier(0.22,1,0.36,1));
  display: inline-block;
}
.settings-section[open] .settings-section-chevron {
  transform: rotate(180deg);
}
.settings-section.is-closing .settings-section-chevron {
  transform: rotate(0deg);
}

.settings-section-body {
  display: grid;
  grid-template-rows: 0fr;
  transition:
    grid-template-rows var(--motion-base) var(--ease-standard, cubic-bezier(0.22,1,0.36,1)),
    border-top-width var(--motion-base) var(--ease-standard, cubic-bezier(0.22,1,0.36,1));
  border-top: 0px solid rgba(255,255,255,0.06);
  overflow: hidden;
}

/* open: fully expanded */
.settings-section[open] .settings-section-body {
  grid-template-rows: 1fr;
  border-top-width: 1px;
}

/* Sprungziel in einem Modal (spotlightInModal in app.js, z. B. „+ Kategorie“
   aus der Statistik): Akzent-Rahmen plus zwei sanfte Aufhell-Pulse, bleibt bis
   zum nächsten Tipp. Innen gezeichnet (outline-offset negativ, inset-Schatten),
   weil Abschnittskörper overflow:hidden haben und außen abschneiden würden. */
.is-spotlight,
.is-spotlight-out {
  outline: 2px solid transparent;
  outline-offset: -2px;
  transition: outline-color 0.35s var(--ease-standard);
}
.is-spotlight {
  outline-color: color-mix(in srgb, var(--accent) 80%, transparent);
  animation: spotlightPulse 1.4s var(--ease-standard) 2;
}
@keyframes spotlightPulse {
  0%, 100% { box-shadow: inset 0 0 0 100vmax transparent; }
  40%      { box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--accent) 12%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .is-spotlight { animation: none; }
}

/* is-opening: first paint still at 0fr so transition fires from 0→1 */
.settings-section.is-opening .settings-section-body {
  grid-template-rows: 0fr;
  border-top-width: 0px;
}

/* is-closing: animate back to 0fr while [open] is still set.
   Use ease-in (0.4,0,1,1) so the collapse accelerates toward 0 and ends
   cleanly — the spring ease-out used for opening would linger near 0fr
   and cause a visible stutter just before the section finishes closing. */
.settings-section.is-closing .settings-section-body {
  grid-template-rows: 0fr;
  border-top-width: 0px;
  transition:
    grid-template-rows var(--motion-base) cubic-bezier(0.4, 0, 1, 1),
    border-top-width var(--motion-base) cubic-bezier(0.4, 0, 1, 1);
}

/* Info-dot for a section moved here from inside <summary> — interactive
   content isn't valid inside a <summary> element (breaks keyboard/AT focus
   consistency). Shown once the section is open, right where the fields it
   explains actually are, instead of the collapsed header. */
.settings-section-info-row {
  display: flex;
  justify-content: flex-end;
  margin-bottom: -0.2rem;
}

.settings-section-body > .settings-section-body-inner {
  min-height: 0;
  /* Symmetric horizontal padding matches the head (1rem). Vertical: a bit
     less top because the head already has bottom-padding via min-height. */
  padding: 0.75rem 1rem 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  overflow: hidden;
}

/* Defensive: ensure scrollable modals can always scroll their content,
   so a freshly-opened <details> section never hides the next section. */
/* When a .modal-body scroll-container exists, prevent double-scroll on the
   parent .modal-card by overriding its overflow-y. The .modal-card itself
   stays as a flex column; only .modal-body scrolls internally. */
.modal-card:has(.modal-body) {
  overflow-y: hidden;
}
.modal-card .modal-body,
.modal-card-wide .modal-body {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  flex: 1;
  min-height: 0;
  /* Same slim track as .modal-card (was the 15px classic bar here). */
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.12) transparent;
}
/* Reserve the scrollbar track up front in both modal scroll containers.
   Expanding a collapsed section (Einstellungen, Profil, "Technische Details")
   made the content scroll for the first time: the bar appeared and pushed
   everything ~10–15px to the left, collapsing pushed it back. With a stable
   gutter the content keeps its x position. No-op for overlay scrollbars
   (iOS/Android), stays reserved while .is-resizing clips to hidden. */
.modal-card,
.modal-card .modal-body {
  scrollbar-gutter: stable;
}
/* Dialoge mit eigenem Scroll-Körper (Taxi-Anfrage, -Abholung, Fahrzeug-Formular): die Karte scrollt dort
   nicht (overflow-y: hidden) und braucht keine Reserve – sie ließ rechts einen leeren Streifen, und der Körper
   legte seine Leiste noch einmal daneben (doppelte Reserve: Inhalt 10 px schmaler als Kopf und Fußleiste).
   Jetzt reserviert nur der Körper, und er ragt um genau die Leistenbreite (--scrollbar-w, app.js) in den
   Karten-Rand: Inhalt, Kopf und Fußleiste enden auf derselben Linie. Kein Sprung, die Reserve bleibt stehen. */
.modal-card:has(> .modal-body) { scrollbar-gutter: auto; }
.modal-card:has(> .modal-body) > .modal-body { margin-right: calc(-1 * var(--scrollbar-w, 0px)); }
/* Tab-Fokus im Scroll-Körper: Chrome ließ ein nur zur Hälfte sichtbares letztes Feld (#tr-message der Gruppenfahrt, 375×812) halb unter der
   Fußleiste stehen; mit 3rem Reserve unten rückt es in den freien Bereich (gemessen: Unterkante 697 → 572 bei Fußleiste ab 665). */
.modal-card:has(> .modal-body) > .modal-body { scroll-padding-bottom: 3rem; }
/* R9-Mobile: Dialoge scrollen nur senkrecht ("in der Handy-App kann man
   seitlich scrollen": Taxifahrt bearbeiten, Gegenangebot, Leihe abschließen …).
   .modal-body und die Listen unten setzten nur overflow-y — overflow-x wurde
   damit zu `auto`, und schon ein paar Pixel Überstand (unsichtbare 44-px-
   Trefferflächen am Rand, die breitere iPhone-Schrift, ein langes Wort)
   machten den ganzen Dialog seitlich wischbar. `hidden` statt `auto`; und weil
   WebKit auch einen overflow-x:hidden-Scroller per Finger seitlich verschiebt,
   erlaubt touch-action nur senkrechtes Wischen (+ Zoom). Echte Querleisten
   (Heatmap, Filter-Chips, Bildleisten, Karte) sind eigene Scroll-Container —
   touch-action gilt nur bis zum nächsten Scroll-Container, dort wischt man
   weiter quer. */
.modal-card .modal-body,
.modal-card :is(.global-search-results, .info-modal-body, .taxi-stop-results, .purpose-other-detail) {
  overflow-x: hidden;
}
.modal-card,
.modal-card :is(.modal-body, #stats-modal-body, #reliability-modal-body, #users-modal-body, #notifications-modal-body, #logs-body, .logs-list-wrap, .global-search-results, .info-modal-body, .taxi-stop-results, .purpose-other-detail) {
  touch-action: pan-y pinch-zoom;
  overscroll-behavior-x: none;
}

/* Inline .modal-actions nested inside a .modal-body (e.g. settings save rows,
   vehicle form) must not inherit the sticky/bleed behaviour from the card-level
   footer rule — they are ordinary inline button groups inside scrollable content. */
.modal-body .modal-actions {
  position: static;
  padding-bottom: 0;
  margin-bottom: 0;
  margin-left: 0;
  margin-right: 0;
  padding-left: 0;
  padding-right: 0;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-top: none;
}

.settings-subhead {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  margin-top: 0.5rem;
  padding-top: 0.75rem;
  border-top: 1px solid rgba(255,255,255,0.06);
}

/* ---- KPI grid for stats modal ---- */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.6rem;
  margin: 0.4rem 0 0.5rem;
}
@media (min-width: 640px) {
  .kpi-grid-hero {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
  /* auto-fit: Sekundär- und Tertiär-Reihe füllen die Zeile unabhängig von
     der Kartenanzahl (2 oder 3 Cards) gleichmäßig. */
  .kpi-grid-secondary,
  .kpi-grid-tertiary {
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  }
}

/* Tertiary row: Zahlungs-Kanäle + Rückgabequote side-by-side.
   Stack to single column on very narrow screens so cards don't get too cramped. */
.kpi-grid-tertiary {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 360px) {
  /* Secondary row can hold 3 cards (e.g. + "Verlängerungsquote"), same as
     tertiary's 2 — the base .kpi-grid's fixed 2-column grid left an odd
     trailing card without room to shrink, clipping its left edge past the
     modal border. Single column avoids the cramped 2-up layout entirely. */
  .kpi-grid-secondary,
  .kpi-grid-tertiary {
    grid-template-columns: minmax(0, 1fr);
  }
}
.kpi-card.kpi-card-wide {
  align-items: center;
  text-align: center;
  padding: 0.85rem 1rem;
}
.kpi-card.kpi-card-wide .kpi-value {
  font-size: 1.3rem;
}

.stat-reliability-good { color: var(--accent); }
.stat-reliability-bad  { color: var(--danger); }
/* One class per band of RELIABILITY_TIERS (app.js) so a score has the same
   colour in the badge, the card, the hero, the chip and the graph. */
.stat-reliability-building { color: var(--rel-building); }
.stat-reliability-shaky    { color: var(--warning); }
.stat-reliability-new      { color: var(--text-muted); }

.kpi-card {
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--radius-tile);
  padding: 0.75rem 0.85rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.25rem;
  min-height: 76px;
}
/* Befund 16: Die Labels brachen unterschiedlich um ("GESAMT VERLIEHEN" auf
   zwei Zeilen, "AKTUELL OFFEN" auf eine) — die Betraege einer Zeile standen
   dadurch auf verschiedener Hoehe. Zwei reservierte Zeilen fuer das Label,
   eine fuer die Sub-Zeile, und der Wert wird per margin-top:auto an die
   gemeinsame Grundlinie geschoben. */
.kpi-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  min-height: 2lh;
  display: flex;
  align-items: center;
  text-align: center;
}
.kpi-value {
  font-size: var(--text-xl);
  font-weight: 800;
  color: var(--text);
  line-height: 1.15;
  margin-top: auto;
}
/* Befund 19: "AKTUELL OFFEN" war rot (--danger), dieselbe Zahl auf der
   Startseite neutral weiss. Offene Forderungen sind kein Fehlerzustand —
   Rot bleibt "ueberfaellig" vorbehalten. */
.kpi-card-outstanding .kpi-value { color: var(--text); }
.kpi-card-received   .kpi-value { color: var(--accent); }
.kpi-card-profit     .kpi-value { color: var(--accent); }
.kpi-card-bonus      .kpi-value { color: var(--warning); }
/* Eigene Klasse, weil .kpi-card-outstanding oben absichtlich neutral wurde:
   _renderExtendQuoteCard hat sie als Warn-Modifier zweckentfremdet (Quote >= 50 %)
   und haette das Signal sonst stillschweigend verloren. */
.kpi-card-warn       .kpi-value { color: var(--warning); }

.kpi-sub {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 0.15rem;
  min-height: 1lh;
}
html:not(.using-touch) .kpi-card-clickable:hover,
.kpi-card-clickable:focus-visible {
  border-color: var(--accent);
  outline: none;
}
/* A11y-R5 (Fokus sichtbar): Hover und Tastaturfokus sahen gleich aus (1px
   Akzentrahmen) — der Fokus braucht eine eigene, kräftigere Markierung. Innen
   liegend wie oben gewollt (kein outline, das der Kachel-Grid beschneidet). */
.kpi-card-clickable:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* ── Rückgabequote-Panel (Statistik-Übersicht) ── */
.repay-rate-panel {
  /* Gleiches Panel-Rezept wie die .kpi-card-Kacheln direkt darueber — vorher
     18px neben 12px im selben Dialog. */
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-btn);
  padding: 0.85rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin-top: 0.6rem;
}
.repay-band-good { border-left-color: var(--accent); }
.repay-band-mid  { border-left-color: var(--warning); }
.repay-band-bad  { border-left-color: var(--danger); }
.repay-rate-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.repay-rate-title {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
}
.repay-rate-trend { font-size: 0.78rem; font-weight: 700; white-space: nowrap; }
.repay-trend-up   { color: var(--accent); }
.repay-trend-down { color: var(--danger); }
.repay-rate-main {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}
.repay-rate-value {
  flex: 0 0 auto;
  min-width: 3.2ch;
  font-size: 1.7rem;
  font-weight: 800;
  line-height: 1;
  color: var(--text);
}
.repay-band-good .repay-rate-value { color: var(--accent); }
.repay-band-mid  .repay-rate-value { color: var(--warning); }
.repay-band-bad  .repay-rate-value { color: var(--danger); }
.repay-rate-bar {
  flex: 1 1 auto;
  height: 8px;
  border-radius: 999px;
  background: rgba(255,255,255,0.08);
  overflow: hidden;
}
.repay-rate-bar--inline {
  height: 5px;
  max-width: 160px;
  margin: 0.15rem 0;
}
.repay-rate-bar-fill {
  height: 100%;
  width: var(--pct, 0%);
  border-radius: inherit;
  transition: width var(--motion-base) var(--ease-standard);
}
.repay-rate-bar-fill[data-band="good"] { background: var(--accent); }
.repay-rate-bar-fill[data-band="mid"]  { background: var(--warning); }
.repay-rate-bar-fill[data-band="bad"]  { background: var(--danger); }
.repay-rate-bar-fill[data-band="none"] { background: rgba(255,255,255,0.15); }
.repay-rate-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.6rem;
  font-size: 0.72rem;
  color: var(--text-muted);
}
.repay-rate-more {
  margin-left: auto;
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}
/* R4 Modals/Touch: Hover nur mit Maus (sonst klebt die Unterstreichung nach
   dem Tipp), dafür ein kurzes Einsinken als Druck-Rückmeldung. */
html:not(.using-touch) .repay-rate-more:hover { text-decoration: underline; }
.repay-rate-more:active { opacity: 0.6; }
.repay-rate-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.repay-rate-lowsample-tag {
  font-size: var(--text-xs);
  color: var(--warning);
  border: 1px solid color-mix(in srgb, var(--warning) 30%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.02rem 0.42rem;
}
/* War opacity:.5 auf dem ganzen Element -> 2.56:1. Farbe dimmen statt
   Element (siehe --text-dim im :root-Block). */
.repay-rate-muted { color: var(--text-dim); }
.repay-band-text-good { color: var(--accent); }
.repay-band-text-mid  { color: var(--warning); }
.repay-band-text-bad  { color: var(--danger); }
/* Per-borrower table: keep the inline bar right-aligned with the % + chips. */
.repay-user-table .year-value { align-items: flex-end; }

/* ── Info-Dot ("i") — kleiner Trigger neben Labels/Titeln, öffnet showInfoModal ── */
.info-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.32rem;
  height: 1.32rem;
  flex-shrink: 0;
  padding: 0.02em 0 0; /* optische Vertikal-Mittigkeit des serifen-„i" */
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: var(--surface2);
  color: var(--text-muted);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 0.82rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  vertical-align: middle;
  margin-left: 6px;
  -webkit-tap-highlight-color: transparent;
  transition: var(--transition-ui);
}
/* Hover nur mit Maus: auf Touch blieb der gruene Hover-Zustand nach dem Tippen
   am Punkt kleben, bis woanders getippt wurde (gleiche Gate-Regel wie ueberall). */
html:not(.using-touch) .info-dot:hover,
.info-dot:focus-visible {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--text);
}
.info-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.info-dot:active { transform: scale(0.9); }

/* Optik bleibt 21px, die Trefferfläche nicht: WCAG 2.2 SC 2.5.8 verlangt
   24x24 CSS-px AUCH mit der Maus — die ::after-Vergrößerung stand bisher
   ausschließlich unter @media (pointer: coarse). -3px auf 21px ergibt 27px,
   sicher über der Schwelle und ohne Überlappung mit Nachbar-Dots. */
.info-dot { position: relative; }
.info-dot::after { content: ""; position: absolute; inset: -3px; }
/* .info-dot-compact ist nur 18px — dort braucht es -4px für 26px. */
.info-dot-compact::after { inset: -4px; }
/* Touch: Trefferfläche auf die App-weite 44px-Norm vergrößern. */
@media (pointer: coarse) {
  /* inset bezieht sich auf die Padding-Box (ohne den 1px-Rand): 21px-Dot
     -12px ergab 43px, der 18px-Kompakt-Dot nur 40px. -13/-14 = volle 44px+. */
  .info-dot::after { inset: -13px; }
  .info-dot-compact::after { inset: -14px; }
}

/* Kompakt — dichte dynamische Kontexte (Fortschrittsbalken, Bonus-Tag, KPI-Trend) */
.info-dot-compact {
  width: 1.12rem;
  height: 1.12rem;
  font-size: 0.72rem;
  margin-left: 4px;
  /* .info-dot's own padding-top (0.06em) was tuned for ITS font-size/height
     combo — em scales with THIS element's smaller font-size too, but glyph
     hinting at 0.72rem doesn't shift by the same proportion, so the inherited
     value left the "i" measurably off-center here (~0.8px too high). Own
     tuned value instead of trusting the ratio. */
  padding-top: 0.12em;
}

/* Wrapper: Dot sauber mittig zur Label-/Titelzeile halten */
.label-with-info,
.title-with-info {
  display: inline-flex;
  align-items: center;
}
/* .form-group label / .label-with-info are flex without a `gap` — the
   browser trims the trailing whitespace of a text node right before a flex
   item, so "Verbrauch <small>(l/100 km)</small>" rendered as
   "Verbrauch(l/100 km)". Scoped to just these two labels rather than adding
   `gap` to those shared base rules, which are reused by many other labels
   app-wide and in several cases already carry their own manual spacing
   (e.g. .info-dot's margin-left) that a global gap would double up on. */
#tv-consumption-unit,
#tv-override-unit,
.label-with-info > .taxi-point-opt {
  margin-left: 0.3em;
}

.info-modal-body {
  font-size: 0.9rem;
  margin-top: 0;
  font-weight: 400;
  text-align: left;
  color: var(--text);
  line-height: 1.55;
  max-height: 50dvh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* V4: ein scrollender Info-Text ist per Tastatur fokussierbar (showInfoModal); der Ring liegt innen, die Karte schneidet außen ab. */
.info-modal-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius-xs); }
@media (max-width: 540px) {
  /* Every info-dot ("i") popup app-wide renders through here — a long
     unbroken token (a URL pasted into help text, …) otherwise overflows the
     modal. overflow-wrap is inherited, so this also covers .info-modal-lead
     and any <p> inside. */
  .info-modal-body { overflow-wrap: anywhere; }
}
.info-modal-body p,
.info-modal-lead {
  margin: 0;
}
.info-modal-body p + p {
  margin-top: 0.7rem;
}
.info-modal-badge {
  display: inline-flex;
  align-items: center;
  margin: 0 0 0.65rem;
  padding: 0.18rem 0.48rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  font-size: 0.72rem;
  font-weight: 800;
  line-height: 1.2;
}
.info-point-list {
  display: grid;
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.info-point-list li {
  position: relative;
  margin: 0;
  padding: 0.55rem 0.65rem 0.55rem 1.85rem;
  border: 1px solid rgba(255,255,255,0.075);
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.035);
}
.info-point-list li::before {
  content: "";
  position: absolute;
  left: 0.72rem;
  top: 1.05rem;
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
.info-modal-body > ol {
  counter-reset: info-step;
  display: grid;
  gap: 0.55rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.info-modal-body > ol > li {
  counter-increment: info-step;
  position: relative;
  margin: 0;
  padding: 0.6rem 0.7rem 0.6rem 2.45rem;
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.035);
}
.info-modal-body > ol > li::before {
  content: counter(info-step);
  position: absolute;
  left: 0.65rem;
  top: 0.58rem;
  width: 1.25rem;
  height: 1.25rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  font-size: 0.72rem;
  font-weight: 900;
}
.info-modal-body ul:not(.info-point-list) {
  margin: 0;
  padding-left: 1.2rem;
}
.info-modal-body ul:not(.info-point-list) li {
  padding-left: 0.15rem;
  margin: 0.35rem 0;
}
.info-modal-body ul:not(.info-point-list) li::marker {
  color: var(--accent);
  font-weight: 800;
}
.info-modal-body code {
  padding: 0.05rem 0.28rem;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.07);
  color: var(--text);
  /* 0.86em auf einem 0.78rem-Elternteil ergab 10.83px — unter der
     --text-xs-Untergrenze (11.2px). max() haelt die relative Skalierung
     und setzt den Boden. */
  font-size: max(0.86em, var(--text-xs));
}
.sync-modal {
  display: grid;
  gap: 0.62rem;
  white-space: normal;
}
.sync-modal-hero {
  display: flex;
  gap: 0.42rem;
  align-items: center;
  min-height: 0;
  padding: 0.62rem 0.72rem;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--accent) 8%, rgba(255,255,255,0.035));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.sync-modal-hero-copy {
  display: grid;
  gap: 0.02rem;
  min-width: 0;
}
.sync-modal-hero strong {
  display: block;
  color: var(--text);
  font-size: var(--text-base);
  line-height: 1.05;
}
.sync-modal-hero p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.76rem;
  line-height: 1.12;
}
.sync-modal-dot {
  /* flex: none — as a flex item next to the text it shrank to an oval on
     narrow cards; the 4px ring needs its own room too (margin). */
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  margin: 0 0.3rem 0 0.25rem;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
}
.sync-modal[data-state="polling"] .sync-modal-hero {
  background: color-mix(in srgb, var(--info) 8%, rgba(255,255,255,0.035));
  border-color: color-mix(in srgb, var(--info) 22%, transparent);
}
.sync-modal[data-state="polling"] .sync-modal-dot {
  background: var(--info);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--info) 18%, transparent);
}
.sync-modal[data-state="syncing"] .sync-modal-hero,
.sync-modal[data-state="starting"] .sync-modal-hero {
  background: rgba(255,255,255,0.04);
  border-color: rgba(255,255,255,0.12);
}
.sync-modal[data-state="syncing"] .sync-modal-dot,
.sync-modal[data-state="starting"] .sync-modal-dot {
  background: var(--text-muted);
  box-shadow: 0 0 0 4px rgba(255,255,255,0.08);
}
.sync-modal[data-state="pending"] .sync-modal-hero {
  background: color-mix(in srgb, var(--warning) 9%, rgba(255,255,255,0.035));
  border-color: color-mix(in srgb, var(--warning) 26%, transparent);
}
.sync-modal[data-state="pending"] .sync-modal-dot {
  background: var(--warning);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--warning) 20%, transparent);
}
.sync-modal[data-state="offline"] .sync-modal-hero {
  background: color-mix(in srgb, var(--danger) 9%, rgba(255,255,255,0.035));
  border-color: color-mix(in srgb, var(--danger) 25%, transparent);
}
.sync-modal[data-state="offline"] .sync-modal-dot {
  background: var(--danger);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 18%, transparent);
}
.sync-modal-freshness,
.sync-modal-queue {
  display: grid;
  gap: 0.12rem;
  padding: 0.62rem 0.72rem;
  border: 1px solid rgba(255,255,255,0.075);
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.035);
}
.sync-modal-freshness span,
.sync-modal-queue span {
  color: var(--text-muted);
  font-size: 0.78rem;
}
.sync-modal-freshness strong,
.sync-modal-queue strong {
  color: var(--text);
  font-size: 0.94rem;
}
.sync-modal-queue {
  border-color: color-mix(in srgb, var(--warning) 24%, transparent);
  background: color-mix(in srgb, var(--warning) 8%, rgba(255,255,255,0.03));
}
.sync-modal-explain,
.sync-modal-note {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.82rem;
  line-height: 1.4;
}
.sync-modal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.sync-modal-btn {
  flex: 1 1 10rem;
  min-height: var(--touch-target);
  margin-top: 0;
}
.sync-modal-details {
  border-top: 1px solid rgba(255,255,255,0.08);
  padding-top: 0.48rem;
}
.sync-modal-details summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  cursor: pointer;
  color: var(--text);
  font-weight: 700;
  font-size: 0.82rem;
  line-height: 1;
  margin: 0 !important;
  padding: 0 !important;
}
.sync-modal-details summary::-webkit-details-marker { display: none; }
/* Own chevron, same idiom as .perm-history: the native marker is gone with
   display:flex, and every other <details> in the app signals expand/collapse
   this way rather than the browser's default disclosure triangle. */
.sync-modal-details summary::before {
  content: '›';
  /* Fixed square box: rotating the bare glyph (line-height 1, no width) swung
     it around its baseline and it ended up clipped and off-centre. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1em;
  height: 1em;
  font-size: 1.15em;
  transform-origin: 50% 50%;
  transition: transform var(--motion-fast) var(--ease-standard);
}
.sync-modal-details[open] summary::before { transform: rotate(90deg); }
.sync-modal-details > .sync-modal-details-list {
  display: grid;
  gap: 0;
  margin: 0 !important;
  padding: 0.18rem 0 0 !important;
}
.sync-modal-details[open] > .sync-modal-details-list {
  margin-top: 0 !important;
}
.sync-modal-details > .sync-modal-details-list > div {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.1rem 0;
  border-bottom: 1px solid rgba(255,255,255,0.055);
}
.sync-modal-details dt {
  color: var(--text-muted);
  font-size: 0.78rem;
  line-height: 1.15;
  margin: 0 !important;
}
.sync-modal-details dd {
  color: var(--text);
  font-size: 0.78rem;
  line-height: 1.15;
  text-align: right;
  margin: 0 !important;
}
.sync-modal-version-text { margin-right: 0.5rem; }
.sync-modal-version-copy {
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}
/* R4 Modals/Touch: wie .repay-rate-more — Hover nur mit Maus, Druck per Opacity. */
html:not(.using-touch) .sync-modal-version-copy:hover { text-decoration: underline; }
.sync-modal-version-copy:active { opacity: 0.6; }
.sync-modal-version-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* Beide Controls sind optisch bewusst winzig (13px Zeilenhoehe) — gemessen
   47x13 fuer "Kopieren", 13px fuer den Summary, auch am Desktop unter der
   WCAG-2.5.8-Grenze von 24px. Nur die Trefferflaeche waechst, das Layout
   bleibt: Maus >= 25px, Touch >= 44px. */
.sync-modal-details summary,
.sync-modal-version-copy { position: relative; }
.sync-modal-details summary::after { content: ""; position: absolute; inset: -6px 0; }
.sync-modal-version-copy::after { content: ""; position: absolute; inset: -6px -4px; }
@media (pointer: coarse) {
  .sync-modal-details summary::after { inset: -16px 0; }
  .sync-modal-version-copy::after { inset: -16px -8px; }
}
.info-breakdown-row {
  display: flex;
  justify-content: space-between;
  padding: 0.3rem 0;
  border-bottom: 1px solid rgba(255,255,255,0.07);
  gap: 0.5rem;
}
.info-breakdown-row.muted { opacity: 0.45; }
.info-breakdown-total {
  font-weight: 700;
  border-bottom: none;
  padding-top: 0.45rem;
  border-top: 1px solid rgba(255,255,255,0.18);
}
.info-breakdown-note {
  font-size: 0.72rem;
  color: var(--text-muted);
  margin-top: 0.6rem;
  line-height: 1.4;
}
/* 3-column table layout for bonus breakdown */
.info-breakdown-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.info-breakdown-table th,
.info-breakdown-table td {
  padding: 0.3rem 0.4rem;
  border-bottom: 1px solid rgba(255,255,255,0.07);
  text-align: left;
  white-space: nowrap;
}
.info-breakdown-table th { color: var(--text-muted); font-weight: 500; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; }
.info-breakdown-table td:first-child,
.info-breakdown-table th:first-child { white-space: normal; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
.info-breakdown-table td:nth-child(2),
.info-breakdown-table td:nth-child(3),
.info-breakdown-table th:nth-child(2),
.info-breakdown-table th:nth-child(3) { text-align: right; }
.info-breakdown-table tfoot td {
  font-weight: 700;
  border-bottom: none;
  border-top: 1px solid rgba(255,255,255,0.18);
  padding-top: 0.45rem;
}
.reliability-breakdown {
  margin-top: 0.6rem;
  border-top: 1px solid var(--border);
  padding-top: 0.5rem;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  text-align: left;
  gap: 0.3rem;
  width: 100%;
}
.reliability-breakdown-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.78rem;
}
.reliability-breakdown-label {
  color: var(--text-muted);
  flex: 1;
  min-width: 0;
}
.reliability-breakdown-penalty {
  color: var(--danger);
  font-weight: 700;
  white-space: nowrap;
  font-size: 0.78rem;
}
.reliability-breakdown-bonus {
  color: var(--accent);
  font-weight: 700;
  white-space: nowrap;
  font-size: 0.78rem;
}
/* Events worth 0 points ("Verlängerung rechtzeitig beantragt") — neutral, so
   they don't read like a penalty. */
.reliability-breakdown-neutral {
  color: var(--text-muted);
  font-weight: 700;
  white-space: nowrap;
  font-size: 0.78rem;
}
.reliability-breakdown-empty {
  font-size: 0.78rem;
  color: var(--accent);
  margin: 0;
}

.reliability-section {
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.07);
  border-radius: var(--radius-card);
  padding: 1rem 1.1rem;
  margin-bottom: 0.5rem;
}
.reliability-trend {
  font-size: 0.7rem;
  font-weight: 700;
  padding: 0.1rem 0.4rem;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}
.reliability-trend-up   { background: rgba(var(--accent-rgb, 100,200,100), 0.15); color: var(--accent); }
.reliability-trend-down { background: rgba(var(--danger-rgb, 200,100,100), 0.15); color: var(--danger); }
.reliability-trend-flat { background: rgba(255,255,255,0.07); color: var(--text-muted); }
.reliability-projection {
  font-size: 0.78rem;
  color: var(--text-muted);
  margin: 0 0 0.6rem;
  padding: 0.35rem 0.65rem;
  background: rgba(255,255,255,0.04);
  border-radius: var(--radius-xs);
  border-left: 2px solid var(--accent);
}
.reliability-projection strong { color: var(--accent); }
.reliability-breakdown-visible {
  margin-top: 0;
  border-top: 1px solid var(--border);
  padding-top: 0.65rem;
}
.reliability-breakdown-actions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-shrink: 0;
}
.reliability-loan-link {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 0;
  width: var(--touch-target);
  height: var(--touch-target);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  opacity: 0.6;
  transition: opacity var(--transition-fast);
}
/* R4 Modals/Touch: nach einem Tipp blieb der Pfeil grün und voll deckend
   (klebender :hover). Hover nur mit Maus; auf Touch leuchtet er nur beim Drücken. */
html:not(.using-touch) .reliability-loan-link:hover,
.reliability-loan-link:active { opacity: 1; color: var(--accent); }
.reliability-breakdown-meta {
  /* Dim-Skala: Farbe statt opacity (0.7 auf --text-muted = ~4.3:1). */
  display: block;
  font-size: var(--text-xs);
  color: var(--text-dim);
  margin-top: 0.1rem;
}

/* Lender-Badge: shown next to global-breakdown rows that come from a DIFFERENT
   lender. Small pill in accent color with a hint of separation — signals
   "this event is from another lender's loan" without dominating the row. */
.reliability-lender-badge {
  display: inline-block;
  margin-left: 0.45rem;
  padding: 0.08rem 0.45rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
  color: var(--accent, #0a7);
  font-size: 0.7rem;
  font-weight: 600;
  vertical-align: middle;
}
/* "Other lender" rows: slightly muted so own loans visually stay primary. */
.reliability-breakdown-row-other { opacity: 0.85; }
.reliability-breakdown-row-other .reliability-breakdown-label { font-style: italic; }
.reliability-how {
  margin-top: 0.75rem;
  border-top: 1px solid var(--border);
  /* 0.2 + the summary's own 0.4rem = the former 0.6rem gap. */
  padding-top: 0.2rem;
}
.reliability-how-summary {
  font-size: 0.78rem;
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  user-select: none;
  /* Rerender-Stabilität: the vertical padding of the pinned (open) bar below
     used to be added only on open — the label jumped ~6 px down on tap.
     Same height in both states now; the sideways bleed stays open-only (it
     does not move the label: margin and padding cancel out). */
  padding: 0.4rem 0 calc(0.4rem + 1px);
}
.reliability-how-summary::before { content: '›'; transition: var(--transition-ui); }
details[open] .reliability-how-summary::before { transform: rotate(90deg); }
/* Sticky section header while scrolled, same mechanism as
   .settings-section-head (only while open — see there for why). This summary
   isn't a card's own header (.reliability-section is, and stays static — the
   how-summary just bleeds past its 1.1rem side padding so the pinned bar
   still spans the full card width instead of floating inset). No corner
   squaring needed: unlike settings-section, .reliability-section was never
   rounded at this nested depth, so there's nothing to square off. */
.reliability-how[open] > .reliability-how-summary {
  position: sticky;
  top: -1px;
  z-index: 2;
  margin: 0 -1.1rem;
  padding-left: 1.1rem;
  padding-right: 1.1rem;
  background: color-mix(in srgb, white 3%, var(--surface));
}
.reliability-how[open] > .reliability-how-summary.is-stuck {
  border-bottom: 1px solid var(--border);
}
/* Touch: die Aufklapp-Zeile war 31px (offen) bzw. ~15px (zu) hoch. */
@media (pointer: coarse) {
  .reliability-how-summary { min-height: var(--touch-target); }
}
.reliability-how-body {
  margin-top: 0.55rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.reliability-how-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-size: 0.76rem;
  color: var(--text-muted);
}
.reliability-how-bad     { color: var(--danger);     font-weight: 700; min-width: 3.5rem; flex-shrink: 0; }
.reliability-how-good    { color: var(--accent);     font-weight: 700; min-width: 3.5rem; flex-shrink: 0; }
.reliability-how-neutral { color: var(--text-muted); font-weight: 700; min-width: 3.5rem; flex-shrink: 0; }
.reliability-how-note {
  /* Dim-Skala: Farbe statt opacity. */
  font-size: 0.72rem;
  color: var(--text-dim);
  margin-top: 0.25rem;
}
/* R12-Raten (Score): Untergruppe „Ratenzahlung“ in der Score-Erklärung. */
.reliability-how-group {
  margin: 0.45rem 0 0;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Stats filter pills (segmented switcher) */
.stats-filter-pills {
  /* Segment-Leiste wie .tab-bar: Radius = Pill-Radius + 4px Innenabstand
     (vorher 16px, die Pillen sassen nicht konzentrisch in den Ecken). */
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  /* Rounded rect, NOT a full pill: the container wraps to 2+ rows once there
     are 4+ borrower tabs. A 999px radius turned the wrapped box into a stadium
     whose 45px corner curves cut inward past the corner pills, so an active
     pill (accent outline) at a row edge poked outside the container's visible
     shape. A moderate radius contains the pills in every row. */
  border-radius: var(--radius-segmented);
  padding: 0.25rem;
  margin: 0.25rem 0 0.6rem;
}
.stats-filter-pills .tab-pill {
  flex: 1 1 auto;
  font-size: 0.85rem;
  padding: 0.45rem 0.85rem;
  min-height: 36px;
}
/* Touch: die Personen-Pills (Statistik, Zuverlaessigkeit) bleiben optisch
   36px, die Trefferflaeche reicht 4px in Container-Padding bzw. Zeilenabstand
   (0.35rem) -> 44px. */
@media (pointer: coarse) {
  .stats-filter-pills .tab-pill { position: relative; }
  .stats-filter-pills .tab-pill::after { content: ""; position: absolute; inset: -4px 0; }
}

/* ---- Profile pill (avatar + name) ---- */
.profile-pill {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 0.85rem;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, transparent), color-mix(in srgb, var(--accent) 4%, transparent));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: var(--radius-btn);
  margin-bottom: 0.85rem;
}
.profile-pill-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--accent);
  /* Dunkler Text auf der hellen Flaeche: 7.08:1 statt 2.71:1 mit #fff. */
  color: var(--bg);
  font-size: 1.15rem;
  font-weight: 800;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.profile-pill-meta {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.profile-pill-name {
  font-size: 1rem;
  font-weight: 800;
  color: var(--text);
  word-break: break-word;
}
.profile-pill-role {
  font-size: 0.75rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* ---- Inline-edit pattern (read mode <-> edit mode) ---- */
.inline-edit-row .inline-edit-edit { display: none; }
.inline-edit-row[data-mode="edit"] .inline-edit-read { display: none; }
.inline-edit-row[data-mode="edit"] .inline-edit-edit {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.inline-edit-read {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 44px;
  padding: 0.55rem 0.75rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--radius-tile);
}
.inline-edit-value {
  font-size: 0.95rem;
  color: var(--text);
  word-break: break-all;
  flex: 1;
  min-width: 0;
}
.inline-edit-toggle { white-space: nowrap; }
/* Runde 6 (Knöpfe): "Bearbeiten" ist hier der Stift-Knopf (.btn-entry-menu,
   44 px) statt eines Textlinks. Die negative Marge frisst einen Teil des
   Zeilen-Innenabstands, damit die Lesezeile nicht auf 64 px anwaechst
   (ersetzt hier auch das mobile margin: -4px der Klasse). */
.inline-edit-read > .btn-entry-menu { margin: -0.3rem -0.45rem -0.3rem 0; flex-shrink: 0; }

.inline-edit-actions {
  display: flex;
  gap: 0.5rem;
}
.inline-edit-actions .btn-primary,
.inline-edit-actions .btn-secondary {
  flex: 1;
  width: auto;
  margin-top: 0;
  min-height: var(--touch-target, 44px);
}

/* Stat block inside the profile-account section */
.profile-stat-block {
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.05);
  border-radius: var(--radius-btn);
  padding: 0.55rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin-top: 0;
}

/* Remove margin-bottom on form-groups inside modal-section / settings-section-body-inner; gap handles spacing */
.settings-section-body-inner .form-group { margin-bottom: 0; }

/* Stack profile-button rows nicely — gap handles spacing */
.settings-section-body-inner .profile-btn-full + .profile-btn-full {
  margin-top: 0;
}

/* S&T: session info block */
.profile-session-info-block {
  /* Geschwister von .profile-stat-block: gleiche Innenabstaende/Radien. */
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.05);
  border-radius: var(--radius-btn);
  padding: 0.55rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.profile-session-info-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.82rem;
  gap: 0.5rem;
  min-width: 0;
}
.profile-session-info-label { color: var(--text-muted); white-space: nowrap; }
.profile-session-info-value {
  color: var(--text);
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* Long values (user-agent, IPs, etc.) must wrap, not push the card wider. */
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

/* Profile-modal: every child must respect the modal width. Long device
   names, session-info values, notify-event labels all had the chance to
   push the card and create a horizontal scrollbar on mobile. */
#modal-profile .modal-card > *:not(.modal-actions, .modal-sticky-header) {
  min-width: 0;
  max-width: 100%;
}

/* S&T: 2FA notice */
.profile-2fa-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.5rem 0.75rem;
  background: rgba(255,255,255,0.015);
  border: 1px solid rgba(255,255,255,0.05);
  border-radius: var(--radius-tile);
  font-size: 0.82rem;
}
.profile-2fa-icon { font-size: 1.1rem; flex-shrink: 0; line-height: 1.4; }
.profile-2fa-label { font-weight: 600; color: var(--text); margin-bottom: 0.1rem; }
.profile-2fa-desc { color: var(--text-muted); line-height: 1.4; }

/* S&T: app version */
.profile-app-version {
  /* Dim-Skala: opacity 0.5 auf --text-muted ergab ~3:1; --text-dim haelt 5.5:1. */
  text-align: center;
  font-size: 0.72rem;
  color: var(--text-dim);
  letter-spacing: 0.04em;
  /* Luft nach unten: ganz nach unten gescrollt klebte die Versionszeile
     sonst am Rand der Scrollfläche (Desktop). */
  padding: 0.25rem 0 var(--space-4);
}

/* Profile: Registered devices */
.profile-devices-block {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.profile-devices-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-top: 0.25rem;
}
.profile-devices-none {
  font-size: 0.82rem;
  color: var(--text-muted);
  padding: 0.25rem 0;
}
.profile-device-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.65rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--radius);
}
.profile-device-icon { font-size: 1rem; flex-shrink: 0; line-height: 1; }
.profile-device-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  min-width: 0;
}
.profile-device-name-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.profile-device-name {
  font-size: 0.85rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Needed for text-overflow:ellipsis to actually engage on a flex item —
     without it, min-width defaults to auto (the text's own full width) and
     the name just overflows past .profile-device-name-row instead of being
     clipped with an ellipsis. */
  min-width: 0;
}
@media (max-width: 540px) {
  /* Same fix as #profile-sessions-list .profile-device-meta below: the
     flex-column parent's align-items:flex-start leaves this row sized to
     fit-content instead of the column's real width, so the ellipsis above
     had no width to clip against on a long "<Browser> auf <OS>" name. */
  .profile-device-name-row { width: 100%; }
}
/* Runde 6 (Knöpfe): der Stift ist jetzt der gemeinsame Bearbeiten-Knopf
   (.btn-entry-menu, 44 px, Rahmen, Druck scale .92) statt eines auf 40 %
   gedimmten Link-Icons — Aussehen/Hover/Druck kommen von dort. */
.profile-device-rename-btn { flex-shrink: 0; }
/* Feedback #3: the pencil followed the name's length, so it sat at a
   different x in every row, and the meta line (nowrap, fit-content column)
   ran under Testen/Entfernen on the phone. The name row and the meta line now
   span the info column: the name takes the free width, the pencil sits at the
   column's right edge in every row, and long meta text wraps inside it. */
.profile-device-name-row { width: 100%; }
.profile-device-name { flex: 1 1 auto; }
.profile-device-meta { width: 100%; min-width: 0; }
.profile-device-lastseen { white-space: normal; overflow-wrap: anywhere; }
.profile-device-name-edit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-bottom: 0.2rem;
  width: 100%;
  min-width: 0;
}
/* Input takes the full first line; OK/✕ drop to their own line underneath
   instead of squeezing in next to it — on narrow (mobile) widths three
   flex items on one row left too little room for the input to stay legible. */
.profile-device-name-edit .profile-device-label-input {
  flex-basis: 100%;
}
/* While the device-name is being edited, hide the row's Testen/Entfernen
   buttons so the Speichern/Abbrechen pair owns the action area without
   sitting awkwardly next to unrelated actions. Restored on cancel/save. */
.profile-device-row:has(.profile-device-name-edit:not(.hidden)) .profile-device-actions {
  display: none;
}
/* Hide the name-row + meta while editing so the input row uses the full
   width of the info column instead of squeezing in next to the static name. */
.profile-device-row:has(.profile-device-name-edit:not(.hidden)) .profile-device-name-row,
.profile-device-row:has(.profile-device-name-edit:not(.hidden)) .profile-device-meta {
  display: none;
}
.profile-device-label-input {
  flex: 1;
  min-width: 0;
  /* 16px, not the visually-matching 0.82rem (~13px) — iOS Safari auto-zooms
     on focus for any input font-size below 16px. */
  font-size: 16px;
  padding: 0.2rem 0.45rem;
  background: var(--input-bg, rgba(255,255,255,0.07));
  border: 1px solid var(--border-input);
  border-radius: var(--radius-sm);
  color: var(--text);
}
/* Same size, side by side — matches the Speichern/Abbrechen button-pair
   pattern used elsewhere (e.g. edit-username), rather than mismatched
   shapes (rectangle + circle). */
.profile-device-save-label,
.profile-device-cancel-label {
  flex: 1;
  font-size: 0.75rem;
  padding: 0.2rem 0.6rem;
}
.profile-device-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.1rem;
}
.profile-device-self {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.4rem;
  white-space: nowrap;
  /* Anchored top-left of the device-info column — own line above the name
     so it acts as a section label rather than a trailing meta chip. */
  align-self: flex-start;
  margin-bottom: 0.2rem;
}
.profile-device-date {
  font-size: 0.72rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.profile-device-expired {
  font-size: 0.72rem;
  color: var(--badge-overdue-text);
  white-space: nowrap;
}
.profile-device-lastseen {
  font-size: 0.72rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.profile-device-failing {
  font-size: 0.72rem;
  color: var(--badge-overdue-text);
  white-space: nowrap;
  font-weight: 600;
}
.profile-device-actions {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.profile-device-test,
.profile-device-remove {
  flex-shrink: 0;
  font-size: 0.78rem;
  padding: 0.25rem 0.6rem;
}
/* Two stacked buttons at the full 44px touch-target would dominate this
   otherwise compact row on desktop — bump only where it actually matters. */
@media (hover: none) and (pointer: coarse) {
  .profile-device-test,
  .profile-device-remove {
    min-height: var(--touch-target);
  }
}
.profile-devices-remove-others,
.profile-devices-prune-stale {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* === Notifications modal (personal push/email delivery history) === */
.notifications-rate {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 1.1rem;
  padding: 0.6rem 0.9rem;
  margin: 0 0.9rem 0.5rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--radius);
}
.notifications-rate-item {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
}
.notifications-rate-value {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text);
}
.notifications-rate-label {
  font-size: 0.78rem;
  color: var(--text-muted);
}
.notifications-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0 0.9rem 0.7rem;
}
.notifications-day-group {
  padding: 0 0.9rem;
  margin-bottom: 0.6rem;
}
/* Kein zusätzlicher Innenabstand: Quote, Filter und Liste sprangen um
   0.9rem gegenüber Titel und Footer ein (die Karte hat schon ihr Padding) —
   auf 320px gingen so ~29px Breite verloren, alle anderen Modals fluchten
   mit dem Titel. */
#notifications-modal-body .notifications-rate { margin-left: 0; margin-right: 0; }
#notifications-modal-body .notifications-filters,
#notifications-modal-body .notifications-day-group { padding-left: 0; padding-right: 0; }
/* Handy: Filter-Chips als eine wischbare Zeile mit Ausblendung rechts (wie
   die Log-Filter) statt eines Umbruchs, bei dem "Fehlgeschlagen" allein in
   einer zweiten 44px-Zeile stand. */
@media (max-width: 540px) {
  #notifications-modal-body .notifications-filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    padding-top: 0.3rem;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
  }
  #notifications-modal-body .notifications-filters::-webkit-scrollbar { display: none; }
  #notifications-modal-body .notifications-filters .status-pill { flex: 0 0 auto; white-space: nowrap; }
  #notifications-modal-body .notifications-filters .status-pill:last-child { margin-right: 28px; }
}
.notifications-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0 0 0.3rem;
}
.notification-row-wrap {
  display: flex;
  flex-direction: column;
}
.notification-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.65rem;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--radius);
  cursor: pointer;
}
html:not(.using-touch) .notification-row:hover {
  border-color: rgba(255,255,255,0.14);
  background: rgba(255,255,255,0.05);
}
.notification-row-failed {
  border-color: color-mix(in srgb, var(--danger) 30%, transparent);
  background: color-mix(in srgb, var(--danger) 5%, rgba(255,255,255,0.03));
}
.notification-new-dot {
  flex-shrink: 0;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
  align-self: center;
}
.notification-icon {
  font-size: 1.1rem;
  flex-shrink: 0;
  line-height: 1;
}
.notification-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.notification-text {
  font-size: 0.85rem;
  color: var(--text);
  overflow-wrap: anywhere;
}
/* One-line preview of the message a notification carried (full text in the detail panel). */
.notification-message {
  font-size: 0.8rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.notification-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.notification-failed-badge {
  color: var(--badge-overdue-text);
  font-weight: 600;
}
.notification-row-actions {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.notification-retry {
  flex-shrink: 0;
  font-size: 0.78rem;
  padding: 0.25rem 0.6rem;
}
/* Signals "tap to expand" before the first interaction — same rotate-on-open
   idea as .logs-day-chevron in the lender logs modal. */
.notification-chevron {
  display: inline-flex;
  align-items: center;
  font-style: normal;
  flex-shrink: 0;
  color: var(--text-muted);
  font-size: 0.75rem;
  transition: transform var(--motion-fast) var(--ease-standard);
}
.notification-row-expanded .notification-chevron {
  transform: rotate(180deg);
}
.notification-detail {
  overflow: hidden;
}
.notification-detail-inner {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.6rem 0.9rem 0.7rem 2.5rem;
  font-size: 0.78rem;
}
.notification-detail-row {
  display: flex;
  gap: 0.5rem;
}
.notification-detail-label {
  flex-shrink: 0;
  min-width: 5.5rem;
  color: var(--text-muted);
  font-weight: 600;
}
.notification-detail-value {
  color: var(--text);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* tabular-nums helper (additive – guarded to not collide with existing rule) */
.tabular-nums { font-variant-numeric: tabular-nums; }
/* Zweitzeile/Zusatz in Fliesstext oder Tabellenzelle: Farbe statt opacity (Dim-Skala, Kontrast >= 4.5:1; opacity .5/.6 maß ~3:1). */
.text-dim-sub { color: var(--text-dim); }
.text-dim-sub.is-small { font-size: 0.8em; }

/* === Users Modal: enhanced admin view (additive) === */
.users-toolbar {
  display: flex;
  gap: 0.5rem;
  margin: 0 0 1rem;
  align-items: center;
}
.users-toolbar > .info-dot { margin-left: 0; }
/* R9-Mobile: the dot sits at the right edge of #users-modal-body — its 44 px
   touch hit area (::after, -14px) stuck out 10 px and made the list a
   sideways scroller on phones. Room for it inside the body. */
@media (pointer: coarse) {
  .users-toolbar > .info-dot-compact { margin-right: 0.75rem; }
}
.users-filter {
  /* Gleiches Feld-Rezept wie .form-group input (Flaeche --surface2, Rand
     --border-input fuer 3:1 Non-text-Contrast, 1rem gegen iOS-Zoom, 44px). */
  flex: 1;
  padding: 0.55rem 0.75rem;
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  outline: none;
  transition: border-color var(--transition-fast);
  min-height: var(--touch-target);
}
.users-filter:focus { border-color: var(--accent); }

/* Override: switch user-card to grid layout for admin view */
.user-list .user-card {
  display: grid;
  gap: 0.55rem;
  /* Compact: tighter padding so each card takes less vertical space on mobile. */
  padding: 0.7rem 0.85rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 0.55rem;
  /* Bug B: prevent inline-edit slot or notify-matrix children from pushing the
     card wider than its container, which would enable horizontal scroll. */
  overflow: hidden;
  min-width: 0;
  max-width: 100%;
}
.user-list .user-card > * { min-width: 0; max-width: 100%; }
.user-card-head {
  display: grid;
  /* avatar | info — the status pills always wrap onto their own full-width row
     below (see .user-status-pills). The card is capped at 640px, so a 3rd
     side-by-side pills column never had comfortable room and the pills collided
     with the name/email at tighter widths. */
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.5rem 0.75rem;
}
.user-avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 0.95rem;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
}
.user-avatar-borrower {
  background: rgba(96, 165, 250, 0.16);
  color: #88b8ff;
}
.user-name-line {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.user-name { font-weight: 700; font-size: 1rem; }
.user-email { font-size: 0.82rem; color: var(--text-muted); }

.role-pill {
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 0.18rem 0.55rem;
  border-radius: var(--radius-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.role-pill-lender { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.role-pill-borrower { background: rgba(96, 165, 250, 0.16); color: #88b8ff; }
.role-pill-admin { background: color-mix(in srgb, var(--warning) 16%, transparent); color: var(--warning); }
.role-pill-taxi, .role-pill-export { background: rgba(255, 255, 255, 0.06); color: var(--text-muted); }

.user-protected-note {
  margin: 0.5rem 0 0;
  padding: 0.5rem 0.7rem;
  border: 1px dashed rgba(255, 255, 255, 0.12);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
/* Long usernames wrap instead of pushing the card wider than the phone. */
.user-name-line { min-width: 0; flex-wrap: wrap; }
.user-name { min-width: 0; overflow-wrap: anywhere; }

/* Permission editor in the user card ("Rechte"). A fieldset for the group
   label, so its own box model is reset here. */
.perm-options {
  display: grid;
  gap: 0.4rem;
  margin: 0.35rem 0 0.6rem;
  padding: 0;
  border: 0;
  min-width: 0;
}
.perm-options > legend { padding: 0; margin-bottom: 0.35rem; }
/* .interest-pct-buttons' own margin-bottom (for its original flex-flow
   context, e.g. the taxi bonus-% picker) would stack on top of this grid's
   own `gap`, doubling the space above the rights-preset row. */
.perm-options > .interest-pct-buttons { margin-bottom: 0; }
/* The rights-preset labels ("Leihnehmer"/"Leihgeber"/…) are full words, not
   the 2-4 char chips (10%/25%/…) .btn-pct was built for — tighter padding
   keeps all four on one row inside the narrow inline user-card editor instead
   of wrapping awkwardly to two. */
.perm-options > .interest-pct-buttons .btn-pct { padding: 0.4rem 0.6rem; font-size: var(--text-base); }
.perm-option-alias {
  margin-left: 0.35rem;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-muted);
}
.perm-option-reason:not(:empty)::before { content: ' · '; }
.perm-option-reason { color: var(--warning); }
.perm-option input { accent-color: var(--accent); width: 1.1rem; height: 1.1rem; }
.perm-option:has(input:checked) {
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.perm-error:empty { display: none; }
.perm-open-summary {
  margin: 0.35rem 0 0.2rem;
  padding: 0.45rem 0.65rem;
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  /* Befund 23: rgba(245,180,80,.08) war eine vierte, unbenannte Orange-Nuance
     neben --warning, --orange und --map-stop — und stand direkt neben dem
     --warning-Rand darueber. */
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.perm-error { margin: 0 0 0.5rem; }
.perm-option {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--radius);
  cursor: pointer;
  /* Without this, toggling a preset (e.g. "Admin") snapped every dependent
     card between its checked/disabled looks in the same frame — no fade, the
     :has(input:checked)/:has(input:disabled) rules above and below just
     switched instantly. */
  transition: border-color var(--transition-fast), background-color var(--transition-fast), opacity var(--transition-fast);
}
.perm-option input { margin-top: 0.2rem; flex-shrink: 0; }
.perm-option:has(input:disabled) { opacity: var(--dim-disabled); cursor: default; }
.perm-option-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.perm-option-text strong { font-size: var(--text-base); }
.perm-option-text small { font-size: var(--text-sm); color: var(--text-muted); }

/* "Verliehen / Geschuldet" switch for accounts active on both sides. Own
   classes on purpose: render() wires every .tab-pill on the page as a lender
   or borrower tab, so reusing that class would hijack these buttons. */
.perspective-switch {
  display: flex;
  gap: 4px;
  padding: 4px;
  margin: 0 0 1rem;
  background: rgba(255, 255, 255, 0.04);
  border-radius: var(--radius-segmented);
}
.perspective-tab {
  flex: 1 1 0;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-weight: 700;
  font-size: 0.95rem;
  min-height: var(--touch-target, 44px);
  padding: 0.55rem 0.75rem;
  border-radius: var(--radius);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
html:not(.using-touch) .perspective-tab:hover:not(.is-active) { color: var(--text); }
/* Touch-Audit R4: Verliehen/Geschuldet hatte als einziger Umschalter keinen
   Druckzustand (die Tab-Pillen darunter: scale .93). Etwas flacher, weil das
   Segment breit ist; der gleitende Indikator (::before der Leiste) bleibt
   unberuehrt, nur die Beschriftung gibt nach. */
.perspective-tab:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  transform: scale(0.96);
  transition-duration: var(--motion-xs);
}
/* Filled, unlike the outlined view tabs right below — so the two rows don't
   read as the same control. */
.perspective-tab.is-active {
  background: var(--accent);
  color: var(--bg);
}
.perspective-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius); /* sonst überschreibt *:focus-visible (4px) die Segment-Rundung */ }
.perspective-tab .tab-unread { margin-left: 0; }

/* Rights scheduled for revocation: box unticked but still held for open loans. */
.perm-option.is-scheduled {
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
  background: color-mix(in srgb, var(--warning) 6%, transparent);
}
.role-pill-ending { border-style: dashed; }
.role-pill-ending-mark { font-weight: 500; opacity: 0.85; }

/* Rights history below the editor. */
.perm-history { margin-top: 0.6rem; font-size: var(--text-sm); }
.perm-history > summary {
  cursor: pointer;
  color: var(--text-muted);
  min-height: 36px;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  list-style: none;
}
.perm-history > summary::-webkit-details-marker { display: none; }
/* Own chevron: the default marker is gone with display:flex. */
.perm-history > summary::before {
  content: '›';
  display: inline-block;
  transition: transform var(--transition-fast);
}
.perm-history[open] > summary::before { transform: rotate(90deg); }
.perm-history-list { list-style: none; margin: 0.25rem 0 0; padding: 0; display: grid; gap: 0.35rem; }
.perm-history-item {
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.03);
  overflow-wrap: anywhere;
}
.perm-history-meta { display: block; font-size: var(--text-xs); color: var(--text-muted); }
.perm-history-empty { margin: 0.25rem 0 0; color: var(--text-muted); }

/* Four-eyes removal of an admin right on an admin's user card. */
.admin-revocation { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.5rem; }
.admin-revocation > .btn-secondary { align-self: flex-start; }
.admin-revocation.is-pending {
  padding: 0.6rem 0.8rem;
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 8%, transparent);
}
.admin-revocation p { margin: 0; font-size: 0.84rem; }
.admin-revocation-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.admin-revocation-actions button { margin: 0; flex: 0 1 auto; min-height: var(--touch-target, 44px); }

/* Profile: signed-in devices (sessions) above the 2FA notice. */
.profile-sessions-block { margin: 0.9rem 0 0.8rem; }
.profile-sessions-block .profile-devices-list { margin-top: 0.45rem; }
#profile-sessions-list .profile-device-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.1rem 0;
  font-size: var(--text-sm);
  line-height: 1.35;
  color: var(--text-muted);
}
#profile-sessions-list .profile-device-meta span { white-space: nowrap; }
#profile-sessions-list .profile-device-meta span + span::before { content: '·'; margin: 0 0.4rem; }
@media (max-width: 540px) {
  /* .profile-device-info (the flex-column parent) uses align-items:flex-start,
     so without an explicit width this row sizes to fit-content instead of the
     column's actual width — flex-wrap then has nothing to wrap against and the
     "angemeldet …zuletzt aktiv …" spans render as one unbroken line that
     overflows past the card on narrow screens. */
  #profile-sessions-list .profile-device-meta { width: 100%; }
  /* A non-current session also renders a "Beenden" action button in the same
     row, leaving .profile-device-info roughly half as wide as the current-
     device row — narrow enough that a single fact ("zuletzt aktiv vor 21
     Min.") can be wider than the column even after the row above wraps
     between facts. Each fact has real spaces, so letting it wrap onto a
     second line (instead of forcing one unbroken line via nowrap) keeps it
     inside the card instead of overflowing. This override must stay after
     the base nowrap rule above: equal specificity, and CSS resolves ties by
     source order, so it would otherwise lose to the later rule. */
  #profile-sessions-list .profile-device-meta span { white-space: normal; }
}

/* Profile: label above the sections that depend on the account's rights. */
.profile-group-label {
  margin: 1.1rem 0 0.4rem;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Rights presets in "Neuer Benutzer" — same look as the rights editor. */
.nu-presets { display: grid; gap: 0.4rem; margin: 0 0 0.9rem; padding: 0; border: 0; min-width: 0; }
.nu-presets > legend { padding: 0; margin-bottom: 0.4rem; font-size: 0.85rem; font-weight: 600; }
.nu-preset {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--radius);
  cursor: pointer;
}
.nu-preset input { accent-color: var(--accent); width: 1.05rem; height: 1.05rem; margin-top: 0.2rem; flex-shrink: 0; }
.nu-preset > span { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.nu-preset strong { font-size: 0.9rem; }
.nu-preset small { font-size: 0.76rem; color: var(--text-muted); }
/* Extra right on top of the preset (Export): a dashed box, set apart from the radio group. */
.nu-preset-extra { margin-top: 0.35rem; border-style: dashed; }
.nu-preset-extra:has(input:disabled) { opacity: var(--dim-disabled); cursor: default; }
.nu-preset-hint { margin: 0.15rem 0 0; font-size: 0.78rem; color: var(--text-muted); }
.nu-preset-hint.is-warning { color: var(--warning); }

/* Own right scheduled for revocation. */
.winding-down-notice {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  margin: 0 0 var(--space-4);
  padding: 0.65rem 0.85rem;
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 8%, transparent);
  font-size: var(--text-base);
}
.winding-down-notice p { margin: 0; }

/* Advisory lend-limit notice on the decide/taxi-pickup screens — same visual
   language as .winding-down-notice (warning-tinted, left border) since both
   are "heads up, not a blocker" banners. */
.lend-limit-notice {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  margin: 0 0 var(--space-3);
  padding: 0.65rem 0.85rem;
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 8%, transparent);
  font-size: var(--text-base);
}
.lend-limit-notice-icon { flex-shrink: 0; }
/* Parallele Anfragen: „Du hast bereits 2 offene Anfragen an X“ ist nur eine
   Information, keine Sperre – neutraler Rand statt Warnton. */
.lend-limit-notice.open-requests-hint {
  border-left-color: var(--info);
  background: color-mix(in srgb, var(--info) 8%, transparent);
}

/* Always-visible usage read (profile global-limit field) and per-borrower
   badge (loan card) — unlike .lend-limit-notice above, these show the current
   position regardless of whether it's over the limit, just color-coded. */
.lend-limit-usage {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.4rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.lend-limit-usage-bar {
  position: relative;
  flex: 1;
  height: 5px;
  border-radius: var(--radius-pill);
  background: rgba(255,255,255,0.06);
  overflow: hidden;
}
.lend-limit-usage-bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--usage-pct, 0%);
  border-radius: var(--radius-pill);
  background: var(--accent);
  transition: width var(--motion-slow) var(--ease-standard);
}
.lend-limit-usage-bar-fill[data-state="warn"] { background: var(--warning); }
.lend-limit-usage-bar-fill[data-state="over"] { background: var(--danger); }
.lend-limit-usage-amount { flex-shrink: 0; font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
  .lend-limit-usage-bar-fill { transition: none; }
}

.lend-limit-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-left: 0.4rem;
  padding: 0.05rem 0.45rem;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
  color: var(--text-muted);
  white-space: nowrap;
  vertical-align: middle;
}
.lend-limit-badge[data-state="warn"] { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--warning); }
.lend-limit-badge[data-state="over"] { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }

/* Net overview for people who are owed money and owe money at the same time. */
.net-overview {
  margin: 0 0 var(--space-4);
  padding: 0.85rem var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}
.net-overview-title {
  margin: 0 0 0.6rem;
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.net-overview-totals { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); }
.net-figure { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.net-label { font-size: 0.74rem; color: var(--text-muted); }
.net-figure strong { font-size: 1.05rem; font-variant-numeric: tabular-nums; overflow-wrap: break-word; }
.net-figure-result strong.is-positive { color: var(--accent); }
.net-figure-result strong.is-negative { color: var(--warning); }
.net-overview-people {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: var(--space-3) 0 0;
  border-top: 1px solid var(--border);
  display: grid;
  gap: 0.8rem;
}
.net-person-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem; }
.net-person-figures { font-size: 0.8rem; color: var(--text-muted); }
.net-person-net, .net-settle-state { margin: 0.2rem 0 0; font-size: 0.84rem; }
/* N23: Die Gesamtsumme ("+€5,00", grün) stand direkt über "Netto schuldest du
   p_admin €25,00" — ohne Hinweis, dass das eine über alle Personen und das
   andere pro Person rechnet, las es sich als Widerspruch. Eigene Unterzeile
   für den Personen-Teil; das Per-Person-Ergebnis trägt dieselbe Farblogik. */
.net-overview-subtitle {
  margin: var(--space-3) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
}
.net-overview-subtitle + .net-overview-people { margin-top: 0.5rem; padding-top: 0; border-top: none; }
.net-person-net.is-positive { color: var(--accent-text); }
.net-person-net.is-negative { color: var(--warning); }
.net-settle-state.is-incoming { font-weight: 600; }
.net-settle-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: 0.45rem; }
/* Informativer Hinweis unter einem Verrechnungsvorschlag, wenn sich seit dem
   Vorschlag etwas bewegt hat (Server: `changedSinceProposal`). */
.net-settle-note { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--text-muted); }
/* Angenommene Verrechnungen (report_11 K1, Teil 2). */
.net-settled { margin-top: var(--space-3); }
.net-settled-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.net-settled-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.net-settled-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; font-size: var(--text-base); }
.net-settled-main { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.net-settled-date { color: var(--text-muted); font-variant-numeric: tabular-nums; font-size: var(--text-sm); }
/* .btn-secondary hat `flex: 1` fuer Modal-Footer. In dieser Listenzeile blies
   das "Rueckgaengig" live auf 240px auf — breiter als der Eintrag selbst, und
   damit das lauteste Element einer Liste, die nur zur Kenntnis genommen wird.
   Nur die Breite war falsch — die 44px Hoehe (--touch-target) bleibt. */
.net-settled-row > .btn-secondary {
  flex: 0 0 auto;
  margin-left: auto;
  padding: 0.5rem var(--space-3);
  font-size: var(--text-base);
}
/* Side by side where there is room, stacked on narrow phones. */
.net-settle-actions button {
  flex: 1 1 9rem;
  width: auto;
  margin: 0; /* .btn-primary carries a top margin of its own */
  min-height: var(--touch-target, 44px);
  padding: 0.45rem 0.9rem;
  font-size: 0.86rem;
}
@media (min-width: 720px) {
  .net-overview, .winding-down-notice { max-width: 40rem; margin-inline: auto; }
}
/* D13: 40rem zentriert ueber einem 1288px breiten Grid liess die Sektion als
   schmale Insel stehen, obwohl die Karten darunter die volle Breite nutzen. */
@media (min-width: 1280px) {
  .net-overview, .winding-down-notice { max-width: 56rem; }
}
@media (min-width: 720px) {
  .perspective-switch { max-width: 28rem; margin-inline: auto; }
}
.perspective-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.35rem;
  border-radius: var(--radius-sm);
  background: var(--warning);
  color: var(--bg);
  font-size: 0.7rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.perspective-count.is-overdue { background: var(--danger); color: #fff; }

.user-status-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  /* Own full-width row under the name/email, at every width. */
  grid-column: 1 / -1;
  justify-content: flex-start;
}
.user-status-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding-top: 0.55rem;
  border-top: 1px solid rgba(255, 255, 255, 0.04);
}
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  font-size: var(--text-xs);
  font-weight: 600;
  padding: 0.16rem 0.5rem;
  border-radius: var(--radius-xs);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-muted);
  border: none;
  cursor: default;
  font-family: inherit;
}
button.status-pill { cursor: pointer; transition: var(--transition-ui); }
.status-pill-on { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
/* opacity:0.7 auf dem ganzen Chip drueckte --text-muted auf 4.29:1 — bei
   11.2px/700 zu wenig. Gedimmte Farbe statt gedimmtem Element. */
.status-pill-off { background: rgba(255, 255, 255, 0.04); color: var(--text-dim); }
.status-pill-warn { background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--badge-pending-text); }
html:not(.using-touch) button.status-pill-clickable:hover { filter: brightness(1.15); }
button.status-pill-clickable { cursor: pointer; }

/* Live "online" dot inside the presence pill */
.presence-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #56bb8e;
  flex-shrink: 0;
  position: relative;
}
/* Befund 28: war box-shadow + infinite. Jetzt Pseudo-Ring, opacity/transform,
   drei Iterationen. */
.presence-dot::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1.5px solid rgba(86, 187, 142, 0.55);
  opacity: 0;
  pointer-events: none;
  animation: presencePulse 2s ease-out 3;
}
@keyframes presencePulse {
  0%   { opacity: 0.9; transform: scale(1); }
  70%  { opacity: 0;   transform: scale(2.4); }
  100% { opacity: 0;   transform: scale(2.4); }
}
@media (prefers-reduced-motion: reduce) { .presence-dot::after { animation: none; } }

/* Inline presence detail panel (click on the presence pill) */
.presence-detail { display: flex; flex-direction: column; gap: 0.5rem; }
.presence-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.06);
}
.presence-row--active {
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.presence-row-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.presence-row-label { font-size: 0.78rem; font-weight: 700; color: var(--text); }
.presence-row-value { font-size: 0.8rem; color: var(--text-muted); overflow-wrap: anywhere; }
.presence-star {
  flex: 0 0 auto;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--text-muted);
  padding: 0.2rem;
  transition: color var(--transition-fast), transform var(--transition-fast);
}
.presence-star.is-active { color: var(--badge-pending-text); }
.presence-star:active { transform: scale(0.9); }
.presence-hint { font-size: 0.72rem; color: var(--text-muted); margin: 0.1rem 0 0; }

.user-actions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.user-actions-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.user-actions-group-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  padding: 0 0.25rem;
  width: 100%;
}
/* Formular-Audit (M18): seltene Aktionen hinter „Weitere Aktionen“ (klappt in
   der Karte auf, kein schwebendes Menü – das würde vom scrollenden Dialog
   abgeschnitten), die endgültigen in einem eigenen Gefahrenbereich. */
.user-more-summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 36px;
  padding: 0.4rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
  transition: color var(--motion-base) var(--ease-standard), border-color var(--motion-base) var(--ease-standard);
}
.user-more-summary::-webkit-details-marker { display: none; }
html:not(.using-touch) .user-more-summary:hover { color: var(--text); border-color: var(--text-muted); }
.user-more-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.user-more-chevron { display: inline-flex; transition: transform var(--motion-base) var(--ease-standard); }
.user-more-menu[open] .user-more-chevron { transform: rotate(180deg); }
.user-more-menu[open] .user-more-summary { color: var(--text); }
.user-more-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding-top: 0.6rem;
}
.user-actions-danger {
  padding: 0.5rem 0.55rem 0.55rem;
  border: 1px solid color-mix(in srgb, var(--danger) 28%, transparent);
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--danger) 5%, transparent);
}
.user-actions-danger .user-actions-group-label { color: var(--danger-text); padding: 0; }
@media (pointer: coarse) {
  .user-more-summary { min-height: var(--touch-target, 44px); }
}
@media (max-width: 540px) {
  .user-more-summary { display: flex; width: 100%; justify-content: center; }
  .user-card .user-actions-primary > [data-action="reset-pw"] { grid-column: 1 / -1; }
}
/* Consistent compact sizing for ALL buttons inside the Users-Admin modal —
   overrides generic .btn-primary/.btn-secondary/.btn-danger/.btn-toggle-role
   sizing (44px / 1rem) which was visually inconsistent across the action
   strip. Specificity (.user-actions button) is enough to win against the
   single-class generic button selectors. */
.user-actions button,
.user-actions .btn-primary,
.user-actions .btn-secondary,
.user-actions .btn-danger,
.user-actions .btn-delete-user-inline {
  font-size: 0.78rem;
  padding: 0.4rem 0.7rem;
  min-height: 36px;
  border-radius: var(--radius-xs);
  font-weight: 600;
  flex: 0 0 auto;
  width: auto;
  margin-top: 0;
  box-shadow: none;
  transition:
    background var(--motion-base) var(--ease-standard),
    border-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}
/* Active highlight while an inline edit slot is open for this action. */
.user-actions button.is-active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.user-edit-slot {
  padding: 0.6rem 0;
  border-top: 1px solid rgba(255, 255, 255, 0.04);
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  /* Bug B: keep inline-edit content (matrix, inputs, button rows) within the
     card width so the user-card never overflows horizontally. */
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  overflow: hidden;
}
.user-edit-slot.hidden { display: none; }
.user-edit-slot > *,
.user-edit-slot .user-edit-row,
.user-edit-slot .user-edit-events,
.user-edit-slot .notify-events-list,
.user-edit-slot .notify-events-matrix {
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
}
.user-edit-slot .user-edit-row {
  flex-wrap: wrap;
  align-items: stretch;
}
/* Input takes the full row on its own; Abbrechen/Speichern share the next
   row 50/50 so they're aligned and easy to hit on touch — never one
   above/below the other or mis-aligned with the Entfernen button outside
   the edit slot. */
.user-edit-slot .user-edit-row > input { flex: 1 1 100%; }
.user-edit-slot .user-edit-row > button {
  flex: 1 1 calc(50% - 0.25rem);
  min-height: var(--touch-target);
}
.user-edit-slot input[type="text"],
.user-edit-slot input[type="email"] { min-width: 0; }

@media (max-width: 540px) {
  .user-card-head { grid-template-columns: auto 1fr; }
  .user-card-head .user-status-pills {
    grid-column: 1 / -1;
    justify-content: flex-start;
  }
  /* On mobile keep buttons wrapping so the two action groups stay visually
     separated; horizontal scroll would cut the divider/labels. */
  .user-actions { overflow-x: visible; flex-direction: column; }
  .user-actions-group { flex-wrap: wrap; }
  .user-actions button { white-space: normal; }
  /* Aktionen als 2-Spalten-Raster statt Flatter-Umbruch: vorher standen
     3 + 2 + 1 + 1 Knöpfe unterschiedlicher Breite untereinander. */
  .user-card .user-actions-group {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .user-card .user-actions-group-label { grid-column: 1 / -1; }
  .user-card .user-actions-group > button,
  .user-card .user-actions-group > .btn-secondary,
  .user-card .user-actions-group > .btn-delete-user-inline { width: 100%; }
}
/* Lange E-Mail-Adressen ohne Umbruchstelle liefen über den Kartenrand
   (gemessen 412px: ~40px abgeschnitten). */
.user-email { overflow-wrap: anywhere; min-width: 0; }
.user-card-info { min-width: 0; }
/* Klickbare Pillen (Geräte, Präsenz) waren auf Touch 44px hoch, die reinen
   Info-Pillen daneben 24px — die Pillen-Reihe hatte dadurch zwei Höhen bzw.
   wurde komplett auf 44px gestreckt. Optisch kompakt wie die Nachbarn, die
   44px-Trefferfläche liefert ein unsichtbares ::after (Muster .modal-close). */
.user-list .user-status-pills button.status-pill {
  min-height: 0;
  position: relative;
}
.user-list .user-status-pills button.status-pill::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--touch-target);
  transform: translateY(-50%);
}
.user-list .user-status-pills { align-items: center; }
/* "Admin-Recht entziehen …" stand in normaler Button-Größe zwischen den
   kompakten Aktionsknöpfen der Karte. */
.user-card .admin-revocation > .btn-secondary {
  font-size: 0.8rem;
  padding: 0.45rem 0.75rem;
  min-height: 36px;
  width: auto;
  margin-top: 0;
}
@media (hover: none) and (pointer: coarse) {
  .user-card .admin-revocation > .btn-secondary { min-height: var(--touch-target); }
}

/* =========================================================================
   Mobile / responsive bug fixes (consolidated)
   ========================================================================= */

/* Fix 2: Tap-able status-pills must satisfy 44px touch-target.
   Informational pills stay compact (~24-28px). */
button.status-pill,
.status-pill[role="button"],
.status-pill[tabindex] {
  min-height: var(--touch-target, 44px);
  padding: 0.4rem 0.7rem;
  cursor: pointer;
}

/* Fix 12: Touch-target for tap-able link-buttons */
button.link-btn,
.link-btn[role="button"],
.link-btn[tabindex] {
  min-height: var(--touch-target, 44px);
  display: inline-flex;
  align-items: center;
}

/* Fix 10 (P2-8): Offline-banner sticky on mobile, parked under the header.
   Header sits at z-index 10, so banner uses 9 to slot just beneath it. */
@media (max-width: 768px) {
  .offline-banner {
    position: sticky;
    top: 68px;
    z-index: var(--z-offline-mob);
  }

  /* The SW-update banner is centered and up to ~420px wide — on typical
     phone widths (375-430px) that's nearly the full header width, blocking
     the sync-status pill and burger menu underneath until dismissed. Park it
     below the header instead of floating over it (same 68px used above). */
  .sw-update-banner {
    top: calc(env(safe-area-inset-top, 0px) + 68px);
  }
}

/* Fix 4: Sticky section-header inside settings/wide modals on small screens */
/* Sticky settings-section-head was causing scroll glitches when multiple
   <details> are stacked: each head competes for top:0 and the scroll
   container can not show following sections. Reverted to non-sticky.
@media (max-width: 820px) {
  .modal-card-wide .settings-section-head,
  .settings-section .settings-section-head {
    position: sticky;
    top: 0;
    z-index: var(--z-settings-head);
    background:
      linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01)),
      var(--surface);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }
}
*/

/* Fix 8: Slightly thicker drag-handle indicator on small screens for tactile feel */
@media (max-width: 540px) {
  .modal-drag-handle::after {
    height: 5px;
    width: 42px;
  }
}

/* Fix 1 (companion to existing 540px override): tighten loan-snapshot below 360px */
@media (max-width: 360px) {
  .loan-snapshot {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.4rem;
  }
  .loan-snapshot .snapshot-item {
    padding: 0.55rem 0.6rem;
  }
}

/* Fix 6: Action-banner padding/wrap at 320-360px */
@media (max-width: 360px) {
  .action-banner {
    padding: 0.65rem 0.8rem;
    gap: 0.6rem;
    flex-wrap: wrap;
  }
  .action-banner-cta {
    width: 100%;
  }
}

/* Fix 7: Compress tab-pills at 320-360px */
@media (max-width: 360px) {
  .tab-pill {
    font-size: 0.78rem;
    padding: 0.45rem 0.5rem;
    gap: 0.25rem;
  }
  .tab-count {
    font-size: var(--text-xs);
    min-width: 1.1rem;
    height: 1.1rem;
  }
}

/* Fix 5 (companion): stack inline-edit action buttons on very small screens */
@media (max-width: 360px) {
  .inline-edit-actions {
    flex-direction: column;
    gap: 0.4rem;
  }
}

/* === Sticky-bar / note-empty / profile-desktop fixes === */

/* 1. Sticky action bar layout on mobile:
     - Pending loans (Approve+Reject): 2 columns side-by-side.
     - Active loans (Manual-Addition + Add-Payment + optional Close-Loan):
       row 1 = Ergänzung + Zahlung eintragen side-by-side, row 2 = Abschließen full-width below.
   Uses flex with explicit per-button basis values so DOM order matters but
   layout is robust. Falls back to single column on tiny screens. */
@media (max-width: 540px) {
  .loan-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
  }
  .loan-actions > button,
  .loan-actions .btn-approve,
  .loan-actions .btn-reject,
  .loan-actions .btn-add-payment,
  .loan-actions .btn-close-loan,
  .loan-actions .btn-manual-entry,
  .loan-actions .btn-withdraw,
  .loan-actions .btn-borrower-action,
  .loan-actions .btn-borrower-req-active,
  .loan-actions .btn-extend-approve,
  .loan-actions .btn-extend-reject,
  .loan-actions .btn-primary,
  .loan-actions .btn-secondary,
  .loan-actions .btn-danger {
    flex: 1 1 calc(50% - 0.2rem);
    min-width: 0;
    font-size: 0.9rem;
    padding: 0.55rem 0.6rem;
    white-space: normal;
    min-height: var(--touch-target);
    margin-top: 0;
  }
  /* Active loans: Manual + Payment side-by-side (50% each), Close full-width below. */
  .loan-actions .btn-add-payment {
    background: var(--accent-btn);
    color: #fff;
    border-color: var(--accent-btn);
    font-weight: 700;
  }
  .loan-actions .btn-close-loan {
    flex: 1 1 100%;
    font-size: 0.85rem;
    padding: 0.5rem 0.75rem;
    min-height: var(--touch-target);
  }
  /* Withdraw is a single button: full width. */
  .loan-actions .btn-withdraw {
    flex: 1 1 100%;
  }
  /* Borrower: active-request buttons span full width (badge + text doesn't fit at 50%) */
  .loan-actions .btn-borrower-req-active {
    flex: 1 1 100%;
  }
}

@media (max-width: 360px) {
  .loan-actions > button,
  .loan-actions .btn-approve,
  .loan-actions .btn-reject,
  .loan-actions .btn-add-payment,
  .loan-actions .btn-close-loan,
  .loan-actions .btn-manual-entry,
  .loan-actions .btn-borrower-action,
  .loan-actions .btn-borrower-req-active,
  .loan-actions .btn-extend-approve,
  .loan-actions .btn-extend-reject {
    font-size: 0.85rem;
    padding: 0.55rem 0.6rem;
  }
}

/* 2. Empty notes: make the whole empty area an obvious clickable target */
.note-empty {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.5rem 0.85rem;
  border: 1px dashed rgba(255,255,255,0.12);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  text-align: center;
  gap: 0.5rem;
  font-style: normal;
  font-size: 0.85rem;
  transition: var(--transition-ui);
}

html:not(.using-touch) .note-empty:hover,

.note-empty:focus-visible  {
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 4%, transparent);
  color: var(--text);
  outline: none;
}
/* A11y R5 (WCAG 2.4.7): nur die blasse Rand-Toenung (35 % Gruen auf Dunkel)
   markierte den Tastaturfokus — derselbe Ring wie ueberall. */
.note-empty:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Touch-Audit R4: "+ Notiz hinzufuegen" zeigte beim Tippen nichts (nur
   Maus-Hover). Druck = kraeftigere Hover-Toenung, doppelt gegen die
   spezifischere Hover-Regel. */
.note-empty:active,
html:not(.using-touch) .note-empty:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 9%, transparent);
  color: var(--text);
  transition-duration: 0s;
}

.note-empty-icon {
  font-size: 1.05rem;
  line-height: 1;
  color: var(--text-muted);
}

/* 3. Profile modal: roomier on desktop.
   D1: Der Breakpoint stand auf 768px und ueberschrieb damit im Band 768-820px
   den Bottom-Sheet-Block (@media max-width:820px) — hoehere Spezifitaet UND
   spaeter im File. Ergebnis @768: Card 707x956, bottom==1024, radius
   18px 18px 0 0, links/rechts ~23px Hintergrund -> eckige Unterkanten, die
   "schweben". Das Profil-Modal war das einzige betroffene. 821px ist die
   Sheet/Dialog-Grenze der gesamten App.
   R5-Tablet: die Grenze liegt jetzt bei 540/541 px (Tablet = Dialog). */
@media (min-width: 541px) {
  #modal-profile .modal-card,
  .modal-profile .modal-card {
    max-width: 720px;
    width: 92vw;
  }
}

@media (min-width: 1200px) {
  #modal-profile .modal-card,
  .modal-profile .modal-card {
    /* auf großen Screens auf die (damalige) xl-Stufe 820px anheben — seit D12
       wächst --modal-w-xl für das Log-Archiv bis 1180px, das Profil (ein
       Formular) soll da nicht mitwachsen. */
    max-width: min(var(--modal-w-xl), 820px);
  }
}

/* === Notification matrix (profile + admin) === */
.notify-events-block {
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid rgba(255,255,255,0.06);
}
/* Titel + Beschreibung wie die Nachbar-Blöcke der Sektion ("Registrierte
   Geräte", "Fälligkeits-Erinnerungen"): .toggle-label-text/.toggle-desc.
   Früher stand hier uppercase + letter-spacing, das auf beide Kinder
   durchschlug — die Beschreibung stand als Versal-Zeile da, der Block sah
   wie eine andere Sektionsebene aus. */
.notify-events-head {
  margin-bottom: 0.6rem;
}
.notify-events-matrix,
.notify-events-list {
  display: grid;
  gap: 0.4rem;
}
/* Group-Header in der Notify-Matrix (Leihe / Taxi / …). Klein, gemuted,
   gleich-stark wie die Header-Zelle "Ereignis". Trennt 23 Events in
   verdauliche thematische Cluster. */
.notify-events-group {
  display: grid;
  gap: 0.4rem;
}
.notify-events-group + .notify-events-group {
  margin-top: 0.65rem;
}
.notify-events-group-title {
  margin: 0 0 0.1rem;
  padding: 0 0.7rem;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
}
.notify-events-row {
  display: grid;
  grid-template-columns: 1fr 56px 56px;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  background: rgba(255,255,255,0.02);
  border-radius: var(--radius);
}
.notify-events-row .notify-events-toggle {
  margin: 0 auto;
}
.notify-events-header {
  display: grid;
  grid-template-columns: 1fr 56px 56px;
  gap: 0.6rem;
  padding: 0 0.7rem 0.25rem;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  text-align: center;
}
.notify-events-header .notify-events-cell-label,
.notify-events-row .notify-events-cell-label {
  text-align: left;
}
.notify-events-cell-label {
  font-size: 0.88rem;
  font-weight: 500;
}
/* Kopfzeile: "Ereignis" stand in Zeilen-Schriftgröße neben den kleinen
   Spaltenköpfen "Push"/"Mail". */
.notify-events-header .notify-events-cell-label {
  font-size: inherit;
  font-weight: inherit;
}
.notify-events-cell-channel {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 56px;
}
.notify-events-toggle {
  cursor: pointer;
}

/* User-modal nested matrix (slimmer) */
.user-edit-events .notify-events-block {
  margin-top: 0.5rem;
  padding-top: 0.5rem;
}

/* === Manual-entry send-status hint === */
.ml-send-status {
  margin-top: 0.5rem;
  padding: 0.55rem 0.75rem;
  border-radius: var(--radius-xs);
  font-size: 0.82rem;
  line-height: 1.4;
}

/* === Bonus tag in history === */
.loan-bonus-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--badge-pending-text);
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 540px) {
  .notify-events-row,
  .notify-events-header {
    grid-template-columns: 1fr 44px 44px;
    gap: 0.4rem;
  }
  .notify-events-cell-channel { min-width: 0; }
}

/* =========================================================================
   Mobile-scan polish (post Welle-1) — touch targets, overflow, safe-areas.
   ========================================================================= */

/* B1: User-actions buttons were 36px → bump to the project's 44px touch
   target on touch devices, matching the neighboring .inline-edit-toggle
   below (was 40px, an inconsistent halfway point between the two). */
@media (hover: none) and (pointer: coarse) {
  .user-actions button,
  .user-actions .btn-primary,
  .user-actions .btn-secondary,
  .user-actions .btn-danger,
  .user-actions .btn-delete-user-inline {
    min-height: var(--touch-target, 44px);
    padding: 0.45rem 0.75rem;
    font-size: 0.8rem;
  }
}

/* B1: Inline-edit toggle is a tap target — ensure 44px on touch. */
@media (hover: none) and (pointer: coarse) {
  .inline-edit-toggle {
    min-height: var(--touch-target, 44px);
    display: inline-flex;
    align-items: center;
  }
}

/* B2: Settings-section-head with long status-pills must wrap rather than push
   the chevron off-screen on 320px viewports. */
@media (max-width: 360px) {
  .settings-section-head {
    flex-wrap: wrap;
    row-gap: 0.35rem;
  }
  .settings-section-head .status-pill {
    font-size: 0.7rem;
    padding: 0.2rem 0.5rem;
  }
}

/* B2: User-card status-pills + action-strip wrapping fix at 320px.
   The 540px rule already moves pills to a new row; ensure groups stack
   cleanly on the very narrow end too. */
@media (max-width: 360px) {
  .user-actions-group { gap: 0.3rem; }
  .user-actions-group-label { font-size: var(--text-xs); }
  .user-status-pills { flex-wrap: wrap; gap: 0.3rem; }
}

/* B2: History card with new .loan-bonus-tag — make the headline row wrap
   gracefully when the bonus tag is present alongside borrower + amount. */
.loan-bonus-tag { flex-shrink: 0; max-width: 100%; }
@media (max-width: 360px) {
  .loan-bonus-tag {
    font-size: 0.7rem;
    padding: 0.18rem 0.45rem;
  }
}

/* B3: Sticky loan-actions bar on mobile — respect iOS bottom safe-area so
   the bar doesn't sit under the home-indicator. */
@media (max-width: 768px) {
  .loan-card .loan-actions {
    padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
  }
}

/* B3: Toast-container already lifts above the sticky bar (Fix 3). Add a
   parallel guard for the action-banner CTA when stacked above sticky bar:
   ensure the banner has bottom-margin reserve so its CTA never tucks under
   the sticky bar on very short viewports. */
@media (max-width: 540px) and (max-height: 720px) {
  .action-banner { margin-bottom: 0.85rem; }
}

/* B4: iOS Safari — viewport units. Patch any leftover 100vh users with dvh
   for the overlay used by the install banner / onboarding (already 100dvh
   in most spots; this is a defensive fallback for elements that fall back
   to fixed-positioned full-viewport layers). */
@supports (height: 100dvh) {
  .modal-overlay {
    min-height: 100dvh;
  }
}

/* =========================================================================
   Welle-B2: Users-modal compactness + horizontal-overflow safety net.
   ========================================================================= */

/* Slightly smaller status-pills inside the users-admin modal so the pill
   strip fits more comfortably next to the avatar/name. */
.user-list .status-pill {
  padding: 0.2rem 0.5rem;
  font-size: 0.7rem;
}

/* Belt-and-braces: the page itself never scrolls horizontally, even if some
   nested layout briefly overflows. The vertical scroll is unaffected. */
html, body { overflow-x: clip; }
/* Prevent Chrome-Android native pull-to-refresh from competing with our
   own PTR handler in app.js. The native PTR otherwise fires before we get
   a touch-start and the page reloads mid-swipe. */
html { overscroll-behavior-y: contain; }
/* Runde 4 (Native): in the installed app the page no longer rubber-bands as a
   whole — on iOS header, content and tab bar sprang back together past the
   bottom edge, the clearest "this is a web page" tell. `none` only switches
   off the root's own bounce/glow: sheets, the menu and lists keep their local
   overscroll (contain), and pull-to-refresh is our own touch handler
   (setupPullToRefresh, preventDefault at the very top), not the browser's
   overscroll, so it is unaffected. The browser tab keeps `contain` above. */
/* Rubber band wieder an (Nutzerwunsch): das leichte Zurückfedern am Anfang
   und Ende der Seite gehört auf iOS zum App-Gefühl. `contain` (oben) hält
   nur Scroll-Chaining/Browser-Gesten ab, das Federn bleibt. */

/* Desktop multi-column grids: ab ≥ 1100 px haben wir genug Breite, um
   zwei Loan-Cards nebeneinander zu zeigen; ab ≥ 1440 px drei. Bricht
   automatisch zurück auf einspaltig wenn der Container schmal wird (z.B.
   wenn die App in ein iPad-Splitscreen wandert). */
@media (min-width: 1100px) {
  #active-loans-content,
  #borrower-active-section,
  .borrower-pending-section {
    display: grid;
    /* D3 aus dem Audit ("der Kommentar verspricht drei Spalten, es werden nie
       drei") wurde erst umgesetzt und wieder zurückgenommen: mit minmax(410px)
       entstehen zwar drei Spalten, die Karten sind dann aber nur 419px breit —
       für eine Loan-Card mit Kopfzeile, Betrag, Fortschrittsring und
       Aktivitätstabelle spürbar zu eng. Zwei Spalten à 636px sind die bessere
       Darstellung; der main-Content ist bei 1440-2560px ohnehin auf 1288px
       gedeckelt. Nicht der Wert war falsch, sondern das Versprechen im alten
       Kommentar.
       auto-fit statt auto-fill: eine nicht gefüllte Spur kollabiert, statt als
       Leerraum neben den Karten stehen zu bleiben. */
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 1rem;
    /* Befund 44: das Raster streckte bisher jede Karte auf die Hoehe der
       hoechsten Karte der Zeile (Grid-Default align-items: stretch). Live
       gemessen bei 1440px mit einer vollen (574px) und einer kurzen (304px)
       Karte: 286px leere KARTENFLAECHE — also Rahmen und Hintergrund um
       nichts herum, was wie ein Renderfehler aussieht, nicht wie Leerraum.
       `start` laesst jede Karte auf ihrer Inhaltshoehe enden; der Platz
       darunter ist dann Seitenhintergrund. Gemessen: 286px -> 0px.
       Echtes Packen (Karte 3 rutscht unter die kurze Karte 2) geht damit
       nicht — siehe der Kommentar unter diesem Block. */
    align-items: start;
  }
  /* .loan-card bringt margin-bottom: 1rem fuer den einspaltigen Fluss mit.
     Im Raster addiert sich das auf die gap und ergibt 2rem Zeilenabstand,
     waehrend der Spaltenabstand 1rem bleibt. */
  #active-loans-content > .loan-card,
  #active-loans-content > .loan-card-lender,
  #borrower-active-section > .loan-card,
  #borrower-active-section > .loan-card-lender,
  .borrower-pending-section > .loan-card,
  .borrower-pending-section > .loan-card-lender {
    margin-bottom: 0;
  }
  /* Im Zwei-Spalten-Raster ist eine Karte nur 420-636px breit. Die vier
     Kennzahl-Kacheln (Bonus / Gezahlt / Ursprungszweck / Auszahlung) lagen
     trotzdem in vier Spalten à ~100-140px: Titel mit Silbentrennung
     ("UR-SPRUNGS-ZWECK", "AUSZAH-LUNG"), "Nicht angegeben" auf zwei Zeilen,
     Kacheln bis 210px hoch. Mehrspaltig daher 2×2 wie auf dem Handy; eine
     einzelne Karte (bis 46rem breit, siehe unten) behält die vier Spalten. */
  #active-loans-content:has(> .loan-card ~ .loan-card) .loan-snapshot,
  .borrower-pending-section:has(> .loan-card ~ .loan-card) .loan-snapshot,
  /* Leihnehmer-Aktiv: jede Karte steckt in einer eigenen .section ("Leihe bei
     …"); zwei Karten = zwei solche Sections. :has() darf nicht geschachtelt
     werden, daher über Geschwister: die erste mit Nachfolger, alle weiteren. */
  #borrower-active-section > .section:has(> .loan-card):has(~ .section > .loan-card) .loan-snapshot,
  #borrower-active-section > .section:has(> .loan-card) ~ .section .loan-snapshot {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
  }
  /* Dieselbe Doppelung wie bei den Karten oben: .section bringt 2rem
     margin-bottom mit, im Raster kam die gap dazu — zwischen zwei
     Kartenreihen standen ~76px Leere. */
  #borrower-active-section { row-gap: 1.5rem; }
  #borrower-active-section > .section { margin-bottom: 0; }

  /* Warum kein Masonry und keine CSS-Columns (beides gemessen, 2026-09-19):
     - `grid-template-rows: masonry` wird nicht unterstuetzt
       (CSS.supports(...) === false im ausgelieferten Chromium), die
       Deklaration fiel wirkungslos durch — Messwerte identisch zum Status quo.
     - `column-count: 2` packt zwar (4 ungleiche Karten: 1086px -> 894px
       Gesamthoehe, -18%), kippt die Lesereihenfolge aber auf spaltenweise:
       Karte 2 steht dann UNTER Karte 1 in derselben Spalte. Bei einer Liste,
       die auf zwanzig Leihen wachsen kann, muss man die erste Spalte ganz
       nach unten scrollen und danach wieder ganz nach oben, um die zweite zu
       lesen. Der Hoehengewinn ist den Preis nicht wert.
     - "ab Hoehendifferenz > 50% auf einspaltig" (Vorschlag B6 des Reports)
       ist in CSS nicht ausdrueckbar: es gibt keinen Selektor, der
       Inhaltshoehen vergleicht. Das braucht JS in app.js. */

  /* Eine einzelne Leihe stand als ~410px schmaler Streifen in einem 1288px
     breiten Bereich — 1fr verteilt die Breite auf alle Spuren, auch die leeren.
     Eine einzelne Karte bekommt deshalb eine eigene, lesbare Spaltenbreite.
     Linksbündig, nicht zentriert: die Karte bleibt damit an derselben Kante wie
     die Abschnittsüberschrift darüber und wie alle mehrspaltigen Ansichten.
     :has zählt die Karten, weil das Raster das selbst nicht kann. */
  #active-loans-content:has(> .loan-card):not(:has(> .loan-card ~ .loan-card)),
  #borrower-active-section:has(> .loan-card):not(:has(> .loan-card ~ .loan-card)),
  .borrower-pending-section:has(> .loan-card):not(:has(> .loan-card ~ .loan-card)) {
    grid-template-columns: minmax(0, 46rem);
    justify-content: start;
  }
  /* Sub-elements wie h2.section-title sollen die volle Grid-Zeile einnehmen. */
  #active-loans-content > h2,
  #borrower-active-section > section > h2,
  .borrower-pending-section > h2 {
    grid-column: 1 / -1;
  }
  /* Im Raster ist der gap (1rem) bereits der Abstand Titel -> Karte; die
     44px-Trefferflaeche des klappbaren Titels (siehe .section-toggle) kaeme
     sonst obendrauf. So bleibt Text -> Karte bei ~1rem wie auf Mobile. */
  #active-loans-content > h2.section-toggle { margin-bottom: -0.7rem; }

  #borrower-active-section > .section:has(.empty-state),
  #borrower-pending-section > .section:has(.empty-state),
  #borrower-history-section > .section:has(.empty-state) {
    grid-column: 1 / -1;
    display: flex;
    justify-content: center;
  }

  #borrower-active-section > .section:has(.empty-state) .empty-state,
  #borrower-pending-section > .section:has(.empty-state) .empty-state,
  #borrower-history-section > .section:has(.empty-state) .empty-state {
    width: min(100%, 420px);
  }
}
/* Suppress iOS long-press context-menu on loan cards so the swipe and
   wiggle hint affordances are not interrupted by the native share sheet. */
.loan-card, .loan-card-lender, .history-card { -webkit-touch-callout: none; }

/* =========================================================================
   Welle-B2: Notify-matrix column greying when the global Push/Mail toggle
   is off. Both the channel cell and the toggle input get dimmed and the
   toggle becomes non-interactive; the row label stays readable.
   ========================================================================= */
/* Push column: 1st label in data rows, 2nd span overall in header (after the label-span) */
.notify-events-matrix.matrix-push-off
  .notify-events-row .notify-events-toggle:nth-of-type(1),
.notify-events-list.matrix-push-off
  .notify-events-row .notify-events-toggle:nth-of-type(1),
.notify-events-matrix.matrix-push-off
  .notify-events-header .notify-events-cell-channel:nth-of-type(2),
.notify-events-list.matrix-push-off
  .notify-events-header .notify-events-cell-channel:nth-of-type(2),
.notify-events-matrix.matrix-push-off
  [data-notify-channel="push"],
.notify-events-list.matrix-push-off
  [data-notify-channel="push"] {
  opacity: 0.4;
  cursor: not-allowed;
}
/* Mail column: 2nd label in data rows, 3rd span overall in header */
.notify-events-matrix.matrix-mail-off
  .notify-events-row .notify-events-toggle:nth-of-type(2),
.notify-events-list.matrix-mail-off
  .notify-events-row .notify-events-toggle:nth-of-type(2),
.notify-events-matrix.matrix-mail-off
  .notify-events-header .notify-events-cell-channel:nth-of-type(3),
.notify-events-list.matrix-mail-off
  .notify-events-header .notify-events-cell-channel:nth-of-type(3),
.notify-events-matrix.matrix-mail-off
  [data-notify-channel="mail"],
.notify-events-list.matrix-mail-off
  [data-notify-channel="mail"] {
  opacity: 0.4;
  cursor: not-allowed;
}
/* Lock the inner checkbox itself so flips are still impossible, but keep the
   label/cell clickable so the JS click-handler can show a "turn on the global
   toggle first" toast. */
.matrix-push-off [data-notify-channel="push"],
.matrix-mail-off [data-notify-channel="mail"] {
  pointer-events: auto;
}
.matrix-push-off [data-notify-channel="push"] input,
.matrix-mail-off [data-notify-channel="mail"] input {
  pointer-events: none;
}


/* === Unified sticky modal header (all modals) ===
   Every modal-card scrolls internally (overflow-y: auto). Pin the
   title row at the top of that scroll container so it stays visible
   while the user scrolls through the body. Works with multiple
   header-wrapper conventions:
     - `<h2>` direct child of .modal-card (decide, payment, new-loan,
       manual-entry, change-pw, new-user, temp-pw, onboarding)
     - `.modal-top`   (settings, stats, users)
     - `.modal-head`  (profile)
     - `.logs-modal-head` (logs)
   Same visual treatment everywhere: opaque surface background,
   matching card padding bleed, subtle bottom-border, z-index 4.

   Desktop: pinned at top of card.
   Mobile:  pinned just below the sticky drag handle (2.25rem). */
.modal-card > h2:first-of-type {
  position: sticky;
  top: 0;
  z-index: var(--z-modal-head);
  background: var(--surface);
  text-align: left;
  /* Bleed left/right across card padding for full-width background.
     No negative margin-top needed: card padding-top is 0, the 4px colour
     strip is handled by the ::before pseudo. Header owns its own top padding. */
  margin-left: -2rem;
  margin-right: -2rem;
  padding-left: 2rem;
  padding-right: 2rem;
  padding-top: 1.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid transparent;
  transition: border-bottom-color var(--motion-fast) ease;
}
/* No divider line on scrolled modal-cards — for visual consistency every
   modal looks the same regardless of whether its content scrolls (Logs
   used to show this line, smaller modals did not = inkonsistent). The
   sticky-header keeps its opaque background so content fading out under
   it is still readable as "more below". */
/* Logs: header + footer are fixed flex children, only #logs-body scrolls.
   Override the sticky header rules — this modal has its own scroll architecture. */
.modal-card-logs {
  padding-top: 1.5rem;
}
.modal-card-logs > .modal-actions {
  flex-shrink: 0;
}
/* Logs-Modal uses overflow:hidden + inner scroll on #logs-body, so its
   .modal-actions is a normal flex child (not sticky). No content scrolls
   beneath it, so we don't need the long gradient-fade the other modals use.
   Match the slim padding of the standard sticky bar (1.1rem top / 0.7rem
   bottom) and use a simple border-top for separation. Same 540 px breakpoint
   as the other modals so logs doesn't get a "different" bar on tablets. */
@media (max-width: 540px) {
  .modal-card-logs > .modal-actions {
    background: var(--surface);
    border-top: 1px solid rgba(255, 255, 255, 0.06);
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    padding: 1.1rem 1.25rem calc(0.7rem + env(safe-area-inset-bottom, 0px));
  }
}

.modal-card > h2:first-of-type {
  margin-bottom: 0.5rem;
}

/* Prevent scrolled content from hiding behind the sticky modal header.
   scroll-padding-top tells the browser to treat the top of the scroll
   container as starting below the sticky header (~header height). */
.modal-card {
  scroll-padding-top: 4.5rem;
}

/* Mobile: drag-handle is sticky at top:0 with height 2.25rem, header
   sits directly below it. Card padding shrinks to 1.25rem horizontally.
   R5-Tablet: nur im Sheet (Handy), siehe Sheet-Block. */
@media (max-width: 540px) {
  .modal-card {
    padding-top: 0;
  }
  .modal-card > h2:first-of-type {
    top: 2.25rem;
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    padding-top: 1.25rem;
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }
}

/* ─── Reliability Modal ─────────────────────────────────────── */

/* SVG score ring */
.rel-ring { display: block; flex-shrink: 0; }
.rel-ring-large { width: 120px; height: 120px; }
.rel-ring-small  { width: 88px;  height: 88px; }
.rel-ring-bg   { fill: none; stroke: var(--surface2); stroke-width: 10; }
.rel-ring-fill {
  fill: none;
  stroke-width: 10;
  stroke-linecap: round;
  stroke-dasharray: calc(var(--score) / 100 * var(--circ, 326.73)) var(--circ, 326.73);
  transform: rotate(-90deg);
  transform-origin: center;
  transition: stroke-dasharray var(--motion-slow) var(--ease-standard);
}
.rel-ring-fill-good { stroke: var(--accent); }
.rel-ring-fill-mid  { stroke: var(--warning); }
.rel-ring-fill-building { stroke: var(--rel-building); }
.rel-ring-fill-shaky    { stroke: var(--warning); }
.rel-ring-fill-bad  { stroke: var(--danger); }
.rel-ring-text      { font-weight: 700; fill: var(--text); }

/* Hero section (large ring + info) */
.rel-hero {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 1.25rem 0 1rem;
}
.rel-hero-info { flex: 1; min-width: 0; }
.rel-hero-score-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.4rem;
}
.rel-hero-score { font-size: 2rem; font-weight: 800; line-height: 1; }
.rel-hero-desc  { font-size: 0.85rem; color: var(--text-muted); margin: 0 0 0.5rem; line-height: 1.4; }

/* Breakdown section */
.rel-breakdown-section { margin-top: 1.25rem; }
.rel-breakdown-section > .modal-section-label:first-child { margin-top: 0; }

/* Borrower cards (lender all-view) */
.rel-borrower-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
  padding: 0.75rem 0 1rem;
}
.rel-borrower-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 1.25rem 1rem;
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
  text-align: center;
}
html:not(.using-touch) .rel-borrower-card:hover {
  border-color: var(--border-strong);
  background: var(--surface);
}
.rel-borrower-card-info { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; }
.rel-borrower-name  { font-weight: 600; font-size: 0.95rem; }
.rel-borrower-meta  { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.rel-borrower-desc  { font-size: 0.8rem; color: var(--text-muted); }

/* New-borrower variant: greyed-out card so "noch keine Bewertung" visually
   reads as "nicht bewertet", not as a low score. Border + background stay
   subtle, ring shows "—" instead of a percent. */
.rel-borrower-card-new {
  opacity: 0.85;
  background: transparent;
}
.rel-borrower-card-new .rel-borrower-name { opacity: 0.85; }
.rel-borrower-new-tag {
  display: inline-block;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-sm);
  background: var(--surface2, rgba(255,255,255,0.06));
  border: 1px solid var(--border, rgba(255,255,255,0.14));
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.rel-ring-empty .rel-ring-bg { stroke: var(--border, rgba(255,255,255,0.18)); }
.rel-ring-text-empty { fill: var(--text-muted); }

/* Single-borrower "neu" panel inside the modal — bigger placeholder when a
   lender clicks a borrower-pill that hasn't been used yet. */
.rel-borrower-new-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 1.5rem 1rem;
  text-align: center;
}
.rel-borrower-new-panel h3 { margin: 0.5rem 0 0; font-size: 1.1rem; }
.rel-borrower-new-panel .muted { margin: 0; color: var(--text-dim); font-size: var(--text-md); max-width: 32ch; }

/* ─── Score Graph ───────────────────────────────────────────── */
.rel-graph-wrap {
  margin: 0.75rem 0 0.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
  padding: 0.6rem 0.75rem 0.3rem;
  overflow: hidden;
}
.rel-graph { width: 100%; height: 80px; display: block; overflow: visible; }
.rel-graph-line  { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linejoin: round; stroke-linecap: round; }
.rel-graph-area  { fill: currentColor; opacity: 0.12; }
.rel-graph-dot   { fill: currentColor; cursor: default; }
.rel-graph-grid  { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 3; }
.rel-graph-grid-target { stroke: var(--accent); opacity: 0.25; stroke-dasharray: 4 4; }
/* Typo-Untergrenze: 7px war die kleinste Schrift der App. */
.rel-graph-label { font-size: 11px; fill: var(--text-muted); font-family: inherit; }
.rel-graph-label-target { fill: var(--accent); opacity: 0.6; }
.rel-graph-good  { color: var(--accent); }
.rel-graph-mid   { color: var(--warning); }
.rel-graph-building { color: var(--rel-building); }
.rel-graph-shaky    { color: var(--warning); }
.rel-graph-bad   { color: var(--danger); }
.rel-graph-caption { font-size: 0.72rem; color: var(--text-muted); margin: 0.25rem 0 0; text-align: right; }

/* ─── Confidence Badge ──────────────────────────────────────── */
.rel-confidence {
  display: inline-flex;
  flex-direction: column;
  gap: 0.12rem;
  font-size: 0.72rem;
  border-radius: var(--radius-sm);
  padding: 0.28rem 0.65rem;
  /* Eigenständiger Block-Eigenständigkeit: Badge oben, Projection-Box
     darunter — vorher klebten beide aneinander, weil Confidence nur
     margin-top hatte. */
  margin: 0.5rem 0 0.65rem;
  border: 1px solid transparent;
}
.rel-confidence small {
  color: inherit;
  opacity: 0.75;
  font-size: var(--text-xs);
  line-height: 1.25;
}
.rel-confidence-high { color: var(--accent);     border-color: color-mix(in srgb, var(--accent)  30%, transparent); background: color-mix(in srgb, var(--accent)  8%, transparent); }
.rel-confidence-mid  { color: var(--text-muted); border-color: var(--border); background: var(--surface2); }
.rel-confidence-low  { color: var(--warning);    border-color: color-mix(in srgb, var(--warning) 30%, transparent); background: color-mix(in srgb, var(--warning) 8%, transparent); }

.rel-score-change {
  font-size: 0.75rem;
  font-weight: 700;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.18rem 0.5rem;
  background: var(--surface2);
}
.rel-score-change.is-up { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 28%, transparent); }
.rel-score-change.is-down { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 28%, transparent); }
.reliability-next-action {
  margin: 0.25rem 0 0;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface2));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--text);
  font-size: 0.82rem;
  line-height: 1.35;
}

.rel-legend,
.rel-taxi-metric,
.rel-coaching,
.rel-timeline {
  margin: 0.75rem 0;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  background: var(--surface2);
}
.rel-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.32rem 0.65rem;
  margin: 0.45rem 0 0.75rem;
  padding: 0.45rem 0.6rem;
  border-style: dashed;
  background: color-mix(in srgb, var(--surface2) 70%, transparent);
  font-size: 0.74rem;
  color: var(--text-muted);
}
.rel-legend-label {
  font-weight: 800;
  color: var(--text);
  opacity: 0.72;
}
.rel-taxi-metric {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.15rem 0.7rem;
  align-items: center;
}
.rel-taxi-label { color: var(--text-muted); font-size: 0.78rem; }
.rel-taxi-metric strong { font-size: 1.25rem; grid-row: span 2; }
.rel-taxi-metric span:last-child { color: var(--text-muted); font-size: 0.78rem; }
.rel-taxi-metric-good strong { color: var(--accent); }
.rel-taxi-metric-mid strong { color: var(--warning); }
.rel-taxi-metric-bad strong { color: var(--danger); }
.rel-timeline ol {
  list-style: none;
  margin: 0.35rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.4rem;
}
.rel-timeline li {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  gap: 0.55rem;
  align-items: center;
  font-size: 0.8rem;
}
.rel-timeline-value { font-weight: 800; }
.rel-timeline li.is-bonus .rel-timeline-value { color: var(--accent); }
.rel-timeline li.is-penalty .rel-timeline-value { color: var(--danger); }
.rel-timeline li.is-neutral .rel-timeline-value { color: var(--text-muted); }
.rel-timeline-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rel-timeline-date { color: var(--text-muted); font-size: 0.74rem; }
.rel-coaching p:not(.modal-section-label) {
  margin: 0.35rem 0 0;
  color: var(--text-muted);
  font-size: 0.82rem;
  line-height: 1.35;
}
.rel-breakdown-filters {
  display: flex;
  gap: 0.35rem;
  overflow-x: auto;
  /* Setting only overflow-x makes the browser compute overflow-y as `auto`,
     so this strip clips vertically too. Without top padding the pills' hover
     glow and focus ring were shaved off at the top edge. */
  padding-top: 0.35rem;
  padding-bottom: 0.4rem;
  margin-bottom: 0.35rem;
  /* Hide the horizontal scrollbar like the other filter strips (.tab-bar,
     .log-filters) — it appears/disappears with the pill count and shifts
     everything below it. */
  scrollbar-width: none;
}
.rel-breakdown-filters::-webkit-scrollbar { display: none; }
@media (max-width: 540px) {
  /* On phones the strip always overflows (5 pills, ~353px in a 316px row) and
     the hidden scrollbar left no hint that more filters follow: fade the right
     edge and snap the pills so the cut-off is readable as "scroll me". */
  .rel-breakdown-filters {
    scroll-snap-type: x proximity;
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent 100%);
            mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent 100%);
  }
  .rel-filter-pill { scroll-snap-align: start; }
}
.rel-filter-pill {
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text-muted);
  border-radius: var(--radius-xs);
  padding: 0.32rem 0.65rem;
  font-size: 0.76rem;
  white-space: nowrap;
  cursor: pointer;
  transition:
    transform var(--transition-fast),
    border-color var(--transition-fast),
    background var(--transition-fast),
    color var(--transition-fast),
    box-shadow var(--transition-fast);
}
/* :not(.active): mit dem Maus-Gate wird die Regel spezifischer als
   .rel-filter-pill.active und wuerde den gewaehlten Zustand unter dem Cursor
   ueberdecken (gleiche Falle wie bei .log-filter-btn). */
html:not(.using-touch) .rel-filter-pill:hover:not(.active) {
  color: var(--text);
  border-color: color-mix(in srgb, var(--accent) 26%, var(--border));
  background: color-mix(in srgb, var(--accent) 7%, var(--surface2));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 8%, transparent);
}
/* Voll deckendes --accent wie ueberall sonst (global Z. 240, sowie
   .history-filter-pill, .btn-preset, .btn-borrower-cta). Die 55%-Mischung war
   der einzige halbtransparente Fokusring der Datei und damit sichtbar
   schwaecher als der der Geschwister-Pills. */
.rel-filter-pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.rel-filter-pill:active {
  transform: scale(0.98);
}
.rel-filter-pill.active {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent);
}
.rel-filter-empty {
  margin: 0.45rem 0 0;
  padding: 0.65rem 0.75rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--surface2) 78%, transparent);
  color: var(--text-muted);
  font-size: 0.82rem;
  text-align: center;
}

/* Trust-Badge — replaces the bare "%"-chip on loan cards. Shows tier label +
   score so the viewer learns the semantics ("Verlässlich") and the precise
   value at once. Pill layout, color-coded per tier, gracefully wraps on
   narrow cards. */
.trust-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.12rem 0.5rem 0.12rem 0.4rem;
  margin-left: 0.4rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.45;
  border: 1px solid transparent;
  vertical-align: middle;
  white-space: nowrap;
}
.trust-badge-icon  { font-size: 0.75rem; line-height: 1; }
.trust-badge-label { letter-spacing: 0.01em; }
.trust-badge-score { font-weight: 800; opacity: 0.9; }
.trust-badge-good     { color: var(--accent);  border-color: color-mix(in srgb, var(--accent)  35%, transparent); background: color-mix(in srgb, var(--accent)  10%, transparent); }
.trust-badge-building { color: var(--rel-building); border-color: rgba(76, 214, 196, 0.32); background: rgba(76, 214, 196, 0.08); }
.trust-badge-shaky    { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 35%, transparent); background: color-mix(in srgb, var(--warning) 10%, transparent); }
.trust-badge-bad      { color: var(--danger);  border-color: color-mix(in srgb, var(--danger)  35%, transparent); background: color-mix(in srgb, var(--danger)  10%, transparent); }
.trust-badge-new      { color: var(--text-muted); border-color: var(--border, rgba(255,255,255,0.16)); background: rgba(255,255,255,0.04); }

/* Schulden-Übersicht für den Borrower — Aggregat-Card im Aktiv-Tab über
   den einzelnen Leihen. Zeigt Gesamtsumme + Überfälligkeit + nächste
   Fälligkeit als kompakte Key/Value-Reihen. Mobil-only: auf Desktop zeigt
   das Hero-Dashboard dieselben Zahlen bereits oben. */
@media (min-width: 821px) {
  .debt-overview { display: none; }
}
.debt-overview { padding: 0.9rem 1rem; }
.debt-overview .section-title { margin: 0 0 0.6rem; font-size: 1rem; }
.debt-overview-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.4rem;
}
.debt-overview-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0.35rem 0;
  border-bottom: 1px solid rgba(255,255,255,0.05);
  font-size: 0.92rem;
}
.debt-overview-row:last-child { border-bottom: none; }
.debt-overview-row span { color: var(--text-muted); }
.debt-overview-row strong { font-weight: 700; }
/* Farbe statt opacity: 0.7 auf dem roten Ueberfaellig-Betrag ergab 2.8:1. */
.debt-overview-row strong small { color: var(--text-muted); font-weight: 400; margin-left: 0.25rem; }
.debt-overview-total strong { font-size: 1.1rem; color: var(--accent, #0a7); }
.debt-overview-overdue strong { color: var(--danger, #f55); }

/* ─── Reliability Chip in Decide Modal ──────────────────────── */
#decide-reliability-chip { margin: 0.5rem 0 0.75rem; }
.decide-rel-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.55rem 0.85rem;
  border-radius: var(--radius);
  border: 1px solid transparent;
  font-size: 0.85rem;
}
.decide-rel-chip-good { background: color-mix(in srgb, var(--accent)  8%, transparent); border-color: color-mix(in srgb, var(--accent)  30%, transparent); }
.decide-rel-chip-mid  { background: color-mix(in srgb, var(--warning) 8%, transparent); border-color: color-mix(in srgb, var(--warning) 30%, transparent); }
.decide-rel-chip-bad  { background: color-mix(in srgb, var(--danger)  8%, transparent); border-color: color-mix(in srgb, var(--danger)  30%, transparent); }
.decide-rel-chip-label { color: var(--text-muted); }
.decide-rel-chip-score { font-weight: 700; }
.decide-rel-chip-good .decide-rel-chip-score { color: var(--accent); }
.decide-rel-chip-mid  .decide-rel-chip-score { color: var(--warning); }
.decide-rel-chip-bad  .decide-rel-chip-score { color: var(--danger); }

/* Trust-Tier-Varianten (zusätzlich zu good/mid/bad, da der neue Tier-Helper
   "building"/"shaky" einführt) */
.decide-rel-chip-building { background: rgba(76, 214, 196, 0.08); border-color: rgba(76, 214, 196, 0.30); }
.decide-rel-chip-building .decide-rel-chip-score { color: var(--rel-building); }
.decide-rel-chip-shaky    { background: color-mix(in srgb, var(--warning) 8%, transparent); border-color: color-mix(in srgb, var(--warning) 30%, transparent); }
.decide-rel-chip-shaky    .decide-rel-chip-score { color: var(--warning); }

/* "Erste Leihe"-Variante: anders gestaltet als die Score-Chips — mehrzeilig,
   informativ statt nur Zahl. Klar als "Vorsicht, neuer Borrower" lesbar. */
.decide-rel-chip-new {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius);
  background: rgba(255, 196, 0, 0.08);
  border: 1px solid rgba(255, 196, 0, 0.30);
}
.decide-rel-chip-new .decide-rel-chip-label { color: #ffc400; font-weight: 700; }
.decide-rel-chip-hint { font-size: 0.82rem; opacity: 0.85; line-height: 1.4; }

/* Global badge on reliability overview cards (lender view) */
.rel-card-global-badge {
  margin-top: 0.4rem;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-sm);
  font-size: 0.78rem;
  color: var(--text-muted);
  background: var(--surface2);
  border: 1px solid var(--border);
}

/* --- Responsive refinements --- */
/* Landscape phones with low viewport height: allow modals to use more height */
@media (orientation: landscape) and (max-height: 500px) {
  .modal-card,
  .modal-card-wide,
  .modal-card-logs {
    /* P1-9: respect bottom safe-area so the submit row stays reachable. */
    max-height: min(94dvh, calc(100dvh - 1rem - env(safe-area-inset-bottom, 0px) - var(--modal-kb-inset)));
    max-width: min(92vw, 720px);
  }
  .modal-overlay { padding: 0.5rem; align-items: flex-start; }
  /* Modal-System: kurze Ja/Nein-Dialoge bleiben schmal (wie Desktop) — auf
     720px gestreckt stand eine Zeile Text über einer riesigen Button-Leiste. */
  .modal-card.modal-card-confirm { max-width: min(92vw, 400px); }
  /* Modal-System: bei ~390px Höhe fraßen Kopf (≈100px) und Fuß (≈105px) mehr
     als die Hälfte der Karte, für den Inhalt blieben ~130px. Kompakter Kopf
     und Fuß geben ihm rund 90px zurück; der statische Kicker („Admin-Bereich“
     …) ist hier reine Dekoration und entfällt wie im mobilen Log-Sheet.
     Kicker mit id werden per JS befüllt (Anfrage-Typ, Status) und bleiben. */
  .modal-sticky-header { padding-top: 0.85rem; }
  .history-modal-header { padding-bottom: 0.75rem; }
  .modal-sticky-header .modal-kicker:not([id]) { display: none; }
  .modal-sticky-header h2 { font-size: 1.15rem; }
  .modal-card:not(.modal-card-confirm) > .modal-actions,
  .modal-card:not(.modal-card-confirm) > .modal-actions.modal-actions-spaced {
    padding-top: 0.6rem;
    padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
  }
}
/* Logs im Querformat (844×390): Header + Filterleiste + Footer füllten die
   ganze Kartenhöhe, #logs-body (flex:1) blieb 0px hoch — die Liste war gar
   nicht sichtbar. Hier scrollt deshalb die Karte selbst, die Liste behält
   eine Mindesthöhe von fast einem Bildschirm; redundanter Footer und Kicker
   fallen weg (wie schon hochkant ≤820px). */
@media (orientation: landscape) and (max-height: 500px) {
  /* padding-top 0: der Sticky-Header klebt sonst 1.5rem unter der Kante und
     die Liste scrollt in dem Streifen darüber sichtbar durch. */
  .modal-card-logs { overflow-y: auto; padding-top: 0; }
  .modal-card-logs > .modal-actions,
  .modal-card-logs .modal-kicker { display: none; }
  .modal-card-logs #logs-body {
    flex: 1 0 auto;
    min-height: calc(100dvh - 6rem);
  }
}
/* Logs hochkant auf dem Handy: Kopf, Typ-Chips, Suche, drei Filter und die
   Kennzahlen belegten ~60 % des Blatts, die Liste scrollte in einem eigenen
   Kasten darunter — sichtbar waren nur zwei, drei Einträge, aufgeklappt noch
   weniger. Jetzt scrollt die ganze Karte: Filter und Kennzahlen rollen mit
   weg, der Kopf bleibt stehen, die Liste bekommt beim Scrollen das ganze
   Blatt. "Weitere laden" ist ein Knopf, kein Scroll-Trigger. */
@media (max-width: 540px) {
  .modal-card-logs { overflow-y: auto; overscroll-behavior: contain; }
  .modal-card-logs #logs-body { flex: 1 0 auto; overflow: visible; }
  .modal-card-logs .logs-summary-grid { position: static; }
}

/* Very small phones (iPhone SE 1st gen, Galaxy Fold cover) */
@media (max-width: 320px) {
  /* Modal-System: kein Overlay-Padding mehr — es löste das Bottom-Sheet vom
     unteren Rand (8px Lücke unter eckigen Unterkanten, rundum schwebend). */
  .modal-card,
  .modal-card-wide { padding-left: 0.85rem; padding-right: 0.85rem; }
  /* Die bündig ausblutenden Sheet-Teile (Drag-Leiste, Sticky-Kopf, Fußleiste)
     rechneten weiter mit 1.25rem Karten-Padding: 0.4rem pro Seite ragten über
     die Karte hinaus (scrollWidth > clientWidth, horizontal wischbar). */
  .modal-sticky-header,
  .modal-card-confirm .modal-sticky-header {
    width: calc(100% + 2 * 0.85rem);
    margin-left: -0.85rem;
    margin-right: -0.85rem;
    padding-left: 0.85rem;
    padding-right: 0.85rem;
  }
  .modal-drag-handle {
    width: calc(100% + 2 * 0.85rem);
    margin-left: -0.85rem;
    margin-right: -0.85rem;
  }
  .modal-card > .modal-actions {
    margin-left: -0.85rem;
    margin-right: -0.85rem;
    padding-left: 0.85rem;
    padding-right: 0.85rem;
  }
  .modal-card-confirm > .modal-actions { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
  .toggle-label-inline { grid-template-columns: minmax(0, 1fr) max-content; gap: 0.5rem; }
  /* P2-7: shrink tab-pill labels so 3 tabs + counters fit on 320px viewports. */
  .tab-pill { font-size: 0.72rem; padding: 0.4rem 0.35rem; }
}

/* Side safe-area for every modal layer (landscape notch). Mirrors the side
   padding each breakpoint above sets on .modal-overlay and lifts it to at
   least the inset; `body > .modal-overlay` outranks those plain rules, so this
   block does not depend on source order. env() is 0 without a cut-out. */
body > .modal-overlay {
  padding-left: max(1rem, env(safe-area-inset-left, 0px));
  padding-right: max(1rem, env(safe-area-inset-right, 0px));
}
/* R5-Tablet: bündig nur für das Sheet; der Tablet-Dialog behält 1rem Rand. */
@media (max-width: 540px) {
  body > .modal-overlay {
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
  }
}
@media (orientation: landscape) and (max-height: 500px), (max-width: 320px) {
  body > .modal-overlay {
    padding-left: max(0.5rem, env(safe-area-inset-left, 0px));
    padding-right: max(0.5rem, env(safe-area-inset-right, 0px));
  }
}

/* Keyboard-shortcuts cheat-sheet (toggled by `?`). Floating bottom-right card. */
.shortcuts-cheatsheet {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);
  right: 1rem;
  z-index: var(--z-top);
  max-width: 320px;
  width: calc(100vw - 2rem);
  animation: fadeInUp var(--motion-base) var(--ease-spring) both;
  pointer-events: auto;
}
.shortcuts-cheatsheet-inner {
  background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01)), var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-card);
  padding: 0.85rem 1rem 1rem;
  box-shadow: var(--elev-3);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.shortcuts-cheatsheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}
.shortcuts-cheatsheet-head strong {
  font-size: 0.9rem;
  letter-spacing: 0.02em;
}
.shortcuts-cheatsheet-close {
  background: rgba(255,255,255,0.07);
  border: none;
  color: var(--text-muted);
  width: 26px; height: 26px;
  border-radius: var(--radius-xs);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}
/* Touch-R4: Hover hinter der Touch-Weiche (klebte nach dem Tipp). */
html:not(.using-touch) .shortcuts-cheatsheet-close:hover {
  background: rgba(255,255,255,0.14);
  color: var(--text);
}
.shortcuts-cheatsheet-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem 0.85rem;
  margin: 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.shortcuts-cheatsheet-list dt { color: var(--text); display: flex; gap: 0.25rem; }
.shortcuts-cheatsheet-list dd { margin: 0; }
.shortcuts-cheatsheet-list kbd {
  display: inline-block;
  font-family: 'SF Mono', Menlo, monospace;
  font-size: 0.78rem;
  padding: 0.1rem 0.4rem;
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 4px;
  min-width: 1.5em;
  text-align: center;
}
@media (max-width: 820px) and (hover: none) {
  /* Hide the cheat-sheet on touch devices — keyboard shortcuts are pointless
     without a keyboard, and the panel would just be visual noise. */
  .shortcuts-cheatsheet { display: none; }
}

/* Payment-Heatmap in Stats-Modal: 52 weeks × 7 days. Color-intensity scales
   with daily payment volume (5 levels: 0 = empty, 1-4 = increasing accent). */
.payment-heatmap-block {
  margin-top: 1.5rem;
}
.payment-heatmap-header {
  /* Umschalter Verliehen/Zurückgezahlt echt mittig: drei Spalten mit gleich
     breiten Seiten (1fr | auto | 1fr). Vorher flex + space-between – die
     Mitte hing von der Breite von Titel und Summe ab. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "title toggle summary";
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.payment-heatmap-header > .modal-section-label { grid-area: title; justify-self: start; margin: 0; }
.payment-heatmap-header > .heatmap-mode-toggle { grid-area: toggle; justify-self: center; }
.payment-heatmap-header > .payment-heatmap-summary { grid-area: summary; justify-self: end; text-align: right; }
/* Schmal: Titel und Summe oben, Umschalter darunter mittig. */
@media (max-width: 540px) {
  .payment-heatmap-header {
    grid-template-columns: 1fr auto;
    grid-template-areas: "title summary" "toggle toggle";
    align-items: baseline;
    row-gap: 0.6rem;
  }
}
.payment-heatmap-summary {
  font-size: 0.8rem;
  color: var(--text-muted);
}
/* Rerender-Stabilität: beide Perspektiven (Verliehen/Zurückgezahlt) stapeln
   sich in einer Zelle, die inaktive ist unsichtbar (visibility, damit sie
   Platz behält und nicht vorgelesen wird). Kopf und Legende haben dadurch in
   beiden Modi dieselbe Größe — der Umschalter bleibt unter dem Finger. */
.heatmap-mode-swap { display: inline-grid; }
.heatmap-mode-swap > [data-hm-only] { grid-area: 1 / 1; }
.payment-heatmap-block[data-heatmap-mode="lent"] .heatmap-mode-swap > [data-hm-only="repaid"],
.payment-heatmap-block[data-heatmap-mode="repaid"] .heatmap-mode-swap > [data-hm-only="lent"] {
  visibility: hidden;
}
.payment-heatmap {
  display: flex;
  gap: 3px;
  overflow-x: auto;
  /* Room for .heatmap-cell:hover scale(1.3) and the selected outline — the
     implicit overflow-y shaved the top row otherwise. */
  padding-top: 0.3rem;
  padding-bottom: 0.25rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.12) transparent;
}
.heatmap-col {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex-shrink: 0;
}
.heatmap-cell {
  width: 14px;
  height: 14px;
  border-radius: 2px;
  background: rgba(255,255,255,0.05);
  transition: transform var(--transition-fast);
}

@media (max-width: 540px) {
  .heatmap-col--old { display: none; }
  .payment-heatmap {
    gap: 4px;
    padding: 0.15rem 0 0.45rem;
  }
  .heatmap-col { gap: 4px; }
  .heatmap-cell {
    width: 18px;
    height: 18px;
    border-radius: 4px;
  }
}
.heatmap-cell.heatmap-level-0 { background: rgba(255,255,255,0.05); }
.heatmap-cell.heatmap-level-1 { background: color-mix(in srgb, var(--accent) 30%, transparent); }
.heatmap-cell.heatmap-level-2 { background: color-mix(in srgb, var(--accent) 55%, transparent); }
.heatmap-cell.heatmap-level-3 { background: color-mix(in srgb, var(--accent) 78%, transparent); }
.heatmap-cell.heatmap-level-4 { background: var(--accent); }
.heatmap-cell.is-future { background: rgba(255,255,255,0.02); opacity: 0.4; }
.heatmap-cell.has-loan { cursor: pointer; }
html:not(.using-touch) .heatmap-cell.has-loan:hover { transform: scale(1.3); outline: 1px solid var(--accent); }
.heatmap-cell.is-selected { outline: 2px solid var(--accent); transform: scale(1.15); z-index: 1; }
@media (hover: none) and (pointer: coarse) {
  .heatmap-cell.has-loan {
    outline: 1px solid rgba(255,255,255,0.12);
    outline-offset: 1px;
  }
  .heatmap-cell.is-selected {
    outline-width: 3px;
    transform: none;
  }
}

/* === Heatmap detail panel === */
.heatmap-detail {
  margin-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
/* Kontext-Banner über den Detail-Karten. Erklärt dem User, dass die
   Heatmap-Zelle eine ganze Woche aggregiert — Datums-Range + KW-Nummer +
   Gesamtsumme machen die Aggregation explizit. */
.heatmap-detail-banner {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface2));
  border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.heatmap-detail-banner-main {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.heatmap-detail-banner-main strong {
  font-size: var(--text-lg);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.heatmap-detail-banner-range {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.heatmap-detail-banner-sub {
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.heatmap-detail-card {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.heatmap-detail-top {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.heatmap-detail-name {
  font-weight: 700;
  font-size: 0.95rem;
  flex: 1;
  min-width: 0;
}
.heatmap-detail-amount {
  font-weight: 800;
  color: var(--accent);
  font-size: 1rem;
  margin-left: auto;
}
.heatmap-detail-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.heatmap-detail-bar-wrap {
  height: 6px;
  background: rgba(255,255,255,0.08);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.heatmap-detail-bar {
  height: 100%;
  background: var(--accent);
  border-radius: var(--radius-pill);
  transition: width var(--motion-slow) var(--ease-standard);
  max-width: 100%;
}
.heatmap-detail-bar-label {
  font-size: 0.75rem;
  color: var(--text-muted);
}
.heatmap-detail-jump {
  align-self: flex-start;
  margin-top: 0.2rem;
  font-size: 0.8rem;
  padding: 0.25rem 0.75rem;
  min-height: 32px;
}
.heatmap-month-labels {
  display: flex;
  gap: 3px;
  margin-bottom: 0.25rem;
  /* Clip (instead of overflow) so the row can be scroll-synced with the grid
     below via scrollLeft. On mobile the 52-week grid scrolls horizontally; the
     labels must follow or they sit above the wrong columns. */
  overflow-x: hidden;
  /* R9-Mobile: overflow-x alone made overflow-y `auto` — a scroll container
     a finger could pan sideways on iOS, out of sync with the grid. */
  overflow-y: hidden;
  /* Reine Beschriftung (aria-hidden): kein Zeiger-/Wischziel, damit sie nicht gegen das Raster verrutscht. */
  pointer-events: none;
}
.heatmap-label {
  width: 14px;
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: left;
  white-space: nowrap;
  overflow: visible;
  flex-shrink: 0;
}
@media (max-width: 540px) {
  .heatmap-label--old { display: none; }
  /* Raster-Zellen sind hier 18px breit mit 4px Abstand (siehe .heatmap-cell): die Beschriftung
     brauchte dieselbe Spaltenbreite, sonst lief sie beim Mitscrollen um ~40px vor und der
     Monatsname stand über der falschen Woche. */
  .heatmap-month-labels { gap: 4px; }
  .heatmap-label { width: 18px; }
}
.payment-heatmap-legend {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: 0.4rem;
  flex-wrap: wrap;
}
.payment-heatmap-legend .heatmap-cell { transition: none; cursor: default; }

/* Skip-link removed per user — visible during rubber-band overscroll was
   surprising. Kept .sr-only utility (below) for future a11y needs. */
/* A11y R5 (WCAG 2.4.1): wieder da, aber ohne den alten Fehler. Der frühere
   Link lag per negativem top ÜBER dem Viewport und tauchte beim Gummiband-
   Überscrollen auf. Jetzt ist er wie .sr-only weggeclippt (nichts, was ein
   Überscrollen freilegen könnte) und erscheint nur bei Tastaturfokus, fixiert
   unter der Statusleiste. */
.skip-link:not(:focus) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.skip-link:focus {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 0.5rem);
  left: calc(env(safe-area-inset-left, 0px) + 0.75rem);
  z-index: calc(var(--z-top) + 2);
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 1rem;
  background: linear-gradient(180deg, var(--accent-btn), var(--accent-btn-strong));
  color: #fff;
  font-weight: 700;
  text-decoration: none;
  border-radius: var(--radius-btn);
  outline: 2px solid var(--text);
  outline-offset: 2px;
}

/* Screen-reader-only utility class — visually hidden but available to AT. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Hide skip-link target outline when focused via skip — looks weird otherwise. */
main:focus-visible { outline: none; }

/* SW-Update-Banner: appears when a new service-worker is waiting. Sticky at
   the top of the viewport, dismissable, tap to reload. */
.sw-update-banner {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 0.6rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-top);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.75rem 0.55rem 0.85rem;
  max-width: calc(100vw - 1.5rem);
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 16%, transparent), color-mix(in srgb, var(--accent) 8%, transparent)), var(--surface);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: var(--radius-btn);
  color: var(--text);
  font-size: 0.88rem;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.35);
  animation: swBannerIn var(--motion-base) var(--ease-spring) both;
}
@keyframes swBannerIn {
  from { opacity: 0; transform: translate(-50%, -120%); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.sw-update-banner-icon {
  display: inline-flex;
  width: 22px; height: 22px;
  align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--accent);
  border-radius: 50%;
  font-weight: 800;
}
.sw-update-banner-copy {
  display: grid;
  gap: 0.02rem;
  min-width: 0;
  /* Absorb the slack. The banner is capped at `calc(100vw - 1.5rem)`; once the
     content shrinks below that cap, no child claimed the leftover space, so it
     piled up AFTER the close button and left it floating ~25px short of the
     right edge on narrow screens. */
  flex: 1 1 auto;
}
.sw-update-banner-msg {
  font-weight: 800;
  line-height: 1.12;
  white-space: nowrap;
}
.sw-update-banner-version {
  color: var(--text-muted);
  font-size: 0.72rem;
  line-height: 1.1;
  white-space: nowrap;
}
.sw-update-banner-btn {
  background: var(--accent);
  color: #0f0f0f;
  border: none;
  border-radius: var(--radius-xs);
  padding: 0.4rem 0.8rem;
  font-weight: 700;
  font-size: 0.85rem;
  cursor: pointer;
  transition: filter var(--transition-fast), transform var(--transition-fast);
}
/* Touch-Audit R4: Hover war als einziger im App-Rahmen nicht an
   html:not(.using-touch) gekoppelt — auf dem Handy blieb "Neu laden" nach
   dem Tippen aufgehellt stehen. Dazu ein Druckzustand fuer Knopf und ✕. */
html:not(.using-touch) .sw-update-banner-btn:hover { filter: brightness(1.1); }
.sw-update-banner-btn:not(:disabled):active { filter: brightness(0.92); transform: scale(0.96); --press-opacity: 1; }
.sw-update-banner-close:active { transform: scale(0.9); background: rgba(255, 255, 255, 0.1); --press-opacity: 1; }
.sw-update-banner-btn:disabled {
  cursor: wait;
  opacity: 0.8;
}
.sw-update-banner-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: transparent;
  border: none;
  border-radius: var(--radius-xs);
  color: var(--text-muted);
  /* Was 24px with a -0.25rem pull to the left: too small to hit reliably and
     visually detached from the banner's right edge. */
  width: 32px; height: 32px;
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
}
html:not(.using-touch) .sw-update-banner-close:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.08);
}
@media (min-width: 769px) {
  body:has(#offline-banner:not(.hidden)) .sw-update-banner {
    top: calc(env(safe-area-inset-top, 0px) + 3rem);
  }
}
@media (max-width: 540px) {
  .sw-update-banner {
    width: min(calc(100vw - 1rem), 420px);
    top: calc(env(safe-area-inset-top, 0px) + 0.45rem);
    gap: 0.5rem;
    padding: 0.5rem 0.58rem 0.5rem 0.65rem;
    border-radius: var(--radius-btn);
  }
  .sw-update-banner-icon {
    width: 20px;
    height: 20px;
  }
  .sw-update-banner-msg {
    font-size: 0.82rem;
  }
  .sw-update-banner-version {
    font-size: var(--text-xs);
  }
  .sw-update-banner-btn {
    flex: 0 0 auto;
    min-height: 34px;
    padding: 0.32rem 0.58rem;
    font-size: 0.78rem;
  }
  .sw-update-banner-close {
    /* Mobile is exactly where a 22px hit area is worst — keep it thumb-sized.
       The banner has room: the copy column absorbs the difference. */
    width: 32px;
    height: 32px;
  }
}

/* Search-filter helper: hides loan-cards (and empty subsection wrappers)
   that don't match the active loan-search-query. Pure DOM-side filter —
   no full re-render. */
/* display:none !important couldn't be transitioned at all — a card matched
   one keystroke ago vanished/reappeared in the very same frame. max-height +
   opacity fades it out instead while still fully leaving the layout once
   collapsed (overflow:hidden + 0 margin/padding, so nothing reserves space
   for it). max-height's target (600px) is generous headroom above any real
   card/group height — the transition's perceived speed only depends on the
   declared numeric range, not the content's actual height. */
/* No max-height baseline on the cards any more: `[data-loan-id] { max-height:
   2000px }` (the transition's start value) capped EVERY loan card at 2000px, so
   a card with a long timeline or many messages ran out under the next card.
   applySearchFilter() now hands the real height over as the start value just
   before hiding (_setSearchHidden in app.js). Showing again was never animated
   (the transition lives on .search-hidden only), so nothing is lost there. */
.search-hidden {
  opacity: 0;
  max-height: 0;
  margin-top: 0 !important;
  margin-bottom: 0 !important;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
  border-width: 0 !important;
  overflow: hidden;
  pointer-events: none;
  transition: opacity var(--motion-fast) var(--ease-standard), max-height var(--motion-fast) var(--ease-standard), margin var(--motion-fast) var(--ease-standard), padding var(--motion-fast) var(--ease-standard);
}

/* === Counter-Offer Feature === */
.counter-offer-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.35rem;
  padding: 0.25rem 0.7rem;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--orange), #fb923c);
  /* Dark brown text instead of white: white on amber (#f59e0b) is only ~2:1
     contrast (hard to read). This dark tone clears the WCAG 4.5:1 threshold
     while keeping the badge bold and orange. */
  color: #3d2600;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.counter-comparison-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 1rem 0;
}

.counter-comparison-col {
  padding: 1rem;
  border-radius: var(--radius-card);
  background: var(--surface2);
  border: 1px solid var(--border);
}

.counter-comparison-original {
  opacity: 0.85;
}

.counter-comparison-new {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
}

.counter-comparison-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text-muted);
  margin-bottom: 0.5rem;
}

.counter-comparison-amount {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 0.25rem;
  font-variant-numeric: tabular-nums;
}

.counter-comparison-interest {
  font-size: 0.85rem;
  color: var(--text-muted);
}

.counter-comparison-value {
  font-size: 1.25rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* Highlights values in the counter-offer column that diverge from the
   borrower's original request — accent color + subtle background tint so
   the borrower can't miss a changed amount, bonus, or due date. */
.counter-comparison-changed {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.15rem 0.35rem;
  margin: -0.15rem -0.35rem;
}

.timeline-counter-detail {
  border-left: 3px solid var(--orange);
  padding-left: 0.75rem;
  margin: 0.5rem 0;
}

.timeline-counter-detail > summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem 0.5rem 0;
  list-style: none;
  /* Hover-Affordance: leicht hervorgehobener Background damit die Zeile
     "klickbar" wirkt — Disclosure-Pattern braucht visuelles Feedback. */
  border-radius: var(--radius-sm);
  transition: background var(--transition-fast);
}
html:not(.using-touch) .timeline-counter-detail > summary:hover {
  background: rgba(255,255,255,0.04);
}

.timeline-counter-detail > summary::-webkit-details-marker {
  display: none;
}

/* Chevron-Disclosure-Indikator rechts: ▾ nach unten = "kann aufklappen",
   rotiert auf ▴ wenn [open]. Eindeutiger als das vorherige drehende ⇄-
   Icon (war ein Symbol-Wechsel, kein Aufklapp-Hinweis). */
.timeline-counter-detail > summary::after {
  content: '';
  width: 8px;
  height: 8px;
  margin-left: auto;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform var(--transition-fast);
  flex-shrink: 0;
}
.timeline-counter-detail[open] > summary::after {
  /* von ▾ (45°) auf ▴ (-135°) — gleicher Pfeil, eine halbe Drehung. */
  transform: rotate(-135deg);
}

.timeline-counter-detail .timeline-icon {
  color: var(--orange);
  transition: transform var(--transition-fast);
}

.timeline-counter-detail[open] > summary .timeline-icon {
  transform: rotate(180deg);
}

.timeline-counter-body {
  padding: 0.5rem 0 0.75rem 0;
}

.timeline-counter-decision {
  margin-top: 0.5rem;
  font-size: 0.85rem;
  color: var(--text-muted);
  font-style: italic;
}

.counter-offer-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 0.5rem 0;
}

.counter-offer-form .form-error,
.counter-offer-form .error-msg {
  color: var(--danger-text); /* Befund 13: eine Quelle fuer Fehlertexte */
  font-size: 0.85rem;
  min-height: 1.25rem;
}

/* Prevent iOS Safari auto-zoom on input focus — only triggers below 16px. */
#counter-offer-new-amount,
#counter-offer-new-interest {
  font-size: 16px;
}

/* Same floor for every remaining field that styled itself below 16px (found
   by measuring every input/select/textarea of every modal on a touch
   viewport): focusing one zoomed the whole installed app in on iOS and left
   it zoomed after the keyboard closed. Touch-only, so the compact desktop
   toolbars (logs filters, admin feedback filters) keep their density.
   `html` prefix: outranks the component rules regardless of source order,
   feedback.css (loaded later) included. */
@media (pointer: coarse) {
  html .note-textarea,
  html .logs-search-input,
  html .logs-select,
  html .logs-date-input,
  html .users-filter,
  html .fb-input,
  html #smtp-test-to {
    font-size: 16px;
  }
}

/* Long currency values + long lender names must wrap, not push the modal wider. */
.counter-comparison-amount,
.counter-comparison-value,
.counter-comparison-interest {
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}
#counter-decide-lender-label {
  overflow-wrap: anywhere;
  word-break: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

@media (max-width: 360px) {
  .counter-comparison-col {
    padding: 0.7rem;
  }
  .counter-comparison-amount {
    font-size: 1.1rem;
  }
}

.btn-counter-offer,
.btn-counter-withdraw,
.btn-taxi-counter-offer,
.btn-taxi-counter-withdraw,
.btn-taxi-counter-edit {
  border-radius: var(--radius-btn);
  padding: 0.6rem 0.9rem;
  min-height: var(--touch-target);
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--transition-fast), border-color var(--transition-fast),
              color var(--transition-fast), opacity var(--transition-fast),
              transform var(--transition-fast), filter var(--transition-fast);
}

/* Taxi-Gegenangebot teilt das orange Primär-Design des normalen Gegenangebots. */
.btn-counter-offer,
.btn-taxi-counter-offer {
  background: linear-gradient(135deg, var(--orange), #fb923c);
  /* A11y R5 (WCAG 1.4.3): Weiß auf Amber lag bei ~2:1 — dasselbe dunkle
     Braun wie .counter-offer-badge (>= 4.5:1 auf beiden Verlaufsenden). */
  color: #3d2600;
  border: 1px solid color-mix(in srgb, var(--orange) 55%, transparent);
  font-size: 0.85rem;
}

html:not(.using-touch) .btn-counter-offer:hover,
html:not(.using-touch) .btn-taxi-counter-offer:hover {
  filter: brightness(1.08);
  border-color: var(--orange);
}

.btn-counter-offer:active,
.btn-taxi-counter-offer:active { transform: translateY(1px); }

/* Zurückziehen + Bearbeiten: dezente Sekundär-Buttons neben dem orange CTA. */
.btn-counter-withdraw,
.btn-taxi-counter-withdraw,
.btn-taxi-counter-edit {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  font-size: 0.85rem;
}

html:not(.using-touch) .btn-counter-withdraw:hover,
html:not(.using-touch) .btn-taxi-counter-withdraw:hover,
html:not(.using-touch) .btn-taxi-counter-edit:hover {
  border-color: var(--text-muted);
  color: var(--text);
}

/* ===========================================================================
   Leihkarten R2 — Handy zuerst, Desktop mitgeprüft
   ---------------------------------------------------------------------------
   Steht bewusst HIER (nach den älteren Mobile-Blöcken ~9570-9700 und
   ~12815-12890), damit gleiche Spezifität per Reihenfolge gewinnt, ohne
   !important. Ziel: ruhige Karte mit klarer Reihenfolge — wer, wie viel,
   Status, nächste Aktion.
   =========================================================================== */

/* --- N6: EIN Fortschritt — beschrifteter Balken statt Ring + Balken --- */
.loan-progress { margin: 0.2rem 0 0.85rem; }
.loan-progress > .loan-progress-bar { margin: 0; }
.loan-progress-caption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  margin-top: 0.4rem;
  font-size: 0.78rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.loan-progress-pct { font-weight: 700; color: var(--text); flex-shrink: 0; }

/* Metadaten in der Statusreihe ("Letzte Aktivität …") als leiser Text. */
.signal-meta {
  font-size: 0.74rem;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
  padding: 0.2rem 0.1rem;
}

/* Abgeschlossene Leihe: Gesamtbetrag in Textfarbe statt Akzentgrün (grün
   heißt überall sonst "offen"). */
.loan-amount.loan-amount-closed { color: var(--text); }

/* --- M24: Auszahlungs-Schritt in der Karte --- */
.payout-callout {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0 0 0.9rem;
  padding: 0.6rem 0.6rem 0.6rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background: var(--surface2);
}
.payout-callout.is-fresh {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 12%, transparent), color-mix(in srgb, var(--accent) 5%, transparent)), var(--surface2);
}
.payout-callout-icon { color: var(--accent); display: inline-flex; flex-shrink: 0; }
.payout-callout-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1; font-size: 0.82rem; color: var(--text-muted); line-height: 1.3; }
.payout-callout-text > strong { color: var(--text); font-size: 0.9rem; }
.payout-callout-text > span { overflow-wrap: anywhere; }
.payout-callout-btn {
  flex-shrink: 0;
  min-height: var(--touch-target);
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius-btn);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  color: var(--accent-text);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  touch-action: manipulation;
}
.payout-callout.is-fresh .payout-callout-btn { background: var(--accent-btn); border-color: var(--accent-btn); color: #fff; }
html:not(.using-touch) .payout-callout-btn:hover { filter: brightness(1.1); }
.payout-callout-btn:active,
html:not(.using-touch) .payout-callout-btn:active { transform: scale(0.97); }

/* --- R5: Überweisung abhaken ---
   Offen: Daten + „Als überwiesen markieren“ in einer eigenen Zeile unter dem
   Text (zwei Knöpfe neben dem Text passten auf 320–412 px nicht). Erledigt:
   ruhige Zeile mit Haken und Datum, ohne Akzent-Rahmen. */
.payout-callout:has(> .payout-callout-actions) { flex-wrap: wrap; }
.payout-callout-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  flex: 1 1 100%;
  min-width: 0;
}
/* Nebeneinander, solange beide ganz hineinpassen; darunter (320–375 px) je
   eine volle Zeile — der Haken-Knopf bricht nie dreizeilig um. */
.payout-callout-actions > .payout-callout-btn-data { flex: 1 1 5rem; white-space: nowrap; }
.payout-callout-actions > .payout-callout-btn-done { flex: 3 1 13rem; }
.payout-callout-btn-done {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  white-space: nowrap;
}
.payout-callout-btn-done svg { flex-shrink: 0; }
/* Frisch genehmigt: der Haken ist der nächste Schritt, die Daten Nebensache. */
.payout-callout.is-fresh .payout-callout-btn-data {
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
  color: var(--accent-text);
}
.payout-callout.is-done {
  background: transparent;
  border-color: var(--border);
}
.payout-callout.is-done .payout-callout-text > strong { font-size: 0.86rem; }
.payout-callout.is-notice { padding-right: 0.8rem; }

/* Statuszeile im Auszahlungs-Dialog, sobald markiert. */
.payout-modal-status {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.75rem 0 0;
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--accent-text);
}
.payout-modal-status svg { flex-shrink: 0; }

/* --- M24: QR-Block im Auszahlungs-Dialog ---
   Bug: `.decide-payout-qr svg` traf auch das Download-Symbol im Knopf
   "QR-Code speichern" — der Pfeil wurde 200×200 px groß, der Block lief rechts
   aus dem Dialog und der Code selbst rutschte aus dem Bild. */
.decide-payout-qr { max-width: 100%; }
.decide-payout-qr .decide-payout-qr-download svg {
  width: 15px;
  height: 15px;
  max-width: none;
  max-height: none;
}
.decide-payout-qr-download { min-height: var(--touch-target); padding: 0.5rem 1rem; }
/* Kopieren: 32 px hohe, volle Breite Balken unter jedem Wert → kompakte
   44-px-Knöpfe rechts neben dem Wert, wie in Banking-Apps. */
.decide-payout-row { grid-template-columns: minmax(0, 1fr) max-content; column-gap: 0.75rem; row-gap: 0.1rem; padding: 0.5rem 0; }
.decide-payout-label { grid-column: 1; font-size: 0.78rem; }
.decide-payout-value { grid-column: 1; }
.decide-payout-copy {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  min-height: var(--touch-target);
  min-width: var(--touch-target);
  padding: 0.4rem 0.8rem;
  font-size: 0.8rem;
}
@media (max-width: 540px) {
  .decide-payout-row { grid-template-columns: minmax(0, 1fr) max-content; }
  .decide-payout-copy { grid-column: 2; justify-self: end; width: auto; }
}

/* B4: Schrift ab 150 % (html.text-big): zwei Spalten (Label | Wert) lassen dem Wert nichts übrig – er stand
   Buchstabe für Buchstabe untereinander. Dann eine Spalte: Label darüber, Wert darunter, beide linksbündig. */
@media (max-width: 540px) {
  html.text-big .loan-card .loan-snapshot { grid-template-columns: minmax(0, 1fr); }
  html.text-big .loan-card .loan-snapshot .snapshot-label,
  html.text-big .loan-card .loan-snapshot .snapshot-inline-label { padding-bottom: 0; }
  html.text-big .loan-card .loan-snapshot .snapshot-item > .snapshot-value:not(.snapshot-value-stacked),
  html.text-big .loan-card .loan-snapshot .snapshot-line-value,
  html.text-big .loan-card .loan-snapshot .snapshot-value-stacked > span:not(.snapshot-line) {
    grid-column: 1; text-align: left; padding-left: 0; border-top: none; padding-top: 0.15rem;
  }
}

html.text-big .loan-signals > .signal-pill, html.text-big .loan-signals > .badge { white-space: normal; max-width: 100%; }

/* --- Kennzahlen als ruhige Liste auf dem Handy ---
   Vier zentrierte Kacheln mit versalen Titeln ("URSPRUNGS-ZWECK",
   "AUSZAHLUNG / RÜCKZAHLUNG" auf zwei Zeilen) waren auf 375 px die unruhigste
   Stelle der Karte und bis 230 px hoch. Als Label/Wert-Liste liest sich das
   wie ein Beleg: links was, rechts wie viel. Kein Markup-Umbau nötig —
   display:contents hebt Label und Wert direkt ins Raster. */
@media (max-width: 540px) {
  .loan-card .loan-snapshot {
    display: grid;
    grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
    /* Kein column-gap: die Trennlinien sitzen an den Zellen und liefen sonst
       mit einer Lücke zwischen Label und Wert. Der Abstand kommt aus dem
       padding-left der Wertzellen. */
    column-gap: 0;
    row-gap: 0;
    /* stretch statt baseline: Label- und Wertzelle einer Zeile gleich hoch,
       sonst standen ihre Trennlinien um ein paar Pixel versetzt. */
    align-items: stretch;
    padding: 0.1rem 0.85rem;
    margin-bottom: 0.9rem;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: var(--radius-btn);
  }
  .loan-card .loan-snapshot > .snapshot-item,
  .loan-card .loan-snapshot .snapshot-value-stacked,
  .loan-card .loan-snapshot .snapshot-line { display: contents; }
  .loan-card .loan-snapshot .snapshot-label,
  .loan-card .loan-snapshot .snapshot-inline-label {
    grid-column: 1;
    text-transform: none;
    letter-spacing: 0;
    font-size: 0.84rem;
    font-weight: 500;
    color: var(--text-muted);
    text-align: left;
    padding: 0.55rem 0;
    border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
    hyphens: none;
  }
  .loan-card .loan-snapshot .snapshot-item > .snapshot-value:not(.snapshot-value-stacked),
  .loan-card .loan-snapshot .snapshot-line-value,
  .loan-card .loan-snapshot .snapshot-value-stacked > span:not(.snapshot-line) {
    grid-column: 2;
    text-align: right;
    font-size: 0.9rem;
    font-weight: 700;
    line-height: 1.35;
    padding: 0.55rem 0 0.55rem 1rem;
    border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
    min-width: 0;
    overflow-wrap: anywhere;
    hyphens: none;
  }
  /* Erste Zeile ohne Trennlinie. */
  .loan-card .loan-snapshot > .snapshot-item:first-child > :is(.snapshot-label, .snapshot-value) { border-top: none; }
  /* Die Doppelkachel bekommt keinen eigenen Titel mehr — ihre beiden Zeilen
     ("Auszahlung", "Rückzahlung") tragen ihn schon. */
  .loan-card .loan-snapshot .snapshot-item-action:has(.snapshot-line) > .snapshot-label { display: none; }
  /* "bis 07.10.2026" hängt als Zusatz unter dem Rückzahlungs-Wert. */
  .loan-card .loan-snapshot .snapshot-value-stacked > span.snapshot-value-due {
    border-top: none;
    padding-top: 0;
    margin-top: -0.35rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
  }
  .loan-card .loan-snapshot .snapshot-item-primary > .snapshot-value:not(.is-empty-value) { color: var(--warning); }
  .loan-card .loan-snapshot .text-placeholder { font-weight: 500; }
}

/* --- M23: Aktionsleiste ---
   1) Nur die Leiste der Karte im Fokus klebt (app.js _wireLoanActionsHeight,
      Klasse .bar-in-focus). Alle anderen stehen an ihrem Kartenende — die
      Leiste der nächsten Karte liegt nicht mehr über deren Betrag.
   2) Eine Zeile statt zwei: 113 px → ~60 px auf dem iPhone SE. */
.loan-actions--lender-active > button,
.loan-actions--lender-pending > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
}
@media (max-width: 600px) {
  .loan-card .loan-actions { position: relative; }
  .loan-card.bar-in-focus > .loan-actions { position: sticky; }
  /* Perf-R5: the blur only has something to blur while the bar floats over
     the list (sticky). At its card end it sits on the card's own flat
     background — every card still paid for its own backdrop-filter layer
     (40 of them with 60 entries), each an extra blur pass while it is on
     screen during scrolling. */
  .loan-card:not(.bar-in-focus) > .loan-actions {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  /* Der klebende "Weniger anzeigen"-Knopf rechnet mit einer klebenden
     Leiste darunter; ohne Fokus klebt er wieder bündig unten. */
  .loan-card:not(.bar-in-focus):has(.loan-actions) .btn-activity-toggle.expanded { bottom: 0; }
  .loan-card:not(.bar-in-focus):has(.loan-actions) .btn-activity-toggle.expanded::after { display: none; }
}
@media (max-width: 540px) {
  .loan-card .loan-actions.loan-actions--lender-active,
  .loan-card .loan-actions.loan-actions--lender-pending {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    min-height: 0;
    padding-top: 0.55rem;
    padding-bottom: calc(0.55rem + env(safe-area-inset-bottom, 0px));
  }
  .loan-card .loan-actions.loan-actions--lender-active > button,
  .loan-card .loan-actions.loan-actions--lender-pending > button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    flex: 0 1 auto;
    min-width: 0;
    padding: 0.55rem 0.75rem;
    font-size: 0.88rem;
    white-space: nowrap;
    overflow-wrap: normal;
  }
  .loan-card .loan-actions.loan-actions--lender-active > .btn-add-payment { flex: 1 1 auto; }
  .loan-card .loan-actions.loan-actions--lender-active > .btn-close-loan { flex: 0 1 auto; }
  .loan-card .loan-actions.loan-actions--lender-pending > button { flex: 1 1 0; padding: 0.55rem 0.5rem; font-size: 0.85rem; }
  .loan-card .loan-actions.loan-actions--lender-pending > .counter-offer-badge { flex: 1 1 100%; }
  /* Genehmigen ist die Primäraktion der Anfrage — gefüllt wie "Zahlung
     eintragen" in der Aktiv-Leiste; Gegenangebot tritt als Kontur zurück. */
  .loan-card .loan-actions.loan-actions--lender-pending > .btn-approve {
    background: var(--accent-btn);
    border-color: var(--accent-btn);
    color: #fff;
    font-weight: 700;
  }
  .loan-card .loan-actions.loan-actions--lender-pending > :is(.btn-counter-offer, .btn-taxi-counter-offer) {
    background: color-mix(in srgb, var(--orange) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--orange) 45%, transparent);
    color: var(--orange);
  }
  /* Leihnehmer, aktive Leihe: "Abschluss anfragen · Datum verlängern" standen
     je in voller Breite untereinander (119 px). Nebeneinander, solange beide
     ganz hineinpassen; auf 320 px bricht die Reihe von selbst um. Ein offener
     Antrag ("Abschluss angefragt · Zurückziehen") behält die volle Zeile. */
  .loan-card .loan-actions:has(> .btn-borrower-action) { flex-wrap: wrap; }
  .loan-card .loan-actions:has(> .btn-borrower-action) > .btn-borrower-action {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    padding-inline: 0.7rem;
  }
  /* Leihnehmer: Zurückziehen · Gegenangebot prüfen in einer Reihe. */
  .loan-card .loan-actions:has(> .btn-counter-offer-decide):not(:has(> .counter-offer-badge)),
  .loan-card .loan-actions:has(> .btn-taxi-counter-offer-decide):not(:has(> .counter-offer-badge)) {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.4rem;
  }
  .loan-card .loan-actions:has(> .btn-counter-offer-decide) > button,
  .loan-card .loan-actions:has(> .btn-taxi-counter-offer-decide) > button { flex: 1 1 auto; min-width: 0; }
  .loan-card .loan-actions:has(> .btn-counter-offer-decide) > .btn-withdraw,
  .loan-card .loan-actions:has(> .btn-taxi-counter-offer-decide) > .btn-withdraw { flex: 0 1 auto; }
}
@media (max-width: 440px) {
  .loan-actions--lender-active .la-long { display: none; }
  .loan-actions--lender-pending > :is(.btn-counter-offer, .btn-taxi-counter-offer) > svg { display: none; }
}
/* "Abschluss angefragt" (Anfragen-Tab des Leihgebers): Ablehnen stand als
   rot gefüllter Hauptknopf neben einem blassen "Leihe abschließen" — die
   Karte empfahl optisch das Ablehnen. Abschließen ist die Zustimmung und wird
   Primärknopf, Ablehnen tritt als getönte Kontur zurück (wie in der
   Anfrage-Leiste). */
.decision-request-close .loan-actions > .btn-close-loan:not(:disabled) {
  background: var(--accent-btn);
  border-color: var(--accent-btn);
  color: #fff;
  font-weight: 700;
}
/* Zwei Entscheidungen, eine Reihe (vorher je volle Breite untereinander). */
@media (max-width: 540px) {
  .loan-card.decision-request-card .loan-actions:has(> button + button) { flex-wrap: nowrap; }
  .loan-card.decision-request-card .loan-actions:has(> button + button) > button { flex: 1 1 0; min-width: 0; }
}

/* --- Zeitleiste auf dem Handy ---
   Jede Zeile führte mit einem fetten "23.09.2026" und darunter der Art in
   Versalien ("RÜCK-ZAHLUNG", auf 320 px mit Trennstrich). Gelesen wird aber
   zuerst, WAS passiert ist. Jetzt: Art fett oben, Datum leise darunter.
   Status-Pills und "offen danach" behalten ihre Zeile. */
@media (max-width: 540px) {
  .loan-card .activity-date-cell { flex-direction: column-reverse; justify-content: flex-end; gap: 0.1rem; }
  .loan-card .activity-date-sub {
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--text);
    text-transform: none;
    letter-spacing: 0;
    overflow-wrap: normal;
    word-break: normal;
    -webkit-hyphens: none;
    hyphens: none;
  }
  .loan-card .activity-date-main { font-size: 0.78rem; font-weight: 500; color: var(--text-muted); }
  /* Taxi audit (320px): with the menu column the date column shrank below
     its text and "28.09.2026" ran into "Bonus €2,00" of the amount cell. The
     date keeps its width; the amount text wraps instead. */
  .loan-card .activity-row { grid-template-columns: minmax(min-content, 1fr) auto; }
  .loan-card .activity-table--editable .activity-row { grid-template-columns: minmax(min-content, 1fr) auto auto; }
  .loan-card .activity-date-main { white-space: nowrap; }
  .loan-card .activity-amount-main { font-size: 0.95rem; }
  .loan-card .activity-note-row { font-size: 0.84rem; color: var(--text-muted); }
  .loan-card .activity-meta-cell .badge-compact { font-size: 0.72rem; }
}

/* --- N21: Desktop-Raster ---
   Befund 44 hatte die Karten auf Inhaltshöhe enden lassen (align-items:
   start), weil eine gestreckte Karte ohne Fußzeile wie ein Renderfehler
   aussah. Zwei nebeneinanderstehende Karten endeten dadurch auf völlig
   verschiedenen Höhen. Jetzt gestreckt UND die Aktionsleiste an den unteren
   Kartenrand gedrückt: gleiche Kartenkanten, Aktionen auf einer Linie, der
   Leerraum liegt über der Fußzeile statt unter der Karte.
   Eine einzelne Karte nimmt die volle Breite, statt als halbe Spalte neben
   einer leeren Fläche zu stehen (die Kennzahlen laufen dann in vier Spalten).
   Ausstehende Anfragen des Leihgebers (#pending-section, je Person eine
   .section) bekommen dasselbe Raster wie die aktiven Leihen. */
@media (min-width: 1100px) {
  #active-loans-content,
  #borrower-active-section,
  .borrower-pending-section { align-items: stretch; }
  #active-loans-content:has(> .loan-card):not(:has(> .loan-card ~ .loan-card)),
  #borrower-active-section:has(> .loan-card):not(:has(> .loan-card ~ .loan-card)),
  .borrower-pending-section:has(> .loan-card):not(:has(> .loan-card ~ .loan-card)) {
    grid-template-columns: minmax(0, 1fr);
  }
  #active-loans-content > .loan-card,
  .borrower-pending-section > .loan-card,
  #borrower-active-section > .section > .loan-card,
  #pending-section > .section > .loan-card {
    display: flex;
    flex-direction: column;
  }
  #active-loans-content > .loan-card > .loan-actions,
  .borrower-pending-section > .loan-card > .loan-actions,
  #borrower-active-section > .section > .loan-card > .loan-actions,
  #pending-section > .section > .loan-card > .loan-actions { margin-top: auto; }
  /* Die kompakte Entscheidungs-Karte ("Abschluss angefragt", ~270 px) neben
     einer vollen Leihkarte (~700 px) gestreckt wäre eine fast leere Fläche —
     genau der Befund-44-Fall. Sie bleibt auf Inhaltshöhe. */
  .borrower-pending-section > .decision-request-card { align-self: start; }
  #borrower-active-section > .section:has(> .loan-card) { display: flex; flex-direction: column; }
  #borrower-active-section > .section > .loan-card { flex: 1 1 auto; }
  #pending-section:has(> .section ~ .section) {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 1rem;
    align-items: stretch;
  }
  #pending-section:has(> .section ~ .section) > .section { display: flex; flex-direction: column; margin-bottom: 0; }
  #pending-section:has(> .section ~ .section) > .section > .loan-card { flex: 1 1 auto; margin-bottom: 0; }
  /* Zwei Personen mit offenen Anfragen: jede Karte ist ~565 px breit, die vier
     Kennzahl-Kacheln brachen dort in "RÜCKZAH-LUNG"/"URSPRUNGS-ZWECK" um. Wie bei
     den aktiven Leihen im Zwei-Spalten-Raster (siehe oben) daher 2×2. */
  #pending-section:has(> .section ~ .section) .loan-snapshot {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
  }
}

/* --- N2: Desktop-Schriftgrößen ---
   Auf Laptop/Desktop lagen die Kartentexte bei 11,2-11,8 px (Labels, Badges,
   Zähler, Vertrauens-Chip) — gemessen 40 von 112 Textknoten unter 12 px.
   Behutsam auf ≥12 px angehoben; Handy-Größen bleiben, wie sie sind. */
@media (min-width: 1024px) {
  .dashboard-label { font-size: 0.8rem; }
  .snapshot-label { font-size: 0.76rem; }
  .loan-signals > .badge,
  .loan-signals > .signal-pill,
  .signal-meta { font-size: 0.8rem; }
  .tab-count { font-size: 0.76rem; min-width: 1.4rem; height: 1.4rem; }
  .trust-badge { font-size: 0.76rem; }
  .loan-kicker { font-size: 0.8rem; }
  .loan-amount { font-size: 1.7rem; }
  .loan-amount-sub { font-size: 0.8rem; }
  .loan-date { font-size: 0.85rem; }
  .activity-date-sub { font-size: 0.78rem; }
  .activity-row .badge-compact { font-size: 0.76rem; }
  .note-history-label { font-size: 0.8rem; }
  .conv-meta { font-size: 0.8rem; }
  .conv-time { font-size: 0.75rem; }
  .loan-progress-caption { font-size: 0.82rem; }
  .history-kpi-label { font-size: 0.78rem; }
}

@media (max-width: 374px) {
  /* 320er: "+ Ergänzung" wird zum reinen Plus-Knopf (aria-label trägt den
     Namen), die Anfrage-Leiste geht zurück auf zwei Reihen. */
  .loan-actions--lender-active .la-label { display: none; }
  .loan-card .loan-actions.loan-actions--lender-active > .btn-manual-entry { padding: 0; width: var(--touch-target); flex: 0 0 auto; }
  .loan-card .loan-actions.loan-actions--lender-pending:has(.btn-counter-offer, .btn-taxi-counter-offer) > :is(.btn-counter-offer, .btn-taxi-counter-offer) { flex-basis: 100%; order: 3; }
}

.log-badge-counter {
  background: color-mix(in srgb, var(--orange) 20%, transparent);
  color: var(--orange);
}

@media (prefers-reduced-motion: reduce) {
  .timeline-counter-detail .timeline-icon { transition: none; }
}

@media (max-width: 540px) {
  .counter-comparison-grid {
    grid-template-columns: 1fr;
    gap: 0.6rem;
  }
  .counter-comparison-new {
    position: relative;
    margin-top: 0.6rem;
  }
  .counter-comparison-new::before {
    content: "↓";
    position: absolute;
    top: -1.1rem;
    left: 50%;
    transform: translateX(-50%);
    color: var(--orange);
    font-size: 1.1rem;
    font-weight: 700;
    line-height: 1;
  }
  .counter-comparison-amount { font-size: 1.25rem; }
}

/* ============================================================================
   TAXI FEATURE
   ============================================================================ */

/* Settings section + vehicles modal */
#taxi-vehicles-summary { margin: 0.25rem 0 0.5rem; opacity: 0.85; }
.taxi-vehicles-list { display: flex; flex-direction: column; gap: 0.55rem; margin-bottom: 0.75rem; }
.taxi-vehicle-card {
  display: flex;
  gap: 0.6rem;
  border: 1px solid var(--border, rgba(255,255,255,0.1));
  border-radius: var(--radius-tile);
  padding: 0.8rem 1rem;
  background: rgba(255,255,255,0.03);
  transition: var(--transition-ui);
  cursor: pointer;
}
html:not(.using-touch) .taxi-vehicle-card:hover { border-color: rgba(255,255,255,0.18); }
/* Runde 4 (Taxi): the whole card is the "Bearbeiten" target (see
   _wireVehicleListEvents) but gave no sign of life on touch until the form
   slid in, and a long-press selected the vehicle name like page text. Press =
   background only (row idiom, no scale: the card carries its own ▲/▼ and ⋯);
   not while one of those inner controls is the one being pressed. */
.taxi-vehicle-card {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.taxi-vehicle-card:active:not(:has(.taxi-vehicle-order :active, .entry-action-menu :active)) {
  --press-opacity: 1; /* own press state — keeps the generic press dim off */
  background: rgba(255,255,255,0.07);
  transition-duration: 0s;
}
/* Paused: still fully readable (name, stats, actions), just visually
   receded — matches how the app marks a disabled-but-editable state
   elsewhere rather than hiding the vehicle outright. */
/* Dimmed per part, not via opacity on the card: opacity < 1 made the card a
   stacking context, so its "…" menu came out 60% see-through AND was painted
   below the next card — "Aktivieren"/"Löschen" of a paused vehicle could not
   be tapped where the menu overlapped the card underneath. */
.taxi-vehicle-card.is-inactive { border-style: dashed; }
.taxi-vehicle-card.is-inactive :is(.taxi-vehicle-order, .taxi-vehicle-head, .taxi-vehicle-stats, .taxi-vehicle-total, .taxi-vehicle-usage) {
  opacity: 0.6; transition: opacity var(--motion-fast);
}
html:not(.using-touch) .taxi-vehicle-card.is-inactive:hover :is(.taxi-vehicle-order, .taxi-vehicle-head, .taxi-vehicle-stats, .taxi-vehicle-total, .taxi-vehicle-usage) {
  opacity: 0.8;
}
/* Reorder column + the action menu carry their own click targets — the
   whole-card tap-to-edit (wired in _wireVehicleListEvents) must not fight them. */
.taxi-vehicle-order, .taxi-vehicle-menu { cursor: auto; }
.taxi-vehicle-order {
  display: flex; flex-direction: column; justify-content: center; gap: 0.2rem;
  flex-shrink: 0;
}
.taxi-vehicle-move-up, .taxi-vehicle-move-down {
  width: 1.7rem; height: 1.7rem;
  display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,0.1); border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.03); color: var(--text-muted);
  cursor: pointer; transition: var(--transition-ui);
}
html:not(.using-touch) .taxi-vehicle-move-up:hover:not(:disabled),
html:not(.using-touch) .taxi-vehicle-move-down:hover:not(:disabled) {
  border-color: rgba(255,255,255,0.22); color: var(--text);
}
.taxi-vehicle-move-up:disabled, .taxi-vehicle-move-down:disabled { opacity: 0.3; cursor: default; }
.taxi-vehicle-move-up:active:not(:disabled), .taxi-vehicle-move-down:active:not(:disabled) { transform: scale(0.9); }
/* Touch-only devices get the app's 44px minimum tap target around the small
   glyph, same idiom as .taxi-fav-chip-remove. */
@media (hover: none) and (pointer: coarse) {
  .taxi-vehicle-move-up, .taxi-vehicle-move-down {
    width: var(--touch-target); height: var(--touch-target);
  }
}
.taxi-vehicle-body { flex: 1; min-width: 0; }
.taxi-vehicle-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.taxi-vehicle-head strong { font-size: 0.95rem; }
/* Dot + name as one unit: as separate wrapping flex items a long name
   dropped to its own line and left the color dot alone above it. */
.taxi-vehicle-title {
  display: inline-flex; align-items: center; gap: 0.45rem;
  min-width: 0; max-width: 100%;
}
.taxi-vehicle-title strong { min-width: 0; overflow-wrap: anywhere; }
/* Deterministic per-vehicle color tag (_vehicleColor) so several cards are
   told apart at a glance, not only by reading the name. */
.taxi-vehicle-dot {
  width: 0.6rem; height: 0.6rem; border-radius: 50%; flex-shrink: 0;
}
@media (max-width: 540px) {
  /* The vehicle name is free text (no spaces guaranteed). As a flex item its
     default min-width:auto keeps it from shrinking below its own content
     width, so a long name pushes the fuel-type pill out and overflows the
     card — min-width:0 lets it actually shrink, overflow-wrap breaks the
     word itself once it does. */
  .taxi-vehicle-head strong { min-width: 0; overflow-wrap: anywhere; }
}
.taxi-vehicle-fuel {
  font-size: 0.78rem; opacity: 0.7;
  padding: 0.1rem 0.45rem; border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
}
.taxi-vehicle-badge {
  font-size: 0.72rem; font-weight: 600;
  padding: 0.1rem 0.45rem; border-radius: var(--radius-sm);
}
.taxi-vehicle-badge-default { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
.taxi-vehicle-badge-paused { background: rgba(255,255,255,0.08); color: var(--text-muted); }
.taxi-vehicle-badge-inuse { background: color-mix(in srgb, var(--badge-pending-text) 16%, transparent); color: var(--badge-pending-text); }
.taxi-vehicle-stats {
  display: flex; flex-wrap: wrap; gap: 0.3rem;
  font-size: 0.82rem; opacity: 0.8; margin: 0.45rem 0 0.2rem;
}
.taxi-vehicle-stats span {
  padding: 0.1rem 0.4rem; border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.05);
}
.taxi-vehicle-stats em { font-style: normal; opacity: 0.75; }
.taxi-vehicle-total { font-size: 0.82rem; font-weight: 600; margin-bottom: 0.15rem; }
.taxi-vehicle-usage { font-size: 0.76rem; opacity: 0.65; margin-bottom: 0.3rem; }
/* Reuses .entry-action-menu/.entry-menu-panel (same fade + anchor-positioned
   dropdown as the taxi favorites and loan-entry menus) — only the alignment
   differs: those sit right-aligned in a table row, this sits left-aligned
   under the card's own text. */
/* Gleicher Abstand wie jedes .entry-action-menu (0.6rem), siehe
   .lend-limit-row-menu. */
.taxi-vehicle-menu { justify-content: flex-start; margin-top: 0.5rem; }
/* gap 0: den Abstand zwischen den Feldgruppen liefert ihr margin-bottom (1rem) — mit dem
   früheren gap 0.5rem dazu waren es 24 px statt 16 px wie in allen anderen Formularen;
   in den Zeilen kam auf dem Handy (gestapelt) noch der Zeilenabstand 0.75rem obendrauf. */
.taxi-vehicle-form { display: flex; flex-direction: column; gap: 0; }
.taxi-vehicle-form .form-row { display: flex; column-gap: 0.75rem; row-gap: 0; flex-wrap: wrap; }
.taxi-vehicle-form .form-row .form-group { flex: 1 1 45%; min-width: 0; }
.taxi-consumption-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
.taxi-consumption-chip {
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14);
  /* Was 21px tall — the smallest tap target in the whole form. Same 30px
     chip height as the favorite chips of the request modal. */
  min-height: 1.9rem; display: inline-flex; align-items: center;
  border-radius: var(--radius-xs); padding: 0.2rem 0.65rem;
  font-size: 0.76rem; color: var(--text-muted); cursor: pointer;
  transition: var(--transition-ui);
}
html:not(.using-touch) .taxi-consumption-chip:hover { background: rgba(255,255,255,0.12); color: var(--text); }
.taxi-consumption-chip:active { transform: scale(0.95); }
/* Touch: Richtwert-Chips im Fahrzeug-Formular (30px) auf die Mindest-Trefferhöhe. */
@media (pointer: coarse) {
  .taxi-consumption-chip { min-height: var(--touch-target); }
}
.taxi-vehicle-preview {
  font-size: 0.82rem; color: var(--text-muted);
  background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-xs); padding: 0.45rem 0.6rem; margin: 0 0 var(--space-4);
  /* Rerender-Audit (Fahrzeuge): the preview swaps between a one-line price
     ("Bei 10 km: …") and the two-line "Live-Kraftstoffpreis …" note while
     one types the price override — reserve both lines so the form below (and,
     at the card's height cap, the field being typed in) does not jump 17px. */
  box-sizing: content-box;
  min-height: 2lh;
}

/* Two-up field rows in the taxi edit + taxi counter-offer modals: side by side
   on wider screens, stacked on narrow phones (consistent with the rest of the
   modal forms). Scoped to these modals so no existing block-layout .form-row
   is affected. */
#modal-taxi-edit .form-row,
#modal-taxi-counter-offer .form-row,
#modal-taxi-request .form-row {
  display: flex; gap: 0.75rem; flex-wrap: wrap;
}
#modal-taxi-edit .form-row > .form-group,
#modal-taxi-counter-offer .form-row > .form-group,
#modal-taxi-request .form-row > .form-group {
  flex: 1 1 45%; min-width: 0;
}
/* Fahrzeug-Auswahl braucht mehr Platz als die Abfahrtszeit: bei 45/45 schnitt das
   Select "Auto … · Super E10" am Rand ab (gemessen 179 > 168 px). */
#modal-taxi-edit .form-row:has(> .form-group > select) > .form-group,
#modal-taxi-counter-offer .form-row:has(> .form-group > select) > .form-group { flex: 1 1 0; }
#modal-taxi-edit .form-row:has(> .form-group > select) > .form-group:has(> select),
#modal-taxi-counter-offer .form-row:has(> .form-group > select) > .form-group:has(> select) { flex: 1.3 1 0; }
@media (max-width: 540px) {
  #modal-taxi-edit .form-row > .form-group,
  #modal-taxi-counter-offer .form-row > .form-group,
  #modal-taxi-request .form-row > .form-group,
  #modal-taxi-edit .form-row:has(> .form-group > select) > .form-group,
  #modal-taxi-counter-offer .form-row:has(> .form-group > select) > .form-group,
  #modal-taxi-edit .form-row:has(> .form-group > select) > .form-group:has(> select),
  #modal-taxi-counter-offer .form-row:has(> .form-group > select) > .form-group:has(> select) {
    flex: 1 1 100%;
  }
}

/* Abstands-Audit Taxi-Anfrage: ein Raster statt vier. Feld → Hilfetext = Label → Feld
   (0.4rem; vorher 0.45rem bei .settings-help-text, 0.2rem bei .field-hint im selben
   Dialog). Die Stoppliste hält ihren „+ Zwischenstopp“-Knopf auf 0.5rem; Rückfahrt-
   Schalter und Karte sind eigene Blöcke und haben wie alle Feldgruppen 1rem Abstand
   (vorher klebte der Schalter ohne Lücke an „+ Zwischenstopp“, die Karte hatte 0.6rem).
   Die Preisbox endet mit 0.5rem wie das Fehlerfeld darunter — kein Sprung beim Einblenden. */
#modal-taxi-request .form-group > :is(.field-hint, .settings-help-text),
:is(#modal-taxi-pickup, #modal-taxi-edit, #modal-taxi-counter-offer, #modal-taxi-vehicle-form) .form-group > :is(.field-hint, .settings-help-text) { margin-top: 0.4rem; }
#modal-taxi-request .settings-actions-row { margin-bottom: var(--space-4); }
#modal-taxi-request .taxi-map { margin: var(--space-4) 0; }
#modal-taxi-request .taxi-quote-box { margin: 0 0 var(--space-2); }
#modal-taxi-request .lend-limit-notice { margin-bottom: var(--space-4); }
/* Zusagen- und Gegenangebot-Dialog: Preisbox 1rem unter und (ohne eigenes margin-top) über sich wie die
   Feldgruppen (vorher 0.4rem oben auf die 1rem der Reihe, 0.8rem unten); Karte hängt mit 0.5rem an ihrer Legende,
   die Legende hat 1rem Abstand zur nächsten Feldgruppe (vorher 0.6rem). */
:is(#modal-taxi-pickup, #modal-taxi-counter-offer) .taxi-counter-price-box { margin: 0 0 var(--space-4); }
#modal-taxi-pickup .taxi-map { margin: 0 0 var(--space-2); }
#modal-taxi-pickup .taxi-route-legend--pins { margin: 0 0 var(--space-4); }
#tv-edit-hint { margin-top: 0; }
/* „+ Merken“ ist 36 px hoch (wie .quick-reply-chip); die Tippfläche wächst unsichtbar auf 44 px. */
@media (pointer: coarse) {
  #modal-taxi-request .purpose-save-btn { position: relative; }
  #modal-taxi-request .purpose-save-btn::after { content: ""; position: absolute; inset: -5px 0; }
}

/* Borrower hero action button */
.btn-taxi-request {
  margin-top: 0.75rem;
  width: 100%;
  /* compact variant: sits inline in the hero dashboard, smaller than the standalone button */
}
.btn-taxi-request--compact {
  width: auto;
  align-self: flex-start;
  padding: 0.45rem 0.9rem;
  font-size: 0.85rem;
  min-height: unset;
  border-radius: var(--radius-btn);
  box-shadow: 0 1px 6px rgba(232,156,18,0.2);
}
.btn-taxi-request {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  background: linear-gradient(135deg, #c8952e, #b07c10);
  color: #1a1a1a;
  border: none;
  padding: 0.65rem 1rem;
  min-height: unset;
  border-radius: var(--radius-btn);
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(180, 130, 20, 0.2);
  transition: var(--transition-ui);
}
html:not(.using-touch) .btn-taxi-request:hover { transform: translateY(-1px); filter: brightness(1.05); }
.btn-taxi-request:active { transform: translateY(0); filter: brightness(0.95); }
/* Runde 6 (Knöpfe): Maus-Stufe wie bei .btn-borrower-cta (direkt daneben in
   derselben Zeile). Die Hover-Regel (0,3,1) schlug das schlichte :active —
   gemessen blieb "Taxifahrt anfragen" beim Klicken angehoben und aufgehellt. */
html:not(.using-touch) .btn-taxi-request:not(:disabled):active {
  transform: translateY(0);
  filter: brightness(0.95);
}
.btn-taxi-request:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-taxi-request:disabled { transform: none; }
.btn-taxi-icon { font-size: 1.2rem; }

.taxi-stops { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.5rem; }
.taxi-stops-header {
  display: flex; align-items: baseline; justify-content: space-between;
  margin: 0 0 0.4rem; gap: 0.5rem;
}
.taxi-stops-header strong { font-size: 0.95rem; white-space: nowrap; flex-shrink: 0; }
/* The long budget text ("2 von 23 Stopps · 2 weitere sind für den Fahrer
   reserviert") squeezed the heading into "Deine / Strecke" on phones; the
   counter wraps inside its own box now, right-aligned. */
.taxi-stop-counter { font-size: 0.78rem; color: var(--text-muted); min-width: 0; text-align: right; }
.taxi-stop-row {
  /* align-items: center keeps the row height bound to the input field
     height instead of the 3-button reorder column on the left — without
     this the row was ~96 px tall (3 × 32 px touch targets), leaving a
     large empty strip under the address input. */
  /* Two columns: reorder controls | fields. Number badge, ✓ and ✕ live in
     the fields' head line (.taxi-stop-head) instead of taking their own
     columns — that gave the address input ~55px more width on a 375px phone
     (was ~210px, the address was cut after the street name). */
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: center; column-gap: 0.5rem;
  padding: 0.5rem 0.6rem 0.55rem 0.4rem;
  border-radius: var(--radius);
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  position: relative;
  transition: transform var(--motion-fast), opacity var(--motion-fast), box-shadow var(--motion-fast), border-color var(--motion-fast);
  touch-action: pan-y;
}
html:not(.using-touch) .taxi-stop-row:focus-within {
  border-color: rgba(255,255,255,0.12);
}
.taxi-stop-row--dragging {
  opacity: 0.88; z-index: var(--z-drag);
  box-shadow: 0 8px 22px rgba(0,0,0,0.4);
  transition: none;
}
.taxi-stop-row--swiping { transition: none; }
.taxi-stop-controls {
  display: flex; flex-direction: column; align-items: center;
  /* gap 0: drei Knöpfe à 24 px = 72 px — mit 0.1rem Lücke + Polster waren es 76 px,
     und die mittleren Zeilen wurden 3 px höher als die erste/letzte. */
  gap: 0; user-select: none;
}
.taxi-stop-handle,
.taxi-stop-move-up,
.taxi-stop-move-down {
  background: transparent; border: none; color: var(--text-muted);
  padding: 0.15rem 0.35rem; line-height: 1; cursor: pointer;
  font-size: 0.7rem; border-radius: 4px; touch-action: none;
  /* Compact stacked column: three ~24 px buttons → ~72 px column height,
     which now fits alongside the input row instead of dominating it
     (was 3 × 32 px = 96 px before, dwarfing the actual input). Touch
     remains acceptable because the middle drag-handle is the only button
     hit frequently. */
  min-height: 24px; min-width: 32px;
  display: inline-flex; align-items: center; justify-content: center;
}
.taxi-stop-handle { cursor: grab; font-size: 0.95rem; padding: 0.2rem 0.35rem; }
.taxi-stop-handle:active { cursor: grabbing; }
.taxi-stop-handle:focus-visible {
  outline: 2px solid var(--accent, #0a7); outline-offset: 1px;
}
@media (hover: none) and (pointer: coarse) {
  .taxi-stop-controls { gap: 0.2rem; }
  .taxi-stop-handle,
  .taxi-stop-move-up,
  .taxi-stop-move-down,
  /* Previously only bumped by the max-width:480px fallback further down —
     stayed under 44px on a touch device wider than that (tablet landscape,
     2-in-1 laptop) even though these sibling controls in the same row were
     already fixed via the correct pointer-capability query. */
  .btn-stop-del,
  .taxi-bonus-chip {
    min-width: var(--touch-target);
    min-height: var(--touch-target);
  }
}
/* Taxi-R2 (QA M12): on touch the stop row keeps only the drag grip. Three
   stacked 44px controls (▲ grip ▼) made every row ~140-170px tall and
   squeezed the address field. ▲/▼ stay in the DOM, visually hidden, so screen
   readers (VoiceOver/TalkBack) still reach them; with a keyboard they show up
   again on focus, and the grip itself reorders with ArrowUp/ArrowDown. */
@media (hover: none) and (pointer: coarse) {
  .taxi-stop-row:not(.taxi-stop-row--phantom) :is(.taxi-stop-move-up, .taxi-stop-move-down):not(:focus-visible):not([disabled]) {
    position: absolute !important;
    width: 1px !important; height: 1px !important;
    min-width: 0 !important; min-height: 0 !important;
    padding: 0 !important; margin: -1px !important;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .taxi-stop-row--phantom :is(.taxi-stop-move-up, .taxi-stop-move-down) { display: none; }
  .taxi-stop-controls { align-self: stretch; justify-content: center; }
  .taxi-stop-row .taxi-stop-handle { position: relative; min-height: var(--touch-target); min-width: 2.25rem; }
  /* 36 px breit sichtbar, 44 px getroffen (wie .modal-close::after). */
  .taxi-stop-row .taxi-stop-handle::after { content: ""; position: absolute; inset: 0 -4px; }
  .taxi-stop-row { column-gap: 0.3rem; padding-left: 0.2rem; }
}
.taxi-stop-move-up[disabled], .taxi-stop-move-down[disabled], .taxi-stop-handle[disabled] {
  visibility: hidden; pointer-events: none;
}
/* First/last row: the dead ▲/▼ is dropped instead of kept as an invisible
   44px block on touch — it only made those rows taller than their content. */
.taxi-stop-row:not(.taxi-stop-row--phantom) :is(.taxi-stop-move-up, .taxi-stop-move-down)[disabled] {
  display: none;
}
html:not(.using-touch) .taxi-stop-handle:hover,
html:not(.using-touch) .taxi-stop-move-up:hover,
html:not(.using-touch) .taxi-stop-move-down:hover {
  background: rgba(255,255,255,0.08); color: var(--text);
}
.taxi-stop-results.taxi-stop-recent::before {
  content: 'Zuletzt verwendet';
  display: block; padding: 0.4rem 0.7rem 0.2rem;
  font-size: var(--text-xs); opacity: 0.55;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.taxi-stop-swipe-bg {
  position: absolute; top: 0; bottom: 0; right: 0; width: 0;
  background: var(--danger, #c0392b);
  border-radius: 0 10px 10px 0;
  display: flex; align-items: center; justify-content: flex-end;
  /* padding only while swiping — with border-box, width:0 + padding would
     otherwise leave a permanently visible red sliver on every row */
  padding-right: 0; color: #fff; font-weight: 700;
  pointer-events: none; overflow: hidden; white-space: nowrap;
}
.taxi-stop-row .taxi-stop-swipe-bg { transition: width var(--motion-fast); }
.taxi-stop-row--swiping .taxi-stop-swipe-bg { transition: none; padding-right: 0.7rem; }
.taxi-stop-num {
  flex: 0 0 1.35rem; height: 1.35rem; line-height: 1.35rem;
  text-align: center; border-radius: 50%; background: var(--accent-btn, #3a7f61);
  color: #fff; font-weight: 700; font-size: 0.72rem;
  box-shadow: 0 1px 4px rgba(0,0,0,0.25);
}
.taxi-stop-fields { min-width: 0; display: flex; flex-direction: column; gap: 0.3rem; position: relative; }
/* Head line of a stop: ② Label ··· ✓ ✕ */
.taxi-stop-head {
  display: flex; align-items: center; gap: 0.45rem;
  min-height: 1.5rem; min-width: 0;
}
.taxi-stop-head .taxi-stop-kicker {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.taxi-stop-head .taxi-stop-ok { display: inline-flex; align-items: center; }
.taxi-stop-head .taxi-stop-ok:empty { display: none; }
/* The ✕ keeps its 44px hit area on touch (rule below), but must not blow the
   head line up to 44px — pull it into the row padding instead. */
.taxi-stop-head .btn-stop-del {
  margin: -0.55rem -0.45rem -0.55rem 0;
  font-size: 1.25rem; line-height: 1;
}
/* Mit dem 44-px-✕ (Touch / schmal) ist die Kopfzeile 26.4 px hoch, ohne ✕ (genau
   2 Stopps) nur 24 px — beim dritten Stopp wuchsen alle Zeilen um 2.4 px. Die
   Mindesthöhe gilt deshalb immer, auch für die Phantomzeilen. */
@media (hover: none) and (pointer: coarse), (max-width: 480px) {
  .taxi-stop-head { min-height: 1.65rem; }
  /* Die 44 px aus dem Touch-Block weiter oben verlor gegen `.btn-stop-del { min-height: 32px }`
     (später in der Datei): auf Tablets ab 481 px blieb das ✕ 32 px groß. */
  .taxi-stop-head .btn-stop-del { min-width: var(--touch-target); min-height: var(--touch-target); }
}
/* Empty aux-slot (no autocomplete results, no spinner, no error) — collapse
   it so the flexbox `gap` doesn't leave a phantom 0.2rem strip under the
   input. Keeps the row tight when the field is just a saved address. */
.taxi-stop-aux:empty { display: none; }
.taxi-stop-kicker { opacity: 0.7; font-size: 0.75rem; }
.taxi-stop-input {
  width: 100%; font-size: 16px; padding: 0.5rem 0.6rem;
  background: rgba(255,255,255,0.05); border: 1px solid var(--border-input, rgba(255,255,255,0.1));
  border-radius: var(--radius-xs); color: inherit;
}
/* Any wrapper holding an autocomplete list must be its containing block —
   otherwise `top: 100%` resolves against .modal-card (the nearest positioned
   ancestor) and the list renders below the card, where overflow clips it away.
   #te-pickup had exactly that bug: the suggestions were unreachable, so the
   pickup address could never be picked and submit failed validation. */
.form-group:has(> .taxi-stop-results) { position: relative; }
.taxi-stop-results {
  position: absolute; top: 100%; left: 0; right: 0;
  background: var(--surface, #222); border: 1px solid var(--border, rgba(255,255,255,0.15));
  border-radius: var(--radius); margin-top: 2px; padding: 0.25rem 0;
  list-style: none; z-index: var(--z-autocomplete); max-height: 220px; overflow-y: auto;
  box-shadow: 0 6px 20px rgba(0,0,0,0.35);
}
/* Anchor-positioned variant — only the new-taxi-request stop rows get this
   (anchor-name/position-anchor set in _flipStopResultsIfNeeded()); the
   taxi-edit rows and #te-pickup-results still use the plain rule above
   unmodified, since they were out of scope for this and never had flip
   support to begin with. position:fixed anchored to the input also escapes
   the modal's own overflow/scroll clipping, which the old JS math
   (spaceBelow/spaceAbove vs. the modal-body rect) had to route around
   explicitly. left+right both anchored (matching the input's width) means
   there's no independent horizontal flip axis, so flip-block alone is
   correct here — unlike the entry-menu panel (right-edge-only alignment),
   which needed all three fallback candidates (verified live, see there). */
.taxi-stop-results.taxi-stop-results--anchored {
  position: fixed;
  position-try-fallbacks: flip-block;
  top: anchor(bottom);
  left: anchor(left);
  right: anchor(right);
  margin-top: 2px;
}
/* Rerender-Audit (Taxi): "Suche…" / "Keine Adresse gefunden" take the
   suggestion list's place as the same anchored overlay (app.js
   _renderStopAuxHtml) — as flow content under the field they pushed every
   row below down and back up on each search. */
.taxi-stop-status.taxi-stop-results--anchored {
  position: fixed;
  position-try-fallbacks: flip-block;
  top: anchor(bottom);
  left: anchor(left);
  right: anchor(right);
  z-index: var(--z-autocomplete);
  margin: 2px 0 0;
  padding: 0.55rem 0.7rem;
  display: flex; align-items: center; gap: 0.4rem;
  background: var(--surface, #222);
  border: 1px solid var(--border, rgba(255,255,255,0.15));
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(0,0,0,0.35);
  color: var(--text-muted);
  opacity: 1;
}
/* An aux slot holding only an overlay (no favourites row) has no height of
   its own — without this the flex gap above it still opened and closed a
   ~5px strip whenever a list or status appeared. */
.taxi-stop-aux:not(:has(> :not(.taxi-stop-results, .taxi-stop-status))) { display: contents; }
/* Dasselbe, wenn auf Handy/Touch die aufgelöste Favoritenzeile ausgeblendet ist
   (s. u.): der leere Slot ließ sonst 0.3rem Lücke unter dem Feld stehen, die Zeile
   war unten 5 px höher als oben gepolstert. */
@media (max-width: 820px), (hover: none) and (pointer: coarse) {
  .taxi-stop-aux:not(:has(> :not(.taxi-stop-results, .taxi-stop-status, .taxi-stop-favs--resolved))) { display: contents; }
}
/* The static single-field lists (pickup + return address in the accept modal,
   pickup in the edit modal) had no anchoring at all: absolute under the input
   they ran into the modal body's bottom edge and the lower suggestions sat
   hidden behind the sticky action bar (measured on 320/375px: list bottom
   650px vs. body bottom 512px). Same anchored + flipping treatment, static
   names because these inputs exist once. */
@supports (anchor-name: --a) {
  #tp-address { anchor-name: --tp-address; }
  #tp-return-address { anchor-name: --tp-return-address; }
  #te-pickup { anchor-name: --te-pickup; }
  #te-return { anchor-name: --te-return; }
  #tp-results { position-anchor: --tp-address; }
  #tp-return-results { position-anchor: --tp-return-address; }
  #te-pickup-results { position-anchor: --te-pickup; }
  #te-return-results { position-anchor: --te-return; }
  #tp-results, #tp-return-results, #te-pickup-results, #te-return-results {
    position: fixed;
    position-try-fallbacks: flip-block;
    top: anchor(bottom);
    left: anchor(left);
    right: anchor(right);
    margin-top: 2px;
  }
}
/* Fallback set by _ensureSingleListVisible() when anchor positioning leaves the
   list somewhere the user cannot see it: plain flow layout under the field. */
.taxi-stop-results.is-flow {
  position: static !important;
  margin-top: 4px;
}
.taxi-stop-results li { padding: 0.5rem 0.7rem; cursor: pointer; font-size: 0.9rem; overflow-wrap: anywhere; }
/* Taxi-R2 (QA M13): distance of a suggestion from the route, right-aligned. */
.taxi-stop-results li[role="option"] { display: flex; align-items: baseline; gap: 0.6rem; }
.taxi-stop-result-label { flex: 1 1 auto; min-width: 0; }
.taxi-stop-result-dist { flex-shrink: 0; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
/* Taxi-R2 (QA M12): on phones/touch a stop that already has its address names
   its favourite in the head line instead of repeating the chip row (see
   _stopFavsHtml). With room to spare (desktop) the chip row stays and marks it. */
.taxi-stop-fav-name { display: none; }
@media (max-width: 820px), (hover: none) and (pointer: coarse) {
  .taxi-stop-favs.taxi-stop-favs--resolved { display: none; } /* beats the later .taxi-stop-favs { display: flex } */
  .taxi-stop-fav-name { display: inline-flex; }
}
.taxi-stop-fav-name {
  align-items: center; gap: 0.25rem; min-width: 0; max-width: 9rem;
  flex: 0 1 auto;
  font-size: var(--text-xs); font-weight: 600; color: var(--badge-pending-text);
}
.taxi-stop-fav-name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.taxi-stop-results li.is-active { background: rgba(255,255,255,0.08); }
html:not(.using-touch) .taxi-stop-results li:hover { background: rgba(255,255,255,0.08); }
/* Runde 4 (Taxi): suggestions and "Zuletzt verwendet" entries are picked on
   mousedown (keeps the field focused), i.e. only when the finger lifts — the
   row now answers the touch itself, like a native list row. */
.taxi-stop-results li:active { --press-opacity: 1; background: rgba(255,255,255,0.12); transition-duration: 0s; }
/* Status row in the static single-field lists (accept + edit dialog): "Suche…",
   "Keine Adresse gefunden", "nicht verfügbar". Not an option — no hover, no press. */
.taxi-stop-results li.taxi-stop-status-row {
  display: flex; align-items: center; gap: 0.4rem;
  color: var(--text-muted); cursor: default;
}
html:not(.using-touch) .taxi-stop-results li.taxi-stop-status-row:hover,
.taxi-stop-results li.taxi-stop-status-row:active { background: none; }
.taxi-stop-empty { margin: 0.25rem 0 0; font-size: 0.85rem; opacity: 0.75; font-style: italic; }
/* Full-width "save this point as favorite" action per recognized stop. */
.taxi-stop-fav-save {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  width: auto;
  margin-top: 0.3rem;
  padding: 0.2rem 0.55rem;
  font-size: 0.74rem;
  font-weight: 600;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border, rgba(255,255,255,0.16));
  background: transparent;
  color: var(--text-muted, #9aa);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
html:not(.using-touch) .taxi-stop-fav-save:hover { background: rgba(230, 200, 75, 0.12); border-color: rgba(230, 200, 75, 0.45); color: var(--badge-pending-text); }
.taxi-stop-fav-save:active { transform: scale(0.99); }
/* Taxi-R2 (QA M12): the save star sits in the head line (☆ ✓ ✕) instead of
   taking its own line; 44px target pulled into the row padding like the ✕. */
.taxi-stop-favslot { display: inline-flex; min-width: 0; flex: 0 1 auto; }
/* Der Stern darf bei 320 px nicht unter 44 px zusammengedrückt werden — dann gibt die Bezeichnung nach. */
.taxi-stop-favslot:has(> .taxi-stop-fav-save) { flex-shrink: 0; }
/* The kicker ("Ziel", "Zwischenstopp 2") keeps a readable minimum next to a
   favourite name — it was squeezed to nothing by "★ Bahnhof Altenkirchen". */
.taxi-stop-head .taxi-stop-kicker { min-width: min(4.5rem, 40%); }
.taxi-stop-favslot:empty { display: none; }
.taxi-stop-head .taxi-stop-fav-save {
  /* Rerender-Audit (Taxi): pulled in by exactly the part that exceeds the
     head line's 1.5rem — with -0.55rem the 44px target still stood 2.4px
     taller, so the head (and every row below) grew when an address was
     recognised and the star appeared. */
  margin: calc((1.5rem - var(--touch-target)) / 2) 0; padding: 0;
  width: var(--touch-target); height: var(--touch-target);
  justify-content: center;
  border: none; border-radius: var(--radius);
}
.taxi-stop-ok { color: var(--accent, #0a7); font-weight: 700; align-self: center; }
.taxi-stop-row--phantom { opacity: 0.55; }
.taxi-stop-row--phantom .taxi-stop-num { background: rgba(255,255,255,0.18); }
/* The phantom rows only keep the controls column for its width (no horizontal
   shift once a real row appears). Its 76px height used to set the row height,
   which centered the short kicker+placeholder and pushed the kicker to ~18px
   below the row's top edge, vs ~9px in the address rows next to it. */
.taxi-stop-row--phantom .taxi-stop-controls { height: 0; visibility: hidden; }
.taxi-stop-placeholder {
  width: 100%; padding: 0.5rem 0.6rem;
  background: rgba(255,255,255,0.02);
  border: 1px dashed var(--border, rgba(255,255,255,0.1));
  border-radius: var(--radius-xs); color: inherit;
  font-style: italic; font-size: 0.9rem;
  overflow-wrap: anywhere;
}
.btn-stop-del {
  background: transparent; border: none; color: var(--text-muted);
  font-size: 1.3rem; cursor: pointer; padding: 0.3rem 0.5rem;
  min-height: 32px; min-width: 32px;
  display: inline-flex; align-items: center; justify-content: center;
}

/* Leaflet map container */
.taxi-map {
  width: 100%; height: var(--taxi-map-h, 230px); border-radius: var(--radius-tile);
  border: 1px solid var(--border, rgba(255,255,255,0.12));
  overflow: hidden; margin: 0.6rem 0;
  position: relative; z-index: 0;
  /* contain:paint clips Leaflet's GPU-composited tile layers to this box on
     mobile Safari, preventing them from bleeding over sticky/fixed elements. */
  contain: paint;
  isolation: isolate;
  box-shadow: 0 2px 12px rgba(0,0,0,0.2);
}
/* Taxi-R2: the request sheet on small screens.
   - ≤380px wide the title broke into "Taxifahrt / anfragen" next to the
     info dot, the favourite star and ✕. Fits one line again at 375px; at
     320px it stays two lines, just lower.
   - Landscape phone (~390px high): header + footer still took ~140px of the
     367px card after round 1; tighter paddings for this form give the stops
     and the map back ~40px (touch targets stay 44px). */
@media (max-width: 380px) {
  #modal-taxi-request .history-modal-header h2 { font-size: 1.2rem; }
}
/* Taxi audit (320×700): sticky header (kicker + two-line title with the info
   dot floating detached between title and star) and the footer with two
   stacked full-width buttons ate a large part of the short sheet. On small
   heights: no decorative kicker, smaller title whose info dot stays glued to
   the last word (inline flow instead of a flex row), buttons side by side.
   Touch targets stay 44px. */
@media (max-width: 540px) and (max-height: 760px) {
  #modal-taxi-request .modal-sticky-header .modal-kicker { display: none; }
  #modal-taxi-request .history-modal-header { padding-bottom: 0.6rem; }
  #modal-taxi-request .history-modal-header h2 { font-size: 1.1rem; line-height: 1.25; }
  #modal-taxi-request .history-modal-header h2.title-with-info { display: block; }
  #modal-taxi-request .modal-card > .modal-actions:has(> button + button) {
    flex-direction: row;
    padding-top: 0.55rem;
  }
  #modal-taxi-request .modal-card > .modal-actions > button {
    flex: 1 1 0;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
  #modal-taxi-request .modal-drag-handle { margin-bottom: 0.2rem; }
}
@media (orientation: landscape) and (max-height: 500px) {
  #modal-taxi-request.modal-overlay { padding-top: 0.25rem; padding-bottom: 0.25rem; }
  #modal-taxi-request .modal-card { max-height: calc(100dvh - 0.5rem - env(safe-area-inset-bottom, 0px) - var(--modal-kb-inset)); }
  #modal-taxi-request .modal-sticky-header { padding-top: 0.45rem; }
  #modal-taxi-request .history-modal-header { padding-bottom: 0.4rem; }
  #modal-taxi-request .modal-card > .modal-actions {
    padding-top: 0.4rem;
    padding-bottom: max(0.45rem, env(safe-area-inset-bottom, 0px));
  }
  #modal-taxi-request .taxi-map { --taxi-map-h: 180px; }
}
/* Quote box */
/* A new price is on its way: the old one stays readable but clearly stale. */
.taxi-quote-box.is-stale,
.taxi-counter-price-box.is-stale {
  opacity: 0.5;
  transition: opacity var(--motion-base) var(--ease-standard);
}

.taxi-quote-box {
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-left: 4px solid var(--accent, #0a7);
  border-radius: var(--radius-tile);
  padding: 0.85rem 1rem; margin: 0.6rem 0;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.taxi-quote-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.taxi-quote-total { font-size: 1.7rem; font-weight: 700; color: var(--accent, #0a7); line-height: 1; }
.taxi-quote-distance { opacity: 0.75; font-size: 0.88rem; }
.taxi-quote-return-note { opacity: 0.8; font-weight: 400; white-space: nowrap; }
.taxi-quote-breakdown {
  list-style: none; padding: 0; margin: 0.5rem 0 0;
  font-size: 0.87rem;
  border-top: 1px solid rgba(255,255,255,0.07); padding-top: 0.4rem;
}
.taxi-quote-breakdown li { padding: 0.12rem 0; }

/* Receipt-style row: label | per-km rate | × km | = total */
.taxi-bd-row {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: baseline;
  gap: 0 0.5rem;
  padding: 0.15rem 0;
  font-size: 0.84rem;
}
.taxi-bd-row--total {
  border-top: 1px solid rgba(255,255,255,0.12);
  margin-top: 0.25rem;
  padding-top: 0.35rem;
  font-weight: 600;
  font-size: 0.88rem;
}
.taxi-bd-row--markup {
  color: var(--text-muted);
}
.taxi-bd-label { min-width: 0; }
.taxi-bd-rate { opacity: 0.65; font-size: 0.8rem; white-space: nowrap; }
/* War opacity:.5 -> 4.26-4.47:1. */
.taxi-bd-times { color: var(--text-dim); font-size: 0.8rem; white-space: nowrap; }
.taxi-bd-amt { text-align: right; white-space: nowrap; font-weight: 500; }
/* Narrow boxes (phone modals, loan cards): four columns left the label column
   at a few px — "Fahrpreis" was painted over "0,29 €/km", the fuel-price pill
   collapsed into a 2-line sliver. Below ~360px the row becomes
   label ··· amount / rate × km (second line, muted). */
.taxi-quote-breakdown,
.taxi-detail-breakdown { container-type: inline-size; }
@container (max-width: 22.5rem) {
  .taxi-bd-row { grid-template-columns: auto auto minmax(0, 1fr) auto; row-gap: 0.05rem; }
  .taxi-bd-label { grid-column: 1 / 4; grid-row: 1; }
  .taxi-bd-amt { grid-column: 4; grid-row: 1; }
  .taxi-bd-rate { grid-column: 1; grid-row: 2; }
  .taxi-bd-times { grid-column: 2; grid-row: 2; }
  .taxi-bd-rate:empty, .taxi-bd-times:empty { display: none; }
}

/* --- Taxi counter-offer: price preview (send modal) --- */
.taxi-counter-price-box {
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-left: 4px solid var(--accent, #0a7);
  border-radius: var(--radius-tile);
  padding: 0.7rem 0.9rem;
  margin: 0.4rem 0 0.8rem;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.taxi-counter-price-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 0.5rem; padding: 0.12rem 0; font-size: 0.9rem;
}
.taxi-counter-price-total {
  border-top: 1px solid rgba(255,255,255,0.12);
  margin-top: 0.3rem; padding-top: 0.4rem;
  font-weight: 700; font-size: 1.05rem;
}
.taxi-counter-price-total span:last-child { color: var(--accent, #0a7); }
/* Accept modal before both addresses are picked: a "—" head, an empty
   breakdown and two separator lines stacked over "Endpreis —" read as broken. */
.taxi-counter-price-box.is-empty .taxi-counter-price-head,
.taxi-counter-price-box.is-empty .taxi-quote-breakdown { display: none; }
.taxi-counter-price-box.is-empty .taxi-counter-price-total {
  border-top: 0; margin-top: 0; padding-top: 0;
}
.taxi-counter-price-box.is-empty .taxi-counter-price-total span:last-child { color: var(--text-muted); }
/* Counter-offer modal: the total is the box's only row — its separator line
   then hung above nothing. */
.taxi-counter-price-box > .taxi-counter-price-total:first-child {
  border-top: 0; margin-top: 0; padding-top: 0;
}
.taxi-counter-price-head {
  display: flex; justify-content: flex-end;
  font-size: 0.78rem; opacity: 0.65; margin-bottom: 0.25rem;
}
.taxi-counter-price-box .taxi-quote-breakdown { margin: 0 0 0.4rem; }
/* Der Bonus steht optisch abgesetzt vom Fahrpreis: der Leihgeber verhandelt ihn
   nicht, er kommt aber zu dem, was der Leihnehmer zahlt, dazu.
   (Klassenname bleibt -tip, die sichtbaren Texte heissen ueberall "Bonus".) */
.taxi-counter-price-tip {
  margin-top: 0.35rem; padding-top: 0.35rem;
  border-top: 1px dashed rgba(255,255,255,0.14);
  opacity: 0.75; font-size: 0.84rem;
}
.taxi-counter-price-grand { font-weight: 700; }

/* Manueller Rabatt/Aufschlag in jeder Aufschlüsselung — der einzige Posten,
   den der Leihgeber frei gesetzt hat, deshalb farblich abgesetzt. */
.taxi-bd-row--adjust .taxi-bd-label { font-weight: 600; }
.taxi-bd-row--adjust .taxi-bd-amt { color: var(--orange, #f59e0b); font-weight: 700; }

/* Lender "offer sent" status block: a small status badge on its own line,
   then the edit + withdraw buttons side by side in a row below it.
   Column layout so the badge keeps its intrinsic pill size (align-items:
   flex-start) and never stretches into a giant orange bar — the earlier
   flex-basis:100% approach fought width:fit-content and lost. */
.counter-offer-status {
  display: flex;
  flex-direction: column;
  align-items: center;       /* badge centered above the button row */
  gap: 0.6rem;
  margin-top: 0.5rem;
}
.counter-offer-status > .counter-offer-badge {
  max-width: 100%;           /* wrap long text instead of overflowing */
  text-align: center;
}
.counter-offer-status-btns {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  width: 100%;               /* button row spans the card width */
}
.counter-offer-status-btns > button {
  flex: 1 1 0;               /* buttons share the row equally */
  min-width: 0;
  margin-top: 0;
}

/* Borrower-side twin of .counter-offer-status: badge above, action button
   below. Was rendered without a single rule, so badge and button fell into
   raw block flow — no gap, no centering, button at its default width. */
.counter-offer-addition-decide {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.5rem;
}
.counter-offer-addition-decide > .counter-offer-badge {
  max-width: 100%;
  text-align: center;
}
.counter-offer-addition-decide > button {
  width: 100%;
  margin-top: 0;
}

/* --- Taxi counter-offer: read-only history summary (both roles) --- */
.taxi-counter-summary {
  border: 1px solid var(--border);
  border-left: 4px solid var(--orange, #f59e0b);
  border-radius: var(--radius-tile);
  padding: 0.7rem 0.9rem;
  margin: 0.6rem 0;
  background: var(--surface2);
  font-size: 0.88rem;
}
.taxi-counter-summary-accepted  { border-left-color: var(--accent, #0a7); }
.taxi-counter-summary-declined  { border-left-color: var(--danger, #e05252); }
.taxi-counter-summary-withdrawn { border-left-color: var(--text-muted, #888); }
.taxi-counter-summary-head {
  display: flex; justify-content: space-between; align-items: center;
  gap: 0.5rem; margin-bottom: 0.4rem; flex-wrap: wrap;
}
.taxi-counter-summary-status { font-weight: 700; font-size: 0.82rem; }
.taxi-counter-summary-row {
  display: flex; justify-content: space-between; gap: 0.75rem;
  padding: 0.15rem 0; border-top: 1px solid rgba(255,255,255,0.05);
}
.taxi-counter-summary-row:first-of-type { border-top: none; }
.taxi-counter-summary-row > span:first-child { opacity: 0.7; }
.taxi-counter-summary-row > span:last-child { text-align: right; }
.taxi-bd-row--total .taxi-bd-amt { color: var(--accent, #0a7); font-weight: 700; }
.taxi-bd-fuel-source {
  display: inline-block;
  font-size: 0.72rem;
  opacity: 0.6;
  background: rgba(255,255,255,0.06);
  border-radius: var(--radius-sm);
  padding: 0.05rem 0.3rem;
  margin-left: 0.25rem;
  vertical-align: middle;
}
.taxi-bd-divider {
  border: none;
  border-top: 1px dashed rgba(255,255,255,0.08);
  margin: 0.2rem 0;
}
/* opacity:.65 auf dem Element -> nicht berechenbarer Kontrast. Farbe dimmen. */
.taxi-quote-hint { margin: 0; color: var(--text-dim); font-style: italic; font-size: var(--text-md); }
/* Hinweis auf eine geschaetzte (statt gerouteten) Strecke. Farbe und Groesse
   kommen aus .field-hint/.field-hint-bad, die app.js mitgibt — hier fehlt nur
   der Abstand zum Block darueber. */
.taxi-estimate-note { margin: var(--space-2) 0 0; }
.taxi-quote-error { margin: 0; color: var(--danger-text); font-size: 0.75rem; }

/* Borrower bonus rows inside the quote box: subtle separator + bold grand total
   so the user instantly sees what they'll actually owe including the bonus. */
.taxi-quote-bonus-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 0.5rem;
  padding-top: 0.4rem;
  border-top: 1px solid rgba(255,255,255,0.07);
  font-size: 0.92rem;
}
.taxi-quote-bonus-row strong { color: var(--accent, #0a7); }
.taxi-quote-grandtotal {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 0.3rem;
  font-size: 1.05rem;
  font-weight: 700;
}
.taxi-quote-grandtotal strong { color: var(--accent, #0a7); font-size: 1.25rem; }
/* Mitfahrer + Reinigungspauschale (Rückmeldung #4): Stepper wie im Ratenplan
   (.inst-stepper, Touch-Ziel 44 px), Preis pro Person nur als stille Zusatzzeile. */
.taxi-pax-stepper { max-width: 12rem; }
/* wie #tv-consumption-unit: ein Textknoten direkt vor einem Flex-Element verliert sein Leerzeichen */
.taxi-pax-group label small { margin-left: 0.3em; }
/* UI2 (Personen platzsparend): „Personen“ steht nicht mehr allein in einer Zeile.
   - Anfrage: Stepper in einer Zeile mit der Abfahrtszeit (ab 541 px zweispaltig, Oberkanten bündig); auf
     schmalen Handys Label links / Stepper rechts in EINER 44-px-Zeile. Der Hilfetext erscheint nur noch, wenn das
     Fahrzeug eine Reinigungspauschale hat (ein Satz neben/unter dem Stepper statt einer eigenen Zeile).
   - Zusagen/Bearbeiten (.taxi-pax-group--inline): Label, Stepper und der kurze Hinweis in einer Zeile,
     der Hinweis bricht erst bei Platzmangel unter die Zeile. Touch-Ziele bleiben 44 px (.inst-step). */
#modal-taxi-request .taxi-time-pax-row { row-gap: 0; }
@media (min-width: 541px) {
  #modal-taxi-request .taxi-time-pax-row > .form-group:first-child { flex: 1 1 12rem; }
  #modal-taxi-request .taxi-time-pax-row > .taxi-pax-group { flex: 0 0 13.5rem; }
}
.taxi-pax-group--inline {
  display: flex; flex-wrap: wrap; align-items: center; column-gap: 0.9rem; row-gap: 0.4rem;
}
.taxi-pax-group--inline > label { margin: 0; flex: 0 1 auto; }
.taxi-pax-group--inline > .taxi-pax-stepper { flex: 0 0 9.5rem; width: 9.5rem; max-width: none; }
:is(#modal-taxi-pickup, #modal-taxi-edit) .taxi-pax-group--inline > .settings-help-text { margin: 0; flex: 1 1 14rem; }
@media (max-width: 540px) {
  #modal-taxi-request .taxi-pax-group {
    display: flex; flex-wrap: wrap; align-items: center; column-gap: 0.75rem; row-gap: 0.4rem;
  }
  /* display: block, sonst werden Text und „(du eingeschlossen)“ zu Flex-Items und „Personen“ bricht mitten im Wort */
  :is(.taxi-pax-group--inline, #modal-taxi-request .taxi-pax-group) > label { display: block; margin: 0; flex: 1 1 0; min-width: 5.5rem; overflow-wrap: normal; }
  #modal-taxi-request .taxi-pax-group > label small { display: block; margin-left: 0; }
  #modal-taxi-request .taxi-pax-group > .taxi-pax-stepper { flex: 0 0 9.5rem; width: 9.5rem; max-width: none; }
  :is(#modal-taxi-request, #modal-taxi-pickup, #modal-taxi-edit) .taxi-pax-group > .settings-help-text { margin: 0; flex: 1 1 100%; }
}
.inst-step[aria-disabled="true"] { color: var(--text-dim); cursor: default; }
html:not(.using-touch) .inst-step[aria-disabled="true"]:hover,
.inst-step[aria-disabled="true"]:active { background: transparent; }
.taxi-bd-row--perperson { color: var(--text-muted); }
.taxi-bd-row--perperson .taxi-bd-label small { font-size: var(--text-xs); }
.taxi-bd-row--perperson .taxi-bd-amt { font-weight: 600; font-variant-numeric: tabular-nums; }
.taxi-quote-perperson,
.taxi-perperson-line {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: 0.25rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.taxi-quote-perperson svg,
.taxi-perperson-line svg { flex: none; align-self: center; }
.taxi-quote-perperson small { font-size: var(--text-xs); }
.taxi-loan-paxline { flex: 0 0 100%; text-align: right; white-space: nowrap; font-size: 0.82rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.taxi-loan-paxline svg { vertical-align: -2px; }
@media (max-width: 480px) { .taxi-loan-paxline { text-align: left; white-space: normal; } }
.taxi-counter-price-perperson { color: var(--text-muted); font-size: var(--text-sm); }
.taxi-counter-price-perperson span:last-child { font-variant-numeric: tabular-nums; }
/* Taxi-Anfrage: the request prices only the rider's route — the driver's way
   to the pickup and back home comes with "Zusagen". Shown as an open, dashed
   part (map colours: start green, return red) and the price as "ab". */
.taxi-quote-from { font-size: 0.55em; font-weight: 600; color: var(--text-muted); margin-right: 0.15em; }
.taxi-quote-legs {
  display: grid;
  grid-template-columns: 1fr 1.6fr 1fr;
  gap: 0.35rem;
  margin: 0.7rem 0 0.2rem;
}
.taxi-quote-leg { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; font-size: var(--text-xs); color: var(--text-muted); text-align: center; }
.taxi-quote-leg > i { display: block; height: 4px; border-radius: 2px; background: var(--accent); }
.taxi-quote-leg.is-open > i {
  background: repeating-linear-gradient(90deg, var(--map-start) 0 0.32rem, transparent 0.32rem 0.55rem);
  opacity: 0.8;
}
.taxi-quote-leg.is-open.is-return > i {
  background: repeating-linear-gradient(90deg, var(--map-return) 0 0.32rem, transparent 0.32rem 0.55rem);
}
.taxi-quote-leg > span { line-height: 1.2; overflow-wrap: anywhere; }
.taxi-quote-leg > strong { color: var(--text); font-size: 0.85rem; }
.taxi-quote-leg.is-open > strong { color: var(--text-muted); }
.taxi-bd-row--pending { color: var(--text-muted); }
.taxi-bd-row--pending .taxi-bd-label small { display: block; font-size: var(--text-xs); }
.taxi-bd-row--pending .taxi-bd-amt {
  border: 1px dashed color-mix(in srgb, var(--text-muted) 60%, transparent);
  border-radius: var(--radius-xs, 6px);
  padding: 0 0.4rem;
  color: var(--text-muted);
}
.taxi-quote-final-note { margin: 0.35rem 0 0; font-size: var(--text-xs); color: var(--text-muted); line-height: 1.35; }

/* Conflict warning inside the quote box — yellow/amber tone so it reads as
   "soft heads-up", not "error". */
.taxi-quote-conflict {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  margin-top: 0.6rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius);
  background: rgba(255, 196, 0, 0.08);
  border: 1px solid rgba(255, 196, 0, 0.35);
}
.taxi-quote-conflict-icon { font-size: 1.1rem; line-height: 1.2; flex-shrink: 0; }
.taxi-quote-conflict-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.taxi-quote-conflict-body strong { font-size: 0.92rem; }
.taxi-quote-conflict-body small { font-size: 0.8rem; opacity: 0.85; line-height: 1.35; }

/* Bonus input row in the request modal: input + € unit aligned inline. */
.taxi-bonus-input-row {
  position: relative;
  display: flex;
  align-items: center;
}
.taxi-bonus-input-row input {
  flex: 1 1 auto;
  min-width: 0;
  /* Platz für das €-Zeichen im Feld */
  padding-right: 2.4rem;
  -moz-appearance: textfield;
}
.taxi-bonus-input-row input::-webkit-inner-spin-button,
.taxi-bonus-input-row input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
/* Das €-Zeichen sitzt im Feld (vorher daneben): das Feld war dadurch ~17 px
   schmaler als alle anderen Felder des Dialogs und endete nicht bündig. */
.taxi-bonus-unit {
  position: absolute;
  right: 0.9rem;
  top: 50%;
  transform: translateY(-50%);
  font-weight: 600;
  opacity: 0.75;
  pointer-events: none;
}

/* Star button in the request modal header — saves the current route as a
   favorite. Hollow ☆ when route is new, filled ★ when matching an existing
   favorite. Disabled state has reduced opacity to read as "not ready". */
/* Sitzt im Kopf neben dem ✕ (.modal-close): gleiche sichtbare Größe, Fläche und
   Rundung (vorher 38 px / 44 px auf Touch neben dem 36-px-✕); die 44-px-
   Tippfläche kommt wie dort über ::after. */
.btn-taxi-fav-save {
  position: relative;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: var(--radius);
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: var(--transition-ui);
}
@media (pointer: coarse) {
  .btn-taxi-fav-save::after { content: ""; position: absolute; inset: -5px; }
}
html:not(.using-touch) .btn-taxi-fav-save:hover:not(:disabled) { border-color: var(--accent, #0a7); background: rgba(255,255,255,0.12); }
.btn-taxi-fav-save:active:not(:disabled) { transform: scale(0.94); }
/* Knopf-Gruppe im Dialogkopf (Stern + ✕). Den Abstand zwischen den Knöpfen liefert
   allein der margin-left des ✕ — vorher kam ein inline `gap: 0.5rem` dazu (20 px). */
.modal-head-actions { display: flex; align-items: center; }
.taxi-fav-star { font-size: 1.1rem; line-height: 1; color: var(--accent, #0a7); }

/* Favoriten direkt unter dem Textfeld jedes leeren Stopps (.taxi-stop-aux).
   Das Stift-Menue nutzt das Panel des Eintrags-Menues (.entry-menu-panel). */
.taxi-stop-favs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.3rem;
}
.taxi-stop-favs-label {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-muted);
}
.taxi-fav-item {
  gap: 0;
  border: 1px solid var(--border, rgba(255,255,255,0.14));
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.03);
  transition: var(--transition-ui);
}
html:not(.using-touch) .taxi-fav-item:hover {
  background: rgba(255,255,255,0.07);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.taxi-fav-item.open { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
/* The favorite that is currently filled into this stop. */
.taxi-fav-item.is-selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.taxi-fav-item.is-selected .taxi-fav-chip { color: var(--accent); font-weight: 600; }
.taxi-fav-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.45rem 0.3rem 0.65rem;
  min-height: 1.9rem;
  border: 0;
  border-radius: var(--radius-xs) 0 0 var(--radius-xs);
  background: transparent;
  color: var(--text);
  font-size: 0.82rem;
  cursor: pointer;
}
/* Taxi audit: the visible chip is 30px high; an invisible expansion brings
   the tap target to 44px without changing the look. Upwards only as far as
   the gap to the address field (0.3rem), the rest below. */
.taxi-fav-chip { position: relative; }
.taxi-fav-chip::after { content: ""; position: absolute; inset: -0.3rem 0 -0.55rem; }
.taxi-fav-chip-name {
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.taxi-fav-chip-edit {
  position: relative;
  width: 1.8rem;
  align-self: stretch;
  display: grid;
  place-items: center;
  border: 0;
  border-left: 1px solid var(--border, rgba(255,255,255,0.1));
  border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  /* Was missing entirely — hover/press used to snap instantly while every
     sibling button (favorite chip, edit/delete icons elsewhere) fades. */
  transition: var(--transition-ui);
}
/* Touch-Flaeche auf 44px vergroessern, ohne die Optik zu veraendern. */
.taxi-fav-chip-edit::after { content: ""; position: absolute; inset: -0.5rem -0.55rem; }   /* 1.8rem + 2 × 0.55rem ≈ 44-px-Breite (vorher 0.4rem = 41 px) */
html:not(.using-touch) .taxi-fav-chip-edit:hover,
.taxi-fav-chip-edit:focus-visible,
.taxi-fav-item.open .taxi-fav-chip-edit { color: var(--text); background: rgba(255,255,255,0.06); }
/* Background press, not scale(): this segment is glued flush to the chip's
   edge (align-self:stretch), scaling it would visually detach it. Two rules,
   same reasoning as .entry-menu-item:active above — the compound
   html:not(.using-touch)…:hover rule otherwise outranks a plain :active on
   desktop, where the mouse is still "hovering" during the press. */
.taxi-fav-chip-edit:active { background: rgba(255,255,255,0.12); }
html:not(.using-touch) .taxi-fav-chip-edit:active { background: rgba(255,255,255,0.12); }
/* Runde 4 (Taxi): the name half of the split chip had no press state at all
   (only the pencil half did). Same background press as its sibling — scaling
   it would tear it away from the pencil segment. */
.taxi-fav-chip:active,
html:not(.using-touch) .taxi-fav-chip:active { --press-opacity: 1; background: rgba(255,255,255,0.12); }

/* Quick-tip chips row above the manual input. Wraps on mobile so 6 chips
   never get clipped or push the input out of the modal. */
/* Fixed grid instead of flex-wrap: 6 chips wrapped 5 + 1 on phones and the
   lone "+50 €" stretched over the full width. 3 × 2 on phones, 6 × 1 above. */
.taxi-bonus-chips {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}
@media (min-width: 480px) {
  .taxi-bonus-chips { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
.taxi-bonus-chip {
  min-width: 0;
  white-space: nowrap;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--border, rgba(255,255,255,0.14));
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.03);
  color: var(--text);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition: var(--transition-ui);
}
/* Maus: 36 px wie .quick-reply-chip (vorher 35,4 px aus dem Polster). Touch behält 44 px (s. o.). */
@media (pointer: fine) {
  .taxi-bonus-chip { min-height: 2.25rem; }
}
/* Guarded like the other chips: a bare :hover stuck on touch after a tap and
   left a green frame on a chip that was not the selected one. */
html:not(.using-touch) .taxi-bonus-chip:not(.is-active):hover {
  background: rgba(255,255,255,0.07);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.taxi-bonus-chip.is-active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: var(--accent, #0a7);
  color: var(--accent, #0a7);
}
/* Runde 4 (Taxi): bonus and "Zuletzt:" chips had no press state — the
   selection only appeared after the finger lifted. Same chip press as the
   quick-reply/filter chips (scale down fast, spring back); doubled for the
   mouse tier, whose :hover rule would otherwise outrank a bare :active.
   The selected look (.is-active / .is-selected) is untouched by the press. */
.taxi-bonus-chip:active,
html:not(.using-touch) .taxi-bonus-chip:active,
.taxi-recent-chip:active,
html:not(.using-touch) .taxi-recent-chip:active {
  --press-opacity: 1; /* own press state — keeps the generic press dim off */
  transform: scale(0.95);
  transition-duration: var(--motion-xs);
}
@media (prefers-reduced-motion: reduce) {
  .taxi-bonus-chip:active,
  html:not(.using-touch) .taxi-bonus-chip:active,
  .taxi-recent-chip:active,
  html:not(.using-touch) .taxi-recent-chip:active { transform: none; filter: brightness(1.25); }
}

/* Loan card taxi block */
.taxi-loan-block {
  border: 1px solid var(--border, rgba(255,255,255,0.12));
  border-radius: var(--radius-tile);
  padding: 0.65rem 0.85rem;
  margin: 0.6rem 0;
  background: rgba(255,255,255,0.03);
  min-width: 0;
  position: relative;
}
.taxi-loan-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 0.5rem; font-size: 0.88rem; margin-bottom: 0.25rem;
}
.taxi-loan-head-vehicle { display: flex; align-items: baseline; gap: 0.4rem; min-width: 0; flex: 1; }
.taxi-loan-fuel-label { font-size: 0.76rem; opacity: 0.65; font-weight: 400; }
.taxi-loan-head-right { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; gap: 0 0.4rem; flex: 0 1 auto; min-width: 0; }
.taxi-loan-price { font-weight: 700; color: var(--accent, #0a7); }
.taxi-loan-distance { opacity: 0.6; white-space: nowrap; font-size: 0.82rem; }
.taxi-loan-time { margin: 0.15rem 0 0.4rem; font-size: 0.82rem; color: var(--text-muted); }
/* Open ride request whose time has passed (taxiRideExpired in app.js). */
.taxi-ride-expired-notice {
  display: flex; align-items: flex-start; gap: 0.35rem;
  margin: 0 0 0.5rem; padding: 0.4rem 0.6rem; border-radius: var(--radius-xs);
  font-size: 0.82rem; line-height: 1.35;
  background: color-mix(in srgb, var(--warning) 18%, transparent); color: var(--badge-pending-text);
}
.taxi-ride-expired-notice svg { flex: none; margin-top: 0.1rem; }
.taxi-loan-route-preview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.35rem;
  margin: 0.35rem 0 0.2rem;
  padding: 0.42rem 0.5rem;
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.035);
  border: 1px solid rgba(255,255,255,0.07);
  font-size: var(--text-sm);
}
.taxi-loan-route-point {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.taxi-loan-route-arrow { color: var(--text-muted); }
.taxi-loan-route-count {
  color: var(--text-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
}
/* Phones: "5 Stopps inkl. Fahrer-Start/Rückfahrt" took the whole row and
   squeezed start and destination to "Star… → Rüc…". The count gets its own
   line under the route there. */
@media (max-width: 540px) {
  /* fit-content: the arrow follows the start label instead of sitting at the
     middle of the row behind a wide gap. */
  .taxi-loan-route-preview { grid-template-columns: fit-content(50%) auto minmax(0, 1fr); }
  .taxi-loan-route-count { grid-column: 1 / -1; white-space: normal; }
}
.taxi-loan-purpose { font-size: 0.84rem; opacity: 0.75; margin: 0.3rem 0 0; font-style: italic; }
.taxi-loan-block--action-required {
  border-left: 3px solid var(--warning);
  background: color-mix(in srgb, var(--warning) 4%, rgba(255,255,255,0.03));
}
.taxi-loan-block--waiting {
  border-left: 3px solid rgba(255,255,255,0.2);
}

/* Detail panel inside taxi-loan-block: stops list, lazy-init Leaflet map,
   price breakdown, and lender edit-button. */
.taxi-details { margin-top: 0.45rem; border-top: 1px solid var(--border, rgba(255,255,255,0.07)); padding-top: 0.3rem; }
.taxi-details > summary {
  cursor: pointer; font-size: 0.82rem; opacity: 0.72; padding: 0.2rem 0;
  list-style: revert; user-select: none;
}
.taxi-details > summary::marker { color: var(--text-muted); }
/* A11y R5 (WCAG 2.5.8): „Details & Karte anzeigen“ war 23 px hoch (Grenze
   24). Etwas mehr Polster; auf Touch zusätzlich eine unsichtbare Treffer-
   fläche auf ~44 px (App-Linie), ohne den Abstand zum Inhalt zu ändern —
   gleiches Muster wie .section-toggle::after. */
.taxi-details > summary { padding: 0.25rem 0; position: relative; }
@media (pointer: coarse) {
  .taxi-details > summary::after { content: ""; position: absolute; inset: -9px 0; }
}
.taxi-details[open] > summary { opacity: 0.9; }
/* Runde 4 (Taxi): press feedback for "Details & Karte" / "Route, Karte &
   Preis" — the muted row lights up under the finger (it had none; only the
   open/closed opacity changed after the tap). */
.taxi-details > summary:active { --press-opacity: 1; opacity: 1; }
.taxi-details-body { padding: 0.5rem 0 0.2rem; display: flex; flex-direction: column; gap: 0.7rem; }
.taxi-details-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.taxi-detail-pill {
  font-size: 0.78rem; padding: 0.1rem 0.45rem; border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
}
.taxi-detail-status {
  /* Rand wie die Pillen daneben (.taxi-detail-pill): sonst ist das Status-Badge 2 px niedriger. */
  border: 1px solid transparent;
  font-size: 0.72rem; padding: 0.1rem 0.5rem; border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.08); text-transform: uppercase; letter-spacing: 0.05em;
}
.taxi-detail-status-active { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent); }
.taxi-detail-status-pending { background: color-mix(in srgb, var(--warning) 22%, transparent); color: var(--badge-pending-text, var(--warning)); }
.taxi-detail-status-taxi-confirm-pending { background: rgba(154,180,255,0.22); color: var(--signal-date-color); }
.taxi-route-summary {
  display: grid;
  gap: 0.18rem;
  padding: 0.55rem 0.65rem;
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.taxi-route-summary strong { color: var(--text); font-size: var(--text-base); }
.taxi-route-surcharge { color: var(--warning); }
.taxi-detail-tabs {
  display: none;
  grid-template-columns: 1fr 1fr;
  gap: 0.3rem;
  padding: 0.2rem;
  border-radius: var(--radius-xs);
  background: rgba(255,255,255,0.045);
  border: 1px solid rgba(255,255,255,0.08);
}
.taxi-detail-tab {
  min-height: var(--touch-target);
  padding: 0.55rem 0.65rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font-weight: 700;
  cursor: pointer;
}
.taxi-detail-tab.is-active {
  background: color-mix(in srgb, var(--accent) 18%, rgba(255,255,255,0.05));
  color: var(--text);
}
/* Runde 4 (Taxi): Karte/Stopps segments had no press state. Brightening only
   (no scale inside the segmented bar); the selected segment stays selected. */
.taxi-detail-tab:active { --press-opacity: 1; background: rgba(255,255,255,0.08); color: var(--text); transition-duration: 0s; }
.taxi-detail-tab.is-active:active { background: color-mix(in srgb, var(--accent) 26%, rgba(255,255,255,0.08)); }
.taxi-detail-panel { display: block; }
.taxi-detail-map-wrap {
  position: relative;
  min-height: var(--taxi-detail-map-h, 300px);
  /* The map overlays (fallback / stop-sheet / info button) carry z-index 450-460
     to clear Leaflet's panes. Leaflet is already contained by `isolation` on
     .taxi-detail-map, so without a stacking context here those values leak into
     the loan-card and paint straight through the sticky action bar. */
  isolation: isolate;
}
.taxi-detail-stops {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: 0.85rem;
  display: flex;
  flex-direction: column;
}
.taxi-detail-stop {
  display: grid;
  grid-template-columns: 2rem 1fr;
  gap: 0.45rem;
  min-height: 48px;
  padding: 0.15rem 0 0.4rem;
  cursor: pointer;
  border-radius: var(--radius-xs);
}
html:not(.using-touch) .taxi-detail-stop:hover,
.taxi-detail-stop:focus-visible,
.taxi-detail-stop.is-active {
  background: rgba(255,255,255,0.045);
}
/* Runde 4 (Taxi): a stop row jumps the map to that stop — press state like the
   other tappable rows (background only, no transform in lists). The address
   text stays selectable (content). */
.taxi-detail-stop:active { --press-opacity: 1; background: rgba(255,255,255,0.09); }
/* A11y R5: Tastatur-Ziel der Zeile ist ein Knopf um den Stopp-Namen (die <li>
   bleibt Listeneintrag). Sieht aus wie der bisherige Text; der Fokusring
   umrahmt die ganze Zeile — vorher markierte nur eine 4,5-%-Aufhellung den
   Fokus. */
.taxi-detail-stop-btn {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: inherit;
  min-width: 0;
}
.taxi-detail-stop-btn:active { --press-opacity: 1; }
.taxi-detail-stop-btn:focus-visible { outline: none; }
.taxi-detail-stop:has(.taxi-detail-stop-btn:focus-visible) {
  background: rgba(255,255,255,0.045);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.taxi-detail-stop-rail {
  position: relative;
  display: flex;
  justify-content: center;
}
.taxi-detail-stop:not(:last-child) .taxi-detail-stop-rail::after {
  content: "";
  position: absolute;
  top: 1.45rem;
  bottom: -0.35rem;
  width: 2px;
  background: rgba(255,255,255,0.13);
}
.taxi-detail-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.45rem; height: 1.45rem;
  /* Gemessen mit #fff: 2.07 (--map-stop) / 2.56 (--map-start) / 3.21
     (--map-pickup) / 3.68 (--map-end) / 3.12:1 (--map-return). Mit --bg:
     7.08 / 7.50 / 5.97 / 5.20 / 6.15:1. */
  background: var(--accent, #0a7); color: var(--bg); border-radius: 50%; font-weight: 800; font-size: 0.75rem;
  position: relative; z-index: 1;
}
.taxi-detail-stop--start .taxi-detail-num { background: var(--map-start); }
.taxi-detail-stop--pickup .taxi-detail-num { background: var(--map-pickup); }
.taxi-detail-stop--stop .taxi-detail-num { background: var(--map-stop); }
.taxi-detail-stop--end .taxi-detail-num { background: var(--map-end); }
.taxi-detail-stop--return .taxi-detail-num { background: var(--map-return); }
.taxi-detail-stop-main {
  min-width: 0;
  display: grid;
  gap: 0.12rem;
}
.taxi-detail-stop-top {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--text-sm);
}
.taxi-detail-addr {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}
.taxi-detail-stop small {
  color: var(--text-muted);
  font-size: var(--text-xs);
}
.taxi-stop-warning-dot {
  display: inline-grid;
  place-items: center;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--warning) 18%, transparent);
  color: var(--warning);
  font-size: 0.72rem;
  font-weight: 900;
}
/* Detail map (in the loan card) is a bit taller than the compact request-form
   map so the route + stops are easier to read. Own variable so the request
   map keeps its height. */
.taxi-detail-map { height: var(--taxi-detail-map-h, 300px); min-height: var(--taxi-detail-map-h, 300px); border-radius: var(--radius-xs); overflow: hidden; border: 1px solid rgba(255,255,255,0.1); position: relative; z-index: 0; contain: paint; isolation: isolate; }

/* Taxi route fullscreen = a normal app modal (#modal-taxi-map). It reuses all
   the standard .modal-overlay / .modal-card chrome (correct iOS dvh viewport,
   sticky header, close button, ESC + backdrop dismiss). We only need the map to
   fill the modal body. */
.modal-card-map .modal-body-map {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 1 1 auto;
  min-height: 0;
  /* More breathing room below the map, and clear of the iOS home indicator. */
  padding-bottom: max(env(safe-area-inset-bottom, 0px), 1.25rem);
}
/* Taxi-R2 (QA M15): "Vollbild" really is full screen — it opened a sheet of
   ~70 % height. Edge to edge at 100dvh on every size; the header and its
   close button stay, the drag handle still swipes it away. */
#modal-taxi-map.modal-overlay {
  padding: 0;
  align-items: stretch;
  justify-content: stretch;
}
#modal-taxi-map .modal-card.modal-card-map {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: 100dvh;
  border-radius: 0;
  border: none;
  padding-top: env(safe-area-inset-top, 0px);
  padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
  padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
}
/* Header and drag handle bled out with the generic -1.25rem/-2rem while this
   card only pads 0.75rem: both stuck out 2–20px on the right, and the card
   could be pushed sideways. Bleed exactly by this card's own padding. */
#modal-taxi-map .modal-card-map > .modal-sticky-header,
#modal-taxi-map .modal-card-map .modal-drag-handle {
  width: auto;
  margin-left: calc(-1 * max(0.75rem, env(safe-area-inset-left, 0px)));
  margin-right: calc(-1 * max(0.75rem, env(safe-area-inset-right, 0px)));
  padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
  padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
}
#modal-taxi-map .modal-body-map { padding-bottom: max(env(safe-area-inset-bottom, 0px), 0.75rem); }
#modal-taxi-map .taxi-modal-map { min-height: 0; }
.taxi-modal-map {
  flex: 1 1 auto;
  min-height: 55dvh;   /* tall by default; grows to fill the wide modal card */
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid rgba(255,255,255,0.1);
  z-index: 0;
}

/* ==========================================================================
   Taxi: genauer Punkt auf der Karte („Auf Karte wählen“)
   #modal-taxi-map dient auch der Punktwahl (data-mode="pick", app.js
   openTaxiPointPicker): im Routenmodus gilt nur .taxi-route-only, in der
   Punktwahl nur .taxi-point-only. Der Pin ist ein ziehbarer Leaflet-Marker.
   ========================================================================== */
#modal-taxi-map:not([data-mode="pick"]) .taxi-point-only { display: none !important; }
#modal-taxi-map[data-mode="pick"] .taxi-route-only { display: none !important; }
#modal-taxi-map[data-mode="pick"] .modal-body-map {
  gap: 0.55rem;
  overscroll-behavior: contain;
}
/* The map keeps a usable height even on a short phone (header + search +
   panel + buttons already take ~350px); the body scrolls below that. */
#modal-taxi-map[data-mode="pick"] .taxi-modal-map {
  flex: 1 1 200px;
  min-height: 180px;
  cursor: crosshair;
}
#modal-taxi-map[data-mode="pick"] .taxi-modal-map .leaflet-marker-draggable { cursor: grab; }
#modal-taxi-map[data-mode="pick"] .taxi-modal-map .leaflet-marker-draggable.is-dragging { cursor: grabbing; }
/* The card pads 0.75rem (not the generic 1.25-2rem): the footer bleeds by exactly that, like the header. */
#modal-taxi-map[data-mode="pick"] .modal-card-map > .modal-actions {
  margin-left: calc(-1 * max(0.75rem, env(safe-area-inset-left, 0px)));
  margin-right: calc(-1 * max(0.75rem, env(safe-area-inset-right, 0px)));
  padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
  padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
  padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
}

/* Keyboard open (iOS/Android): the sheet ends above it, the map gives way. */
/* The overlay itself must shrink too: the card's height floor (min-height: min(Npx, 100%),
   set by the first touch) is a percentage of the overlay and this overlay has no padding,
   so without it the floor kept the sheet full height behind the keyboard. */
html.modal-kb-open #modal-taxi-map[data-mode="pick"].modal-overlay { padding-bottom: var(--modal-kb-inset); }
html.modal-kb-open #modal-taxi-map[data-mode="pick"] .modal-card.modal-card-map {
  height: calc(100dvh - var(--modal-kb-inset));
  max-height: calc(100dvh - var(--modal-kb-inset));
}
html.modal-kb-open #modal-taxi-map[data-mode="pick"] .taxi-modal-map { min-height: 110px; }
/* Landscape phone (e.g. 844x390): map on the left, search + fields on the
   right — stacked there the map would be pushed out of the 390px height. */
@media (orientation: landscape) and (max-height: 500px) {
  #modal-taxi-map[data-mode="pick"] .taxi-point-only#taxi-point-kicker { display: none !important; }
  #modal-taxi-map[data-mode="pick"] .modal-body-map {
    display: grid;
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr);
    column-gap: 0.75rem; row-gap: 0.4rem;
    overflow: hidden;
    padding-bottom: 0.25rem;
  }
  #modal-taxi-map[data-mode="pick"] .taxi-modal-map { grid-column: 1; grid-row: 1 / -1; min-height: 0; height: 100%; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-search { grid-column: 2; grid-row: 1; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-banner { grid-column: 2; grid-row: 2; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-panel { grid-column: 2; grid-row: 3; overflow-y: auto; min-height: 0; overscroll-behavior: contain; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-status { min-height: 0; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-fields { grid-template-columns: minmax(0, 1fr); }
  #modal-taxi-map[data-mode="pick"] .modal-actions { flex-direction: row; padding-top: 0.4rem; padding-bottom: max(0.45rem, env(safe-area-inset-bottom, 0px)); }
  #modal-taxi-map[data-mode="pick"] .modal-actions > button { flex: 1 1 0; }
}

/* UI2: Punktwahl auf Desktop und Tablet quer (>= 900 px breit, >= 560 px hoch) als zentriertes, großes Modal statt
   Vollbild: bis 1080 x min(85dvh, 800px), gerundet wie die anderen Dialoge, Hintergrund bleibt sichtbar, Klick daneben
   schließt (mit „Verwerfen?“-Rückfrage). Innen: Karte links, Suche + Felder rechts (wie im Handy-Querformat), die
   Fußleiste bleibt unten. Handy (Bottom-Sheet), Handy quer (<= 500 px hoch) und der Routenmodus bleiben unverändert.
   Die Karte füllt die linke Spalte; app.js _initTaxiPointMap hält ihre Größe per ResizeObserver aktuell. */
@media (min-width: 900px) and (min-height: 560px) {
  #modal-taxi-map[data-mode="pick"].modal-overlay,
  html.modal-kb-open #modal-taxi-map[data-mode="pick"].modal-overlay {
    padding: 1rem;
    padding-top: calc(1rem + env(safe-area-inset-top, 0px));
    padding-bottom: calc(1rem + var(--modal-kb-inset));
    align-items: center;
    justify-content: center;
  }
  #modal-taxi-map[data-mode="pick"] .modal-card.modal-card-map,
  html.modal-kb-open #modal-taxi-map[data-mode="pick"] .modal-card.modal-card-map {
    width: 100%;
    max-width: 1080px;
    height: min(85dvh, 800px, calc(100dvh - var(--modal-kb-inset) - 2rem));
    max-height: min(85dvh, 800px, calc(100dvh - var(--modal-kb-inset) - 2rem));
    border: 1px solid rgba(255,255,255,0.09);
    border-top: 4px solid color-mix(in srgb, var(--accent) 92%, transparent);
    border-radius: var(--radius-modal);
    padding-top: 0;
    padding-left: 1.25rem;
    padding-right: 1.25rem;
    overflow: hidden;
  }
  #modal-taxi-map[data-mode="pick"] .modal-card-map > .modal-sticky-header {
    margin-left: -1.25rem; margin-right: -1.25rem;
    padding-left: 1.25rem; padding-right: 1.25rem;
  }
  #modal-taxi-map[data-mode="pick"] .modal-card-map > .modal-actions {
    margin-left: -1.25rem; margin-right: -1.25rem;
    padding: 0.75rem 1.25rem max(0.9rem, env(safe-area-inset-bottom, 0px));
  }
  #modal-taxi-map[data-mode="pick"] .modal-body-map {
    display: grid;
    grid-template-columns: minmax(0, 1.7fr) minmax(300px, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr);
    column-gap: 1rem; row-gap: 0.5rem;
    overflow: hidden;
    padding-bottom: 0.5rem;
  }
  #modal-taxi-map[data-mode="pick"] .taxi-modal-map { grid-column: 1; grid-row: 1 / -1; min-height: 0; height: 100%; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-search { grid-column: 2; grid-row: 1; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-banner { grid-column: 2; grid-row: 2; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-panel { grid-column: 2; grid-row: 3; overflow-y: auto; min-height: 0; overscroll-behavior: contain; }
  #modal-taxi-map[data-mode="pick"] .taxi-point-fields { grid-template-columns: minmax(0, 1fr); }
}

/* Short phone (320x568, 375x667): no decorative kicker, a one-line title and
   the two buttons side by side — like the request sheet — so the fields keep
   their room under the map. Touch targets stay 44px. */
@media (max-width: 540px) and (max-height: 760px) {
  #modal-taxi-map[data-mode="pick"] .modal-sticky-header .modal-kicker { display: none !important; }
  #modal-taxi-map[data-mode="pick"] .history-modal-header { padding-bottom: 0.5rem; }
  #modal-taxi-map[data-mode="pick"] .history-modal-header h2 { font-size: 1.1rem; line-height: 1.25; }
  #modal-taxi-map[data-mode="pick"] .modal-card-map > .modal-actions { flex-direction: row; padding-top: 0.55rem; }
  #modal-taxi-map[data-mode="pick"] .modal-card-map > .modal-actions > button { flex: 1 1 0; padding-left: 0.5rem; padding-right: 0.5rem; }
  #modal-taxi-map[data-mode="pick"] .modal-drag-handle { margin-bottom: 0.2rem; }
}
/* Search above the map */
.taxi-point-search { position: relative; flex: 0 0 auto; }
.taxi-point-search input {
  width: 100%; min-height: var(--touch-target); font-size: 16px; padding: 0.5rem 0.7rem;
  background: rgba(255,255,255,0.05); border: 1px solid var(--border-input, rgba(255,255,255,0.1));
  border-radius: var(--radius-xs); color: inherit;
}
.taxi-point-search .taxi-stop-results { max-height: 40dvh; }
.taxi-point-search-empty { color: var(--text-muted); font-style: italic; cursor: default !important; }
html:not(.using-touch) .taxi-point-search-empty:hover { background: transparent !important; }

/* Notes about the map itself (tiles not loading, point far from the route) */
.taxi-point-banner {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.2rem;
  padding: 0.45rem 0.7rem; border-radius: var(--radius-xs);
  font-size: var(--text-sm); line-height: 1.35;
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
  color: var(--text);
}

/* Panel under the map */
.taxi-point-panel { flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.4rem; }
/* In a height-capped (scrolling) panel the children must keep their own height instead of shrinking and overlapping the fields (landscape phone). */
.taxi-point-panel > * { flex-shrink: 0; }
.taxi-point-status {
  font-size: var(--text-sm); line-height: 1.35; color: var(--text-muted);
  /* Two lines reserved: the suggestion arriving must not move the fields. */
  min-height: 2.7em; margin: 0;
}
.taxi-point-status.is-busy { color: var(--text-dim); }
.taxi-point-status.is-warn { color: var(--badge-pending-text); }
.taxi-point-status.is-error { color: var(--danger-text); }
.taxi-point-status strong { color: var(--text); }
.taxi-point-fields { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.45rem; }
.taxi-point-fields .form-group, .taxi-point-coords-grid .form-group { margin: 0; }
.taxi-point-fields label, .taxi-point-coords-grid label { font-size: var(--text-sm); margin-bottom: 0.2rem; display: block; }
.taxi-point-opt { color: var(--text-dim); font-weight: 400; }
.taxi-point-fields input, .taxi-point-coords-grid input { width: 100%; min-height: var(--touch-target); font-size: 16px; }
@media (min-width: 640px) {
  .taxi-point-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Short phones: the labels become placeholders' job, the fields stay 44px. */
@media (max-height: 700px) {
  .taxi-point-fields label { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .taxi-point-status { min-height: 2.7em; }
}
.taxi-point-coords > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
  min-height: var(--touch-target); cursor: pointer; list-style: none;
  font-size: var(--text-sm); color: var(--text-muted);
}
.taxi-point-coords > summary::-webkit-details-marker { display: none; }
.taxi-point-coords-edit { color: var(--accent); font-weight: 600; white-space: nowrap; }
.taxi-point-coords > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-xs); }
.taxi-point-coords-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.45rem; padding-bottom: 0.3rem; }
.taxi-point-privacy { font-size: var(--text-xs); line-height: 1.4; color: var(--text-dim); margin: 0; }

/* Pin */
/* Dark ground while tiles load or fail (Leaflet's default is light grey). */
#modal-taxi-map[data-mode="pick"] .leaflet-container { background: #26343a; }
/* The pin is dragged by finger: no text selection, no iOS long-press callout,
   no browser gesture on it (Leaflet's Draggable owns the touch). */
.taxi-point-pin-wrap {
  background: none; border: 0;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: none;
}
.taxi-point-pin {
  position: relative; width: 36px; height: 46px;
  transform-origin: 50% 94%;
  filter: drop-shadow(0 3px 3px rgba(0,0,0,0.45));
  transition: transform var(--motion-fast) var(--ease-spring);
}
/* A fat invisible hit area: the pin is small, fingers are not. */
.taxi-point-pin::after { content: ''; position: absolute; inset: -10px; }
.taxi-point-pin svg { display: block; overflow: visible; }
.taxi-point-pin-body { fill: var(--accent-strong); stroke: #fff; stroke-width: 1.8; stroke-linejoin: round; }
.taxi-point-pin-dot { fill: #fff; }
.taxi-point-pin-shadow { fill: rgba(0,0,0,0.35); opacity: 0.6; transition: opacity var(--motion-fast) var(--ease-standard); }
.taxi-point-pin-num {
  position: absolute; left: 0; top: 10px; width: 36px; text-align: center;
  font: 700 9px/12px system-ui, sans-serif; color: #12251c; pointer-events: none;
}
/* Lifted while dragged: the finger covers the tip, the lifted pin and its
   growing shadow show where it will land. */
.leaflet-marker-icon.is-dragging .taxi-point-pin { transform: translateY(-12px) scale(1.12); }
.leaflet-marker-icon.is-dragging .taxi-point-pin-shadow { opacity: 1; }
.leaflet-marker-icon.is-drop .taxi-point-pin { animation: taxiPinDrop 0.42s var(--ease-spring); }
@keyframes taxiPinDrop {
  0% { transform: translateY(-18px) scale(1.08); }
  60% { transform: translateY(1px) scale(0.97, 1.03); }
  100% { transform: translateY(0) scale(1); }
}
.leaflet-marker-icon.taxi-point-pin-wrap:focus-visible .taxi-point-pin {
  outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--radius-tile);
}
/* The other stops of the route: smaller, calmer than the active pin. */
.taxi-number-marker.taxi-point-other { transform: scale(0.82); opacity: 0.85; pointer-events: none; }

/* Buttons on the map (re-uses .taxi-map-ctl) */
.taxi-point-controls { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.55rem 0.55rem 0 0 !important; border: 0 !important; background: none !important; }
.taxi-point-controls .taxi-map-ctl { color: var(--text); }
.taxi-point-locate.is-busy svg { animation: taxiPointBusy 1s var(--ease-standard) infinite; }
@keyframes taxiPointBusy { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) {
  .taxi-point-pin, .taxi-point-pin-shadow { transition: none; }
  .leaflet-marker-icon.is-drop .taxi-point-pin { animation: none; }
  .taxi-point-locate.is-busy svg { animation: none; opacity: 0.5; }
}

/* "Auf Karte wählen" next to every place field, and the "genauer Punkt" note */
.taxi-field-row { display: flex; align-items: stretch; gap: 0.4rem; }
.taxi-field-row > input { flex: 1 1 auto; min-width: 0; }
.taxi-point-btn {
  flex: 0 0 var(--touch-target); width: var(--touch-target); min-height: var(--touch-target);
  display: inline-grid; place-items: center; padding: 0;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-input, rgba(255,255,255,0.1));
  background: rgba(255,255,255,0.05);
  color: var(--accent);
  cursor: pointer;
  transition: background var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard), transform var(--motion-fast) var(--ease-standard);
}
html:not(.using-touch) .taxi-point-btn:hover { background: var(--accent-soft); border-color: var(--accent); }
.taxi-point-btn:active, html:not(.using-touch) .taxi-point-btn:active { --press-opacity: 1; transform: scale(0.94); }
.taxi-point-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* prefers-reduced-motion: Stern im Kopf, „Auf Karte wählen“ und der Stopp-Stern sanken
   beim Druck trotzdem ein (die Chips und .modal-close hatten die Ausnahme schon). */
@media (prefers-reduced-motion: reduce) {
  .btn-taxi-fav-save:active:not(:disabled),
  .taxi-point-btn:active,
  html:not(.using-touch) .taxi-point-btn:active,
  .taxi-stop-fav-save:active { transform: none; }
}
/* An exact point is read-only: tap it to move the point on the map. */
input.is-point[readonly] {
  cursor: pointer;
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border-input, #6f6f6f));
  text-overflow: ellipsis;
}
.taxi-point-note {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  font-size: var(--text-xs); line-height: 1.3; color: var(--accent); min-width: 0;
}
.taxi-point-note-text { display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0; }
.taxi-point-note-text > span { overflow-wrap: anywhere; }
.taxi-point-note-coords { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.taxi-point-note-btn {
  flex: 0 0 auto; min-height: var(--touch-target); margin: -0.7rem -0.4rem -0.7rem 0; padding: 0 0.4rem;
  background: none; border: 0; color: var(--accent); font: inherit; font-weight: 600; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.taxi-point-note-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px; }
.taxi-point-note-btn:active { --press-opacity: 1; opacity: 0.7; }
.taxi-point-chip {
  display: inline-flex; align-items: center; gap: 0.2rem;
  font-size: var(--text-xs); font-weight: 600; color: var(--accent); white-space: nowrap;
}

/* Pending taxi-addition card (Ausstehend tab): der Kopf („bei …“, Titel, Badges „Ausstehend“ und
   „Taxi-Gegenangebot erhalten“) steht linksbündig wie in jeder anderen Karte (.loan-card-heading).
   Bis B3 war er bewusst zentriert; das fiel in der Liste neben den linksbündigen Karten auf und
   entfällt auf Wunsch. Nur das Gegenangebot-Abzeichen behält seine Textbreite (in der Spalte würde es sich sonst über die ganze Kartenbreite strecken). */
.taxi-addition-pending-card .loan-card-heading > .counter-offer-badge { align-self: flex-start; }

/* Decision-request cards (Ausstehend tab): close / extend / taxi-cancel
   requests on an ALREADY ACTIVE loan. Visually a pending card like the others,
   but with a left accent so it reads as "follow-up on something running"
   rather than a brand-new application sitting next to it in the same list. */
.decision-request-card {
  border-left: 3px solid var(--warning);
}
.decision-request-card .loan-amount {
  font-size: 0.95rem;
}
/* Free-text messages can be long; the shared detail row is a baseline-aligned
   space-between flex, which would otherwise push them off the right edge. */
.decision-request-card .pending-addition-detail > span:last-child {
  text-align: right;
  overflow-wrap: anywhere;
  -webkit-hyphens: auto;
  hyphens: auto;
}
/* Direction arrows drawn along route segments (rotated inline in travel dir). */
.taxi-dir-arrow-wrap { background: none; border: 0; }
.taxi-route-leg-arrow { display: inline-flex; align-items: center; margin-right: 0.3rem; opacity: 0.85; font-style: normal; }
.taxi-dir-arrow {
  display: inline-flex;
  line-height: 1;
  /* Dark halo so the coloured arrow stays visible over any map tile.
     War text-shadow — der Pfeil ist jetzt ein Inline-SVG (ICONS.navArrow)
     und ein SVG nimmt text-shadow nicht an. Die Rotation setzt app.js
     weiterhin inline auf dem <span>, nicht auf dem SVG. */
  filter: drop-shadow(0 0 1.5px rgba(0,0,0,0.9)) drop-shadow(0 0 1.5px rgba(0,0,0,0.9));
  pointer-events: none;
}
.taxi-map-fallback,
.taxi-map-warning {
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 24%, transparent);
  color: var(--warning);
  font-size: var(--text-sm);
}
.taxi-map-fallback {
  position: absolute;
  left: 0.55rem;
  right: 0.55rem;
  z-index: 450;
  /* Over the map the 12% tint was see-through: the warning text sat
     illegibly on top of the tiles (and was cut through by the attribution).
     Opaque dark base like the stop sheet next to it, tint kept. Lifted above
     the attribution line. */
  bottom: 1.6rem;
  background: color-mix(in srgb, var(--warning) 14%, rgba(24,24,24,0.94));
  box-shadow: var(--shadow-card);
}
/* Estimate mode (no road route by design): neutral note instead of the
   yellow "could not be loaded" warning. */
.taxi-map-fallback--estimate {
  background: rgba(24,24,24,0.9);
  border-color: rgba(255,255,255,0.14);
  color: var(--text-muted);
}
.taxi-map-stop-sheet {
  position: absolute;
  left: 0.55rem;
  right: 0.55rem;
  bottom: 0.55rem;
  z-index: 460;
  display: grid;
  gap: 0.12rem;
  padding: 0.65rem 2.2rem 0.65rem 0.75rem;
  border-radius: var(--radius);
  background: rgba(24,24,24,0.94);
  border: 1px solid rgba(255,255,255,0.14);
  box-shadow: var(--shadow-card);
  font-size: var(--text-sm);
}
.taxi-map-stop-sheet span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
}
.taxi-map-info-btn {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  z-index: 455;
  width: var(--touch-target);
  height: var(--touch-target);
  border-radius: var(--radius);
  border: 1px solid rgba(255,255,255,0.24);
  background: rgba(24,24,24,0.82);
  color: var(--text);
  font-weight: 800;
  font-size: var(--text-sm);
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(0,0,0,0.28);
}
.taxi-map-info-popover {
  position: absolute;
  top: 2.5rem;
  right: 0.55rem;
  z-index: 456;
  max-width: min(260px, calc(100% - 1.1rem));
  padding: 0.55rem 0.65rem;
  border-radius: var(--radius-xs);
  background: rgba(24,24,24,0.94);
  border: 1px solid rgba(255,255,255,0.14);
  color: var(--text-muted);
  box-shadow: var(--shadow-card);
  font-size: var(--text-xs);
  line-height: 1.35;
}
/* R13-Anim: der Hinweis-Popover sprang hart auf — jetzt wie .entry-menu-panel
   aus der Ecke des Ausloesers (oben rechts). Schliessen: app.js (WAAPI). */
@keyframes mapInfoPopIn {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: none; }
}
.taxi-map-info-popover:not(.hidden) {
  transform-origin: top right;
  animation: mapInfoPopIn var(--motion-fast) var(--ease-standard);
}
@media (prefers-reduced-motion: reduce) {
  .taxi-map-info-popover:not(.hidden) { animation: none; }
}
.taxi-route-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem 0.7rem;
  margin-top: 0.1rem;
  color: var(--text-muted);
  font-size: var(--text-xs);
}
.taxi-route-legend span {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
.taxi-route-leg {
  width: 1.25rem;
  height: 0.22rem;
  border-radius: 999px;
  display: inline-block;
}
.taxi-route-leg--approach { background: var(--map-start); }
.taxi-route-leg--passenger { background: var(--signal-date-color); }
.taxi-route-leg--return { background: repeating-linear-gradient(90deg, var(--map-return) 0 0.32rem, transparent 0.32rem 0.52rem); border: 1px solid color-mix(in srgb, var(--map-return) 70%, transparent); }
/* Taxi-R2 (QA N25): pin key for the accept map (same colours as the markers). */
.taxi-route-dot {
  width: 0.7rem; height: 0.7rem; border-radius: 50%; display: inline-block;
  box-shadow: 0 0 0 1.5px rgba(0,0,0,0.35);
}
.taxi-route-dot--start { background: var(--map-start); }
.taxi-route-dot--pickup { background: var(--map-pickup); }
.taxi-route-dot--end { background: var(--map-end); }
.taxi-route-dot--return { background: var(--map-return); }
.taxi-route-legend--pins { margin: 0.35rem 0 0.6rem; }

/* Taxi-R2 (QA M15): map controls float on the map (top right), one style,
   icon-only with a 44px target — replaces three full-width buttons in three
   different styles under the map. Leaflet's own +/- sit top left. */
.taxi-map-controls {
  position: absolute; top: 0.55rem; right: 0.55rem; z-index: 455;
  display: flex; flex-direction: column; gap: 0.4rem;
}
.taxi-map-ctl,
.taxi-map-controls .taxi-map-info-btn {
  position: static;
  width: var(--touch-target); height: var(--touch-target);
  display: inline-grid; place-items: center;
  padding: 0; margin: 0;
  border-radius: var(--radius);
  border: 1px solid rgba(255,255,255,0.2);
  background: rgba(24,24,24,0.86);
  color: var(--text);
  box-shadow: 0 3px 10px rgba(0,0,0,0.3);
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: background var(--motion-fast) var(--ease-standard), transform var(--motion-fast) var(--ease-standard);
}
html:not(.using-touch) .taxi-map-ctl:hover { background: rgba(44,44,44,0.94); }
.taxi-map-ctl:active,
html:not(.using-touch) .taxi-map-ctl:active,
/* Runde 4 (Taxi): the "i" in the same stack had the look but not the press. */
.taxi-map-controls .taxi-map-info-btn:active { --press-opacity: 1; transform: scale(0.94); }
.taxi-map-ctl:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.taxi-map-controls .taxi-map-info-popover { top: 0; right: calc(var(--touch-target) + 0.45rem); }
.taxi-map-controls:has(.taxi-map-info-popover) { align-items: flex-end; }
/* "Route öffnen": one clear external link under the map. */
.taxi-map-foot { display: flex; justify-content: flex-end; margin-top: 0.4rem; }
.taxi-route-open-link {
  display: inline-flex; align-items: center; gap: 0.4rem;
  min-height: var(--touch-target);
  padding: 0.45rem 0.8rem;
  border-radius: var(--radius-pill);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, rgba(255,255,255,0.1));
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent-text, var(--accent));
  font-size: var(--text-sm); font-weight: 600;
  text-decoration: none;
}
html:not(.using-touch) .taxi-route-open-link:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.taxi-route-open-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Runde 4 (Taxi/Native): the external map links look and act like buttons —
   a press state, and a long-press no longer selects the label or opens the
   iOS link preview/callout ("Navigation" in the ride panel is a .btn-secondary
   link and has the button press already). */
.taxi-route-open-link:active,
html:not(.using-touch) .taxi-route-open-link:active {
  --press-opacity: 1;
  background: color-mix(in srgb, var(--accent) 24%, transparent);
  transition-duration: 0s;
}
.taxi-route-open-link,
.taxi-ride-panel-nav {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* Leaflet's own +/− and credit link (vendor/leaflet/leaflet.css):
   - `.leaflet-container a` switches the tap highlight back ON in Leaflet blue
     rgba(51,181,229,.4) — a blue box flashed over +/− on every tap;
   - `.leaflet-bar a:hover, a:focus` (#f4f4f4) is not touch-gated: after a tap
     the button stayed grey (sticky hover + the focused <a href="#">);
   - there was no press state, and a long-press opened the iOS link callout.
   Scoped to the taxi maps. */
:is(.taxi-map, .taxi-detail-map, .taxi-modal-map) a { -webkit-tap-highlight-color: transparent; }
:is(.taxi-map, .taxi-detail-map, .taxi-modal-map) .leaflet-bar a { -webkit-touch-callout: none; }
html.using-touch :is(.taxi-map, .taxi-detail-map, .taxi-modal-map) .leaflet-bar a:not(.leaflet-disabled):is(:hover, :focus):not(:focus-visible, :active) {
  background-color: #fff;
}
/* Runde 6 (Knöpfe): dasselbe mit der Maus — nach dem Klick blieb +/− grau
   stehen, bis woanders hingeklickt wurde (der Fokus des <a href="#">). Hover
   bleibt, nur der reine Maus-Fokus markiert nicht mehr. */
:is(.taxi-map, .taxi-detail-map, .taxi-modal-map) .leaflet-bar a:not(.leaflet-disabled):focus:not(:focus-visible, :hover, :active) {
  background-color: #fff;
}
:is(.taxi-map, .taxi-detail-map, .taxi-modal-map) .leaflet-bar a:not(.leaflet-disabled):active {
  --press-opacity: 1;
  background-color: #e2e2e2;
}
/* Touch: +/− der Vollbild-Karte (Kartenpunkt wählen) 44 px statt Leaflets 30 px — die übrigen Knöpfe
   dort (.taxi-map-ctl: Standort, Zentrieren) sind schon 44 px. Die kleine Vorschaukarte im Dialog bleibt. */
@media (pointer: coarse) {
  .taxi-modal-map.leaflet-touch .leaflet-bar a { width: 44px; height: 44px; line-height: 44px; }
}
/* B4: Auch die kleinen Vorschaukarten (Taxi-Anfrage/-Abholung 180 px, Saldo-Fahrt 160 px, Leihkarte) bekommen
   44-px-Zoomknöpfe – nebeneinander statt untereinander, damit die Leiste (88 × 44 px) die Kartenhöhe nicht
   auffrisst; ein Stapel aus zwei 44-px-Knöpfen wäre mehr als die halbe Karte. Nur mit grobem Zeiger (Touch). */
@media (pointer: coarse) {
  :is(.taxi-map, .sd-map, .taxi-detail-map).leaflet-touch .leaflet-control-zoom { display: flex; }
  :is(.taxi-map, .sd-map, .taxi-detail-map).leaflet-touch .leaflet-bar a { width: 44px; height: 44px; line-height: 44px; }
  :is(.taxi-map, .sd-map, .taxi-detail-map).leaflet-touch .leaflet-bar a.leaflet-control-zoom-in { border-bottom: 0; border-right: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius: 4px 0 0 4px; }
  :is(.taxi-map, .sd-map, .taxi-detail-map).leaflet-touch .leaflet-bar a.leaflet-control-zoom-out { border-radius: 0 4px 4px 0; }
}
@media (max-width: 480px) {
  .taxi-map-foot { justify-content: stretch; }
  .taxi-route-open-link { flex: 1; justify-content: center; }
}

/* Taxi-R2 (QA N22): hairline seams between map tiles. The tile pane sits at
   fractional offsets (fitBounds/pan), which on 2x/2.625x/3x screens puts tile
   edges on fractional device pixels; the anti-aliased edges then let the
   grey container through. Tiles now overlap by half a pixel so the edges
   cover each other. Leaflet's own Chromium workaround (mix-blend-mode:
   plus-lighter) must be switched off with it: additive blending doubles the
   overlap strip — that is where the round-1 attempt drew its own bright lines,
   at 1x too. Measured on the route map (edge row vs. neighbours, luminance):
   Leaflet default 21-35 at 2x-3x (clean at 1x), overlap + plus-lighter 31-45
   at every density, this rule <= 7.5 everywhere (= normal map texture). */
:is(.taxi-map, .taxi-detail-map, .taxi-modal-map) .leaflet-tile-container img.leaflet-tile {
  mix-blend-mode: normal;
  width: 256.5px !important;
  height: 256.5px !important;
}
.taxi-detail-breakdown {
  list-style: none;
  padding: 0.55rem 0 0;
  margin: 0.15rem 0 0.4rem;
  border-top: 1px solid rgba(255,255,255,0.08);
  font-size: 0.85rem;
}
.taxi-detail-breakdown .taxi-bd-row { font-size: 0.85rem; }
.taxi-details-actions { display: flex; gap: 0.4rem; justify-content: flex-end; flex-wrap: wrap; }
/* The buttons in here are .btn-secondary/.btn-danger/.btn-primary, which carry
   `flex: 1` / `width: 100%` of their own — that overrode the right-alignment
   above and stretched a single "Fahrt absagen" across the full card (1053px on
   desktop). Size them to their content instead, so justify-content can do its
   job; they still wrap to their own row when the range runs out. */
.taxi-details-actions > button { flex: 0 1 auto; width: auto; margin-top: 0; }
/* Phones: right-aligned content-width buttons wrapped into a ragged stack
   ("Ich bin am Abholpunkt" alone at the right, the rest below). Every row now
   fills the card width, like the other loan-card action rows. */
@media (max-width: 540px) {
  .taxi-details-actions > button,
  .taxi-details-actions > .taxi-arrived-badge { flex: 1 1 auto; justify-content: center; }
}

/* "Ich bin da" — primary state is a green button, post-click flips to a
   subdued badge so the lender knows it was sent (and we don't double-fire). */
.taxi-arrived-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.7rem;
  /* Button radius, not the old hardcoded 8px: this badge replaces the
     .btn-taxi-arrived button in place and stands in the same action row. */
  border-radius: var(--radius-btn);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  color: var(--accent, #0a7);
  font-size: 0.85rem;
  font-weight: 600;
}

/* Taxi-R2 (QA M11): Fahrt-Panel oben in der Leihkarte — die vereinbarte bzw.
   anstehende Fahrt mit Zeit, Route, Status und nächster Aktion, wie die
   Trip-Karte einer Ride-App. Details (Karte, Stopps, Preis) klappen darunter
   auf; ihr Zustand überlebt Re-Renders (siehe _captureCollapsibleState). */
.taxi-ride-panel {
  --ride-tone: var(--accent);
  margin: 0.2rem 0 0.9rem;
  padding: 0.85rem 0.9rem 0.6rem;
  border-radius: var(--radius-card);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--ride-tone) 9%, transparent), transparent 70%),
    rgba(255,255,255,0.025);
  border: 1px solid color-mix(in srgb, var(--ride-tone) 32%, rgba(255,255,255,0.08));
  display: grid;
  gap: 0.55rem;
  min-width: 0;
}
.taxi-ride-panel--pending { --ride-tone: var(--badge-pending-text); }
.taxi-ride-panel--warn { --ride-tone: var(--warning); }
.taxi-ride-panel--live { --ride-tone: var(--info, var(--accent)); }
.taxi-ride-panel--past { --ride-tone: var(--text-muted); }
.taxi-ride-panel-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.35rem 0.5rem;
  min-width: 0;
}
.taxi-ride-panel-kicker {
  display: inline-flex; align-items: center; gap: 0.35rem; white-space: nowrap;
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-muted);
}
.taxi-ride-panel-status {
  flex-shrink: 0;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 700;
  color: var(--ride-tone);
  background: color-mix(in srgb, var(--ride-tone) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--ride-tone) 35%, transparent);
  white-space: nowrap;
}
.taxi-ride-panel-when {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.1rem 0.5rem;
  min-width: 0;
}
.taxi-ride-panel-day { font-size: var(--text-md); font-weight: 600; color: var(--text); }
.taxi-ride-panel-time {
  font-size: var(--text-2xl); font-weight: 800; line-height: 1.05;
  font-variant-numeric: tabular-nums; color: var(--text);
}
.taxi-ride-panel-eta { font-size: var(--text-sm); color: var(--ride-tone); font-weight: 600; }
.taxi-ride-panel-eta:empty { display: none; }
.taxi-ride-panel-price {
  margin-left: auto;
  font-size: var(--text-lg); font-weight: 800; color: var(--accent);
  font-variant-numeric: tabular-nums;
}
/* Pickup → destination as a two-dot rail, like the native ride apps. */
.taxi-ride-panel-route {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 0.35rem;
  min-width: 0;
}
.taxi-ride-panel-route li {
  position: relative;
  display: grid; grid-template-columns: 1rem minmax(0, 1fr); align-items: center; gap: 0.5rem;
  font-size: var(--text-sm); min-width: 0;
}
.taxi-ride-panel-route li:not(:last-child)::after {
  content: ""; position: absolute; left: calc(0.5rem - 1px); top: 1.05rem; height: calc(100% - 0.7rem);
  width: 2px; background: rgba(255,255,255,0.14);
}
.taxi-ride-panel-dot {
  width: 0.7rem; height: 0.7rem; border-radius: 50%; justify-self: center;
  background: var(--map-pickup);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--map-pickup) 18%, transparent);
}
.taxi-ride-panel-route .is-via .taxi-ride-panel-dot { width: 0.45rem; height: 0.45rem; background: var(--map-stop); box-shadow: none; }
.taxi-ride-panel-route .is-via { color: var(--text-muted); font-size: var(--text-xs); }
.taxi-ride-panel-route .is-to .taxi-ride-panel-dot { border-radius: 3px; background: var(--map-end); box-shadow: 0 0 0 3px color-mix(in srgb, var(--map-end) 18%, transparent); }
.taxi-ride-panel-addr { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.taxi-ride-panel-meta { font-size: var(--text-xs); color: var(--text-muted); }
.taxi-ride-panel-notice {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 0.15rem 0.45rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  font-size: var(--text-sm);
  color: var(--text);
}
.taxi-ride-panel-notice > svg { margin-top: 0.15rem; }
.taxi-ride-panel-notice-text { display: grid; gap: 0.15rem; min-width: 0; }
.taxi-ride-panel-notice--warn {
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}
.taxi-ride-panel-notice--live {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.taxi-ride-panel-note { color: var(--text-muted); font-size: var(--text-xs); overflow-wrap: anywhere; }
.taxi-ride-panel-punct { display: grid; gap: 0.4rem; font-size: var(--text-sm); color: var(--text-muted); }
.taxi-ride-panel-actions { display: grid; gap: 0.45rem; }
.taxi-ride-panel-primary > .btn-primary,
.taxi-ride-panel-primary > .taxi-arrived-badge { width: 100%; justify-content: center; margin: 0; }
.taxi-ride-panel-primary > .btn-primary { display: inline-flex; align-items: center; gap: 0.45rem; }
.taxi-ride-panel-secondary {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
}
.taxi-ride-panel-secondary > :is(button, a) {
  flex: 1 1 8.5rem; width: auto; margin: 0;
  min-height: var(--touch-target);
  display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem;
  padding: 0.5rem 0.7rem;
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
}
.taxi-ride-panel-secondary > .btn-danger {
  flex: 1 1 8.5rem;
}
/* Quiet destructive action: text in the danger colour, no red slab — the
   panel's headline action must stay the primary one. */
.taxi-ride-panel .btn-danger-ghost { color: var(--danger-text, var(--danger)); }
html:not(.using-touch) .taxi-ride-panel .btn-danger-ghost:hover {
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  color: var(--danger-text, var(--danger));
}
.taxi-ride-panel .taxi-loan-block--in-panel { margin: 0; padding: 0; border: 0; background: none; }
.taxi-ride-panel .taxi-loan-block--in-panel > .taxi-details { margin-top: 0; }
@media (max-width: 380px) {
  .taxi-ride-panel { padding: 0.75rem 0.7rem 0.5rem; }
  .taxi-ride-panel-time { font-size: var(--text-xl); }
}

/* Pünktlichkeits-Block: drei kleine Buttons oder Badge mit der bisherigen
   Bewertung. Klar visuell vom Action-Footer abgetrennt. */
.taxi-punctuality-choice,
.taxi-punctuality-mark {
  margin: 0.5rem 0 0.2rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius);
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--border, rgba(255,255,255,0.1));
}
.taxi-punctuality-choice small,
.taxi-punctuality-mark small { opacity: 0.7; font-size: 0.82rem; display: block; margin-bottom: 0.4rem; }
.taxi-punctuality-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.taxi-punctuality-buttons .btn-taxi-punct {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.4rem 0.6rem;
  font-size: 0.82rem;
}
.taxi-punct-badge {
  display: inline-block;
  padding: 0.25rem 0.55rem;
  border-radius: var(--radius-sm);
  font-size: 0.82rem;
  font-weight: 600;
}
.taxi-punct-on-time { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.taxi-punct-slightly-late { background: rgba(255, 196, 0, 0.15); color: #ffc400; }
.taxi-punct-very-late { background: rgba(245, 85, 85, 0.18); color: var(--danger, #f55); }

/* Taxi-Statistik-Sektion im Statistik-Modal. Eigenständige Card unter den
   bestehenden Auswertungen — Header + 2-Spalten-Grid mit Label/Wert-Zeilen. */
.taxi-stats-section {
  margin-top: 1.2rem;
  padding: 0.9rem 1rem;
  border-radius: var(--radius-tile);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.taxi-stats-section .stats-section-title {
  margin: 0 0 0.7rem;
  font-size: 1.02rem;
  font-weight: 700;
}
.taxi-stats-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.45rem;
}
.taxi-stats-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0.35rem 0;
  border-bottom: 1px solid rgba(255,255,255,0.05);
  font-size: 0.92rem;
}
.taxi-stats-row:last-child { border-bottom: none; }
.taxi-stats-row span { opacity: 0.78; }
.taxi-stats-row strong { font-weight: 700; }
.taxi-stats-row strong small { opacity: 0.7; font-weight: 400; margin-left: 0.2rem; }

@media (max-width: 480px) {
  :root { --taxi-map-h: 240px; }
  .taxi-vehicle-form .form-row .form-group { flex: 1 1 100%; }
  .taxi-loan-head { align-items: flex-start; flex-direction: column; }
  .taxi-loan-distance { white-space: normal; }
  .taxi-quote-total { font-size: 1.35rem; }
  .taxi-stop-handle, .taxi-stop-move-up, .taxi-stop-move-down,
  .btn-stop-del { min-height: var(--touch-target); min-width: var(--touch-target); }
  .taxi-bonus-chip { min-height: var(--touch-target); display: flex; align-items: center; justify-content: center; }
  #tr-add-stop, #te-add-stop { width: 100%; }
  .taxi-detail-tabs { display: grid; }
  .taxi-detail-panel { display: none; }
  .taxi-detail-panel.is-active { display: block; }
  .taxi-detail-addr { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
  .taxi-route-legend { gap: 0.4rem 0.55rem; }
}

/* Taxi section inside the profile modal */
.profile-taxi-section .profile-taxi-summary {
  margin: 0.5rem 0;
  opacity: 0.85;
  font-size: 0.9rem;
}
.profile-taxi-section .toggle-field { margin-bottom: 0.5rem; }

/* Vehicle modal opens from within the profile modal — needs higher z-index
   to stack above it (same mechanism as the global confirm dialog). */
#modal-taxi-vehicles { z-index: var(--z-confirm); }
/* The vehicle create/edit form opens from within modal-taxi-vehicles in
   turn — one nesting level deeper again, same +10 step #modal-discard uses
   over the confirm dialog it stacks on. */
#modal-taxi-vehicle-form { z-index: calc(var(--z-confirm) + 10); }

/* Pricing block inside profile taxi section */
.taxi-pricing-block { display: flex; flex-direction: column; gap: 0.8rem; margin: 0.5rem 0 1rem; }
.taxi-pricing-row { display: flex; flex-direction: column; gap: 0.4rem; }
.taxi-pricing-label { font-weight: 600; font-size: 0.95rem; }
/* Fixed 2-column grid instead of flex-wrap: with 4 buttons at min-width
   70px, flex-wrap packed 3 per row on narrow containers (~336px, mobile
   audit) and stretched the lone 4th button across the full next row. A
   2-column grid is a predictable, always-balanced 2×2 regardless of
   container width, rather than "however many happen to fit". */
.taxi-preset-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.4rem; margin: 0.15rem 0 0.35rem; }
.btn-preset {
  flex: 1 1 auto; min-width: 70px;
  min-height: var(--touch-target);
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border, rgba(255,255,255,0.15));
  background: rgba(255,255,255,0.04); color: inherit;
  font-size: 0.9rem; font-weight: 600; cursor: pointer;
  transition: var(--transition-ui);
}
html:not(.using-touch) .btn-preset:hover { background: rgba(255,255,255,0.08); }
.btn-preset:active { transform: scale(0.97); }
.btn-preset:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* --accent als Flaeche hinter Weiss = 2.71:1, --accent-btn = 4.79:1 — gleiche
   Begruendung wie bei .btn-primary/.btn-new-loan. Der baugleiche aktive
   Preset-Zustand .btn-pct.active (Z. 4124) nutzt bereits --accent-btn; dieser
   hier war die letzte Stelle mit rohem --accent unter weisser Schrift. */
.btn-preset-active {
  background: var(--accent-btn); color: #fff;
  border-color: var(--accent-btn);
}
.taxi-pricing-input-row {
  display: flex; align-items: center; gap: 0.4rem;
}
.taxi-pricing-input-row input {
  flex: 1;
  min-width: 0;
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-xs);
  color: var(--text);
  padding: 0.65rem 0.9rem;
  font-size: 16px;
  font-family: inherit;
  outline: none;
  transition: border-color var(--transition-fast);
  -webkit-appearance: none;
  appearance: none;
}
.taxi-pricing-input-row input:focus { border-color: var(--accent); }
.taxi-pricing-input-row span {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-weight: 700;
}
.taxi-night-fields { padding-left: 0.5rem; border-left: 2px solid var(--border, rgba(255,255,255,0.1)); }
/* .form-row has no display of its own, so the flex hints never applied: the
   three fields stacked with the "Ab"/"Bis" labels glued to the input above.
   Grid: percent (with its presets) full width, Ab | Bis side by side. */
.taxi-night-fields .form-row {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem 0.6rem;
}
.taxi-night-fields .form-row > .form-group:first-child { grid-column: 1 / -1; }
.taxi-night-fields .form-group { min-width: 0; margin-bottom: 0; }
/* Room for all four presets in one line from ~400px up (the 2×2 grid above
   stays for narrow phones, see there). */
@media (min-width: 400px) {
  .taxi-preset-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.taxi-pricing-status { font-size: 0.9rem; margin: 0.3rem 0 0; transition: opacity var(--motion-base) var(--ease-standard); }
/* Fades out but keeps its line, so the fields below do not move up. */
.taxi-pricing-status.is-faded { opacity: 0; }

/* Pickup-pending banner on borrower loan cards */
.taxi-pickup-banner {
  display: flex; flex-direction: column; gap: 0.6rem;
  padding: 0.85rem 1rem; margin: 0 0 0.6rem;
  border-radius: var(--radius-tile);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-left: 4px solid var(--accent, #0a7);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  min-width: 0;
}
.taxi-pickup-banner-head { font-weight: 700; display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.92rem; }
.taxi-pickup-banner-pickup {
  font-weight: 500; opacity: 0.9; font-size: 0.88rem; overflow-wrap: anywhere;
  padding: 0.25rem 0.5rem; border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08);
  margin-top: 0.1rem;
}
/* Zeigt dem Leihnehmer, dass der Endbetrag aus Fahrpreis + einem vom Leihgeber
   gesetzten Rabatt/Aufschlag besteht — sonst bestätigt er eine Zahl, deren
   Zustandekommen er nicht sieht. */
.taxi-pickup-banner-adjust {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  font-size: 0.84rem; opacity: 0.85; margin: 0.3rem 0;
}
.taxi-pickup-banner-adjust-tag {
  font-weight: 700; border-radius: var(--radius-sm); padding: 0.05rem 0.4rem;
  color: var(--orange, #f59e0b);
  background: color-mix(in srgb, var(--orange, #f59e0b) 16%, transparent);
}
.taxi-pickup-banner-adjust-tag.is-discount {
  color: var(--accent, #0a7);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.taxi-pickup-banner-prices {
  display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: baseline;
  padding: 0.3rem 0;
}
/* Endpreis + Bonus = Gesamtbetrag — what the confirm button commits to. */
.taxi-pickup-banner-grand {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
  gap: 0.25rem 0.75rem; margin: 0.1rem 0 0.35rem; padding-top: 0.35rem;
  border-top: 1px dashed var(--border);
  font-size: 0.84rem;
}
.taxi-pickup-banner-grand span { color: var(--text-muted); }
/* Taxi-R2 (QA N25): distance/duration on their own line under the price —
   it was "· 47,9 km · 64 min" and wrapped into a line starting with "·". */
.taxi-pickup-banner-meta { order: 1; flex-basis: 100%; color: var(--text-muted); font-size: var(--text-sm); }
/* War color:--danger + opacity:.7 -> 2.84/2.64:1. Der durchgestrichene alte
   Preis ist kein Fehler, sondern ein Vorher-Wert: gedimmte Textfarbe statt
   Rot + Elementopazitaet. */
.taxi-pickup-banner-prices .old {
  text-decoration: line-through; color: var(--text-dim);
  font-size: 1rem;
}
.taxi-pickup-banner-prices .new { font-size: 1.35rem; color: var(--accent, #0a7); font-weight: 700; }
.taxi-pickup-banner-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: stretch; }
/* margin-top resets the 0.5rem that .btn-primary carries by default: without
   it "Fahrt für €X bestätigen" sat 8px lower and 8px shorter than the
   "Fahrt absagen" beside it, at every viewport width. .modal-actions and
   .counter-offer-status-btns already do the same reset.
   The 10rem basis (was 0) lets the pair wrap to full-width rows once the
   banner is narrower than ~21rem — side by side the buttons were 71-116px
   wide, three text lines tall, and clipped the price at 320px. */
.taxi-pickup-banner-actions > button { flex: 1 1 10rem; min-width: 0; margin-top: 0; }

/* Variant: pickup made the ride cheaper (route shortened) — green accent. */
.taxi-pickup-banner.taxi-pickup-banner-savings {
  border-left-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.taxi-pickup-banner.taxi-pickup-banner-savings .new { color: var(--accent); }
.taxi-pickup-banner-savings-tag {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent);
  font-weight: 700; font-size: 0.85rem;
  padding: 0.1rem 0.5rem; border-radius: var(--radius-sm);
}
/* Variant: pickup made the ride more expensive — orange accent. */
.taxi-pickup-banner.taxi-pickup-banner-extra {
  border-left-color: var(--warning);
  background: color-mix(in srgb, var(--warning) 10%, transparent);
}
.taxi-pickup-banner.taxi-pickup-banner-extra .new { color: var(--warning); }
.taxi-pickup-banner-extra-tag {
  background: color-mix(in srgb, var(--warning) 22%, transparent);
  color: var(--warning);
  font-weight: 700; font-size: 0.85rem;
  padding: 0.1rem 0.5rem; border-radius: var(--radius-sm);
}

/* Quote skeleton loader — shown while /api/taxi/quote runs */
.taxi-quote-loading {
  display: flex; flex-direction: column; gap: 0.45rem;
  padding: 0.7rem 0.9rem; margin: 0.5rem 0;
  border-radius: var(--radius);
  border: 1px solid var(--border, rgba(255,255,255,0.1));
  border-left: 4px solid var(--accent, #0a7);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}
/* Uses the global .skeleton shimmer (same direction, timing and colours). This
   block previously carried its own gradient plus a taxiSkeletonShimmer keyframe
   that ran BACKWARDS at a different speed — two skeletons on screen wiped in
   opposite directions. Only the sizing stays local here. */
.taxi-quote-loading .skeleton-line {
  height: 0.85rem;
  /* .skeleton-line's own margin would add to the flex gap above (0.95rem
     instead of 0.45rem). */
  margin-bottom: 0;
}
.taxi-quote-loading .skeleton-line-strong { height: 1.4rem; width: 45%; }
.taxi-quote-loading .skeleton-line-medium { width: 75%; }
.taxi-quote-loading .skeleton-line-short  { width: 55%; }

/* Inline "Suche…" hint + tiny spinner while geocoding a stop */
.taxi-stop-loading {
  margin: 0.25rem 0 0; font-size: 0.85rem; opacity: 0.75;
  display: inline-flex; align-items: center; gap: 0.4rem;
}
.taxi-stop-spinner {
  width: 0.85rem; height: 0.85rem; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-top-color: var(--accent, #0a7);
  animation: entryPendingSpin 0.7s linear infinite;
}

/* Reliability card mini-badge for borrowers with taxi-confirm-pending loans */
.rel-card-taxi-pending {
  margin-top: 0.25rem;
  font-size: 0.78rem;
  color: var(--text-muted);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.55rem;
  display: inline-flex; align-items: center; gap: 0.25rem;
  align-self: flex-start;
}

/* Honor user motion preferences — neutralize transform-on-hover/active. */
@media (prefers-reduced-motion: reduce) {
  .btn-taxi-request, .btn-preset { transition: none; }
  html:not(.using-touch) .btn-taxi-request:hover { transform: none; filter: none; }
  .btn-taxi-request:active { transform: none; filter: none; }
  .btn-preset:active { transform: none; }
  /* Covered by the global `.skeleton::after { animation: none }` now that the
     taxi quote uses the shared shimmer — kept only as a belt-and-braces guard. */
  .taxi-quote-loading .skeleton-line::after { animation: none; }
  .taxi-stop-spinner { animation: none; }
}

/* --- Taxi cancel request flow --- */
/* Absage durch den Leihgeber: gleiche Struktur wie der Stornierungsantrag,
   aber in Danger-Farbe — es ist keine Anfrage mehr, sondern eine vollzogene
   Tatsache, und die Begründung ist das Einzige, was davon dauerhaft bleibt. */
.taxi-cancel-request-notice--cancelled {
  border-left-color: var(--danger, #e05252);
  background: color-mix(in srgb, var(--danger, #e05252) 8%, transparent);
}

.taxi-cancel-request-notice {
  margin: 0.6rem 0;
  padding: 0.75rem 1rem;
  border-radius: var(--radius-tile);
  border: 1px solid color-mix(in srgb, var(--warning) 25%, transparent);
  border-left: 4px solid var(--warning);
  background: color-mix(in srgb, var(--warning) 9%, transparent);
}
.taxi-cancel-request-top {
  display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem;
  flex-wrap: wrap;
}
.taxi-cancel-request-msg {
  margin: 0.3rem 0; font-style: italic; opacity: 0.92;
  overflow-wrap: anywhere;
}
.taxi-cancel-request-decide {
  display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;
}
.taxi-cancel-request-decide > button { flex: 1 1 0; width: auto; min-width: 0; }
.taxi-cancel-request-decide .btn-primary,
.taxi-cancel-request-decide .btn-danger { min-height: var(--touch-target); }

.taxi-cancel-audit {
  margin-top: 0.5rem; padding: 0.55rem 0.75rem;
  border-radius: var(--radius); font-size: 0.88rem;
}
.taxi-cancel-audit-approved {
  border-left: 3px solid color-mix(in srgb, var(--danger, #d04040) 70%, transparent);
  background: color-mix(in srgb, var(--danger, #d04040) 7%, transparent);
}
.taxi-cancel-audit-rejected {
  border-left: 3px solid color-mix(in srgb, var(--text-muted, #888) 70%, transparent);
  background: color-mix(in srgb, var(--text-muted, #888) 6%, transparent);
}
.taxi-cancel-audit-head {
  display: flex; justify-content: space-between; gap: 0.5rem;
  margin-bottom: 0.3rem; flex-wrap: wrap;
}
.taxi-cancel-audit-date { font-size: 0.8rem; opacity: 0.7; }
.taxi-cancel-audit-note { margin: 0.2rem 0; overflow-wrap: anywhere; line-height: 1.4; }

/* Free-text message from one party to the other (request, decision, counter-offer…) */
.party-message {
  margin: 0.45rem 0;
  padding: 0.45rem 0.7rem;
  border-left: 3px solid color-mix(in srgb, var(--accent) 65%, transparent);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.party-message-author { font-weight: 600; opacity: 0.85; margin-right: 0.25rem; }
.party-message-text { white-space: pre-wrap; }
/* R8-Undo: Rückfrage im Countdown („Senden rückgängig“). */
.party-message--pending { opacity: 0.6; }
.party-message-pending {
  display: flex; align-items: center; gap: 0.2rem; margin-top: 0.15rem;
  font-size: 0.7rem; color: var(--text-muted);
}
.modal-card-confirm .message-field { margin-top: 0.75rem; }
/* .form-group textarea sets 44px with higher specificity */
.form-group textarea.message-textarea, .message-textarea { min-height: 64px; }
.reason-hint { display: block; margin-top: 0.3rem; transition: opacity var(--motion-base) var(--ease-standard); }
.reason-hint.is-faded { opacity: 0; visibility: hidden; }

/* Quick replies: tap-to-insert suggestions under message fields */
.quick-replies { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
.quick-replies:empty { display: none; }
/* A validation error under a message field (and its chips) needs air above it. */
.message-field .error-msg:not(:empty) { margin-top: 0.75rem; }
/* The discard question stacks above the message dialog (modal-confirm). */
#modal-discard { z-index: calc(var(--z-confirm) + 10); }
.quick-reply-chip {
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: var(--radius-xs);
  min-height: 36px;
  padding: 0.35rem 0.85rem;
  font-size: 0.84rem;
  color: var(--text);
  cursor: pointer;
  transition: var(--transition-ui);
}
html:not(.using-touch) .quick-reply-chip:hover { background: rgba(255,255,255,0.15); }
.quick-reply-chip:active { transform: scale(0.96); }
@media (prefers-reduced-motion: reduce) { .quick-reply-chip:active { transform: none; } }
/* Touch: Chips bleiben optisch 36px, getroffen wird auf 44px (inset zaehlt ab
   der Padding-Box, also 34px + 2x5px; Zeilenabstand .quick-replies 0.4rem —
   die Flaechen zweier Reihen beruehren sich knapp). */
@media (pointer: coarse) {
  .quick-reply-chip { position: relative; }
  .quick-reply-chip::after { content: ""; position: absolute; inset: -5px 0; }
}
/* Text of this chip is already in the field. */
.quick-reply-chip.is-selected,
.quick-reply-chip-saved-wrap.is-selected {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: var(--accent);
}
.quick-reply-chip.is-selected { color: var(--accent); }
.quick-reply-chip-saved-wrap.is-selected .quick-reply-chip-saved { color: var(--accent); }

/* Own saved quick replies — same pill as a built-in .quick-reply-chip, plus
   edit/delete attached like .purpose-chip's wrap+label split (see below).
   A faint accent tint (instead of the built-in chips' plain gray) is the
   at-a-glance "this one's yours" signal — subtle on purpose, the "⋯" trigger
   already makes the distinction unambiguous up close. */
.quick-reply-chip-saved-wrap {
  display: inline-flex;
  align-items: center;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, rgba(255,255,255,0.15));
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--accent) 7%, rgba(255,255,255,0.08));
  overflow: hidden;
}
.quick-reply-chip-saved-wrap .quick-reply-chip-saved {
  border: none;
  border-radius: 0;
  background: none;
  min-height: 36px;
  padding: 0.3rem 0.3rem 0.3rem 0.6rem;
  /* Has to actually be readable at a glance — only extreme outliers (a saved
     reply can be up to 150 chars) get truncated at all. Edit/delete stay
     behind the "⋯" trigger below specifically so this can afford the width;
     full text is still in the title tooltip and in the edit dialog either
     way. */
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Single collapsed trigger — edit/delete only cost extra width once actually
   requested, instead of two permanently-visible icon buttons on every chip
   (this row sits under every message field in the app, often more than once
   per modal). */
.quick-reply-chip-manage {
  min-height: 36px;
  padding: 0.3rem 0.5rem;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--text-muted);
  background: none;
  border: none;
  border-left: 1px solid rgba(255,255,255,0.15);
  cursor: pointer;
  transition: color var(--transition-fast);
}
/* report_09 C1: hier stand `display: none` — der "⋯"-Trigger verschwand beim
   Aufklappen, damit gab es keinen Weg mehr zuzuklappen (app.js:3078 macht ein
   classList.toggle, der Button MUSS also sichtbar bleiben). Stattdessen
   markiert er sich jetzt als aktiv. */
.quick-reply-chip-saved-wrap.is-expanded .quick-reply-chip-manage {
  display: inline-flex;
  align-items: center;
  color: var(--accent);
}
.quick-reply-chip-actions { display: none; }
.quick-reply-chip-saved-wrap.is-expanded .quick-reply-chip-actions { display: inline-flex; }
.quick-reply-chip-edit,
.quick-reply-chip-delete {
  min-height: 36px;
  padding: 0.3rem 0.5rem;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--text-muted);
  background: none;
  border: none;
  border-left: 1px solid rgba(255,255,255,0.15);
  cursor: pointer;
  transition: color var(--transition-fast);
}
/* No ::after hit-area expansion on the expanded edit/delete pair (same
   reasoning as message-images.css's .mi-thumb-edit/-remove): edge-to-edge
   icons, an invisible expanded zone would overlap between them. */
html:not(.using-touch) .quick-reply-chip-manage:hover,
html:not(.using-touch) .quick-reply-chip-edit:hover { color: var(--accent); }
html:not(.using-touch) .quick-reply-chip-delete:hover { color: var(--danger); }
.quick-reply-chip-manage:focus-visible,
.quick-reply-chip-edit:focus-visible,
.quick-reply-chip-delete:focus-visible,
/* .quick-reply-chip-saved-wrap hat overflow:hidden — der Label-Button braucht
   denselben inneren Fokusring wie die Icon-Buttons daneben. */
.quick-reply-chip-saved-wrap .quick-reply-chip-saved:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.quick-reply-save-btn {
  min-height: 36px;
  padding: 0.35rem 0.75rem;
  font-size: 0.84rem;
  border: 1px dashed color-mix(in srgb, var(--accent) 50%, transparent);
  border-radius: var(--radius-xs);
  background: none;
  color: var(--accent);
  cursor: pointer;
  transition: var(--transition-ui);
}
html:not(.using-touch) .quick-reply-save-btn:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
/* Length countdown once close to QUICK_REPLY_SAVE_MAXLEN, or the "too long"
   explanation once past it — otherwise the button just silently not showing
   up looked like it was broken rather than "this text won't fit". */
.quick-reply-save-hint {
  align-self: center;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.quick-reply-save-hint.is-over { color: var(--danger, #e55); }
/* Inline, not a full-width row (unlike .purpose-chips-limit) — this sits
   under every message field in the app, often several times per modal, so a
   line reserved for it even just-below-the-limit would add up fast. Only
   shown once actually at the cap, where the "+ Merken" button disappearing
   would otherwise be unexplained; skips the early "4/5" heads-up purely to
   save space. */
.quick-reply-limit-note {
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* Purpose field that reads like a message */
.purpose-message-hint {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem;
  margin-top: 0.35rem; font-size: 0.8rem; color: var(--warning);
}
.purpose-message-hint .link-btn { min-height: 36px; padding: 0.25rem 0; }

/* Messages in the decide dialog */
.decide-conversation { margin: 0.25rem 0 0.5rem; }
.decide-conversation:empty { display: none; }

/* Close-request notice: the reject button must not squeeze the request text */
.close-request-notice .btn-close-req-reject { flex: 0 0 auto; width: auto; }

/* Unread-message dot on the view tabs */
.tab-unread {
  display: inline-block; width: 8px; height: 8px; margin-left: 0.3rem;
  border-radius: 50%; background: var(--accent); vertical-align: middle;
}

/* Conversation per loan */
.loan-conversation-wrap { margin: 0.5rem 0; display: flex; flex-direction: column; gap: 0.45rem; }
.loan-conversation-wrap > .btn-conv-write { align-self: flex-start; width: auto; }
.conv-empty { margin: 0; padding: 0 0.75rem 0.6rem; font-size: 0.84rem; color: var(--text-muted); }
.history-conv-count { color: var(--text-muted); font-weight: 500; margin-left: 0.35rem; }
/* Im Details-Knopf (flex, space-between) landete die Nachrichten-Zahl als
   mittleres von drei Kindern mitten in der Leiste – jetzt rechts, direkt vor
   dem Pfeil. */
.history-entry-toggle > .history-conv-count {
  margin-left: auto;
  margin-right: -0.25rem;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-variant-numeric: tabular-nums;
}
.loan-conversation { border: 1px solid var(--border); border-radius: var(--radius-xs); background: var(--surface2); }
.loan-conversation > summary {
  display: flex; align-items: center; gap: 0.5rem; padding: 0.55rem 0.75rem;
  cursor: pointer; font-size: 0.88rem; font-weight: 600; list-style: none;
  /* War 39px — die Zeile darueber ("+ Notiz hinzufuegen") und die Aktions-
     Buttons darunter sind 44px; gleiche Stufe, und auf Touch die Mindestgroesse. */
  min-height: var(--touch-target);
}
/* Touch-Audit R4: Nachrichten-Zeile ("Nachrichten (3) ›") gab beim Tippen
   kein Zeichen. Zeilen-Druck wie .global-search-row (Hintergrund, kein
   transform — die Karte darunter ist wischbar). Radius folgt der Box. */
.loan-conversation > summary { border-radius: 7px; transition: background-color var(--transition-fast); }
.loan-conversation > summary:active {
  --press-opacity: 1; /* generische Dimm-Regel (Global) nicht zusaetzlich */
  background: color-mix(in srgb, var(--text) 7%, var(--surface2));
  transition-duration: 0s;
}
.loan-conversation[open] > summary { border-radius: 7px 7px 0 0; }
.loan-conversation > summary::-webkit-details-marker { display: none; }
.loan-conversation > summary .chevron { margin-left: auto; transition: transform var(--transition-fast); }
.loan-conversation[open] > summary .chevron { transform: rotate(90deg); }
.conv-count { color: var(--text-muted); font-weight: 500; }
.conv-list { list-style: none; margin: 0; padding: 0 0.75rem 0.75rem; display: flex; flex-direction: column; gap: 0.45rem; }
.conv-item {
  max-width: 88%; align-self: flex-start; padding: 0.45rem 0.65rem; border-radius: var(--radius);
  background: color-mix(in srgb, var(--text) 6%, transparent);
  font-size: var(--text-base); line-height: 1.45; overflow-wrap: anywhere;
}
.conv-item--mine { align-self: flex-end; background: color-mix(in srgb, var(--accent) 14%, transparent); }
/* Text sent with a request/decision/counter-offer: tinted card with the step as a tag,
   so it never reads like a chat message written via "Nachricht schreiben". */
.conv-item--action {
  background: color-mix(in srgb, var(--info) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--info) 28%, transparent);
  border-left: 3px solid var(--info);
}
.conv-kind {
  display: inline-flex; align-items: center; padding: 0 0.45rem; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--info) 18%, transparent); color: color-mix(in srgb, var(--info) 70%, var(--text)); font-weight: 600;
}
.conv-item--new { box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent); }
.conv-meta { display: flex; flex-wrap: wrap; gap: 0.1rem 0.45rem; font-size: 0.74rem; color: var(--text-muted); margin-bottom: 0.15rem; }
.conv-meta strong { color: var(--text); font-weight: 600; }
.conv-text { white-space: pre-wrap; }
/* Leihkarten R2 (M22): Messenger-Blasen. Uhrzeit klein unten rechts in der
   Blase, Datums-Trenner zwischen den Tagen, eigene Blasen mit abgeflachter
   Ecke unten rechts, fremde unten links. Aufeinanderfolgende Blasen derselben
   Person rücken enger zusammen (.conv-item--cont). */
.conv-list { gap: 0.3rem; padding-top: 0.15rem; }
.conv-item { position: relative; border-radius: 16px 16px 16px 6px; padding: 0.45rem 0.7rem 0.35rem; max-width: 84%; }
.conv-item--mine { border-radius: 16px 16px 6px 16px; }
.conv-item--cont { margin-top: -0.1rem; }
.conv-item + .conv-item:not(.conv-item--cont) { margin-top: 0.3rem; }
.conv-item--action { border-radius: var(--radius); }
.conv-meta { font-size: 0.76rem; margin-bottom: 0.1rem; }
.conv-meta:empty { display: none; }
.conv-time {
  display: block;
  margin-top: 0.1rem;
  text-align: right;
  font-size: 0.7rem;
  line-height: 1.2;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.conv-day {
  align-self: center;
  margin: 0.45rem 0 0.2rem;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-muted);
}
.conv-day > span {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--text) 7%, transparent);
}
.conv-item .conv-images { margin-top: 0.3rem; }
/* R8-Undo („Senden rückgängig“): eigene Nachricht im Countdown bzw. unterwegs —
   gedimmt, statt der Uhrzeit eine kleine Uhr mit „wird gesendet“. */
.conv-item--pending { opacity: 0.6; }
.conv-time--pending {
  display: flex; align-items: center; justify-content: flex-end; gap: 0.2rem;
}
.conv-time--pending svg { flex: none; }
.badge-unread { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent); }
.taxi-cancel-audit-author {
  font-weight: 600; opacity: 0.85; margin-right: 0.25rem;
}

/* Recent address chips (taxi request modal) */
.taxi-recent-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-bottom: 0.5rem; font-size: 0.82rem; }
.taxi-recent-chips-label { opacity: 0.6; }
.taxi-recent-chip {
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.15);
  /* <button> doesn't inherit the page text color: the UA default (black)
     was near-invisible on this dark chip. */
  color: var(--text);
  border-radius: var(--radius-xs);
  /* 22px → 30px, the chip height used by the favorites right below. */
  min-height: 1.9rem;
  padding: 0.2rem 0.65rem;
  font-size: 0.8rem;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: min(200px, 100%);
  transition: var(--transition-ui);
}
html:not(.using-touch) .taxi-recent-chip:not(.is-selected):hover { background: rgba(255,255,255,0.15); }
/* overflow: hidden (Ellipse) schneidet ein ::after ab — auf Touch bekommt der Chip deshalb echte 44 px. */
@media (pointer: coarse) {
  .taxi-recent-chip { min-height: var(--touch-target); }
}
.taxi-recent-chip.is-selected {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: var(--accent);
  color: var(--accent);
}


/* --- Taxi status timeline (borrower view) --- */
.taxi-status-timeline {
  display: flex; align-items: center; gap: 0;
  margin-bottom: 0.5rem; width: 100%;
}
.taxi-timeline-step {
  display: flex; align-items: center; gap: 0.3rem;
  font-size: 0.74rem; color: var(--text-muted);
  flex: 1; min-width: 0;
}
.taxi-timeline-step-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.taxi-timeline-step:not(:last-child)::after {
  content: '';
  display: block; flex: 0 0 1px;
  height: 1px; margin: 0 0.25rem;
  background: rgba(255,255,255,0.15);
  min-width: 0.5rem;
}
.taxi-timeline-step--done { color: var(--accent, #0a7); }
.taxi-timeline-step--active { color: var(--warning); font-weight: 600; }
.taxi-timeline-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.25rem; height: 1.25rem; border-radius: 50%;
  font-size: var(--text-xs); font-weight: 700;
  background: rgba(255,255,255,0.1);
  flex-shrink: 0;
}
/* --bg statt #fff (2.71:1 → 7.08:1) — dieselbe Korrektur wie bei
   .taxi-detail-num/.taxi-loan-stop-num. Der direkte Nachbar
   .taxi-timeline-step--active (unten) benutzt auf --warning schon dunklen
   Text; hell/dunkel wechselte bisher mitten in derselben Leiste. */
.taxi-timeline-step--done .taxi-timeline-num {
  background: var(--accent, #0a7); color: var(--bg);
}
.taxi-timeline-step--done .taxi-timeline-num::after { content: '✓'; font-size: 0.62rem; }
.taxi-timeline-step--done .taxi-timeline-num > * { display: none; }
.taxi-timeline-step--active .taxi-timeline-num { background: var(--warning); color: #1a1000; }

/* --- Taxi ride countdown --- */
/* Rewritten every minute inline in the card head — proportional digits would
   shift the neighbouring text on each tick. */
.taxi-ride-countdown { color: var(--accent, #0a7); font-size: 0.82rem; font-variant-numeric: tabular-nums; }
/* "vor 1 Stunde": a passed ride time is no good news — muted, not green. */
.taxi-ride-countdown.is-past { color: var(--text-muted); }

/* === First-run swipe affordance ===
   Triggered once via .loan-card-lender.swipe-hint (set by JS the first time a
   lender with at least one active loan opens the active tab). Plays a 1.5s
   horizontal nudge so users discover the swipe gesture without an intrusive
   tutorial overlay. */
@keyframes loanSwipeWiggle {
  0%   { transform: translateX(0); }
  18%  { transform: translateX(22px); }
  32%  { transform: translateX(0); }
  50%  { transform: translateX(-12px); }
  64%  { transform: translateX(0); }
  78%  { transform: translateX(14px); }
  100% { transform: translateX(0); }
}
.loan-card-lender.swipe-hint {
  animation: loanSwipeWiggle 1.5s var(--ease-standard) 1;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .loan-card-lender.swipe-hint { animation: none; }
}

/* === Trust-Score Sparkline (compact, inline on the borrower card) ===
   Mounts after `_loadGlobalBreakdownInto` resolves; before that, an aria-
   hidden placeholder keeps the height steady so the surrounding layout
   doesn't shift. */
.score-sparkline {
  display: block;
  height: 32px;
  width: 100%;
  max-width: 160px;
  overflow: visible;
}
.score-sparkline-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 160px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.04);
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: center;
  padding: 0 0.4rem;
}
.score-sparkline-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.8;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* ===================================================================
   MOTION SYSTEM (2026-05-17)
   Subtle, fast animations layered on top of the existing motion tokens.
   Every animation respects prefers-reduced-motion. None of these
   animations block input — they're all transform/opacity-only so they
   stay on the compositor and don't trigger layout.
   =================================================================== */

/* === View Transitions ===
   Modern Chromium / Safari 18+ runs document.startViewTransition() around
   the DOM swap (see app.js render()). The pseudo-elements below shape the
   crossfade — a touch slower and softer than the default 200ms cut, which
   feels harsh for a full app re-render. */
@supports (view-transition-name: none) {
  ::view-transition-old(root) {
    animation: vtFadeOut var(--motion-fast) var(--ease-exit) both;
  }
  ::view-transition-new(root) {
    animation: vtFadeIn var(--motion-base) var(--ease-standard) both;
  }
  @keyframes vtFadeOut {
    to { opacity: 0; transform: translateY(-6px); }
  }
  @keyframes vtFadeIn {
    from { opacity: 0; transform: translateY(8px); }
  }

  /* PWA-Native: switching Verliehen ⇄ Geschuldet slides the content sideways
     in the direction of the tab (html.vt-slide-fwd/back, set by app.js only
     for that render). Header, tab bar and offline strip get their own
     transition groups so they stay put instead of sliding with the page. The
     in-list tab slide (Ausstehend/Aktiv/Verlauf) does not use a view
     transition, so the two never run together. */
  /* Mobile-Audit A2: nur der Seitenkopf. Das nackte `header` traf auch <header class="pending-addition-head">
     in den Leihkarten – doppelter Name, der Browser verwarf den ganzen Übergang (Konsole: „Unexpected duplicate
     view-transition-name“) und die Seitenanimation fiel bei jeder offenen Ergänzungs-Anfrage aus. */
  html.vt-slide > body > header { view-transition-name: app-header; }
  html.vt-slide .app-tabbar { view-transition-name: app-tabbar; }
  html.vt-slide .offline-banner:not(.hidden) { view-transition-name: app-offline; }
  html.vt-slide::view-transition-group(app-header),
  html.vt-slide::view-transition-group(app-tabbar),
  html.vt-slide::view-transition-group(app-offline),
  html.vt-slide::view-transition-old(app-header),
  html.vt-slide::view-transition-old(app-tabbar),
  html.vt-slide::view-transition-old(app-offline),
  html.vt-slide::view-transition-new(app-header),
  html.vt-slide::view-transition-new(app-tabbar),
  html.vt-slide::view-transition-new(app-offline) { animation: none; }
  html.vt-slide-fwd::view-transition-old(root) { animation: vtSlideOutLeft var(--motion-base) var(--ease-sheet) both; }
  html.vt-slide-fwd::view-transition-new(root) { animation: vtSlideInRight var(--motion-base) var(--ease-sheet) both; }
  html.vt-slide-back::view-transition-old(root) { animation: vtSlideOutRight var(--motion-base) var(--ease-sheet) both; }
  html.vt-slide-back::view-transition-new(root) { animation: vtSlideInLeft var(--motion-base) var(--ease-sheet) both; }
  @keyframes vtSlideOutLeft  { to   { opacity: 0; transform: translateX(-24%); } }
  @keyframes vtSlideInRight  { from { opacity: 0; transform: translateX(24%); } }
  @keyframes vtSlideOutRight { to   { opacity: 0; transform: translateX(24%); } }
  @keyframes vtSlideInLeft   { from { opacity: 0; transform: translateX(-24%); } }
}

/* === List stagger ===
   New loan-cards reveal sequentially so the eye lands on the first card
   before the rest arrive. The CSS variable `--stagger-idx` is set inline
   on each card by JS so we don't have to write nth-child rules. Capped
   at 12 — beyond that everyone arrives together to avoid a 600ms+ wait. */
.loan-card,
.loan-card-lender,
.history-card {
  --stagger-idx: 0;
  /* Motion-Audit: `backwards` statt `both`. Der Endzustand (opacity 1,
     transform none) ist ohnehin der Grundzustand; ein forwards-Fill hielt
     aber transform fest und uebertrumpfte so Hover-Lift und das Inline-
     transform der Wisch-Geste — unter Reduce Motion (dort setzt app.js nie
     is-mounted) folgte die Karte dem Finger gar nicht (gemessen 0 px bei
     60 px). Kein dauerhaftes will-change mehr: es machte jede Karte zum
     Containing Block fuer die position:fixed Eintrag-Menues (Anchor-Flip
     rechnete gegen die Karte statt gegen den Viewport) und blieb ohne
     is-mounted fuer immer stehen. Die Animation selbst laeuft ohnehin auf
     dem Compositor. */
  animation: cardListEnter var(--motion-base) var(--ease-standard) backwards;
  animation-delay: calc(min(var(--stagger-idx), 12) * 35ms);
}
/* When a View Transition is animating the swap, skip the per-card stagger
   so the two systems don't double up. The browser already crossfades the
   old DOM into the new one. */
html.vt-active .loan-card,
html.vt-active .loan-card-lender,
html.vt-active .history-card {
  animation: none;
  will-change: auto;
}
@keyframes cardListEnter {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Befund 25 (gemessen): Der html.vt-active-Guard verhindert die
   Doppelanimation nicht, er VERSCHIEBT sie. Die Klasse liegt nur waehrend der
   View Transition auf <html> und wird auf `transition.finished` entfernt —
   genau in dem Moment startet cardListEnter doch noch (gemessen 390 ms nach
   vtFadeIn). Der Nutzer sah Crossfade UND danach die gestaffelte Einblendung.
   `is-mounted` setzt app.js im VT-Pfad direkt beim Swap; die Klasse ueberlebt
   das Entfernen von vt-active und beendet die Animation endgueltig.
   (Damit kann der Inline-Style `style.animation = 'none'` in app.js weg —
   siehe requests/app.md R4.)
   Zugleich faellt will-change weg, damit keine Compositor-Layer dauerhaft
   stehen bleiben; animation-fill-mode: both haelt den Endzustand.
   Motion-Audit: Die Regel stand mit gleicher Spezifitaet NACH
   .loan-card.history-focus / .overdue-focus / .loan-card-lender.swipe-hint
   und schaltete damit auch deren Animationen ab — der Sprung-Puls nach
   "Ansehen" und der Swipe-Hinweis-Wiggle liefen auf gemounteten Karten nie
   (die swipe-hint-Klasse blieb mangels animationend sogar haengen). Die
   Ausnahmen lassen genau diese Einmal-Animationen durch; nach dem Entfernen
   der Klasse greift wieder animation: none, es gibt keinen Replay. */
.loan-card.is-mounted:not(.history-focus, .overdue-focus, .swipe-hint),
.loan-card-lender.is-mounted:not(.history-focus, .overdue-focus, .swipe-hint),
.history-card.is-mounted:not(.history-focus, .overdue-focus, .swipe-hint) {
  animation: none;
  will-change: auto;
}

/* NOTE: Tab-panel crossfade is already handled by .tab-panel-enter-left /
   .tab-panel-enter-right (see ~Z. 5871) — re-using that system instead of
   adding a parallel one. */

/* === Card micro-interactions ===
   Tiny pressed-state on click + slight hover-lift on devices with hover
   capability. Already partially present for .dashboard-card-clickable;
   this generalises the press-down to all interactive cards. */
@media (hover: hover) and (pointer: fine) {
  .loan-card-lender,
  .loan-card.loan-card-lender,
  .history-card {
    transition: transform var(--motion-fast) var(--ease-standard),
                box-shadow var(--motion-fast) var(--ease-standard),
                border-color var(--motion-fast) var(--ease-standard);
  }
  /* Touch-R4: zusaetzlich hinter der Touch-Weiche — auf Hybrid-Geraeten
     (Laptop mit Touchscreen) meldet die Media-Query hover:hover/pointer:fine,
     ein Fingertipp liess die Karte dort angehoben stehen. */
  html:not(.using-touch) .loan-card-lender:hover {
    transform: translateY(-1px);
  }
}
/* Motion-Audit: .loan-card-lender:active ist hier raus. Die Leihkarte ist
   selbst nicht klickbar, :active greift aber auf jeden Vorfahren — jeder Tipp
   auf einen Knopf IN der Karte (und auf Touch jeder Scroll-Start ueber ihr)
   liess die ganze Karte mit einschrumpfen. Auf dem Desktop war der Press
   ohnehin tot: html:not(.using-touch) .loan-card:hover (0,3,1) schlug ihn.
   .tab-pill:active ist ebenfalls raus — der "Filter-pill interaction"-Block
   am Dateiende definiert den Tab-Press (scale 0.93) und gewann ohnehin. */
.dashboard-card-clickable:active,
.btn-primary:active,
.btn-secondary:active,
.btn-danger:active {
  transform: scale(0.985);
  transition: transform var(--motion-xs) var(--ease-exit);
}
/* Maus-Tier: Waehrend die Maus gedrueckt ist, gilt auch :hover — und
   html:not(.using-touch) X:hover (0,3,1) schlug das schlichte X:active, der
   Knopf blieb beim Klicken angehoben statt einzusinken (gemessen:
   translateY(-1px) waehrend mousedown). Gleiche Spezifitaet, spaeter in der
   Datei → der Press gewinnt, solange gedrueckt wird. Muster wie beim
   Filter-Pill-Block am Dateiende. */
html:not(.using-touch) .dashboard-card-clickable:active,
html:not(.using-touch) .btn-primary:active,
html:not(.using-touch) .btn-secondary:active,
html:not(.using-touch) .btn-danger:active {
  transform: scale(0.985);
  transition: transform var(--motion-xs) var(--ease-exit);
}
/* Runde 6 (Knöpfe): .btn-danger fehlte in beiden Stufen oben — Löschen/
   Verwerfen sank mit der Ruhe-Transition (0.3 s) ein, Speichern/Abbrechen
   daneben mit 0.12 s. Jetzt dieselbe schnelle Druck-Stufe. */
/* Touch-R4: a disabled button gets no click — it must not sink in either
   (on touch the plain :active above pressed "Abschließen" & co. although
   nothing happened). Higher specificity than both tiers above. */
.btn-primary:disabled:active,
.btn-secondary:disabled:active,
.btn-danger:disabled:active,
html:not(.using-touch) .btn-primary:disabled:active,
html:not(.using-touch) .btn-secondary:disabled:active,
html:not(.using-touch) .btn-danger:disabled:active {
  transform: none;
}

/* === Progress-bar pulse on update ===
   When a payment lands and the bar grows, a single soft pulse confirms the
   change. JS toggles `.is-bumped` for one tick after a render with a
   higher --repay-pct than the previous DOM state. */
.loan-progress-bar.is-bumped .loan-progress-bar-fill {
  animation: progressBump var(--motion-slow) var(--ease-spring);
}
/* Der box-shadow-Anteil ist raus (Befund 28): er war der einzige Grund, warum
   dieser kurze Bump ein Repaint der ganzen Leiste ausgeloest hat. filter
   bleibt — es ist eine Einmal-Animation von 0.3s, kein Dauerlaeufer. */
@keyframes progressBump {
  0%   { filter: brightness(1); }
  40%  { filter: brightness(1.35); }
  100% { filter: brightness(1); }
}

/* === Hero number tween ===
   The .dashboard-value briefly scales up and color-shifts when its value
   crossed into a "this changed" state. Used by JS via a data-was-value
   attribute compared on render. */
.dashboard-value.is-tweening {
  animation: heroNumberPop var(--motion-slow) var(--ease-spring);
}
@keyframes heroNumberPop {
  0%   { transform: scale(1);   color: inherit; }
  40%  { transform: scale(1.06); color: var(--accent); }
  100% { transform: scale(1);   color: inherit; }
}

/* === Motion R2: Listen-Choreografie & Zustandswechsel ===
   app.js ListMotion gleicht Eintraege vor/nach einem innerHTML-Swap ab.
   Weggefallene Eintraege blenden als "Geist" (der abgeloeste Originalknoten)
   an ihrer alten Stelle aus: absolut, ohne Layout-Einfluss, nicht bedienbar.
   Die Geister tragen is-mounted, damit keine Eintritts-Animation anspringt. */
.lm-ghost {
  position: absolute !important;
  margin: 0 !important;
  pointer-events: none !important;
  box-sizing: border-box;
  z-index: 1;
  animation: none !important;
  transition: none !important;
}
/* Geaenderter Betrag (Zahl zaehlt hoch, dann dieser Flash) — gleiche Dauer
   wie alle "hier hat sich etwas getan"-Hervorhebungen (--motion-emphasis). */
.value-flash {
  border-radius: 4px;
  animation: valueFlash var(--motion-emphasis) var(--ease-standard);
}
@keyframes valueFlash {
  0% {
    color: var(--accent-text);
    background-color: color-mix(in srgb, var(--accent) 22%, transparent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  }
  100% {
    background-color: transparent;
    box-shadow: 0 0 0 3px transparent;
  }
}
/* Block-Ueberschriften (Banner-Titel) nur farblich — ein Hintergrund-Balken
   ueber die ganze Zeile wirkte wie eine Markierung. */
.action-banner-text strong.value-flash {
  animation-name: valueFlashText;
}
@keyframes valueFlashText {
  0% { color: var(--accent-text); }
}
/* Status-Wechsel (ausstehend → aktiv → abgeschlossen …): das neue Badge
   blendet weich auf statt hart umzuspringen. */
.badge.is-state-changed {
  animation: badgeStateIn var(--motion-slow) var(--ease-spring);
}
@keyframes badgeStateIn {
  0%   { opacity: 0; transform: scale(0.86); filter: blur(2px); }
  60%  { opacity: 1; filter: blur(0); }
  100% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .value-flash,
  .badge.is-state-changed { animation: none; }
}

/* === Motion R2: einheitliche Mikro-Interaktionen ===
   Eine Physik fuer alle Auswahl-Elemente: Druck = schnell hinein
   (--motion-xs), Loslassen/Umschalten = federnd zurueck (--ease-spring);
   Zustandsfarben mit --transition-fast. Vorher: Schalter-Knopf ohne Feder,
   native Checkboxen/Radios ohne jedes Druck-Feedback, Radio-Karten (Neuer
   Benutzer) ohne Auswahl-Toenung, Aufklapp-Pfeile mal 180, mal 300 ms. */

/* Schalter: Knopf gleitet federnd; beim Druck streckt er sich (wie iOS) —
   im An-Zustand nach links, damit er am rechten Rand bleibt. */
.toggle-thumb {
  transition:
    transform var(--motion-base) var(--ease-spring),
    width var(--motion-fast) var(--ease-standard),
    background-color var(--transition-fast);
}
.toggle-control:not(:has(input:disabled)):active .toggle-thumb { width: 24px; }
.toggle-control:not(:has(input:disabled)):active input[type="checkbox"]:checked + .toggle-track .toggle-thumb {
  transform: translateX(16px);
}
/* R4 Modals/Touch: Schalterzeilen wie in iOS-Einstellungen — die ganze Zeile
   schaltet, nicht nur der 44px-Schalter. .toggle-field (Profil, Onboarding)
   hatte Titel und Beschreibung in einem <div> neben dem <label>: ein Tipp auf
   „E-Mail“ tat nichts. Die unsichtbare Checkbox (position:absolute; inset:0,
   s. .toggle-control input) bezieht sich jetzt auf die Zeile statt auf den
   Schalter und deckt sie ganz ab: nativer Toggle, Scrollen über die Zeile
   löst nichts aus, kein Textmarkieren beim Long-Press. Info-Punkte liegen
   darüber und bleiben eigenständig antippbar. */
.toggle-field { position: relative; }
.toggle-field > .toggle-control { position: static; }
.toggle-field > .toggle-control::after { inset: 0; }
.toggle-field .info-dot { position: relative; z-index: 1; }
/* Druck auf die Zeile bzw. auf den Text eines .toggle-label: derselbe
   Knopf-Stretch wie beim Druck direkt auf den Schalter. */
.toggle-label:not(.toggle-label--disabled):active .toggle-thumb { width: 24px; }
.toggle-label:not(.toggle-label--disabled):active input[type="checkbox"]:checked + .toggle-track .toggle-thumb {
  transform: translateX(16px);
}

/* Native Checkboxen/Radios (Rechte-Editor, Presets, Rueckmeldungs-Kontext …):
   derselbe Einwaerts-Druck wie die Chips. */
input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--accent);
  transition: transform var(--motion-fast) var(--ease-spring);
}
input[type="checkbox"]:not(:disabled):active,
input[type="radio"]:not(:disabled):active {
  transform: scale(0.86);
  transition-duration: var(--motion-xs);
}
.toggle-control input[type="checkbox"]:active { transform: none; }

/* Auswahl-Karten (Rechte, Benutzer-Presets): gleiche Toenung bei Auswahl,
   gleicher leichter Druck wie Karten/Buttons (0.985). */
.nu-preset {
  transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--motion-fast) var(--ease-spring);
}
.nu-preset:has(input:checked) {
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.perm-option {
  transition: border-color var(--transition-fast), background-color var(--transition-fast), opacity var(--transition-fast), transform var(--motion-fast) var(--ease-spring);
}
.nu-preset-extra { transition: border-color var(--transition-fast), background-color var(--transition-fast), opacity var(--transition-fast), transform var(--motion-fast) var(--ease-spring); }
.nu-preset:active,
.perm-option:not(:has(input:disabled)):active {
  transform: scale(0.985);
  transition-duration: var(--motion-xs);
}
/* Erinnerungs-Chips: Druck wie Zweck-Chips (0.96). */
.reminder-chip span { transition: var(--transition-ui); }
.reminder-chip:active span {
  transform: scale(0.96);
  transition-duration: var(--motion-xs);
}

/* === R4 Modals/Touch: Druck-Rückmeldung für die restlichen Dialog-Controls ===
   Gemessen per Touch-Emulation (erzwungenes :active bei html.using-touch):
   diese Elemente zeigten beim Drücken GAR nichts — ihr einziges Feedback war
   ein Maus-:hover, das auf Touch (richtigerweise) nie greift. Nur :active,
   keine Klassen: der Zustand endet mit dem Loslassen bzw. beim Scroll-Start
   von selbst. Ausgewählte Zustände (.is-active, [aria-expanded], [open])
   bleiben unberührt; wo das Muster transform schon belegt ist (Heatmap-Zelle
   skaliert ihre Auswahl), drückt stattdessen die Helligkeit.
   Kein Maus-Tier nötig: keine der zugehörigen :hover-Regeln setzt dieselbe
   Eigenschaft wie der Druck hier. */

/* Kleine Pillen/Segmente/Knöpfe: einwärts wie die Chips (0.96). */
button.status-pill-clickable:active,
.heatmap-mode-btn:active,
.btn-logs-subtle.btn-logs-danger:not(:disabled):active,
.btn-delete-user-inline:not(:disabled):active,
.log-delete-btn:active,
.log-goto-loan:active {
  transform: scale(0.96);
  transition-duration: var(--motion-xs);
}
/* „Gegenangebot“ im Entscheiden-Footer: gleicher Druck wie seine Nachbarn
   .btn-primary/.btn-secondary (0.985). */
.decide-secondary-link:active {
  transform: scale(0.985);
  transition: transform var(--motion-xs) var(--ease-exit);
}
.heatmap-mode-btn { transition: var(--transition-ui); }
.heatmap-mode-btn:not(.is-active):active { background: rgba(255,255,255,0.06); }
.btn-delete-user-inline:not(:disabled):active { background: color-mix(in srgb, var(--danger) 16%, transparent); }
.log-delete-btn:active { border-color: color-mix(in srgb, var(--danger) 35%, transparent); color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.log-goto-loan:active { background: color-mix(in srgb, var(--accent) 20%, transparent); }
.heatmap-cell.has-loan:active { filter: brightness(1.35); }

/* Text-Links als Buttons („Bearbeiten“, „Jetzt installieren“, Link in
   Feld-Hinweisen, „In neuem Tab öffnen“ der Lightbox). */
.link-btn { transition: opacity var(--transition-fast); }
.link-btn:not(:disabled):active,
.field-hint a:active,
#fbl-open:active { opacity: 0.6; }

/* Aufklapp-Zeilen und tippbare Karten: Fläche hellt kurz auf. */
.settings-section-head { border-radius: inherit; }
/* Runde 6 (Knöpfe): diese Zeilen/Karten hatten als einzige tippbare Flächen
   gar keine Transition (berechnet: all 0s) — Druck und Hover sprangen, und
   nach dem Loslassen schnappte die Fläche zurück, während Knöpfe daneben
   weich (0.18 s) zurückgleiten. Gleicher Token wie überall. */
.settings-section-head,
.rn-head,
.reliability-how-summary,
.kpi-card-clickable,
.sync-modal-version-copy { transition: var(--transition-ui); }
/* > statt Nachfahre: muss .settings-section[open] > .settings-section-head
   (sticky, eigener Hintergrund, 0,3,0) schlagen — gleiche Spezifität, später. */
.settings-section > .settings-section-head:active { background: color-mix(in srgb, white 6.5%, var(--surface)); }
.user-more-summary:active { color: var(--text); background: rgba(255,255,255,0.05); }
.log-entry-summary:active { background: rgba(255,255,255,0.04); }
.rn-head:active { background: rgba(255,255,255,0.05); }
.rn-head[aria-expanded="true"]:active { background: color-mix(in srgb, white 5%, var(--surface2)); }
.kpi-card-clickable:active,
.rel-borrower-card:active { background: rgba(255,255,255,0.07); }
.perm-history > summary:active,
.reliability-how-summary:active,
.logs-day-header:active .logs-day-title { color: var(--text); }
/* Schon in --text gesetzt — hier trägt die Deckkraft den Druck. */
.sync-modal-details summary:active { opacity: 0.6; }
/* „In neuem Tab öffnen“ ist Bedien-Chrome: kein iOS-Link-Callout, keine
   Textauswahl beim Long-Press. */
#fbl-open {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.loan-search-clear:active { background: rgba(255,255,255,0.14); color: var(--text); }

/* Long-Press auf Auswahl-Karten/-Labels markierte ihren Text (Auswahl-Griffe,
   iOS-Lupe) — gleiche App-Chrome-Behandlung wie die :where()-Liste oben. */
label.perm-option,
.settings-secure-group > label {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Eigener Druck-Zustand vorhanden → die generische :where()-Dimmung
   (--press-opacity, Agent „Global“) soll hier nicht zusätzlich greifen. */
button.status-pill-clickable:active,
.heatmap-mode-btn:active,
.decide-secondary-link:active,
.btn-logs-subtle.btn-logs-danger:active,
.btn-delete-user-inline:active,
.log-delete-btn:active,
.log-goto-loan:active,
.heatmap-cell.has-loan:active,
.link-btn:active,
.field-hint a:active,
#fbl-open:active,
.settings-section-head:active,
.user-more-summary:active,
.log-entry-summary:active,
.rn-head:active,
.kpi-card-clickable:active,
.rel-borrower-card:active,
.perm-history > summary:active,
.reliability-how-summary:active,
.sync-modal-details summary:active,
.logs-day-header:active,
.loan-search-clear:active,
.btn-pct:active,
.repay-rate-more:active,
.sync-modal-version-copy:active,
.reliability-loan-link:active,
.profile-device-rename-btn:active { --press-opacity: 1; }
/* Bewusst NICHT .toggle-label/.toggle-field/.fb-kind/.fb-mode: die Custom
   Property erbt auf ihr verstecktes Eingabefeld (opacity:0) — ein
   --press-opacity:1 dort würde die native Checkbox beim Drücken sichtbar
   machen. */

@media (prefers-reduced-motion: reduce) {
  button.status-pill-clickable:active,
  .heatmap-mode-btn:active,
  .decide-secondary-link:active,
  .btn-logs-subtle.btn-logs-danger:not(:disabled):active,
  .btn-delete-user-inline:not(:disabled):active,
  .log-delete-btn:active,
  .log-goto-loan:active { transform: none; }
}

/* Aufklapp-Pfeile: gleiche Dauer/Kurve wie der aufklappende Inhalt
   (::details-content bzw. animateCollapsible, beide --motion-base). */
.logs-day-chevron,
.btn-activity-toggle .chevron,
.sync-modal-details summary::before,
.reliability-how-summary::before,
.notification-chevron,
.perm-history > summary::before,
.timeline-counter-detail > summary .timeline-icon,
.loan-conversation > summary .chevron,
.rn-head .rn-chevron {
  transition: transform var(--motion-base) var(--ease-standard);
}

/* === Motion R2: Performance auf dem Handy ===
   Der Modal-Hintergrund blurrt den GANZEN Bildschirm und der Radius laeuft
   beim Oeffnen/Schliessen mit --modal-backdrop-k mit — jedes Frame der
   Sheet-Animation muss die volle Flaeche neu weichzeichnen, genau dann, wenn
   das Sheet selbst gleitet. Auf dem Handy bedeckt das Sheet ohnehin ~88 %
   der Hoehe und der Rest liegt unter 72 % Schwarz: der Blur ist dort kaum zu
   sehen, kostet aber am meisten. Abdunklung + gruener Schimmer bleiben. */
@media (max-width: 820px) {
  .modal-overlay,
  .modal-overlay.is-stacked-below.is-unstacking {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* Log-Eintraege (<details>) klappten als einzige Aufklapper noch hart auf. */
@supports selector(::details-content) {
  .log-entry { interpolate-size: allow-keywords; }
  .log-entry::details-content {
    height: 0;
    opacity: 0;
    overflow: hidden;
    transition:
      height var(--motion-base) var(--ease-standard),
      opacity var(--motion-base) var(--ease-standard),
      content-visibility var(--motion-base) allow-discrete;
  }
  .log-entry[open]::details-content { height: auto; opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    .log-entry::details-content { transition: none; height: auto; opacity: 1; }
  }
}

/* === Motion R2: wischbare Chip-Leisten ===
   Die Ausblendung am rechten Rand war statisch: auch ganz nach rechts
   gescrollt blieb der LETZTE Chip halb durchsichtig ("Fehlgeschlagen",
   "Archiv" …), und links gab es nie einen Hinweis, dass schon gescrollt ist.
   Mit scroll-getriebener Animation folgt die Maske der Scrollposition:
   links nur, wenn es links weitergeht, rechts nur, wenn rechts noch etwas
   kommt; ohne Ueberlauf gar keine (inaktive Zeitleiste = Initialwert 0).
   Ohne Unterstuetzung bleibt die bisherige statische Ausblendung.
   Dazu sanftes Einrasten auf Chip-Anfaenge (proximity: nur in der Naehe). */
@property --strip-fade-l { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --strip-fade-r { syntax: '<length>'; inherits: false; initial-value: 0px; }
@keyframes stripEdgeFade {
  0%   { --strip-fade-l: 0px;  --strip-fade-r: 28px; }
  8%   { --strip-fade-l: 28px; }
  92%  { --strip-fade-r: 28px; }
  100% { --strip-fade-l: 28px; --strip-fade-r: 0px; }
}
.log-filters,
#notifications-modal-body .notifications-filters,
.rel-breakdown-filters,
.stats-jump-nav,
#main-content .tab-bar {
  scroll-snap-type: x proximity;
  scroll-padding-inline: 4px;
}
.log-filters > *,
#notifications-modal-body .notifications-filters > *,
.rel-breakdown-filters > *,
.stats-jump-nav > * {
  scroll-snap-align: start;
}
@supports (animation-timeline: scroll()) {
  .log-filters,
  #notifications-modal-body .notifications-filters,
  .rel-breakdown-filters,
  .stats-jump-nav,
  #main-content .tab-bar {
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--strip-fade-l), #000 calc(100% - var(--strip-fade-r)), transparent 100%);
            mask-image: linear-gradient(to right, transparent 0, #000 var(--strip-fade-l), #000 calc(100% - var(--strip-fade-r)), transparent 100%);
    animation: stripEdgeFade linear both;
    animation-timeline: scroll(self inline);
  }
}

@media (prefers-reduced-motion: reduce) {
  .toggle-control:not(:has(input:disabled)):active .toggle-thumb { width: 20px; }
  .toggle-control:not(:has(input:disabled)):active input[type="checkbox"]:checked + .toggle-track .toggle-thumb { transform: translateX(20px); }
  .toggle-label:not(.toggle-label--disabled):active .toggle-thumb { width: 20px; }
  .toggle-label:not(.toggle-label--disabled):active input[type="checkbox"]:checked + .toggle-track .toggle-thumb { transform: translateX(20px); }
  input[type="checkbox"]:not(:disabled):active,
  input[type="radio"]:not(:disabled):active,
  .nu-preset:active,
  .perm-option:not(:has(input:disabled)):active,
  .reminder-chip:active span { transform: none; }
}

/* NOTE: no `.modal-card.closing` rule here! Desktop exit lives at the base
   rule (modalOut, ~Z. 2323); mobile slides the sheet out via sheetDown
   inside @media (max-width: 820px). A duplicate at this point in the file
   would win the cascade over the media-query rule and silently replace the
   mobile slide-out with a fade-in-place (that bug shipped once already). */

@keyframes toastInSoft {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0);    }
}
@keyframes toastOutSoft {
  to   { opacity: 0; transform: translateY(6px); }
}

/* === Global reduced-motion guard ===
   The keyframes above are already gated where they're declared, but this
   blanket rule covers any future addition that forgets the gate. */
@media (prefers-reduced-motion: reduce) {
  .loan-card,
  .loan-card-lender,
  .history-card,
  .loan-progress-bar.is-bumped .loan-progress-bar-fill,
  .dashboard-value.is-tweening,
  .modal-card.is-dragging-close,
  .toast,
  .toast.is-leaving,
  .toast.toast-out {
    animation-duration: 0.001s !important;
    animation-iteration-count: 1 !important;
  }
  /* Motion-Audit: nur der Maus-Hover. `.loan-card-lender:hover/:active` mit
     !important schlug das Inline-transform der Wisch-Geste — auf Touch klebt
     :hover nach dem Tippen, die Karte folgte unter Reduce Motion dem Finger
     nicht (gemessen 0 px bei 60 px). Einen Karten-Press gibt es nicht mehr. */
  html:not(.using-touch) .loan-card-lender:hover,
  .dashboard-card-clickable:active,
  html:not(.using-touch) .dashboard-card-clickable:hover,
  .btn-primary:active,
  .btn-secondary:active,
  .btn-danger:active,
  .tab-pill:active,
  .loan-actions > button:hover,
  .loan-actions > button:active,
  .btn-success-flash {
    transform: none !important;
  }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.001s !important;
  }
}

/* R6-Close: Reduce Motion — dialogs and sheets do not jump in and out but
   fade briefly (opacity only, no slide/scale); the dimming fades along.
   Before, both vanished in one frame (0.001s above / 0.01ms global guard).
   Specific enough to outrank the global `*` guard; a released swipe fades
   where the finger left it (app.js closeModal keeps its transform). */
@media (prefers-reduced-motion: reduce) {
  .modal-overlay > .modal-card {
    animation: modalFade var(--motion-fast) var(--ease-standard) backwards !important;
  }
  .modal-overlay > .modal-card.closing,
  .modal-overlay > .modal-card.closing-success {
    animation: modalFadeOut var(--motion-fast) var(--ease-close) both !important;
  }
  .modal-overlay:not(.is-nested) {
    animation-duration: var(--motion-fast) !important;
  }
}
@keyframes modalFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes modalFadeOut {
  to   { opacity: 0; }
}

/* === Char counter (generic, driven by data-char-counter attribute) === */
/* Befund 18: Der Zaehler stand im Fluss NEBEN dem Feld. #nl-purpose war damit
   das einzige Feld im "Neue Leihe"-Formular, das nicht bis zum Rand ging —
   sichtbar ausgefranste rechte Kante. Der Zaehler rutscht jetzt unter das Feld
   und rechtsbuendig, wie ein .field-hint. Gilt fuer alle
   data-char-counter-Felder, damit es EIN Muster bleibt. */
.char-counter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.15rem 0.4rem;
}
.char-counter-row > input,
.char-counter-row > textarea,
.char-counter-row > .char-counter-field-wrap {
  flex: 1 1 100%;
  min-width: 0;
}
.char-counter-row > .char-counter-display { margin-left: auto; }
/* Positioning context for a .mi-attach-btn moved in here (see the
   data-char-counter init in app.js) — sized to the textarea alone via its
   own width:100%, so the button anchors correctly regardless of the
   counter's rendered width. */
.char-counter-field-wrap { position: relative; }
.char-counter-display {
  font-size: 0.72rem;
  color: var(--text-muted);
  white-space: nowrap;
  padding-bottom: 0.1rem;
  flex-shrink: 0;
}
.char-counter-display.is-near-limit { color: var(--warning); }
.char-counter-display.is-at-limit   { color: var(--danger); font-weight: 700; }

/* === Stats: Sticky-Header (User-Filter-Pills + Sprungnavigation) === */
.stats-sticky-head {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--surface);
  padding-top: 0.25rem;
}
.stats-sticky-head .stats-filter-pills { margin: 0 0 0.45rem; }
/* Etwas Luft zwischen den sticky User-Tabs und dem Inhalt darunter (Reliability
   hat – anders als Stats – keine Sprungnav, daher sonst zu knapp). Opakes
   padding am Sticky-Element, damit kein Inhalt in der Lücke durchscrollt. */
#reliability-modal-body .stats-sticky-head { padding-bottom: 0.5rem; }
.stats-jump-nav {
  display: flex;
  /* zentriert; "safe" fällt bei Overflow auf start zurück, damit der
     erste Chip auf schmalen Screens nicht aus dem Scrollbereich clippt */
  justify-content: center;
  justify-content: safe center;
  gap: 0.4rem;
  /* 0.15rem top left no room for the chips' focus ring — overflow-x also
     clips vertically (implicit overflow-y: auto). */
  padding: 0.35rem 0 0.55rem;
  margin-bottom: 0.25rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.stats-jump-nav::-webkit-scrollbar { display: none; }
.stats-jump-chip { flex-shrink: 0; }

/* === Stats: Heatmap-Modus-Toggle (Verliehen / Zurückgezahlt) === */
.heatmap-mode-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.heatmap-mode-btn {
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius-xs);
  cursor: pointer;
  /* D6: gemessen 70x19 / 97x19 px — auch unter pointer:coarse. WCAG 2.5.5
     verlangt 24px, 2.5.8 ebenfalls. 24px Grundhoehe, auf Touch die
     App-Norm --touch-target. */
  min-height: 24px;
}
@media (pointer: coarse) {
  .heatmap-mode-btn { min-height: var(--touch-target); }
}
.heatmap-mode-btn.is-active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
}

/* === Stats: Purpose-Kategorien (horizontale Balken) === */
.purpose-table {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin-top: 0.4rem;
}
.purpose-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.2rem 0.6rem;
  align-items: baseline;
}
.purpose-label { font-size: 0.85rem; min-width: 0; overflow-wrap: anywhere; }
.purpose-meta  { font-size: 0.82rem; color: var(--text-muted); }
.purpose-meta small { opacity: 0.7; }
.purpose-bar-track {
  grid-column: 1 / -1;
  height: 6px;
  /* 4px auf einem 6px hohen Balken war eine eigene Zwischenstufe fuer das,
     was jede andere Fortschrittsleiste der App als Pille loest. */
  border-radius: var(--radius-pill);
  background: rgba(255,255,255,0.06);
  overflow: hidden;
}
.purpose-bar-fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--accent);
  transition: width var(--motion-slow) var(--ease-standard);
}

/* Gesamtsumme rechts in der Sektionsüberschrift */
.purpose-total {
  float: right;
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* Sonstiges-Zeile: klickbare Affordance + Aufklapp-Detail */
.purpose-row--expandable { cursor: pointer; }
.purpose-row--expandable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
.purpose-caret {
  display: inline-flex;
  align-items: center;
  font-size: 0.7rem;
  opacity: 0.6;
}
.purpose-other-detail {
  grid-column: 1 / -1;
  margin-top: 0.3rem;
  padding: 0.4rem 0.55rem;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,0.04);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  /* Cap height so a long unkategorisiert list scrolls instead of pushing the
     rest of the stats off-screen. */
  max-height: 220px;
  overflow-y: auto;
}
/* The [hidden] attribute is display:none with low priority — our explicit
   display:flex above would otherwise win and keep the panel always open. */
.purpose-other-detail[hidden] { display: none; }
.purpose-other-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.purpose-other-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.purpose-other-item > span.tabular-nums { flex: 0 0 auto; }
.purpose-other-more { font-style: italic; opacity: 0.7; }
/* "→ als Kategorie"-Button je Sonstiges-Text */
.purpose-make-cat {
  flex: 0 0 auto;
  font-size: 0.72rem;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--accent);
  background: transparent;
  color: var(--accent);
  cursor: pointer;
  white-space: nowrap;
}
html:not(.using-touch) .purpose-make-cat:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.purpose-make-cat:active { transform: scale(0.96); }

/* Warnung: viel unkategorisiert */
.purpose-uncat-hint {
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.linklike {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  text-decoration: underline;
}

/* === Stats: Cashflow-Balkenchart === */
.cf-bar-out { fill: color-mix(in srgb, var(--danger) 75%, transparent); }
.cf-bar-in  { fill: var(--accent); }
.cf-legend {
  display: flex;
  gap: 1rem;
  font-size: 0.75rem;
  color: var(--text-muted);
  margin: 0.3rem 0 0.4rem;
}
.cf-legend-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 3px;
  margin-right: 0.3rem;
  vertical-align: -1px;
}
.cf-legend-out { background: color-mix(in srgb, var(--danger) 75%, transparent); }
.cf-legend-in  { background: var(--accent); }
/* Cashflow-Hover: Monatsspalte hinterlegt, Tooltip mit beiden Werten. */
.cf-hover-band { fill: rgba(255,255,255,0.05); pointer-events: none; }
.cf-tip-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--text-muted);
}
.cf-tip-row .cf-legend-swatch { margin-right: 0; }
.cf-tip-row b {
  margin-left: auto;
  padding-left: 0.75rem;
  color: var(--text);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* === Saved Purposes: Chips + Merken-Button === */
.purpose-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.45rem;
}
/* Befund 12: Zweck-Chips und Schnellantwort-Chips sind zwei unabhaengige
   Loesungen fuer dieselbe Idee. Grundflaeche, Rahmen, Accent-Tint ("das ist
   meins"), Chip-Hoehe und max-width sind jetzt identisch zu
   .quick-reply-chip-saved-wrap. rgba(255,255,255,.03) liess die Chips wie
   ausgegraut wirken, 160px schnitt fast jeden Zweck ab. */
.purpose-chip {
  display: inline-flex;
  align-items: center;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, rgba(255,255,255,0.15));
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--accent) 7%, rgba(255,255,255,0.08));
  overflow: hidden;
  /* Motion-Audit: Auswahl (_syncPurposeChipSelection toggelt .is-selected in
     place) blendete hart um; jetzt gleiches Tempo wie die Schnellantwort-
     Chips, dazu derselbe Einwaerts-Press wie die Filter-Pills. */
  transition: background-color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}
.purpose-chip:has(> .purpose-chip-label:active) {
  transform: scale(0.96);
  transition-duration: var(--motion-xs);
}
@media (prefers-reduced-motion: reduce) {
  .purpose-chip:has(> .purpose-chip-label:active) { transform: none; }
}
.purpose-chip.is-selected {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: var(--accent);
}
.purpose-chip.is-selected .purpose-chip-label { color: var(--accent); font-weight: 600; }
.purpose-chip-label {
  padding: 0.3rem 0.5rem 0.3rem 0.75rem;
  font-size: 0.84rem;
  color: var(--text);
  background: none;
  border: none;
  cursor: pointer;
  /* Der Label-Button ist die Hauptaktion des Chips und muss die
     Mindest-Zielgroesse erfuellen wie ✎/×. */
  min-height: 36px;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.3;
  transition: color var(--transition-fast);
}
/* .purpose-chip hat overflow:hidden -> ein Fokusring mit dem globalen
   outline-offset:2px wird abgeschnitten. Die ✎/×-Buttons kompensieren das
   bereits, der Label-Button (= Hauptaktion) tat es nicht. */
.purpose-chip-label:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
html:not(.using-touch) .purpose-chip-label:hover { color: var(--accent); }

/* Befund 12, zweiter Teil: Zweck-Chips zeigten ✎ und × permanent, die
   Schnellantwort-Chips verstecken beides hinter einem "⋯". Ab jetzt beide
   gleich — dieselben Regeln wie .quick-reply-chip-manage/-actions, nur mit
   dem eigenen Praefix. Der Trigger bleibt im aufgeklappten Zustand SICHTBAR
   und faerbt sich accent-gruen; ein `display: none` hier war bei den
   Schnellantworten der Fehler, der das Zuklappen unmoeglich machte
   (report_09 C1). */
.purpose-chip-manage,
.purpose-chip-edit,
.purpose-chip-delete {
  /* inline-flex + center: die drei Buttons tragen jetzt Inline-SVGs statt
     der Zeichen ⋯/✎/×. Ein inline-SVG saesse sonst auf der Textbasislinie
     und haenge unter der Mitte des Chips. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0.3rem 0.5rem;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--text-muted);
  background: none;
  border: none;
  border-left: 1px solid rgba(255,255,255,0.15);
  cursor: pointer;
  transition: color var(--transition-fast);
}
/* Die Icons bringen ihre eigenen fill/stroke-Attribute mit (Hausformat:
   fill="none" stroke="currentColor", siehe der SVG-Helper in app.js), deshalb
   hier KEIN fill/stroke-Override — das wuerde ein gefuelltes Icon unsichtbar
   machen, weil CSS die Praesentationsattribute schlaegt. Nur Groesse als
   Rueckfalloption (falls ohne width/height eingesetzt) und pointer-events,
   damit ein Klick auf das Icon den Button trifft und nicht das SVG. */
.purpose-chip-manage svg,
.purpose-chip-edit svg,
.purpose-chip-delete svg,
.purpose-chip-label svg,
.quick-reply-chip-manage svg,
.quick-reply-chip-edit svg,
.quick-reply-chip-delete svg {
  flex: 0 0 auto;
  pointer-events: none;
}
.purpose-chip-manage svg:not([width]),
.purpose-chip-edit svg:not([width]),
.purpose-chip-delete svg:not([width]),
.quick-reply-chip-manage svg:not([width]),
.quick-reply-chip-edit svg:not([width]),
.quick-reply-chip-delete svg:not([width]) {
  width: 1.05em;
  height: 1.05em;
}
/* Gegenstuecke bei den Schnellantwort-Chips: dieselbe Zentrierung, sonst
   sitzt ein Inline-SVG dort auf der Textbasislinie. */
.quick-reply-chip-manage,
.quick-reply-chip-edit,
.quick-reply-chip-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.purpose-chip-manage:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
html:not(.using-touch) .purpose-chip-manage:hover { color: var(--accent); }
/* Zwei Haken, damit es egal ist, ob app.js die Klasse setzt oder nur
   aria-expanded umschaltet. */
.purpose-chip.is-expanded .purpose-chip-manage,
.purpose-chip .purpose-chip-manage[aria-expanded="true"] {
  color: var(--accent);
}
.purpose-chip-actions { display: none; }
.purpose-chip.is-expanded .purpose-chip-actions,
.purpose-chip:has(.purpose-chip-manage[aria-expanded="true"]) .purpose-chip-actions {
  display: inline-flex;
  align-items: center;
}

/* No ::after hit-area expansion — same reasoning as the quick-reply chips'
   edit/delete pair above (and message-images.css's .mi-thumb-edit/-remove):
   edge-to-edge icons, an invisible expanded zone would overlap between them. */
html:not(.using-touch) .purpose-chip-edit:hover { color: var(--accent); }
html:not(.using-touch) .purpose-chip-delete:hover { color: var(--danger); }
.purpose-chip-edit:focus-visible,
.purpose-chip-delete:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Leerzustand der Zweck-Chips (app.js _renderPurposeChips). Ohne diese Zeile
   war der Container bei null gemerkten Zwecken ganz ausgeblendet: unter
   "Zweck" stand dann nichts, waehrend unter "Nachricht" sofort eingebaute
   Schnellvorlagen-Chips stehen — das Feature wirkte hier schlicht nicht
   vorhanden. Bewusst klein und --text-muted: es ist ein einmaliger Hinweis,
   keine Handlungsaufforderung, und er verschwindet wieder, sobald
   "+ Merken" sichtbar wird. */
.purpose-chips-empty {
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--text-muted);
  padding: 0.1rem 0.1rem 0;
}

/* Der "⋯"-Trigger war in beiden Chip-Familien ein blasses Glyph ohne eigene
   Flaeche — 32x36 px in --text-muted, vom Label nur durch die border-left
   getrennt. Eine leichte eigene Flaeche macht sichtbar, dass da eine zweite
   Schaltflaeche sitzt (Split-Button-Muster), ohne den Chip breiter zu machen
   oder ein neues Farbsystem einzufuehren. Steht absichtlich NACH den
   Basisregeln beider Familien (background: none). */
.purpose-chip-manage,
.quick-reply-chip-manage {
  background: rgba(255,255,255,0.06);
}
html:not(.using-touch) .purpose-chip-manage:hover,
html:not(.using-touch) .quick-reply-chip-manage:hover {
  background: rgba(255,255,255,0.12);
}
/* Aufgeklappt faerbt sich der Trigger bereits accent (siehe oben) — die
   Flaeche zieht mit, sonst wirkt der aktive Zustand blasser als der Ruhe-
   zustand daneben. */
.purpose-chip.is-expanded .purpose-chip-manage,
.purpose-chip .purpose-chip-manage[aria-expanded="true"],
.quick-reply-chip-saved-wrap.is-expanded .quick-reply-chip-manage {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/* === R13-Anim: Chip-Aktionen weich auf- und zuklappen ====================
   app.js animateChipRow(): die Aktionsleiste waechst/schrumpft in der Breite,
   die Nachbar-Chips gleiten per `translate` (FLIP, auch ueber Zeilen-
   umbrueche), die Reihe animiert ihre Hoehe. Hier nur, was CSS dazu braucht:
   - Beim Schliessen bleibt die Leiste sichtbar, bis sie zu ist (.is-collapsing).
   - align-content: flex-start — eine per Animation hoehere Reihe darf ihre
     Zeilen nicht strecken (bei height:auto wirkungslos, also kein Unterschied
     im Ruhezustand).
   - Das ⋯ dreht sich zum ✕ (beide Icons liegen uebereinander im Ausloeser).
   - Eigenes Druck-Feedback fuer ⋯/✎/Papierkorb statt der generischen Dimmung. */
.quick-replies,
.purpose-chips { align-content: flex-start; }
.quick-reply-chip-saved-wrap.is-collapsing .quick-reply-chip-actions,
.purpose-chip.is-collapsing .purpose-chip-actions {
  display: inline-flex;
  align-items: center;
}
.quick-reply-chip-manage,
.purpose-chip-manage { position: relative; }
.quick-reply-chip-manage,
.quick-reply-chip-edit,
.quick-reply-chip-delete,
.purpose-chip-manage,
.purpose-chip-edit,
.purpose-chip-delete { transition: color var(--transition-fast), background-color var(--transition-fast); }
.chip-manage-ico {
  transition:
    opacity var(--motion-fast) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard),
    scale var(--motion-xs) var(--ease-standard);
}
.chip-manage-ico--close {
  position: absolute;
  inset: 0;
  margin: auto;
  opacity: 0;
  transform: rotate(-90deg) scale(0.5);
}
.quick-reply-chip-manage[aria-expanded="true"] .chip-manage-ico--dots,
.purpose-chip-manage[aria-expanded="true"] .chip-manage-ico--dots {
  opacity: 0;
  transform: rotate(90deg) scale(0.5);
}
.quick-reply-chip-manage[aria-expanded="true"] .chip-manage-ico--close,
.purpose-chip-manage[aria-expanded="true"] .chip-manage-ico--close {
  opacity: 1;
  transform: none;
}
.quick-reply-chip-edit svg,
.quick-reply-chip-delete svg,
.purpose-chip-edit svg,
.purpose-chip-delete svg { transition: scale var(--motion-xs) var(--ease-standard); }
/* Druck: Flaeche in der aktuellen Textfarbe (grau, gruen wenn offen, rot beim
   Loeschen-Hover) plus leicht einsinkendes Icon. Zweimal formuliert, weil die
   :hover-Regeln (html:not(.using-touch) …) sonst die Flaeche gewinnen. */
.quick-reply-chip-manage:active,
.quick-reply-chip-edit:active,
.quick-reply-chip-delete:active,
.purpose-chip-manage:active,
.purpose-chip-edit:active,
.purpose-chip-delete:active,
html:not(.using-touch) .quick-reply-chip-manage:active,
html:not(.using-touch) .quick-reply-chip-edit:active,
html:not(.using-touch) .quick-reply-chip-delete:active,
html:not(.using-touch) .purpose-chip-manage:active,
html:not(.using-touch) .purpose-chip-edit:active,
html:not(.using-touch) .purpose-chip-delete:active {
  --press-opacity: 1;
  background-color: color-mix(in srgb, currentColor 18%, transparent);
  transition-duration: var(--motion-xs);
}
.quick-reply-chip-manage:active svg,
.quick-reply-chip-edit:active svg,
.quick-reply-chip-delete:active svg,
.purpose-chip-manage:active svg,
.purpose-chip-edit:active svg,
.purpose-chip-delete:active svg { scale: 0.86; }
/* Der Label-Knopf eines gemerkten Chips sass mit scale(0.96) IN seinem
   geclippten Rahmen — gedrueckt wird jetzt der ganze Chip, wie bei den
   Zweck-Chips (.purpose-chip:has(> .purpose-chip-label:active)). */
.quick-reply-chip-saved-wrap {
  transition: background-color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}
.quick-reply-chip-saved-wrap .quick-reply-chip-saved:active { transform: none; }
.quick-reply-chip-saved-wrap:has(> .quick-reply-chip-saved:active) {
  transform: scale(0.96);
  transition-duration: var(--motion-xs);
}
@media (prefers-reduced-motion: reduce) {
  .chip-manage-ico,
  .quick-reply-chip-edit svg,
  .quick-reply-chip-delete svg,
  .purpose-chip-edit svg,
  .purpose-chip-delete svg { transition: none; }
  .quick-reply-chip-saved-wrap:has(> .quick-reply-chip-saved:active) { transform: none; }
}

.purpose-save-btn {
  margin-top: 0.4rem;
  /* Hatte gar kein min-height: live gemessen 0px, effektiv ~26px. Das
     Gegenstueck .quick-reply-save-btn hat 36px — gleiche Komponente,
     gleiche Zielgroesse. */
  min-height: 36px;
  padding: 0.35rem 0.75rem;
  font-size: 0.84rem;
  border: 1px dashed color-mix(in srgb, var(--accent) 50%, transparent);
  border-radius: var(--radius-xs);
  background: none;
  color: var(--accent);
  cursor: pointer;
  transition: var(--transition-ui);
}
html:not(.using-touch) .purpose-save-btn:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.purpose-chips-limit {
  width: 100%;
  font-size: 0.75rem;
  color: var(--danger-text);
  margin-top: 0.2rem;
  padding: 0 0.1rem;
}
.purpose-chips-limit-warn {
  color: var(--text-muted);
}

/* --- Release Notes ---------------------------------------------------------- */
#modal-release-notes .modal-card { max-width: var(--modal-w-md); }

/* Unread-count pill on the nav item (inline) and burger button (absolute) —
   same look as .header-icon-badge next to the bell. */
.rn-nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.05rem;
  height: 1.05rem;
  padding: 0 0.25rem;
  margin-left: 0.35rem;
  border-radius: 999px;
  background: var(--danger);
  /* Weiss auf --danger = 3.82:1. --bg auf --danger = 5.02:1.
     font-size war 0.62rem = 9.92px, unter der --text-xs-Untergrenze (11.2px). */
  color: var(--bg);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1;
  vertical-align: middle;
}
.btn-menu { position: relative; }
.rn-menu-badge {
  position: absolute;
  top: 0.15rem;
  right: 0.15rem;
  min-width: 1.05rem;
  height: 1.05rem;
  padding: 0 0.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--danger);
  /* Weiss auf --danger = 3.82:1. --bg auf --danger = 5.02:1.
     font-size war 0.62rem = 9.92px, unter der --text-xs-Untergrenze (11.2px). */
  color: var(--bg);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1;
  box-shadow: 0 0 0 2px var(--surface);
}

.rn-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin: 0.4rem 0 0.6rem;
}
.rn-empty { color: var(--text-muted); text-align: center; padding: 1rem 0; }

.rn-item {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface2);
  /* No overflow:hidden here (unlike most cards): it would establish this box
     as the nearest scroll container for position:sticky below, capping the
     sticky head's stickiness to this card's own bounds instead of the modal's
     real scroll container. Nothing inside paints a background that needs
     corner-clipping, so this is safe. */
}
.rn-item-unread { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

.rn-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  width: 100%;
  padding: 0.7rem 0.8rem;
  background: none;
  border: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  min-height: var(--touch-target);
}

/* Sticky section header while scrolled — same mechanism as
   .settings-section-head (see there for why only the OPEN item's head gets
   this, never a closed one: stacking every head regardless of state is
   exactly the reverted "Fix 4" bug in this file). top: -1px + JS-toggled
   .is-stuck squaring the corners: identical trick, reused via
   wireStickySectionHead() in app.js. */
.rn-head[aria-expanded="true"] {
  position: sticky;
  top: -1px;
  padding-top: calc(0.7rem + 1px);
  /* Below .modal-sticky-header (z-index: 5): once a long-open entry scrolls
     fully past the top, its header briefly detaches through the title bar's
     own vertical range before leaving the viewport — a lower z-index keeps
     the title bar (and its close button) on top through that transition
     instead of being covered by the departing entry header. */
  z-index: 2;
  background: var(--surface2);
  border-radius: var(--radius) var(--radius) 0 0;
}
.rn-head[aria-expanded="true"].is-stuck {
  border-radius: 0;
}
.rn-head-main {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
}
/* Feste Spaltenbreite + Tabellenziffern: "v2.11.0" und "v2.9.8" waren
   unterschiedlich breit, die Titel daneben begannen je Zeile woanders. */
.rn-version { font-weight: 700; white-space: nowrap; min-width: 4.4em; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.rn-title {
  color: var(--text-muted);
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rn-head-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}
.rn-date { color: var(--text-muted); font-size: 0.78rem; white-space: nowrap; }
.rn-chevron {
  color: var(--text-muted);
  font-size: 0.8rem;
  transition: transform var(--transition-fast, var(--motion-fast));
}
.rn-head[aria-expanded="true"] .rn-chevron { transform: rotate(180deg); }

.rn-new-chip {
  padding: 0.1rem 0.45rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
}

.rn-body { border-top: 1px solid var(--border); }
/* Publish date inside the body — only shown on narrow screens where the
   head date is hidden to give the title room (see media query below). */
.rn-body-date {
  display: none;
  padding: 0.55rem 0.8rem 0;
  color: var(--text-muted);
  font-size: 0.75rem;
}
.rn-entries {
  list-style: none;
  margin: 0;
  padding: 0.6rem 0.8rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.rn-entry {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
}
.rn-entry-text {
  flex: 1;
  min-width: 0;               /* let the flex child shrink below content size */
  overflow-wrap: anywhere;    /* long words/URLs must never blow the card */
  font-size: 0.88rem;
  line-height: 1.45;
}
/* Deep-link as a small pill on its own line: real touch target without
   inflating the text line-box it would sit in as an inline element. */
.rn-entry-link {
  display: flex;
  width: fit-content;
  align-items: center;
  min-height: 2.25rem;
  margin: 0.35rem 0 0;
  padding: 0 0.65rem;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: var(--radius-xs);
  color: var(--accent);
  font-size: 0.8rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
html:not(.using-touch) .rn-entry-link:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  text-decoration: none;
}
/* Runde 4 (Native): the "Ansehen" pill is a button-looking link — press state,
   and no text selection / iOS link callout on long-press. */
.rn-entry-link {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.rn-entry-link:active,
html:not(.using-touch) .rn-entry-link:active { --press-opacity: 1; background: color-mix(in srgb, var(--accent) 20%, transparent); }

.rn-tag {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 4.6rem;          /* uniform column: text starts aligned across rows */
  margin-top: 0.1rem;
  padding: 0.12rem 0.5rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

/* Read items step back a little so unread ones carry the visual weight. */
.rn-item:not(.rn-item-unread) .rn-version { color: var(--text-muted); }

/* Narrow phones: the head date steals the title's room — move it into the
   body and let the title wrap to two lines instead of hard ellipsis. */
@media (max-width: 420px) {
  .rn-date { display: none; }
  .rn-body-date { display: block; }
  .rn-title {
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

/* Very narrow: the hanging tag column costs too much text width — stack the
   tag above its text instead. */
@media (max-width: 380px) {
  .rn-entry { flex-direction: column; gap: 0.25rem; }
  .rn-tag { align-self: flex-start; min-width: 0; }
}
.rn-tag-added    { background: color-mix(in srgb, var(--accent) 16%, transparent); color: #7fd4a8; }
.rn-tag-changed  { background: color-mix(in srgb, var(--warning) 16%, transparent); color: #d9bc6a; }
.rn-tag-fixed    { background: rgba(154, 180, 255, 0.14); color: #aebfff; }
.rn-tag-security { background: color-mix(in srgb, var(--danger) 16%, transparent);  color: #e88; }
.rn-tag-removed  { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }

/* ==========================================================================
   Filter-pill interaction
   One shared feel for every filter chip in the app: the logs category chips,
   the notification channel chips, the reliability breakdown pills, the stats
   jump-nav chips and the tab pills.

   The press state scales INWARD on purpose. Several of these strips are
   horizontal scrollers (.stats-jump-nav, .rel-breakdown-filters) and setting
   only overflow-x makes the browser compute overflow-y as `auto` — anything
   that grows past the top edge gets clipped. A lift-on-tap would be shaved
   off; a scale-down never can be. Hover lift stays desktop-only.
   Declared last so it wins over the per-component transitions above.
   ========================================================================== */
.log-filter-btn,
.logs-archive-toggle,
.rel-filter-pill,
.stats-jump-chip,
.tab-pill,
.notifications-filters .status-pill {
  transition:
    transform var(--motion-fast) var(--ease-spring, cubic-bezier(.34, 1.4, .64, 1)),
    background var(--transition-fast),
    border-color var(--transition-fast),
    color var(--transition-fast),
    box-shadow var(--transition-fast);
  /* Kill the grey iOS flash — the scale press is the tap feedback now. */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* Going down is snappy, coming back up rides the spring above.
   This tier serves touch, where the :hover rules never match. */
.log-filter-btn:active,
.logs-archive-toggle:active,
.rel-filter-pill:active,
.stats-jump-chip:active,
.tab-pill:active,
.notifications-filters .status-pill:active {
  transform: scale(0.93);
  transition-duration: var(--motion-xs);
}

/* Non-touch pointers get a light uplift; touch devices never see :hover. */
html:not(.using-touch) .rel-filter-pill:hover,
html:not(.using-touch) .stats-jump-chip:hover,
html:not(.using-touch) .notifications-filters .status-pill:hover {
  transform: translateY(-1px);
}

/* Mouse tier. A pointer press keeps :hover active the whole time, and those
   hover selectors carry html + :not() + class + :hover — more specific than a
   bare .x:active, so their translateY won and a click showed no press at all.
   These mirror that specificity exactly and sit after them, so the press wins
   while the button is held. Gated on :not(.using-touch) so touch keeps using
   the tier above. */
html:not(.using-touch) .log-filter-btn:active,
html:not(.using-touch) .logs-archive-toggle:active,
html:not(.using-touch) .rel-filter-pill:active,
html:not(.using-touch) .stats-jump-chip:active,
html:not(.using-touch) .tab-pill:active,
html:not(.using-touch) .notifications-filters .status-pill:active {
  transform: scale(0.93);
  transition-duration: var(--motion-xs);
}

@media (prefers-reduced-motion: reduce) {
  .log-filter-btn,
  .logs-archive-toggle,
  .rel-filter-pill,
  .stats-jump-chip,
  .tab-pill,
  .notifications-filters .status-pill {
    transition: background var(--transition-fast), color var(--transition-fast);
  }
  .log-filter-btn:active,
  .logs-archive-toggle:active,
  .rel-filter-pill:active,
  .stats-jump-chip:active,
  .tab-pill:active,
  .notifications-filters .status-pill:active,
  html:not(.using-touch) .rel-filter-pill:hover,
  html:not(.using-touch) .stats-jump-chip:hover,
  html:not(.using-touch) .notifications-filters .status-pill:hover,
  /* The mouse tier needs its own reset — it outranks the bare :active rules. */
  html:not(.using-touch) .log-filter-btn:active,
  html:not(.using-touch) .logs-archive-toggle:active,
  html:not(.using-touch) .rel-filter-pill:active,
  html:not(.using-touch) .stats-jump-chip:active,
  html:not(.using-touch) .tab-pill:active,
  html:not(.using-touch) .notifications-filters .status-pill:active {
    transform: none;
  }
}

/* ===========================================================================
   Chip-Basis (Befund 11)
   ---------------------------------------------------------------------------
   Es gab neun Varianten derselben "kleiner Status-Chip"-Komponente: acht
   Schriftgroessen (0.65-0.82rem), zehn Padding-Paare und drei Radien. Am
   sichtbarsten nebeneinander: .role-pill "ADMIN" neben .status-pill
   "✓ Onboarded", .badge "Bestaetigt" ueber .signal-pill "Teilweise bezahlt".
   Die beiden juengsten Features (.fb-flag, .lend-limit-badge) fuehrten als
   einzige vollrunde Pillen ein.

   Statt neue Klassennamen ins Markup zu tragen, aliasen die bestehenden
   Klassen auf EINE Basis mit zwei Groessen. Farben/Fuellungen bleiben, wo sie
   sind — die Modifier (.badge-pending, .role-pill-admin, .status-pill-on …)
   setzen ausschliesslich background/color und sind davon nicht betroffen.
   Bewusst am Dateiende: dieser Block soll die verstreuten Alt-Werte
   ueberschreiben. Hoehere Spezifitaeten (z. B. button.status-pill mit
   --touch-target) gewinnen weiterhin.
   =========================================================================== */
.chip,
.badge,
.badge-compact,
.role-pill,
.status-pill,
.signal-pill,
.taxi-detail-pill,
.taxi-detail-status,
.history-filter-pill,
.rel-filter-pill,
.lend-limit-badge {
  --chip-fs: var(--text-sm);
  --chip-pad-y: 0.2rem;
  --chip-pad-x: 0.55rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.28rem;
  padding: var(--chip-pad-y) var(--chip-pad-x);
  border-radius: var(--radius-sm);
  font-size: var(--chip-fs);
  font-weight: 700;
  line-height: 1.3;
  white-space: nowrap;
  vertical-align: middle;
}

/* --size: sm — Mikro-Label, meist versal (Rolle, Limit, Status-Kuerzel). */
.badge-compact,
.role-pill,
.lend-limit-badge {
  --chip-fs: var(--text-xs);
  --chip-pad-y: 0.12rem;
  --chip-pad-x: 0.45rem;
}

/* --size: md ist die Basis oben. Interaktive Filter-Pillen brauchen etwas
   mehr Polsterung, bleiben aber auf derselben Schriftgroesse. */
.history-filter-pill,
.rel-filter-pill {
  --chip-pad-y: 0.35rem;
  --chip-pad-x: 0.7rem;
}

/* ===========================================================================
   Tablet-Landscape (report_10, Vorschlag 6)
   ---------------------------------------------------------------------------
   1024x768 und 1366x1024 laufen in den Desktop-Zweig — die App haelt sie fuer
   Maus-Geraete, weil alle Touch-Anpassungen an `max-width` oder an
   `(hover: none) and (pointer: coarse)` OHNE Breiten-Bedingung haengen und die
   Desktop-Regeln danach wieder feiner werden. Dieser Block holt die
   Trefferflaechen auf die App-Norm zurueck, ohne das Maus-Layout anzufassen.
   =========================================================================== */
@media (pointer: coarse) and (min-width: 821px) {
  .tab-pill,
  .history-filter-pill,
  .rel-filter-pill,
  .stats-jump-chip,
  .heatmap-mode-btn,
  .btn-sm,
  .loan-search-clear,
  .purpose-save-btn,
  .quick-reply-save-btn,
  .purpose-chip-label,
  /* Fehlte hier als einziger der sechs Chip-Knoepfe: der "⋯"-Trigger der
     Zweck-Chips blieb auf 36px, waehrend sein Gegenstueck
     .quick-reply-chip-manage direkt darunter auf --touch-target wuchs. */
  .purpose-chip-manage,
  .purpose-chip-edit,
  .purpose-chip-delete,
  .quick-reply-chip-saved-wrap .quick-reply-chip-saved,
  .quick-reply-chip-manage,
  .quick-reply-chip-edit,
  .quick-reply-chip-delete {
    min-height: var(--touch-target);
  }
  /* Zeilenhoehen in den langen Listen (Logs, Benutzer, Rechte). */
  .log-entry-summary,
  .perm-option,
  .status-pill[role="button"],
  button.status-pill {
    min-height: var(--touch-target);
  }
}

/* ===========================================================================
   Runde 6 (Knöpfe): Mindest-Trefferflaechen, die das Inventar noch verfehlte
   ---------------------------------------------------------------------------
   Gemessen (Playwright, alle Testkonten, 375 px Touch + 1440 px Maus): die
   App-Norm ist 44 px auf Touch und mindestens 32 px mit der Maus
   (--hit-min-fine). Darunter lagen noch:
   - Touch: Filter-Pillen der Zuverlaessigkeit 29 px (die Log-Filter daneben
     haben 36 px + ::after = 46 px), Erinnerungs-Chips im Profil 29 px (alle
     anderen Auswahl-Chips — %, Bonus, Presets — sind 44 px hoch).
   - Maus: Info-Punkte 26/27 px, Stichwort-✕ 26 px, Sync-Punkt 26 px,
     Heatmap-Umschalter 26 px, "Zur Leihe" 29 px, Filter-Pillen 29/31 px,
     Erinnerungs-Chips 29 px, "Technische Details"/"Wie wird der Score
     berechnet?" 27/30 px.
   Wo das Layout kompakt bleiben soll (Punkte, ✕, Sync), waechst nur eine
   unsichtbare ::after-Flaeche; calc((100% - X) / 2) zentriert sie auf X px,
   egal wie gross der sichtbare Knopf ist.
   Bewusst NICHT: Heatmap-Zellen (Datenraster), Leaflet-Zoom (Karten-Standard,
   eigene .taxi-map-ctl daneben), Sortier-Pfeile der Fahrzeuge (zwei 27-px-
   Knoepfe uebereinander — groessere Flaechen wuerden sich gegenseitig
   ueberdecken), Umschalter (44x28 ist die Schalterform selbst).
   =========================================================================== */
.rel-filter-pill { position: relative; }
@media (pointer: coarse) {
  /* Wie .log-filter-btn: der Streifen scrollt quer und schneidet oben/unten
     ab, die Flaeche darf nur in sein Padding (0.35/0.4rem) ragen. */
  .rel-filter-pill { min-height: 36px; }
  .rel-filter-pill::after { content: ""; position: absolute; inset: -5px 0; }
  .reminder-chip span { min-height: var(--touch-target); }
}
@media (pointer: fine) {
  .rel-filter-pill,
  .log-filter-btn,
  .heatmap-mode-btn,
  .log-goto-loan,
  .reminder-chip span,
  .sync-modal-details summary,
  .reliability-how-summary {
    min-height: var(--hit-min-fine);
  }
  .info-dot::after,
  .info-dot-compact::after { inset: min(0px, calc((100% - var(--hit-min-fine)) / 2)); }
  .kw-chip-del,
  .sync-status-pill { position: relative; }
  .kw-chip-del::after,
  .sync-status-pill::after {
    content: "";
    position: absolute;
    inset: min(0px, calc((100% - var(--hit-min-fine)) / 2));
  }
}

/* ===========================================================================
   R10-Raten: Ratenzahlung (Leihart-Wahl, Plan-Baukasten, Raten-Block der
   Leihkarte, Raten-Schnellauswahl in den Zahlungsdialogen, Verschieben).
   Mobile first: alle Raster mit minmax(0, …), Beträge tabellarisch, nichts
   mit fester Breite – 320 px ohne waagerechten Überlauf.
   =========================================================================== */

/* --- Leihart: zwei große Karten --- */
.inst-kind-choice {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  margin: 0 0 0.9rem;
}
.inst-kind-card {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  min-height: 60px;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-card, 10px);
  background: var(--surface2);
  color: var(--text-muted);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition-fast), background-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
.inst-kind-icon { flex: 0 0 auto; }
.inst-kind-text { display: flex; flex-direction: column; min-width: 0; gap: 0.1rem; }
.inst-kind-text strong { color: var(--text); font-size: var(--text-base); line-height: 1.2; overflow-wrap: anywhere; }
.inst-kind-text small { font-size: var(--text-xs); line-height: 1.25; }
.inst-kind-card.is-selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--surface2));
  color: var(--accent-text);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.inst-kind-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.inst-kind-card:disabled { opacity: var(--dim-disabled); cursor: not-allowed; }
html:not(.using-touch) .inst-kind-card:hover:not(.is-selected):not(:disabled) { border-color: var(--border-strong); color: var(--text); }
.inst-kind-card:not(:disabled):active {
  --press-opacity: 1;
  transform: scale(0.98);
  transition-duration: var(--motion-xs);
}
@media (max-width: 360px) {
  .inst-kind-card { flex-direction: column; align-items: flex-start; gap: 0.3rem; }
}

/* --- Plan-Baukasten --- */
.inst-builder {
  margin: 0 0 1rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-card, 10px);
  background: color-mix(in srgb, var(--accent) 4%, var(--surface2));
  min-width: 0;
}
.inst-builder:empty { display: none; }
.inst-builder-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 0.6rem;
  color: var(--accent-text);
  font-weight: 700;
  font-size: var(--text-base);
}
.inst-builder .form-group { margin-bottom: 0.65rem; }
.inst-builder-group .label-text { display: block; margin-bottom: 0.3rem; }
.pc-kinds.inst-seg-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 360px) {
  /* „Wöchentlich“ brach auf 320 px mitten im Wort um. */
  .inst-seg-3 .pc-kind { font-size: var(--text-xs); padding-inline: 0.1rem; letter-spacing: -0.01em; }
}
.inst-builder .pc-kind { font-size: var(--text-sm); padding-inline: 0.3rem; }
.inst-builder-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 0.75rem;
}
.inst-builder-row > .form-group { min-width: 0; }
.inst-builder-row > .hidden { display: none; }
.inst-builder-row input[type="date"],
.inst-builder-row input[type="number"] { min-width: 0; width: 100%; }
@media (max-width: 380px) {
  .inst-builder-row { grid-template-columns: minmax(0, 1fr); }
}
.inst-stepper {
  display: grid;
  grid-template-columns: var(--touch-target) minmax(0, 1fr) var(--touch-target);
  align-items: stretch;
  border: 1px solid var(--border-input, var(--border));
  border-radius: var(--radius-btn);
  overflow: hidden;
  background: var(--surface);
}
.inst-stepper input[type="number"] {
  border: 0;
  border-radius: 0;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  -moz-appearance: textfield;
  appearance: textfield;
  background: transparent;
}
.inst-stepper input::-webkit-outer-spin-button,
.inst-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.inst-step {
  min-height: var(--touch-target);
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: 1.2rem;
  font-weight: 700;
  cursor: pointer;
}
.inst-step:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
html:not(.using-touch) .inst-step:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.inst-step:active {
  --press-opacity: 1;
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
.inst-preview { margin-top: 0.1rem; min-width: 0; }
.inst-preview-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.5rem;
  margin: 0 0 0.45rem;
  font-size: var(--text-base);
}
.inst-preview-summary strong { color: var(--text); font-variant-numeric: tabular-nums; }
.inst-preview-summary span { color: var(--text-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
.inst-preview-empty,
.inst-preview-error,
.inst-diff-same,
.inst-diff-text {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  margin: 0.35rem 0 0;
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--text-muted);
}
.inst-preview-error { color: var(--danger-text); }
.inst-diff-text { color: var(--text); }
.inst-preview-empty svg,
.inst-preview-error svg,
.inst-diff-same svg,
.inst-diff-text svg { flex: 0 0 auto; margin-top: 0.1rem; }
.inst-preview-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem 0.75rem;
  margin-top: 0.45rem;
}
.inst-sum {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin: 0;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.inst-sum.is-ok { color: var(--accent-text); }
.inst-sum.is-bad { color: var(--danger-text); }
.inst-customize { font-size: var(--text-sm); min-height: var(--touch-target); }
.inst-diff { min-height: 0; margin: 0 0 0.6rem; }
.inst-diff:empty { display: none; }

/* --- Ratenliste (Vorschau, Karte, Vergleich) --- */
.inst-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  counter-reset: none;
}
.inst-row {
  position: relative;
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.15rem 0.5rem;
  min-height: 36px;
  padding: 0.3rem 0.45rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}
.inst-row:nth-child(odd) { background: color-mix(in srgb, var(--text) 3%, transparent); }
.inst-row > .inst-chip { grid-column: 2 / -1; justify-self: start; }
@media (min-width: 420px) {
  .inst-row:has(> .inst-chip) { grid-template-columns: 1.6rem minmax(0, 1fr) auto auto; }
  .inst-row > .inst-chip { grid-column: auto; justify-self: end; }
}
.inst-row-n {
  display: inline-grid;
  place-items: center;
  width: 1.45rem;
  height: 1.45rem;
  border-radius: 50%;
  border: 1px solid var(--border-strong, var(--border));
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 700;
}
.inst-row-date { min-width: 0; color: var(--text); overflow-wrap: anywhere; }
.inst-row-amount { font-weight: 700; color: var(--text); white-space: nowrap; }
.inst-row--paid .inst-row-n { background: var(--accent-btn); border-color: var(--accent-btn); color: #fff; }
.inst-row--paid .inst-row-date,
.inst-row--paid .inst-row-amount { color: var(--text-muted); }
.inst-row--overdue .inst-row-n { border-color: var(--danger); color: var(--danger-text); }
.inst-row--due .inst-row-n,
.inst-row--soon .inst-row-n { border-color: var(--warning); color: var(--warning); }
.inst-row--waived .inst-row-date,
.inst-row--waived .inst-row-amount { color: var(--text-muted); text-decoration: line-through; }
.inst-row--fixed { opacity: 0.7; }
.inst-row.is-next { box-shadow: inset 3px 0 0 var(--accent); }
/* R12-Raten: Die Markierung der nächsten Rate hat die Farbe ihres Status –
   eine überfällige Rate 2 trug vorher eine grüne Leiste neben rotem Chip. */
.inst-row--overdue.is-next { box-shadow: inset 3px 0 0 var(--danger); }
.inst-row--due.is-next,
.inst-row--soon.is-next { box-shadow: inset 3px 0 0 var(--warning); }
.inst-row--more { display: block; padding-block: 0.1rem; }
.inst-row--more .link-btn { min-height: 32px; font-size: var(--text-sm); }
.inst-row--edit { grid-template-columns: 1.6rem minmax(0, 1.15fr) minmax(0, 1fr); }
.inst-row--edit input { min-width: 0; width: 100%; min-height: 38px; padding: 0.3rem 0.45rem; font-size: var(--text-sm); }
.inst-row--edit input[type="number"] { text-align: right; font-variant-numeric: tabular-nums; }

.inst-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.1rem 0.45rem;
  border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--border);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.4;
  white-space: nowrap;
  color: var(--text-muted);
}
.inst-chip--paid { color: var(--accent-text); border-color: color-mix(in srgb, var(--accent) 35%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.inst-chip--overdue { color: var(--danger-text); border-color: color-mix(in srgb, var(--danger) 35%, transparent); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.inst-chip--due,
.inst-chip--soon { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 35%, transparent); background: color-mix(in srgb, var(--warning) 8%, transparent); }
.inst-chip--partial { color: var(--signal-progress-color); border-color: color-mix(in srgb, var(--accent) 28%, transparent); }
.inst-chip--open { color: var(--signal-date-color); border-color: rgba(154,180,255,0.28); }
.inst-chip--waived { text-decoration: line-through; }

/* --- Raten-Block der Leihkarte --- */
.inst-block {
  /* Unten 0.5rem = Rasterabstand der Kacheln darunter (.loan-snapshot gap
     8px); 0.2rem klebte die Box an Bonus/Gezahlt (Nutzer-Rückmeldung). */
  margin: 0.6rem 0 0.5rem;
  padding: 0.65rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-card, 10px);
  background: var(--surface2);
  min-width: 0;
}
.inst-block-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.55rem;
  margin-bottom: 0.5rem;
}
.inst-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-pill, 999px);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent-text);
  font-size: var(--text-xs);
  font-weight: 700;
  white-space: nowrap;
}
.inst-block-summary { min-width: 0; color: var(--text-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.inst-progress {
  display: flex;
  gap: 3px;
  height: 8px;
  margin: 0.15rem 0 0.35rem;
}
.inst-progress-seg {
  flex: 1 1 0;
  min-width: 2px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 12%, transparent);
  transition: background-color var(--motion-base) var(--ease-standard);
}
/* R12-Raten (Gegenangebot): 60 Raten (z. B. nach einem Gegenangebot mit 60er-
   Plan) = 60 × 2 px + 59 × 3 px Abstand = 297 px – breiter als die Karte bei
   320 px. Lange Pläne rücken die Segmente enger zusammen. */
.inst-progress:has(> :nth-child(25)) { gap: 1px; }
.inst-progress:has(> :nth-child(25)) > .inst-progress-seg { min-width: 1px; }
.inst-progress-seg--paid { background: var(--accent); }
.inst-progress-seg--overdue { background: var(--danger); }
.inst-progress-seg--due,
.inst-progress-seg--soon { background: var(--warning); }
.inst-progress-seg.is-partial:not(.inst-progress-seg--overdue) {
  background: linear-gradient(90deg, var(--accent) 0 50%, color-mix(in srgb, var(--text) 12%, transparent) 50% 100%);
}
.inst-progress-label { margin: 0 0 0.5rem; font-size: var(--text-sm); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.inst-progress-label strong { color: var(--text); }
.inst-progress-overdue { color: var(--danger-text); font-weight: 600; }
.inst-next {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 0.75rem;
  padding: 0.6rem 0.7rem;
  margin: 0 0 0.5rem;
  border-radius: var(--radius-card, 10px);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.inst-next--overdue { border-color: color-mix(in srgb, var(--danger) 40%, transparent); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.inst-next--due,
.inst-next--soon { border-color: color-mix(in srgb, var(--warning) 40%, transparent); background: color-mix(in srgb, var(--warning) 8%, transparent); }
.inst-next-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1 1 10rem; }
.inst-next-kicker { font-size: var(--text-xs); color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }
.inst-next-amount { font-size: var(--text-lg); color: var(--text); font-variant-numeric: tabular-nums; }
.inst-next-amount small { font-size: var(--text-xs); color: var(--text-muted); font-weight: 500; }
.inst-next-when { font-size: var(--text-sm); color: var(--text-muted); overflow-wrap: anywhere; }
.inst-next--overdue .inst-next-when { color: var(--danger-text); font-weight: 600; }
.inst-next-extra { font-size: var(--text-xs); color: var(--danger-text); }
.inst-next-btn {
  flex: 0 1 auto;
  width: auto;
  min-width: 0;
  margin-top: 0;
  min-height: var(--touch-target);
  white-space: nowrap;
}
@media (max-width: 400px) {
  .inst-next-btn { flex: 1 1 100%; }
}
.inst-details { border-top: 1px solid var(--border); margin-top: 0.1rem; }
.inst-details > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--touch-target);
  padding: 0.2rem 0.1rem;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-sm);
  transition: background-color var(--transition-fast);
}
.inst-details > summary::-webkit-details-marker { display: none; }
.inst-details > summary .chevron { margin-left: auto; transition: transform var(--transition-fast); }
.inst-details[open] > summary .chevron { transform: rotate(90deg); }
.inst-details > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.inst-details > summary:active {
  --press-opacity: 1;
  background: color-mix(in srgb, var(--text) 7%, transparent);
  transition-duration: 0s;
}
.inst-details-body { padding: 0; } /* unten = oben (Box-Padding), vorher +0.3rem */
.inst-details--compare { border-top: 0; margin-top: 0.3rem; }
@supports selector(::details-content) {
  .inst-details { interpolate-size: allow-keywords; }
  .inst-details::details-content {
    height: 0;
    opacity: 0;
    overflow: hidden;
    transition:
      height var(--motion-base) var(--ease-standard),
      opacity var(--motion-base) var(--ease-standard),
      content-visibility var(--motion-base) allow-discrete;
  }
  .inst-details[open]::details-content { height: auto; opacity: 1; }
  .inst-details.is-restoring::details-content { transition: none; }
  @media (prefers-reduced-motion: reduce) {
    .inst-details::details-content { transition: none; height: auto; opacity: 1; }
  }
}
.inst-outdated-hint {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  margin: 0 0 0.5rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  color: var(--warning);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.inst-outdated-hint svg { flex: 0 0 auto; margin-top: 0.1rem; }

/* Offene Verschiebung (Karte) – Info-Blau wie die Verlängerung. */
.inst-reschedule-notice {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0 0 0.5rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--info) 10%, transparent);
  font-size: 0.82rem;
}
.inst-reschedule-top { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.inst-reschedule-top .close-request-date { margin-left: auto; }
.inst-reschedule-change { margin: 0; color: var(--text); overflow-wrap: anywhere; }
.inst-reschedule-wait { display: inline-flex; align-items: center; gap: 0.3rem; margin: 0; color: var(--text-muted); }
.inst-reschedule-decide { display: flex; gap: 0.4rem; }
.inst-reschedule-decide > button { flex: 1 1 0; width: auto; min-width: 0; margin-top: 0; min-height: var(--touch-target); }

/* --- Zahlungsdialoge: Raten-Schnellauswahl --- */
.inst-pay-box {
  margin: 0 0 0.75rem;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-card, 10px);
  background: var(--surface2);
  min-width: 0;
}
.inst-pay-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.5rem; margin-bottom: 0.45rem; }
.inst-pay-progress { min-width: 0; color: var(--text-muted); font-size: var(--text-sm); overflow-wrap: anywhere; }
.inst-pay-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.inst-pay-chips .btn-pct { padding-inline: 0.7rem; font-variant-numeric: tabular-nums; max-width: 100%; white-space: normal; }
.inst-pay-alloc:empty { display: none; }
/* R12-Raten: „€52,50 schon gemeldet – die Auswahl beginnt nach diesen Raten.“ */
.inst-pay-reserved { margin: 0.45rem 0 0; }
.inst-pay-alloc { margin: 0.45rem 0 0; }

/* --- Gegenangebot / Entscheidung --- */
.inst-counter { margin: 0 0 0.75rem; min-width: 0; }
.inst-counter-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.6rem; margin-bottom: 0.4rem; }
.inst-counter-orig { color: var(--text-muted); font-size: var(--text-sm); overflow-wrap: anywhere; }
.inst-counter .inst-builder { margin-bottom: 0; }
.inst-plan-compare {
  margin: 0 0 0.75rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-card, 10px);
  background: var(--surface2);
}
.inst-compare-row {
  display: grid;
  grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr);
  gap: 0.2rem 0.6rem;
  padding: 0.25rem 0;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
@media (max-width: 380px) {
  .inst-compare-row { grid-template-columns: minmax(0, 1fr); }
}
/* R12-Raten (Gegenangebot): Änderungen gegenüber der Anfrage als Chips –
   im Gegenangebot unter der Vorschau-Zusammenfassung, beim Leihnehmer unter
   dem Plan-Vergleich, in der Zeitleiste. Umbrechend, ohne feste Breiten (320 px). */
.inst-preview-note:empty { display: none; }
.inst-preview-note { display: flex; flex-direction: column; gap: 0.4rem; margin: 0 0 0.5rem; min-width: 0; }
.inst-preview-note .inst-outdated-hint { margin: 0; }
.inst-plan-diff {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0.35rem 0 0.15rem;
  padding: 0;
  list-style: none;
  min-width: 0;
}
.inst-preview-note .inst-plan-diff { margin: 0; }
.inst-diff {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.3rem;
  max-width: 100%;
  margin: 0;
  padding: 0.2rem 0.5rem;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: var(--text-sm);
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.inst-diff-label { color: var(--text-muted); }
.inst-diff-from { color: var(--text-muted); text-decoration: line-through; text-decoration-thickness: 1px; }
.inst-diff-arrow { color: var(--text-muted); }
.inst-diff-to { color: var(--accent-text, var(--accent)); font-weight: 600; }
p.inst-plan-diff--same {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
}
.inst-row.is-changed .inst-row-date,
.inst-row.is-changed .inst-row-amount { color: var(--accent-text, var(--accent)); font-weight: 600; }
.inst-row.is-changed .inst-row-n { border-color: var(--accent); }
.inst-compare-note { margin: 0.2rem 0 0; }
.timeline-counter-plan {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-top: 0.5rem;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
  min-width: 0;
}
.inst-decide-plan { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }
.inst-decide-plan .inst-list { max-height: 14rem; overflow-y: auto; overscroll-behavior: contain; }

/* --- Verschieben: Raten-Auswahl --- */
.inst-rate-pick { display: flex; flex-direction: column; gap: 0.35rem; }
.inst-pick {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.2rem 0.6rem;
  min-height: var(--touch-target);
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background: var(--surface2);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}
.inst-pick input { position: absolute; opacity: 0; pointer-events: none; }
.inst-pick-n { font-weight: 700; color: var(--text); white-space: nowrap; }
.inst-pick-date { min-width: 0; color: var(--text-muted); overflow-wrap: anywhere; }
.inst-pick-amount { font-weight: 700; color: var(--text); white-space: nowrap; }
.inst-pick > .inst-chip { grid-column: 2 / -1; justify-self: start; }
.inst-pick:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface2));
  box-shadow: inset 0 0 0 1px var(--accent);
}
.inst-pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
html:not(.using-touch) .inst-pick:hover:not(:has(input:checked)) { border-color: var(--border-strong, var(--border)); }
.inst-pick:active {
  --press-opacity: 1;
  background: color-mix(in srgb, var(--text) 7%, var(--surface2));
}
.ip-modes { margin-bottom: 0.8rem; }
.ip-panel.hidden { display: none; }

@media (prefers-reduced-motion: reduce) {
  .inst-kind-card,
  .inst-kind-card:not(:disabled):active,
  .inst-progress-seg,
  .inst-details > summary .chevron { transition: none; transform: none; }
}

/* R10-Raten: Statistik – kommende Raten (neben dem Überfällig-Banner). */
.stats-inst-banner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.5rem 0 0;
  padding: 0.55rem 0.8rem;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(154,180,255,0.28);
  background: rgba(154,180,255,0.08);
  color: var(--signal-date-color);
  font-size: var(--text-sm);
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* R11-Raten: Verschieben – die Raten-Auswahl stand in einer .form-group, deren
   „.form-group label“ (flex, klein, grau) die Karte überschrieb: Datum brach um,
   der Chip hing in derselben Zeile. Mit höherer Spezifität wieder ein Raster,
   der Status-Chip in einer zweiten Zeile unter dem Datum. */
.inst-rate-pick .inst-pick {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--text);
}
.inst-rate-pick .inst-pick > .inst-chip { grid-column: 2 / -1; grid-row: 2; justify-self: start; }
/* Schnellauswahl für das neue Datum. */
.inst-due-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
.inst-due-chips.hidden { display: none; }
.inst-due-chips .btn-pct { font-variant-numeric: tabular-nums; padding-inline: 0.7rem; }
/* R12-Raten: Die drei Chips (je ~140 px bei 44 px Höhe und 0.85rem) standen
   auf 320 px je in einer eigenen Zeile. Kompakt wie die Schnellantwort-Chips:
   36 px sichtbar, Datum leiser, Tippfläche per ::after auf 44 px (nur grobe
   Zeiger). So stehen zwei bis drei nebeneinander. */
.inst-due-chips { gap: 0.4rem; }
.inst-due-chips .btn-pct.inst-due-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  min-height: 36px;
  padding: 0.3rem 0.65rem;
  font-size: var(--text-sm);
  line-height: 1.2;
  white-space: nowrap;
}
.inst-due-chip-date { font-weight: 500; opacity: 0.8; }
.inst-due-chip.active .inst-due-chip-date { opacity: 1; }
@media (pointer: coarse) {
  .inst-due-chips .inst-due-chip::after { content: ""; position: absolute; inset: -4px 0; }
}
/* Zahlung eintragen: „Voll“/„Hälfte“ weichen bei Raten der Raten-Auswahl. */
.pay-quick-row.inst-hidden { display: none; }

/* R11-Raten: Ratenliste auf dem Handy einzeilig, solange der Chip kurz ist
   („bezahlt 25.09.“): der Wochentag weicht, Datum/Betrag/Chip passen in eine
   Zeile. Unter 360 px und bei langen Chips bleibt es zweizeilig. */
@media (min-width: 360px) and (max-width: 419.98px) {
  .inst-row.inst-row--chip-short { grid-template-columns: 1.6rem minmax(0, 1fr) auto auto; }
  .inst-row.inst-row--chip-short > .inst-chip { grid-column: auto; justify-self: end; }
  .inst-row.inst-row--chip-short .inst-row-wd { display: none; }
}
/* Zusammengefasste bezahlte Raten („Raten 1–20 bezahlt · anzeigen“). */
.inst-row--summary { grid-template-columns: 1.6rem minmax(0, 1fr) auto auto; }
.inst-row--summary .inst-row-n { background: var(--accent-btn); border-color: var(--accent-btn); color: #fff; }
.inst-row--summary .inst-row-date,
.inst-row--summary .inst-row-amount { color: var(--text-muted); }
.inst-paid-expand { min-height: 32px; font-size: var(--text-sm); padding-inline: 0.3rem; }
@media (max-width: 359.98px) {
  .inst-row--summary { grid-template-columns: 1.6rem minmax(0, 1fr) auto; }
  .inst-row--summary .inst-paid-expand { grid-column: 2 / -1; justify-self: start; padding-inline: 0; }
}
/* Einzeilige Ratenzeile: Chip VOR dem Betrag, damit die Beträge aller Zeilen
   rechtsbündig untereinander stehen (vorher rückte der Betrag bezahlter
   Raten nach links, offene Raten hatten keinen Chip). */
@media (min-width: 420px) {
  .inst-row:has(> .inst-chip) > .inst-chip { order: 1; }
  .inst-row:has(> .inst-chip) > .inst-row-amount { order: 2; }
}
@media (min-width: 360px) and (max-width: 419.98px) {
  .inst-row.inst-row--chip-short > .inst-chip { order: 1; }
  .inst-row.inst-row--chip-short > .inst-row-amount { order: 2; }
}

/* R11-Raten: Plan-Vergleich im Gegenangebot – die markierte Zeile zog per
   negativem Rand nach links, ihr eigenes Padding (0.25rem 0) hob das der
   Markierung auf: Beschriftung stand 0.35rem weiter links als darüber. */
.inst-compare-row.counter-comparison-changed { padding: 0.25rem 0.35rem; }

/* V4 (Barrierefreiheit): Windows-Kontrastmodus (forced-colors) – Bedienelemente, die nur über Hintergrund oder Schatten sichtbar waren.
   Systemfarben (ButtonText, Highlight, …) bleiben im Kontrastmodus erhalten; `forced-color-adjust: none` nur an diesen Teilen. */
@media (forced-colors: active) {
  .btn-primary, .btn-danger { border: 1px solid ButtonText; }
  .btn-primary:disabled, .btn-danger:disabled { border-color: GrayText; }
  /* Schalter: Spur mit Rand, Knopf sichtbar, „an“ = gefüllte Spur in Highlight (vorher war er komplett unsichtbar) */
  .toggle-track { box-sizing: border-box; border: 2px solid ButtonText; box-shadow: none; }
  .toggle-thumb { top: 2px; left: 2px; forced-color-adjust: none; background: ButtonText; }
  .toggle-control input[type="checkbox"]:checked + .toggle-track { forced-color-adjust: none; background: Highlight; border-color: Highlight; }
  .toggle-control input[type="checkbox"]:checked + .toggle-track .toggle-thumb { background: HighlightText; transform: translateX(16px); }
  /* Reiter-Leisten der Hauptansicht: gewählter Reiter = Linie in Highlight */
  .tab-pill, .perspective-tab { border-bottom: 3px solid transparent; }
  .tab-pill.is-active, .tab-pill[aria-selected="true"], .perspective-tab.is-active, .perspective-tab[aria-selected="true"] { border-bottom-color: Highlight; }
  .tab-bar.has-seg-indicator::before, .perspective-switch.has-seg-indicator::before { display: none; }
}
