/* ============================================================================
   album.common.css — themeneutrale Struktur-/Verhaltensregeln, die für JEDE
   Instanz gelten. Wird in html.js IMMER *nach* der Theme-CSS geladen.

   Warum diese Datei existiert: die Theme-Dateien (album.css, album.promptheus.css,
   album.violet.css, album.teal.css) sind **vollständige Ersetzungen**, keine
   Ergänzungen — es wird immer genau eine davon geladen. Regeln, die zu einem
   JS-Feature gehören (Grid-Dichte, Tastatur-Fokus, i-Info-Toggle), müssten also
   in allen vier dupliziert werden und liefen sonst auf einzelnen Instanzen
   stillschweigend ins Leere. Genau das war der Fall, bis diese Datei kam.

   Regel: Alles, was **Farbe/Look** ist, gehört in die Theme-CSS. Alles, was
   **Mechanik** ist (Geometrie, Sichtbarkeitszustände von JS-Features), gehört
   hierher — und nutzt Theme-Variablen (--accent …) für die Farbgebung.
   ============================================================================ */

/* ── Kein Pull-to-Refresh ───────────────────────────────────────────────────
   Chrome auf Android laedt die Seite neu, wenn man am Seitenanfang nach unten
   zieht. In der Einzelansicht ist Runterwischen aber unsere Geste (zurueck ins
   Grid), und im Grid reisst es einen aus der Arbeit. `contain` haelt die Geste
   im Dokument, ohne das Scrollen selbst zu beruehren. */
html,body{overscroll-behavior-y:contain}
/* Im PhotoSwipe-Overlay gar keine Ueberzieh-Geste an den Browser geben */
.pswp{overscroll-behavior:none}

/* ── Grid-Dichte (imgtag-Stil: +/- Knoepfe, Tasten +/-, auf Touch auch Pinch) ──
   --tile-min setzt JS auf :root; die Kachelbreite folgt daraus. */
:root{--tile-min:96px;--tile-gap:6px}
.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min),1fr));gap:var(--tile-gap);user-select:none}
.grid-size-ctl{display:flex;gap:0.2rem;flex-shrink:0}
.gsz-btn{padding:0.5rem 0.75rem!important;min-width:2.4rem;font-weight:700}
/* Auf Touch waren die Knoepfe ausgeblendet ("Pinch reicht"). Das war falsch:
   Pinch ist unsichtbar, damit war die Dichte auf dem Handy unauffindbar. Sie
   stehen jetzt in der Kopfzeile, nur kompakter. */
@media (pointer:coarse){.gsz-btn{padding:0.4rem 0.6rem!important;min-width:2.1rem}}

/* ── Tastatur-Navigation im Grid: Fokus-Rahmen + Info-Overlay ── */
.item.gnav-focus{outline:2px solid var(--accent);outline-offset:1px;box-shadow:0 0 12px var(--accent-a40)}
.gnav-info{position:absolute;inset:auto 0 0 0;background:rgba(0,0,0,0.72);color:#fff;font-size:0.6rem;line-height:1.35;padding:0.3rem 0.4rem;pointer-events:none;white-space:pre-wrap;backdrop-filter:blur(3px)}

/* ── Virtualisiertes Grid (TanStack Virtual ueber Zeilen) ──
   .gallery-virt ist der Rahmen, .gv-inner haelt die Gesamthoehe (damit die
   Bildlaufleiste stimmt), .gv-row wird per transform an die richtige Stelle
   geschoben. Nur sichtbare Zeilen liegen im DOM. */
.gallery-virt{position:relative;width:100%}
.gv-inner{position:relative;width:100%}
/* Kein dauerhaftes will-change: das haelt pro sichtbarer Zeile einen eigenen
   Compositing-Layer vor (bei ~30 Zeilen entsprechend viele) und kostete auf dem
   Handy mehr, als die Transform-Optimierung einbringt. */
.gv-row{position:absolute;top:0;left:0;right:0;
  /* Zeilen ausserhalb des Sichtfelds ueberspringt der Browser beim Rendern
     (Layout/Paint) — bei dichten Gittern mit 200+ Kacheln der groesste Hebel,
     den CSS hier bietet. Die Hoehe steht im style-Attribut, die Geometrie bleibt
     also stabil. */
  /* KEIN content-visibility: der Virtualizer haelt ohnehin nur sichtbare Zeilen
     im DOM, die Sichtbarkeitspruefung des Browsers kann also nichts einsparen
     und kostet trotzdem (gemessen 2026-07-29). Zusammen mit loading=lazy fuehrte
     sie auf iOS sogar dazu, dass Kacheln nie geladen wurden.
     `contain` grenzt Layout und Malen der Zeile ab, ohne diese Nachteile. */
  contain:layout paint}
.gv-row .item{position:absolute}

/* ── Dichtes Gitter: eine Zeichenflaeche je Zeile (grid-canvas.js) ──
   Unter --tile-min 80px zeichnet JS die Kacheln, statt sie als Elemente zu
   halten: aus 210 DOM-Kacheln im Sichtfeld werden 21 Flaechen. Die Flaeche IST
   die .gv-row, der Virtualizer positioniert sie wie jede andere Zeile.
   Bewusst KEINE Rundung/Rahmen hier: alles Sichtbare wird gemalt, damit der
   Browser nur eine einfarbige Bitmap zu behandeln hat. */
canvas.gv-flaeche{display:block;touch-action:pan-y}
body.select-mode canvas.gv-flaeche{cursor:copy}

/* Halter fuer die i-Infos ueber einer GEZEICHNETEN Kachel: der Text bleibt DOM,
   aber nur fuer die eine Kachel unter dem Zeiger. Traegt bewusst NICHT die
   Klasse .item — sonst faende ihn jede Abfrage nach Kacheln. */
.gnav-halter{position:absolute;top:0;left:0;pointer-events:none}

/* ── Virtualisierte Listenansicht ──
   Die Detailliste läuft über denselben Zeilen-Virtualizer wie das Grid (vorher
   lagen ALLE Zeilen samt Vorschaubildern im DOM — bei /seychellen 12.148 auf
   einen Klick, genau das Verhalten, wegen dem das Grid virtualisiert wurde).
   Dafür muss die Zeilenhöhe fest und für JS bekannt sein: --dl-row-h hier,
   DETAIL_ROW_H/-GAP in album.js. Ändert sich eines, muss das andere mit. */
:root{--dl-row-h:44px}
.detail-list{display:block} /* Zeilen werden absolut positioniert, kein Flex */
.gv-row.dl-row{position:absolute;top:0;left:0;right:0;height:var(--dl-row-h);
  will-change:transform;box-sizing:border-box}
/* Der Thumb-Platzhalter trägt in der Liste den Lade-Schimmer (im Grid die
   Kachel selbst) — sonst blitzt beim Scrollen ein leeres Kästchen auf. */
.dl-thumb.loading img{opacity:0}
.dl-thumb img{transition:opacity 0.25s}

/* ── Bildunterschrift (Name/Größe) nur bei aktivem i-Toggle ──
   Ohne i zeigt das Grid wirklich nur Bilder, kein reservierter Textplatz. */
/* ── Video-Wiedergabe (Lightbox + Einzelansicht) ────────────────────────────
   Eigene Bedienleiste statt `controls` — Begruendung in video-player.js.
   Kein backdrop-filter (kostet pro Frame, s. docs/pool-alben.md). */
.vp-wrap{position:relative;display:flex;align-items:center;justify-content:center;
  width:100%;height:100%;max-width:100%;max-height:100%;background:#000}
.vp-video{max-width:100%;max-height:100%;width:100%;height:100%;object-fit:contain;display:block;background:#000}
.vp-bar{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:0.5rem;
  padding:0.5rem 0.7rem calc(0.5rem + env(safe-area-inset-bottom,0px));
  background:linear-gradient(to top,rgba(0,0,0,0.72),rgba(0,0,0,0));
  color:#fff;font-size:0.78rem;touch-action:none}
.vp-btn{background:none;border:0;padding:0.3rem;cursor:pointer;color:#fff;opacity:0.9;
  display:flex;align-items:center;justify-content:center;min-width:2rem;min-height:2rem}
.vp-btn:hover{opacity:1}
.vp-btn svg{width:1.15rem;height:1.15rem}
.vp-t{font-variant-numeric:tabular-nums;opacity:0.85;min-width:2.6rem;text-align:center}
.vp-seek{flex:1;min-width:3rem;height:1.6rem;accent-color:var(--accent,#0ef);cursor:pointer;background:none}
.vp-hinweis{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;align-items:center;gap:0.6rem;padding:0.6rem 0.9rem;
  background:rgba(0,0,0,0.6);color:rgba(255,255,255,0.92);font-size:0.85rem;text-align:center}
.vp-spinner{width:1rem;height:1rem;border:2px solid rgba(255,255,255,0.25);border-top-color:#fff;
  border-radius:50%;animation:vp-dreh 0.9s linear infinite;flex:none}
@keyframes vp-dreh{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.vp-spinner{animation:none}}
.vp-laedt .vp-video{opacity:0.75}

/* ── Stapel-Kennzeichen (RAW hinter dem Bild) ───────────────────────────────
   Sitzt unten LINKS; unten rechts liegt bei Videos die Laufzeit. Kein
   backdrop-filter (kostet pro Scroll-Frame, s. docs/pool-alben.md). */
.stapel-marke{position:absolute;left:0.3rem;bottom:0.3rem;pointer-events:none;
  font-size:0.62rem;line-height:1;letter-spacing:0.04em;font-weight:600;
  padding:0.16rem 0.3rem;border-radius:0.2rem;
  background:rgba(0,0,0,0.55);color:rgba(255,255,255,0.92)}
.item.loading .stapel-marke{opacity:0}
.stapel-marke-liste{position:static;margin-left:0.4rem;font-size:0.58rem;
  background:rgba(255,255,255,0.12);color:rgba(255,255,255,0.7)}

/* ── Video-Kachel ───────────────────────────────────────────────────────────
   Die Kachel ist dieselbe wie beim Foto (Poster-Thumb als <img>); nur zwei
   Aufkleber kommen dazu. Bewusst OHNE backdrop-filter — der kostet auf einem
   fixierten/gescrollten Element pro Frame (s. docs/pool-alben.md). */
.vid-badge{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:2.1rem;height:2.1rem;display:flex;align-items:center;justify-content:center;
  border-radius:50%;background:rgba(0,0,0,0.42);color:rgba(255,255,255,0.95);
  pointer-events:none;box-shadow:0 1px 6px rgba(0,0,0,0.45)}
.vid-badge svg{width:1.15rem;height:1.15rem;margin-left:0.1rem}
.vid-dur{position:absolute;right:0.3rem;bottom:0.3rem;pointer-events:none;
  font-size:0.68rem;line-height:1;font-variant-numeric:tabular-nums;
  padding:0.16rem 0.3rem;border-radius:0.2rem;
  background:rgba(0,0,0,0.55);color:rgba(255,255,255,0.92)}
.item.loading .vid-badge,.item.loading .vid-dur{opacity:0}
/* Vorschau beim Zeigen: liegt exakt ueber dem Poster derselben Kachel */
.vp-hover{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;background:#000}
.item-info{display:none}
body.grid-info-on .item-info{display:block}

/* ── Datumskoepfe im virtualisierten Grid ── */
.gv-head{display:flex;align-items:flex-end;padding:0 0.2rem 0.35rem;font-size:0.78rem;font-weight:600;color:var(--accent);cursor:pointer;user-select:none}
.gv-head:hover{color:#fff}

/* ── Auswaehlen-Modus ── */
body.select-mode .item{cursor:copy}
body.select-mode .item .item-check{opacity:1}
/* Waehrend der Wisch-Auswahl kein Text-Markieren/Scrollen in der Galerie */
body.select-mode .gallery-virt{touch-action:pan-y;user-select:none}

/* ── Kopfzeile: Titel + Anzahl, Button-Reihe ──
   Die Anzahl steht schon serverseitig im HTML (erster Paint), der Client traegt
   danach den gefilterten Stand nach. */
/* Titel: war vorher ein inline style="" im HTML. Als Regel hier, damit die
   Quelle an einer Stelle liegt und Themes sie ueberschreiben koennen. */
#app-header h1{color:var(--accent);font-size:1.4rem;margin:0 0 0.2rem}
.hdr-top{display:flex;align-items:center;justify-content:space-between;gap:0.8rem}
.hdr-top-right{display:flex;align-items:center;gap:0.5rem;flex-shrink:0}
.hdr-count{font-size:0.78rem;color:rgba(255,255,255,0.45);white-space:nowrap}
.hdr-desc{color:rgba(255,255,255,0.5);font-size:0.85rem}
.hdr-btns{display:flex;gap:0.6rem;flex-wrap:wrap;margin-top:0.8rem}

/* ── Skeleton: die Form der Seite steht im ersten Paint ──
   Vorher war die Seite bis zum Eintreffen von api/list voellig leer und damit
   nicht von einem leeren Album zu unterscheiden. Serverseitig gerendert, wird
   beim ersten loadPane() durch das echte Grid ersetzt. */
.skel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile-min),1fr));gap:var(--tile-gap)}
.skel-tile{aspect-ratio:1;border-radius:8px}
.skel-tile,.item.loading,.dl-thumb.loading{
  background-color:rgba(255,255,255,0.06);
  position:relative;overflow:hidden;
}
/* Schimmer als eigene Schicht, die per transform laeuft. Eine Animation auf
   background-position laesst den Browser bei jedem Frame die Flaeche neu malen —
   bei hundert Kacheln gleichzeitig war genau das der Grund, warum der Effekt auf
   dem Handy abgeschaltet werden musste. Ueber transform laeuft es auf der GPU
   und kostet auch bei vielen Kacheln praktisch nichts. */
.skel-tile::after,.item.loading::after,.dl-thumb.loading::after{
  content:'';position:absolute;inset:0;
  background:linear-gradient(100deg,transparent 20%,rgba(255,255,255,0.13) 50%,transparent 80%);
  transform:translateX(-100%);
  animation:skel-schimmer 1.4s ease-in-out infinite;
  pointer-events:none;
}
.item.loading img{opacity:0}
.item img{transition:opacity 0.25s}
@keyframes skel-schimmer{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
@media (prefers-reduced-motion:reduce){
  .skel-tile::after,.item.loading::after,.dl-thumb.loading::after{animation:none;opacity:0.5}
}
/* Der Schimmer laeuft jetzt ueber transform (s. oben) und ist damit auch auf dem
   Handy tragbar — die frueher noetige Abschaltung ist entfallen. */

/* ── Stoerung beim Laden (statt stehengebliebener Skeleton-Kacheln) ── */
.pane-error{padding:1.5rem 0.2rem;color:rgba(255,255,255,0.75);font-size:0.88rem;line-height:1.5}
.pane-error-detail{margin:0.4rem 0 0.9rem;font-size:0.78rem;color:rgba(255,255,255,0.45);word-break:break-word}

/* ── Mobile Bedienleiste unten + Mehr-Sheet ──
   Nur unter 600px aktiv (album.js haengt die Header-Buttons per matchMedia um).
   Daumenerreichbar, und der Header schrumpft auf Titel + Anzahl. */
/* `hidden` MUSS gewinnen: die Browser-Regel [hidden]{display:none} hat weniger
   Spezifitaet als .mobile-bar{display:flex} — dadurch stand die Leiste auf dem
   Desktop sichtbar da, leer, weil ihre Knoepfe im Kopf haengen (Marlin
   2026-07-30: "die Leiste unten, die nur noch mehr zeigt"). Gilt genauso fuer das
   Mehr-Blatt und seinen Hintergrund. */
.mobile-bar[hidden],.more-sheet[hidden],.sheet-backdrop[hidden]{display:none!important}
.mobile-bar{position:fixed;left:0;right:0;bottom:0;z-index:70;display:flex;align-items:stretch;gap:0.3rem;
  padding:0.35rem 0.5rem;padding-bottom:max(0.35rem,env(safe-area-inset-bottom));
  background:rgba(18,20,28,0.92);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border-top:1px solid rgba(255,255,255,0.09)}
/* Auf Touch OHNE Milchglas: die Leiste liegt fix ueber dem scrollenden Grid,
   also muss der Browser den Weichzeichner bei jedem Scroll-Frame neu rechnen —
   auf iOS einer der teuersten Effekte ueberhaupt. Deckende Flaeche stattdessen. */
@media (pointer:coarse){
  .mobile-bar{background:#12141c;backdrop-filter:none;-webkit-backdrop-filter:none}
}
.mobile-bar-slots{display:flex;flex:1;gap:0.3rem;min-width:0}
.mobile-bar .btn,.mbar-more{flex:1;min-width:0;justify-content:center;font-size:0.7rem;
  padding:0.55rem 0.3rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* sheet-only: Knoepfe, die nur im Mehr-Blatt Sinn haben. Sie haengen im Kopf,
   solange die Leiste nicht aktiv ist (album.js haengt sie per matchMedia um) —
   dort waeren sie doppelt zur Chip-Leiste. */
.hdr-btns .sheet-only{display:none}

/* In der unteren Leiste NUR Icons: die Woerter wurden bei schmalen Geraeten und
   groesserer Systemschrift abgeschnitten (Marlin 2026-07-29). Die Bedeutung
   steht im title-Attribut; im Kopf am Desktop stehen weiterhin beide. */
.mobile-bar .mb-lbl,.mobile-bar .mbar-more span{display:none}
.mobile-bar .mb-ico{font-size:1.2rem;line-height:1}
.mb-ico + .mb-lbl{margin-left:0.4rem}
.mbar-more{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.05rem;
  max-width:4.5rem;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);
  color:#e0e0e0;font-family:inherit;font-size:1rem;line-height:1;cursor:pointer}
.mbar-more span{font-size:0.6rem;opacity:0.75}
/* Grid nicht hinter der Leiste verstecken */
body.has-mobile-bar{padding-bottom:4.2rem}
body.has-mobile-bar .hdr-btns{display:none}
/* Auf dem Handy zaehlt jede Zeile: Kopfbereich flacher, damit die Bilder
   frueher anfangen. Der Breadcrumb ist eine Zeile Text und braucht keine
   36px-Mindesthoehe plus Aussenabstand. */
body.has-mobile-bar .breadcrumb{min-height:0;padding:0.35rem 0.5rem;margin-bottom:0.45rem;font-size:0.75rem}
body.has-mobile-bar .breadcrumb.bc-trivial{display:none}
body.has-mobile-bar.select-mode .breadcrumb.bc-trivial{display:flex}
body.has-mobile-bar #app-header{margin-bottom:0.4rem!important}
body.has-mobile-bar #app-header h1{font-size:1.05rem!important}
/* Beschreibung auf eine Zeile: sie ist Beiwerk und hat vorher zwei bis drei
   Zeilen ueber den Bildern gekostet. */
body.has-mobile-bar .hdr-desc{margin:0.1rem 0 0;font-size:0.72rem;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
/* Sammlungs-Chips in EINE waagerecht scrollbare Zeile (s. .cb-scroll unten).
   Umgebrochen belegten 15 Sammlungen fuenf Zeilen und schoben die Bilder aus
   dem ersten Bildschirm. */
body.has-mobile-bar .collection-bar{margin-bottom:0.45rem}
/* Auswahl-Leiste ueber die Bedienleiste heben statt sie zu ueberdecken */
body.has-mobile-bar .sel-toolbar{bottom:4.4rem}

.sheet-backdrop{position:fixed;inset:0;z-index:75;background:rgba(0,0,0,0.5)}
.more-sheet{position:fixed;left:0;right:0;bottom:0;z-index:80;
  background:rgba(22,24,32,0.98);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  border-top:1px solid rgba(255,255,255,0.12);border-radius:14px 14px 0 0;
  padding:0.5rem 1rem calc(1rem + env(safe-area-inset-bottom));
  transform:translateY(100%);transition:transform 0.22s ease-out}
.more-sheet.open{transform:translateY(0)}
.sheet-grip{width:2.5rem;height:0.25rem;border-radius:999px;background:rgba(255,255,255,0.25);
  margin:0.25rem auto 0.7rem;cursor:pointer}
.sheet-title{font-size:0.72rem;text-transform:uppercase;letter-spacing:0.06em;
  color:rgba(255,255,255,0.4);margin-bottom:0.6rem}
.sheet-btns{display:grid;grid-template-columns:1fr 1fr;gap:0.5rem}
.sheet-btns .btn{justify-content:flex-start;font-size:0.78rem;padding:0.65rem 0.7rem}
.sheet-btns .grid-size-ctl{grid-column:1 / -1}
@media (prefers-reduced-motion:reduce){.more-sheet{transition:none}}

/* ── Kontextmenue (langes Druecken / Rechtsklick) ── */
#item-menu{position:fixed;z-index:300;min-width:12rem;background:rgba(18,20,28,0.96);border:1px solid rgba(255,255,255,0.16);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);padding:0.35rem;box-shadow:0 10px 40px rgba(0,0,0,0.5)}
.im-name{padding:0.3rem 0.5rem 0.45rem;font-size:0.7rem;color:rgba(255,255,255,0.45);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.im-row{display:block;width:100%;text-align:left;background:none;border:none;color:rgba(255,255,255,0.88);font-family:inherit;font-size:0.82rem;padding:0.5rem 0.55rem;cursor:pointer}
.im-row:hover{background:rgba(255,255,255,0.08);color:var(--accent)}
.im-row.danger{color:#f43f5e}
.im-row.danger:hover{background:rgba(244,63,94,0.12)}

/* ── Sammlungs-Leiste: Chips scrollen, Verknuepfung + Filter kleben rechts ──
   Bei 15 Sammlungen waren die beiden Knoepfe sonst nach rechts weggescrollt
   und praktisch unerreichbar. */
.collection-bar{display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap}
body.has-mobile-bar .collection-bar{flex-wrap:nowrap}
.cb-scroll{display:flex;flex-wrap:wrap;gap:0.4rem;flex:1;min-width:0}
.cb-fixed{display:flex;gap:0.4rem;flex-shrink:0}
body.has-mobile-bar .cb-scroll{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;
  -webkit-overflow-scrolling:touch;padding-bottom:0.15rem}
body.has-mobile-bar .cb-scroll::-webkit-scrollbar{display:none}
body.has-mobile-bar .cb-scroll > *{flex-shrink:0}

/* ── Scrub-Griff (schnelles Durchscrollen) ──────────────────────────────────
   Bei zwölftausend Bildern ist Wischen kein Navigationsmittel mehr. Der Griff
   blendet beim Scrollen ein und verschwindet wieder; beim Ziehen zeigt das Label
   den Aufnahmemonat der Zielzeile. */
/* Bahn endet vor den Bedienfeldern: oben die klebende Kopf-/Chip-Leiste, unten
   die Bedienleiste. Sonst laeuft der Griff unter die Knoepfe (Marlin). */
#scrubber{position:fixed;right:0;width:2.6rem;z-index:66;
  top:var(--scrub-oben,3.5rem);bottom:var(--scrub-unten,1rem);
  pointer-events:none;opacity:0;transition:opacity 0.25s}
body.has-mobile-bar #scrubber{--scrub-unten:4.6rem}
/* Immer greifbar: der Griff ist grundsaetzlich sichtbar (nur gedaempft) und
   nicht erst nach einer Bewegung. Vorher verschwand er nach 1,4s und war dann
   nicht zu fangen (Marlin). */
#scrubber{opacity:0.4}
/* Der Scrubber selbst braucht Platz fuer die eingeruckte Bahn. */
#scrubber{width:4.6rem}
/* Die GANZE Bahn faengt Beruehrungen: beim schnellen Scrollen einen wandernden
   64px-Griff zu treffen ist aussichtslos (Marlin: "immer noch nicht greifbar").
   Ein Tipp auf die Bahn springt an die Stelle und geht direkt ins Ziehen ueber —
   so verhalten sich auch Bildlaufleisten. */
/* Vom rechten Rand eingeruckt: Android reserviert die aeussersten ~24dp fuer die
   Zurueck-Geste. Eine Bahn direkt am Rand faengt dort nichts, besonders nicht
   waehrend eines laufenden Scrollens (Marlin 2026-07-30). Deshalb Abstand halten
   und dafuer breiter sein. */
.scrub-bahn{position:absolute;top:0;bottom:0;right:14px;width:56px;
  pointer-events:auto;touch-action:none;cursor:grab;z-index:1}
#scrubber.sichtbar,#scrubber.aktiv{opacity:1}

/* Position per SCROLL-GETRIEBENER ANIMATION: der Browser rechnet sie auf dem
   Compositor mit, also folgt der Griff auch dem Momentum-Scrollen. Vorher zog JS
   ihn im scroll-Ereignis nach — das feuert auf Mobilgeraeten waehrend des
   Ausrollens gedrosselt, dadurch "hing" der Griff und kam erst am Ende nach.
   --bahn setzt JS bei Groessenaenderungen (nicht beim Scrollen). */
@supports (animation-timeline: scroll()) {
  .scrub-grip{
    animation:scrub-lauf linear both;
    animation-timeline:scroll(root block);
    top:0;
  }
  @keyframes scrub-lauf{
    from{transform:translateY(0)}
    to{transform:translateY(var(--bahn,0px))}
  }
}
/* Der Griff ist reine ANZEIGE — Beruehrungen faengt ausschliesslich .scrub-bahn
   (das Element darunter, ueber die ganze Hoehe). Deshalb `pointer-events:none`:
   der Griff liegt im DOM ueber der Bahn, und mit eigenen Zeiger-Ereignissen
   schluckte er jede Beruehrung, die genau auf ihm landete — man konnte nur noch
   ober- und unterhalb anfassen (Marlin 2026-07-29). */
.scrub-grip{position:absolute;right:0;top:var(--scrub-y,0);width:2.6rem;height:64px;
  pointer-events:none;touch-action:none;
  display:flex;align-items:center;justify-content:flex-end;
  padding-right:0.3rem;background:none;border:none}
.scrub-grip::before{content:'';width:1.5rem;height:44px;
  display:flex;border-radius:999px;background:rgba(255,255,255,0.14);
  border:1px solid rgba(255,255,255,0.22);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.scrub-grip::after{content:'';position:absolute;right:0.85rem;
  width:0.5rem;height:1.4rem;border-radius:999px;background:rgba(255,255,255,0.6);
  pointer-events:none}
#scrubber.aktiv .scrub-grip::before{background:var(--accent-a40);border-color:var(--accent)}
#scrubber.aktiv .scrub-bahn{cursor:grabbing}
.scrub-label{pointer-events:none;position:absolute;right:2.6rem;top:var(--scrub-y,0);transform:translateY(4px);
  padding:0.3rem 0.6rem;border-radius:999px;white-space:nowrap;
  background:rgba(18,20,28,0.94);border:1px solid rgba(255,255,255,0.14);
  color:#fff;font-size:0.72rem;pointer-events:none}


/* ── Weiche Kanten an scrollenden Leisten ───────────────────────────────────
   Pills in einer scrollenden Leiste wurden am Rand hart durchgeschnitten — der
   Glas-Hintergrund endet mitten in der Rundung, das sieht kaputt aus (Marlin
   2026-07-29). Eine Maske laesst den Inhalt stattdessen ausblenden. Die Klassen
   `rand-links`/`rand-rechts` setzt JS je nach Scrollposition, damit am Anfang
   und Ende nichts unnoetig verblasst. */
.rand-weich{--rand:1.4rem;
  -webkit-mask-image:linear-gradient(to right,#000 0,#000 100%);
  mask-image:linear-gradient(to right,#000 0,#000 100%)}
.rand-weich.rand-rechts{
  -webkit-mask-image:linear-gradient(to right,#000 calc(100% - var(--rand)),transparent 100%);
  mask-image:linear-gradient(to right,#000 calc(100% - var(--rand)),transparent 100%)}
.rand-weich.rand-links{
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--rand));
  mask-image:linear-gradient(to right,transparent 0,#000 var(--rand))}
.rand-weich.rand-links.rand-rechts{
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--rand),#000 calc(100% - var(--rand)),transparent 100%);
  mask-image:linear-gradient(to right,transparent 0,#000 var(--rand),#000 calc(100% - var(--rand)),transparent 100%)}

/* ── Sammlungs-Leiste klebt oben ────────────────────────────────────────────
   Beim Scrollen durch zehntausend Bilder will man die Filter erreichen, ohne
   erst hochzuscrollen (Marlin 2026-07-29). Deckender Grund (kein Blur), damit
   das Scrollen fluessig bleibt. */
#collection-bar{position:sticky;top:0;z-index:64;background:var(--base);
  padding-top:0.35rem;padding-bottom:0.35rem}

/* ── Auswahl-Leiste weicht Dialogen ─────────────────────────────────────────
   Die feste Leiste lag ueber offenen Dialogen und schnitt deren untere Knoepfe
   ab (Marlin 2026-07-29, Screenshot). Solange ein Dialog offen ist, tritt sie
   zurueck — ihre Aktionen sind dort ohnehin nicht gemeint. */
body:has(.modal.active) .sel-toolbar{display:none!important}
/* Dialoge auf dem Handy: nie hinter der unteren Bedienleiste enden */
body.has-mobile-bar .modal{align-items:flex-start;padding:0.6rem 0.6rem 5rem;overflow-y:auto}
body.has-mobile-bar .modal-box{max-height:none}

/* ── Ebenen-Ordnung (z-index) ───────────────────────────────────────────────
   Eine Stelle, an der die Reihenfolge steht, statt verstreuter Zahlen. Anlass:
   der Sammlungen-Dialog lag UNTER der klebenden Chip-Leiste (Modal 50 gegen
   Leiste 64) und war halb verdeckt (Marlin 2026-07-29).

     1   Grid-Inhalt
     64  klebende Chip-Leiste
     66  Scrub-Griff
     70  untere Bedienleiste
     75  Blatt-Hintergrund   80  Mehr-Blatt
     200 Dialoge (ueber allem Bedienzeug)
     300 Kontextmenue einer Kachel
     900 PhotoSwipe-Overlay  950 Dialoge IM Overlay
   Wer eine neue Ebene braucht, traegt sie hier ein. */
.modal{z-index:200}


/* ── Hinter offenen Menues leicht weichzeichnen ─────────────────────────────
   Marlin: "wenn ein menü aufgeht, den Rest dahinter leicht geblurrt". Das laeuft
   nur, WAEHREND ein Menue offen ist — dann wird nicht gescrollt, also kostet der
   Effekt keine Bildrate (anders als ein Weichzeichner auf einer fixierten
   Leiste, der bei jedem Scroll-Frame neu gerechnet werden muesste). */
.modal{backdrop-filter:blur(7px) saturate(0.9);-webkit-backdrop-filter:blur(7px) saturate(0.9)}
.sheet-backdrop{backdrop-filter:blur(7px) saturate(0.9);-webkit-backdrop-filter:blur(7px) saturate(0.9)}
/* Das Kachel-Kontextmenue hat keine eigene Hintergrundflaeche — es bekommt eine,
   damit auch dort der Rest zurueckliegt. */
body.hat-kontextmenue::after{content:'';position:fixed;inset:0;z-index:299;
  background:rgba(0,0,0,0.28);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
@media (prefers-reduced-motion:reduce){
  .modal,.sheet-backdrop{backdrop-filter:none;-webkit-backdrop-filter:none}
}


/* ── Auswahl-Leiste bei flachen oder schmalen Fenstern ──────────────────────
   Aniko konnte die Auswahl nicht taggen: bei einem flachen Fenster lief die
   Leiste mit ihren zehn Knoepfen aus dem Bild heraus. Sie darf jetzt umbrechen,
   ist in der Hoehe begrenzt und scrollt notfalls in sich. */
.sel-toolbar{flex-wrap:wrap;max-width:calc(100vw - 1.5rem);
  max-height:min(40vh, 12rem);overflow-y:auto;row-gap:0.4rem}
@media (max-height:620px){
  .sel-toolbar{bottom:0.5rem}
}


/* ── Vollbild-Knopf im Querformat ───────────────────────────────────────────
   Erscheint nur im Querformat und nur, solange die Seite nicht im Vollbild ist.
   Unten links, weil rechts die Scrub-Bahn liegt und oben die Leisten. */
#vollbild-knopf{position:fixed;left:0.7rem;bottom:0.7rem;z-index:68;
  width:2.6rem;height:2.6rem;border-radius:999px;font-size:1.1rem;line-height:1;
  background:rgba(18,20,28,0.72);color:rgba(255,255,255,0.85);
  border:1px solid rgba(255,255,255,0.18);cursor:pointer;
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
#vollbild-knopf:active{transform:scale(0.94)}
body.has-mobile-bar #vollbild-knopf{bottom:4.6rem}
