/* Mobile-first gallery styles. Theme via data-theme attribute (set by script.js),
   falls back to prefers-color-scheme when no manual choice was made. */

:root {
    --gap: 1rem;
    --phi: 1.618;
    /* Golden-ratio-Abstandsskala nur noch für UI-Flächen (body-Padding,
       Abschnittsabstände) — die Bild-Grids nutzen den kompakten --grid-gap */
    --gap-lg: calc(var(--gap) * var(--phi));
    --gap-xl: calc(var(--gap-lg) * var(--phi));
    /* Kompakter Abstand der Justified-Rows-Grids (.gallery/.folders):
       fester px-Wert statt Golden-Ratio-Skala, wird per Media Query nur
       moderat mitskaliert — Bilder sollen dicht stehen (justified-grid-Stil) */
    --grid-gap: 8px;
    --bg: #111;
    --fg: #eee;
    --surface: #1d1d1f;
    --accent: #4c9aff;
    --error: #ff6b6b;
    --border: color-mix(in srgb, var(--fg) 25%, transparent);
}

/* --grid-gap skaliert moderat mit den Zeilenhöhen-Stufen von .gallery/.folders
   (gleiche Breakpoints), bleibt aber bewusst klein statt Golden-Ratio-Eskalation */
@media (min-width: 640px) {
    :root {
        --grid-gap: 10px;
    }
}

@media (min-width: 1100px) {
    :root {
        --grid-gap: 12px;
    }
}

@media (min-width: 2400px) {
    :root {
        --grid-gap: 14px;
    }
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
        --bg: #fff;
        --fg: #111;
        --surface: #f2f2f4;
        --error: #c0392b;
    }
}

:root[data-theme="light"] {
    --bg: #fff;
    --fg: #111;
    --surface: #f2f2f4;
    --error: #c0392b;
}

* {
    box-sizing: border-box;
}

/* Hintergrund sitzt auf html, nicht body: body ist ab 1100px auf die
   Inhaltsspalte begrenzt (s.u.) und würde ein Hintergrundbild auf diese
   Spalte zuschneiden. html füllt immer den ganzen Bildschirm, unabhängig
   von der Breiten-Beschränkung des Inhalts. */
html {
    min-height: 100vh;
    background-color: var(--bg);
    background-image: var(--content-bg, none);
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
    background-repeat: no-repeat;
}

body {
    margin: 0;
    margin-inline: auto;
    /* wächst mit dem Viewport bis --gap-xl, gibt breiten Bildschirmen
       ruhige Randflächen statt Thumbnails randlos bis zum Fenster */
    padding: clamp(var(--gap), 3vw, var(--gap-xl));
    background: transparent;
    color: var(--fg);
    font-family: system-ui, sans-serif;
}

/* Goldener Schnitt bei der Seitenbefüllung: Inhalt ≈ 100vw/phi ≈ 61.8vw,
   Rest Randfläche. Untergrenze 1040px (≈ 1100px-Breakpoint minus 2x ~30px
   body-Padding) vermeidet einen harten Breiten-Sprung am Breakpoint und greift,
   bis 61.8vw darüber liegt; Obergrenze deckelt nur noch extreme Ultrawides —
   auf 4K (3840px ⇒ ~2373px) greift der goldene Schnitt fast voll, damit die
   Galerie dort mehr Spalten zeigt statt breiter Randflächen. */
@media (min-width: 1100px) {
    body {
        max-width: clamp(1040px, calc(100vw / var(--phi)), 2300px);
    }
}

/* Header: eine Zeile — Breadcrumb links, ZIP-Download mittig, Tag/Nacht rechts */

/* Freistellung: der Header sitzt direkt über dem optionalen Ordner-Hintergrundbild
   (html hat kein Deckungs-Padding) — durchgehender Chip über die volle
   Inhaltsbreite statt nackter Text, sonst z.B. der blaue --accent-Link auf
   blauem Himmel unlesbar. Gilt für alle Ordner, nicht nur mit Hintergrundbild,
   damit der Header überall gleich aussieht. */
.site-header {
    /* Stays pinned to the top while scrolling, so breadcrumb, sorting and the
       dark-mode toggle are reachable from any scroll position. z-index is required:
       sticky makes the header a positioned element, and the later .gallery-item /
       .folder-item tiles are position: relative and would otherwise paint over it.
       Stays below .to-top (50) and the lightbox (100). The existing backdrop-filter
       now frosts the content scrolling underneath. */
    position: sticky;
    top: 0.5rem;
    z-index: 40;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-areas:
        "info  tools toggle"
        "desc  .     .     ";
    align-items: center;
    column-gap: var(--gap);
    row-gap: 0.15rem;
    margin-bottom: var(--gap);
    padding: 0.5rem 0.75rem;
    background: color-mix(in srgb, var(--surface) 80%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-radius: 10px;
}

.header-info {
    grid-area: info;
    display: flex;
    min-width: 0;
}

.header-top {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}

/* Zurück zur Ordner-Übersicht — nur sichtbar innerhalb eines Albums (s. index.php,
   $dir !== ''), führt unabhängig von der Verschachtelungstiefe direkt zur Root-Übersicht.
   Gleiche runde Glas-Optik wie #theme-toggle, nur kleiner, da zweitrangig zum Pfad. */
.back-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    background: var(--surface);
    color: var(--fg);
}

.back-button svg {
    width: 0.8rem;
    height: 0.8rem;
    fill: currentColor;
}

.back-button:hover {
    background: color-mix(in srgb, var(--fg) 10%, var(--surface));
}

.header-description {
    grid-area: desc;
    align-self: start;
    min-width: 0;
    margin: 0;
    font-size: 0.8rem;
    opacity: 0.8;
    overflow-wrap: anywhere;
}

#theme-toggle {
    grid-area: toggle;
    justify-self: end;
    align-self: start;
    background: var(--surface);
    color: var(--fg);
    border: none;
    border-radius: 50%;
    width: 2.5rem;
    height: 2.5rem;
    font-size: 1.25rem;
    cursor: pointer;
}

/* Floating "back to top" button. Same glass language as .site-header and the same
   round shape as #theme-toggle, so it reads as part of the gallery chrome rather
   than an overlay artefact. script.js adds .is-visible after half a screen of
   scrolling; the click jumps to the very top instantly, like the Home key.
   z-index stays below the lightbox (100); while the lightbox is open the button is
   dropped entirely — the single-image view does not need it and the comment panel
   sits in the same corner. */
.to-top {
    position: fixed;
    right: clamp(0.75rem, 2vw, 1.25rem);
    bottom: clamp(0.75rem, 2vw, 1.25rem);
    z-index: 50;
    display: grid;
    place-items: center;
    width: 2.75rem;          /* 44px — comfortable touch target */
    height: 2.75rem;
    padding: 0;
    background: color-mix(in srgb, var(--surface) 80%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: var(--fg);
    border: none;
    border-radius: 50%;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease, visibility 0.15s;
}

.to-top svg {
    width: 1.1rem;
    height: 1.1rem;
    fill: currentColor;
}

.to-top:hover {
    background: color-mix(in srgb, var(--fg) 12%, var(--surface));
}

.to-top.is-visible {
    opacity: 1;
    visibility: visible;
}

body:has(#lightbox:not([hidden])) .to-top {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    .to-top { transition: none; }
}

/* Breadcrumb */

.breadcrumb {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    font-size: 0.9rem;
}

.breadcrumb a {
    color: var(--accent);
    text-decoration: none;
}

.breadcrumb a:hover {
    text-decoration: underline;
}

/* Folder cards: gleiches Justified-Rows-Grid wie .gallery (s.u.), Cover im
   eigenen Seitenverhältnis (--ratio, siehe index.php), Name/Anzahl nur bei
   Hover/Fokus eingeblendet — optisch nah an einer reinen Bild-Kachel. */

.folders {
    /* Ziel-Zeilenhöhe etwas größer als .gallery: Ordner-Karten tragen zusätzlich
       den Label-Text, eine zu flache Zeile würde ihn beim Hover eng wirken lassen */
    --row-h: 150px;
    display: flex;
    flex-wrap: wrap;
    gap: var(--grid-gap);
    margin-bottom: var(--gap-lg);
}

.folders::after {
    content: '';
    flex-grow: 100000;
    flex-basis: 0;
}

@media (min-width: 640px) {
    .folders {
        --row-h: 190px;
    }
}

/* Zeilenhöhen-Stufen analog zur .gallery (s.u.); der Gap bleibt kompakt
   (--grid-gap, nur moderat mitskaliert), damit die Karten dicht stehen. */
@media (min-width: 1100px) {
    .folders {
        --row-h: 230px;
    }
}

@media (min-width: 2400px) {
    .folders {
        --row-h: 250px;
    }
}

.folder-item {
    /* --r-Fallback: golden ratio für Karten ohne Cover (gesperrt/leer) —
       analog zu .gallery-item, das ohne --ratio quadratisch (1) zeigt */
    --r: var(--ratio, var(--phi));
    position: relative;
    display: block;
    aspect-ratio: var(--r);
    flex-grow: var(--r);
    flex-basis: calc(var(--r) * var(--row-h));
    background: var(--surface);
    border-radius: 12px;
    overflow: hidden;
    text-decoration: none;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.folder-item:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
}

.folder-cover {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Cover eines gesperrten Ordners: thumb.php liefert dafür bereits eine
   serverseitig unkenntlich gemachte Vorschau aus (siehe create_thumbnail()
   $blur-Parameter in lib.php) — der CSS-Blur hier glättet nur deren
   Pixelierung, er ist keine eigene Schutzmaßnahme. scale() kaschiert den
   dadurch leicht vergrößerten, unscharfen Bildrand. --cover-* kommen aus
   config.php: blur_strength (blur_css_vars() in lib.php, gesetzt auf <html>
   in index.php/locked.php) — Fallbacks hier greifen nur, falls das Attribut
   fehlen sollte. */
.folder-cover-locked {
    filter: blur(var(--cover-blur, 14px)) brightness(var(--cover-dim, 0.55)) saturate(var(--cover-sat, 0.9));
    transform: scale(1.12);
}

/* Download-/Kopier-Hürden: kein natives Drag-Save, kein iOS-Long-Press-Menü,
   keine Text-/Bild-Selektion. Rechtsklick-Blockade dazu in script.js. */
.gallery-item img,
.folder-cover,
.lightbox-img {
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
    user-select: none;
}

/* Lock / empty-folder fallback icon, centered on the card */

.folder-placeholder {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
}

.folder-placeholder svg {
    width: 2.5rem;
    height: 2.5rem;
    fill: color-mix(in srgb, var(--fg) 40%, transparent);
}

/* Schloss-Symbol über dem geblurten Cover (oder, ohne Cover, über dem
   Karten-Hintergrund) — heller/mit Schatten statt der normalen
   Platzhalter-Farbe, damit es auf einem unscharfen Foto lesbar bleibt. */
.folder-lock-overlay {
    background: rgba(0, 0, 0, 0.15);
}

.folder-lock-overlay svg {
    fill: rgba(255, 255, 255, 0.92);
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
}

/* "New" badge on folders whose newest image is within config new_folder_days
   (folder_is_new() in lib.php). Always visible — unlike the label it must be
   spottable without hovering; that is the whole point of the badge. */

.folder-badge {
    position: absolute;
    top: 0.4rem;
    left: 0.4rem;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
}

/* Name and image count over a bottom gradient (readable in both themes).
   Immer sichtbar, damit die Ordnerbezeichnung nicht erst per Hover/Fokus
   erscheint (siehe .gallery-item). */

.folder-label {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 1.25rem 0.75rem 0.6rem;
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
    color: #fff;
}

.folder-name {
    font-weight: 600;
    font-size: 0.95rem;
    overflow-wrap: anywhere;
}

.folder-meta {
    font-size: 0.75rem;
    opacity: 0.85;
}

/* Middle header column: sort switch + ZIP download. Always rendered (see
   index.php), so the theme toggle stays in the right grid column even when
   there is nothing to sort or download. */

.header-tools {
    grid-area: tools;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    /* Folder-sort + image-sort + view + ZIP can be present together; even at
       ≥1100px reicht die verfügbare Breite dafür nicht immer (s. body max-width
       clamp), deshalb bleibt intern flex-wrap als Sicherheitsnetz. */
    flex-wrap: wrap;
}

/* Sort switch (random / by name). Random is the default — the active random
   button doubles as "shuffle again" (it reloads without a sort parameter,
   see gallery_url() in index.php). */

.sort-switch {
    display: flex;
    gap: 2px;
    padding: 2px;
    background: var(--surface);
    border-radius: 8px;
}

.sort-option {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.4rem 0.7rem;
    border-radius: 6px;
    color: var(--fg);
    font-size: 0.85rem;
    text-decoration: none;
    white-space: nowrap;
    opacity: 0.65;
}

.sort-option:hover {
    background: color-mix(in srgb, var(--fg) 10%, var(--surface));
    opacity: 1;
}

.sort-option.sort-active {
    background: color-mix(in srgb, var(--accent) 22%, var(--surface));
    color: var(--fg);
    opacity: 1;
}

.sort-option svg {
    width: 0.95rem;
    height: 0.95rem;
    fill: currentColor;
}

/* A-Z / Z-A toggle: same icon mirrored vertically for the descending state,
   so the button keeps its slot in the layout and only its direction flips */
.sort-option svg.icon-flip {
    transform: scaleY(-1);
}

/* Unter 1100px passt header-tools nicht mehr zuverlässig neben info/toggle in
   eine Zeile (s. .site-header oben) — eigene Zeile für die Werkzeuge, statt sie
   dem Grid-Auto-Placement zu überlassen. */
@media (max-width: 1099px) {
    .site-header {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "info  toggle"
            "desc  desc"
            "tools tools";
        row-gap: 0.15rem;
    }

    .header-tools {
        justify-content: flex-start;
    }

    .header-tools:has(*) {
        margin-top: 0.35rem;
    }
}

/* Icon-only below the label breakpoint: keeps the tools row from wrapping again on phones */
@media (max-width: 639px) {
    .sort-option {
        padding: 0.4rem 0.55rem;
        font-size: 0;
        gap: 0;
    }
}

/* ZIP-download of all images in the current folder, centered in .site-header.
   Only active for folders with their own password (folder_has_own_password()
   in lib.php); otherwise shown greyed-out with an explanatory title. */

.download-all {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.9rem;
    border-radius: 8px;
    background: var(--surface);
    color: var(--fg);
    font-size: 0.85rem;
    text-decoration: none;
    cursor: pointer;
}

.download-all:hover {
    background: color-mix(in srgb, var(--fg) 10%, var(--surface));
}

.download-all svg {
    width: 1rem;
    height: 1rem;
    fill: currentColor;
}

.download-all-disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.download-all-disabled:hover {
    background: var(--surface);
}

/* Image grid */

/* Justified Rows (Flickr-Stil) statt Masonry: Flexbox-Zeilen in Lesereihenfolge —
   links nach rechts, dann nächste Zeile (DOM-Reihenfolge = Anzeige-Reihenfolge,
   die Nummerierung der Dateinamen läuft also fortlaufend durch).
   Jedes Item trägt sein Seitenverhältnis als --ratio (siehe index.php):
   flex-grow proportional zum Verhältnis verteilt die Zeilenbreite so, dass alle
   Bilder einer Zeile exakt gleich hoch stehen — unverzerrt, ohne Zuschnitt
   (thumb.php/create_thumbnail() liefern ohnehin proportional eingepasste Thumbnails). */
.gallery {
    /* Ziel-Zeilenhöhe: flex-basis packt die Zeilen daran, die echte Höhe
       schwankt je nach Bildmix leicht darum herum */
    --row-h: 120px;
    display: flex;
    flex-wrap: wrap;
    gap: var(--grid-gap);
}

/* Spacer: schluckt den Rest der letzten (unvollständigen) Zeile, damit wenige
   Restbilder nicht auf volle Breite/Höhe aufgeblasen werden */
.gallery::after {
    content: '';
    flex-grow: 100000;
    flex-basis: 0;
}

@media (min-width: 640px) {
    .gallery {
        --row-h: 160px;
    }
}

@media (min-width: 1100px) {
    .gallery {
        --row-h: 200px;
    }
}

/* Echte 4K/Ultrawide-Fenster: Zeile nur moderat höher — die zusätzliche
   Inhaltsbreite (body max-width, s.o.) geht in mehr Spalten, nicht in Größe */
@media (min-width: 2400px) {
    .gallery {
        --row-h: 220px;
    }
}

.gallery-item {
    /* Zeilen-Mathematik: Breite ∝ --ratio (flex-grow), aspect-ratio leitet die
       Höhe daraus ab ⇒ alle Bilder einer Flexzeile gleich hoch, Zeile bündig.
       aspect-ratio dient zugleich als Platzhalterhöhe vor dem Lazy-Load.
       --r löst den Fallback (fehlendes --ratio ⇒ quadratisch) einmal auf. */
    --r: var(--ratio, 1);
    position: relative; /* Anker für die Zähler-Badges (.tile-stats) */
    aspect-ratio: var(--r);
    flex-grow: var(--r);
    flex-basis: calc(var(--r) * var(--row-h));
    margin: 0;
    border-radius: 8px;
    overflow: hidden;
    background: var(--surface);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.gallery-item img {
    display: block;
    width: 100%;
    /* Item-Ratio = Bild-Ratio; cover fängt nur Sub-Pixel-Reste der auf zwei
       Nachkommastellen gerundeten --ratio ab, kein sichtbarer Anschnitt */
    height: 100%;
    object-fit: cover;
    cursor: pointer;
    transition: transform 0.25s ease, opacity 0.3s ease;
}

/* Subtle hover: tile lifts like .folder-item, image zooms slightly inside the
   clipped, rounded tile. Only for real pointers — no sticky hover on touch. */
@media (hover: hover) {
    .gallery-item:hover {
        transform: translateY(-2px);
        box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
        z-index: 1; /* shadow must not be covered by neighbouring tiles */
    }

    .gallery-item:hover img {
        transform: scale(1.05);
    }
}

/* Counter badges (TWG-style hit count plus downloads), pinned to the tile corner
   over the thumbnail. pointer-events: none — the whole tile must stay clickable,
   the badges never swallow the click that opens the lightbox. Hidden while the
   thumbnail is still loading, so they don't float on the shimmer placeholder.
   The download badge stays hidden until the image has been downloaded at least
   once (hidden attribute, set server-side in index.php). */

.tile-stats {
    position: absolute;
    right: 0.35rem;
    bottom: 0.35rem;
    display: flex;
    gap: 0.25rem;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.tile-stat {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.15rem 0.4rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 0.7rem;
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
}

.tile-stat[hidden] {
    display: none;
}

.gallery-item:has(img.loaded) .tile-stats {
    opacity: 0.85;
}

.gallery-item:hover .tile-stats {
    opacity: 1;
}

.tile-stat svg {
    width: 0.75rem;
    height: 0.75rem;
    fill: currentColor;
}

/* Map link on the tile — the one badge that IS clickable, so it needs its own
   pointer-events (the .tile-stats bar above switches them off). Only once the
   thumbnail is loaded, i.e. once the bar is actually visible: an invisible but
   clickable link over the shimmer placeholder would be a trap. The click never
   reaches the lightbox because that listener sits on the <img>, not the tile. */

.gallery-item:has(img.loaded) .tile-gps {
    pointer-events: auto;
}

.tile-gps {
    color: #fff;
    text-decoration: none;
}

.tile-gps:hover {
    background: var(--accent);
}

/* Screen-reader-only label (the badge itself is icon + number) */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* Lazy loading: shimmer placeholder while the thumbnail request is pending,
   fade-in once loaded (script.js adds .loaded on the img load event) */

img[data-src],
.gallery-item img:not(.loaded),
.folder-cover:not(.loaded) {
    opacity: 0;
}

.gallery-item img.loaded,
.folder-cover.loaded {
    opacity: 1;
}

.gallery-item:has(img:not(.loaded)),
.folder-item:has(.folder-cover:not(.loaded)) {
    background: linear-gradient(
        100deg,
        var(--surface) 40%,
        color-mix(in srgb, var(--fg) 8%, var(--surface)) 50%,
        var(--surface) 60%
    );
    background-size: 200% 100%;
    animation: shimmer 1.4s linear infinite;
}

@keyframes shimmer {
    from { background-position: 120% 0; }
    to   { background-position: -80% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .gallery-item:has(img:not(.loaded)),
    .folder-item:has(.folder-cover:not(.loaded)) {
        animation: none;
    }

    .gallery-item,
    .gallery-item img {
        transition: none;
    }
}

.empty {
    color: var(--fg);
    opacity: 0.7;
}

/* Login */

.login {
    display: grid;
    place-items: center;
    min-height: 70vh;
}

.login-form {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    width: min(250px, 100%);
    padding: 0.75rem;
    background: var(--surface);
    border-radius: 12px;
}

.login-form h1 {
    margin: 0;
    font-size: 0.95rem;
    overflow-wrap: anywhere;
}

.login-form label {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    font-size: 0.85rem;
}

/* Matches the visible password box in both states — the reveal toggle swaps the
   input between type="password" and type="text", so this must not be tied to a
   single type. The hidden dir/folder/csrf/username inputs are excluded. */
.login-form input:not([type="hidden"]) {
    width: 100%;
    padding: 0.45rem 0.55rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--fg);
    font-size: 1rem;
}

/* Only the submit button — .password-toggle sits nested inside .password-field. */
.login-form > button {
    padding: 0.45rem;
    border: none;
    border-radius: 6px;
    background: var(--accent);
    color: #fff;
    font-size: 0.95rem;
    cursor: pointer;
}

.login-subtitle {
    margin: 0;
    color: color-mix(in srgb, var(--fg) 65%, transparent);
    font-size: 0.85rem;
    overflow-wrap: anywhere;
}

.login-error {
    margin: 0;
    color: var(--error);
    font-size: 0.85rem;
}

/* Password field with the reveal toggle overlaid on the left. */
.password-field {
    position: relative;
    display: block;
}

/* Höhere Spezifität als .login-form input:not([type="hidden"]) nötig (Klasse + Klasse +
   Element schlägt Klasse + Attribut + Element) — sonst gewinnt dessen padding: 0.45rem 0.55rem
   und überschreibt padding-left wieder, der Text läuft dann unter das Icon. */
.login-form .password-field input {
    padding-left: 2.2rem;
}

/* Edge draws its own reveal button on the right; ours is the only one. */
.login-form .password-field input::-ms-reveal {
    display: none;
}

.password-toggle {
    position: absolute;
    top: 50%;
    left: 0.2rem;
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    width: 1.8rem;
    height: 1.8rem;
    padding: 0;
    border: none;
    background: none;
    color: color-mix(in srgb, var(--fg) 55%, transparent);
    cursor: pointer;
}

.password-toggle:hover {
    color: var(--fg);
}

.password-toggle svg {
    width: 1rem;
    height: 1rem;
    fill: currentColor;
}

/* display: grid on .password-toggle places each child SVG in its own implicit
   row instead of stacking them — harmless while script.js's hidden attribute
   actually hides the inactive icon, but Safari doesn't reliably apply
   [hidden] to <svg>, so both icons render at once (see .tile-stat[hidden]
   and .lightbox-nav .lightbox-gps[hidden] for the same fix elsewhere). */
.password-toggle svg[hidden] {
    display: none;
}

/* Locked folder view (locked.php): one minimal glass card — just the login
   form (title, password field, button) — over the folder's own
   (server-blurred) cover, which sits on <html> via --content-bg just like
   the gallery pages. */
.locked-page {
    padding-block: var(--gap-lg);
}

.locked-panel {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    width: min(250px, 100%);
    padding: 0.75rem;
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-radius: 14px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}

/* The form is already inside the glass panel — drop its own surface/padding. */
.locked-panel .login-form {
    width: 100%;
    padding: 0;
    background: none;
    border-radius: 0;
}

/* Lightbox */

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* "safe center" keeps the top reachable when content overflows;
       flex-start is the fallback for older browsers */
    justify-content: flex-start;
    justify-content: safe center;
    gap: 0.6rem;
    padding: 1rem 0;
    background: rgba(0, 0, 0, 0.9);
    overflow-y: auto;
}

.lightbox[hidden] {
    display: none;
}

/* Echtes Vollbild (Diashow, s. script.js toggleFullscreen()): kein Rand verschenken */
.lightbox.is-fullscreen {
    padding: 0;
}

.lightbox.is-fullscreen .lightbox-img {
    max-width: 100vw;
}

.lightbox-img {
    max-width: calc(100vw - 2rem);
    /* Bild füllt die restliche Fensterhöhe (nach Caption, Nav, Kommentaren);
       min-height: 0 erlaubt dem Flex-Item, unter seine intrinsische Höhe zu schrumpfen */
    flex: 1 1 auto;
    min-height: 0;
    object-fit: contain;
    border-radius: 4px;
    transition: opacity 0.18s ease;
    /* Gesten gehören dem Skript (Wischen, Pinch-Zoom, Pan — siehe script.js),
       nicht dem Browser-Scrolling */
    touch-action: none;
    cursor: zoom-in;
}

/* Zoom sits as a CSS transform on the canvas (script.js); the transform origin is
   the canvas center, which the pan math in zoomAt()/clampPan() relies on. */
.lightbox-img.zoomed {
    transform-origin: center;
    cursor: grab;
}

.lightbox-img.zoomed:active {
    cursor: grabbing;
}

/* Das gezoomte Canvas ragt über seine Layout-Box hinaus — ohne dies bekäme die
   scrollbare Lightbox (overflow-y: auto, s.o.) davon Scrollbalken */
.lightbox.is-zoomed {
    overflow: hidden;
}

/* Kurzzeitig sichtbar, während Thumbnail als Platzhalter dient und das
   Vollbild im Hintergrund lädt (siehe show() in script.js) */
.lightbox-img.is-loading {
    filter: blur(6px);
    transition: filter 0.15s ease-out;
}

/* Kurzer Ausblend-Zustand beim Wechsel auf ein bereits vorgeladenes Vollbild
   (siehe show() in script.js), damit der Bildtausch weich statt hart wirkt */
.lightbox-img.is-fading {
    opacity: 0;
}

/* TWG-style navigation bar */

/* Fixed width, independent of the image: the buttons keep the same spot from
   image to image. The filename takes whatever is left (flex: 1), the GPS slot
   keeps its space when hidden (visibility, see below). */
.lightbox-nav {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    width: min(44rem, 100vw - 2rem);
    padding: 0.3rem 0.5rem;
    background: rgba(255, 255, 255, 0.12);
    border-radius: 10px;
}

/* Image filename, left of the nav buttons; fills the remaining bar width and
   truncates instead of pushing the buttons around */
.lightbox-filename {
    flex: 1 1 0;
    min-width: 0;
    padding: 0 0.5rem 0 0.25rem;
    overflow: hidden;
    color: #eee;
    font-size: 0.85rem;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.lightbox-nav button,
.lightbox-nav .lightbox-gps {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 2.75rem;
    height: 2.25rem;
    padding: 0;
    border: none;
    border-radius: 6px;
    background: none;
    color: #eee;
    cursor: pointer;
}

.lightbox-nav button:hover,
.lightbox-nav .lightbox-gps:hover {
    background: rgba(255, 255, 255, 0.18);
}

/* script.js toggles .lightbox-gps[hidden] per image (no GPS, no link).
   visibility instead of display: none — the slot keeps its width, so the
   other buttons don't shift between images with and without GPS. The
   display: grid above deliberately overrides the attribute's display: none. */
.lightbox-nav .lightbox-gps[hidden] {
    visibility: hidden;
}

/* .lightbox-nav button { display: grid } above overrides the browser's
   [hidden] { display: none } for plain nav buttons too (fullscreen, comment) —
   same root cause as .lightbox-gps[hidden] above. Buttons collapse fully
   instead of keeping their slot: unlike GPS, they only toggle once per page
   load, not per image, so a one-time layout shift is fine. */
.lightbox-nav button[hidden] {
    display: none;
}

.lightbox-nav button[aria-pressed="true"] {
    background: var(--accent);
    color: #fff;
}

.lightbox-nav button:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.lightbox-nav button:disabled:hover {
    background: none;
}

.lightbox-nav svg {
    width: 1.1rem;
    height: 1.1rem;
    fill: currentColor;
}

/* Kurzer Hinweistext (z. B. "Diashow gestartet"), siehe showHint() in script.js.
   position: fixed statt an die Nav-Leiste gekoppelt, weil deren Position durch
   "justify-content: safe center" je nach Bildformat variiert. */
.lightbox-hint {
    position: fixed;
    top: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    padding: 0.5rem 1rem;
    background: rgba(0, 0, 0, 0.75);
    color: #fff;
    border-radius: 8px;
    font-size: 0.9rem;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s ease;
}

.lightbox-hint.is-visible {
    opacity: 1;
}

/* Phones: filename on its own line, the buttons share the full row evenly —
   always one row, no wrapping that depends on the filename or GPS, so the
   positions stay put here as well. Slightly taller buttons for touch. */
@media (max-width: 639px) {
    .lightbox-nav {
        flex-wrap: wrap;
        gap: 0.15rem;
        padding: 0.3rem;
    }

    .lightbox-filename {
        flex: 0 0 100%;
        padding: 0 0.25rem 0.15rem;
        text-align: center;
    }

    .lightbox-nav button,
    .lightbox-nav .lightbox-gps {
        flex: 1 1 0;
        width: auto;
        min-width: 0;
        height: 2.5rem;
    }
}

/* Comment-count badge on the speech-bubble nav button */

.lightbox-nav [data-nav="comment"] {
    position: relative;
}

.comment-badge {
    position: absolute;
    top: 0.05rem;
    right: 0.15rem;
    min-width: 1rem;
    padding: 0 0.2rem;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-size: 0.65rem;
    line-height: 1rem;
    text-align: center;
}

/* Tooltip preview on hover/focus of the comment button: purely informational,
   visibility toggled via `hidden` in script.js (no CSS :hover — touch has no
   hover and would otherwise leave it stuck open after a tap). */

.comment-tip {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-bottom: 0.5rem;
    z-index: 102;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    width: min(320px, calc(100vw - 2rem));
    padding: 0.6rem 0.75rem;
    background: var(--surface);
    color: var(--fg);
    border-radius: 12px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
    text-align: left;
    pointer-events: none;
    cursor: default;
}

/* display: flex oben überstimmt sonst die Browser-Regel [hidden] { display: none } —
   ohne diese Regel steht der Tooltip dauerhaft offen (Sichtbarkeit kommt aus script.js
   über das hidden-Attribut). Gleiches Muster wie .lightbox-gps[hidden]. */
.comment-tip[hidden] {
    display: none;
}

.comment-tip p {
    margin: 0;
    font-size: 0.8rem;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.comment-tip-more {
    opacity: 0.75;
    font-style: italic;
}

.comment-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    max-height: 45vh;
    min-height: 0;
    overflow-y: auto;
}

.comment-list li {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.5rem 0.6rem;
    background: var(--bg);
    border-radius: 8px;
}

/* Zeitstempel über dem Kommentartext, sowohl beim Lesen als auch beim Bearbeiten
   (startEdit() lässt dieses <time> stehen) — kein Layout-Sprung beim Wechsel. */
.comment-list time {
    flex: 0 0 auto;
    font-size: 0.75rem;
    opacity: 0.65;
    white-space: nowrap;
}

.comment-list p {
    margin: 0;
    font-size: 0.9rem;
    white-space: normal;
    overflow-wrap: break-word;
}

.comment-empty {
    margin: 0;
    font-size: 0.9rem;
    opacity: 0.7;
}

/* Combined comments overlay (native <dialog>, opened from the nav bar):
   read-only list on top, write form below */

.comment-dialog {
    width: min(560px, calc(100vw - 2rem));
    padding: var(--gap);
    border: none;
    border-radius: 12px;
    background: var(--surface);
    color: var(--fg);
}

/* Layout nur für den offenen Zustand: display: flex in der Basisregel würde die
   Browser-Regel dialog:not([open]) { display: none } überstimmen und den
   geschlossenen Dialog als leeren Kasten im Seitenfluss rendern. */
.comment-dialog[open] {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.comment-dialog::backdrop {
    background: rgba(0, 0, 0, 0.6);
}

.comment-dialog h2 {
    margin: 0;
    font-size: 1.1rem;
}

.comment-form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

/* commentForm.hidden is set via JS in readonly mode; display: flex above
   overrides the browser's [hidden] { display: none } rule otherwise —
   same pattern as .comment-tip[hidden]/.lightbox-gps[hidden]. */
.comment-form[hidden] {
    display: none;
}

.comment-form textarea {
    padding: 0.5rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--fg);
    font: inherit;
    font-size: 0.9rem;
    resize: vertical;
}

.comment-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
}

.comment-dialog-actions button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 6px;
    background: var(--accent);
    color: #fff;
    font-size: 0.9rem;
    cursor: pointer;
}

.comment-dialog-actions .comment-cancel {
    background: none;
    border: 1px solid var(--border);
    color: var(--fg);
}

.comment-error {
    margin: 0;
    color: var(--error);
    font-size: 0.85rem;
}

/* Edit/delete controls on own comments; also save/cancel while editing */

.comment-actions {
    display: flex;
    flex: 0 0 auto;
    gap: 0.5rem;
    /* Spacing comes from the li's own gap now (same row when it fits,
       wraps below the ellipsis text/textarea otherwise) */
}

.comment-actions button {
    padding: 0.2rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: none;
    color: var(--fg);
    font-size: 0.75rem;
    cursor: pointer;
}

.comment-actions button:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* Edit/delete are icon-only (pencil/trash) — square instead of the text
   pill, label lives in aria-label/title */
.comment-edit,
.comment-delete {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
}

.comment-edit svg,
.comment-delete svg {
    width: 0.85rem;
    height: 0.85rem;
    fill: currentColor;
}

.comment-list textarea {
    width: 100%;
    margin-top: 0.3rem;
    padding: 0.5rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--fg);
    font: inherit;
    font-size: 0.9rem;
    resize: vertical;
}

.lightbox-close {
    position: absolute;
    top: 0.25rem;
    right: 0.25rem;
    background: none;
    border: none;
    color: #eee;
    font-size: 1.5rem;
    padding: 1rem;
    cursor: pointer;
    z-index: 101;
}

/* Grid / map toggle in the header — same chip look as the sort switch next to it.
   Only rendered for folders that released their GPS data and have a tile source
   configured (map_enabled() in lib.php). */

.view-switch {
    display: flex;
    gap: 2px;
    padding: 2px;
    background: var(--surface);
    border-radius: 8px;
}

.view-option {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.4rem 0.7rem;
    border-radius: 6px;
    color: var(--fg);
    font-size: 0.85rem;
    text-decoration: none;
    white-space: nowrap;
    opacity: 0.65;
}

.view-option:hover {
    background: color-mix(in srgb, var(--fg) 10%, var(--surface));
    opacity: 1;
}

.view-option.view-active {
    background: color-mix(in srgb, var(--accent) 22%, var(--surface));
    opacity: 1;
}

.view-option svg {
    width: 0.95rem;
    height: 0.95rem;
    fill: currentColor;
}

@media (max-width: 639px) {
    .view-option {
        padding: 0.4rem 0.55rem;
        font-size: 0;
        gap: 0;
    }
}

/* --- Map view ---
   The base layer (basemap.json, Natural Earth) is drawn from our own domain and the
   optional tiles come through map.php on our own server — never a foreign domain, see
   CLAUDE.md. Two stacked canvases: base + tiles below, markers above, so dark mode can
   dim the map without touching the pins.
   The base layer paints itself from these variables, so its palette lives here in CSS
   rather than in the renderer (script.js reads them via getComputedStyle). */

.map-view {
    --map-marker: var(--error);
    --map-marker-edge: #fff;
    --map-water: #aac9e0;
    --map-land: #ece5d6;
    --map-border: #b7ab93;
    --map-label: #6b6152;
    position: relative;
    /* Break out of the golden-ratio content column: the map fills the window width.
       Its height is set by script.js, which measures the space left below the header. */
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    height: 70dvh;
    overflow: hidden;
    background: var(--surface);
}

/* In map view the grid is still in the DOM (the lightbox navigates it), just hidden —
   which also keeps the lazy-load observer from fetching thumbnails nobody looks at. */
.gallery-hidden,
.content-map .folders {
    display: none;
}

.map-tiles,
.map-markers {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.map-markers {
    /* The browser must not hijack drag/pinch — panning and zooming are ours */
    touch-action: none;
}

/* Dark mode. The base layer simply gets a dark palette — it is drawn by us, so there is
   nothing to filter. Raster tiles from a provider are made for light backgrounds, so
   those are dimmed instead; the filter is bound to .has-tiles (set by script.js) so it
   never dulls the base layer when no tiles are configured. The markers sit on their own
   canvas above and stay untouched either way. */

:root[data-theme="dark"] .map-view {
    --map-water: #16222c;
    --map-land: #2a2b26;
    --map-border: #4b4a41;
    --map-label: #9a9384;
}

:root[data-theme="dark"] .has-tiles .map-tiles {
    filter: brightness(0.82) saturate(0.85);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .map-view {
        --map-water: #16222c;
        --map-land: #2a2b26;
        --map-border: #4b4a41;
        --map-label: #9a9384;
    }

    :root:not([data-theme="light"]) .has-tiles .map-tiles {
        filter: brightness(0.82) saturate(0.85);
    }
}

.map-popup {
    position: absolute;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    max-width: 15rem;
    padding: 0.4rem;
    background: color-mix(in srgb, var(--surface) 92%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border-radius: 8px;
    box-shadow: 0 4px 16px rgb(0 0 0 / 35%);
    /* Follows the cursor, so it must not swallow the pointer events the canvas needs.
       Only the cluster strip (see .is-interactive) takes clicks. */
    pointer-events: none;
}

.map-popup.is-interactive {
    pointer-events: auto;
}

.map-popup-thumb {
    display: block;
    width: 100%;
    border-radius: 5px;
}

.map-popup-name {
    color: var(--fg);
    font-size: 0.8rem;
    font-weight: 600;
    overflow-wrap: anywhere;
}

.map-popup-stats {
    color: var(--fg);
    font-size: 0.72rem;
    opacity: 0.7;
}

.map-popup-strip {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.25rem;
}

.map-popup-strip .map-popup-thumb {
    aspect-ratio: 1;
    object-fit: cover;
    cursor: pointer;
}

/* Credit for the map data: the public-domain base layer always, the tile provider only
   while its tiles run. Its link stays a plain link — the request to the foreign domain
   only happens if the visitor clicks it (same rule as osm_url() in lib.php). */
.map-attribution {
    position: absolute;
    right: 0;
    bottom: 0;
    z-index: 2;
    padding: 0.15rem 0.4rem;
    background: color-mix(in srgb, var(--surface) 85%, transparent);
    border-top-left-radius: 6px;
    color: var(--fg);
    font-size: 0.65rem;
    opacity: 0.8;
}

.map-attribution a {
    color: inherit;
    text-decoration: none;
}

.map-attribution:hover {
    opacity: 1;
}

.map-attribution a:hover {
    text-decoration: underline;
}
