/* Thème sombre, image-first — cf. CLAUDE.md / brief de mission (niveau
   1x.com / pic-time.com). Aucune police externe : uniquement la pile
   système, cohérent avec la posture vie privée du projet (HMAC sur les
   IP, pas de requête tierce). Refonte 2026-07-12 : barre de navigation
   sticky, accent doré discret, transitions (fondu des images, zoom au
   survol, Ken Burns sur la bannière, panneau EXIF animé, view transitions
   entre pages) — toutes les animations sont désactivables par le système
   (prefers-reduced-motion). */

* {
  box-sizing: border-box;
}

:root {
  --bg: #0a0a0b;
  --bg-elevated: #131316;
  --bg-raised: #1b1b20;
  --ink: #ececee;
  --ink-muted: #8c8c93;
  --ink-faint: #59595f;
  /* Accent — doré discret, réservé aux états (survol, actif, focus),
     jamais à de grandes surfaces : ne doit jamais concurrencer les
     photos. */
  --accent: #c2a878;
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);
  --radius: 10px;
  --radius-sm: 4px;
  --shadow-lift: 0 14px 36px rgba(0, 0, 0, 0.45);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* Espace entre vignettes — album_id (page d'accueil, .albums-grid) ET
     photos (album.html, .photo-gallery, les 4 dispositions) : UNE seule
     variable, lue par tout le monde (CSS grid/flex `gap`, plus le calcul
     JS de layoutMasonry/layout() dans gallery.js via `readPx("--gap")`,
     donc jamais à resynchroniser à la main entre CSS et JS). Doublé le
     2026-07-16 (3/4/6px -> 6/8/12px) sur demande explicite — pour
     changer cette valeur à l'avenir, éditer uniquement ces 3 lignes
     (ici + les 2 media queries juste en dessous), rien d'autre dans le
     projet ne code une valeur de gap en dur.
     ───────────────────────────────────────────────────────────────── */
  --gap: 6px;
  --row-height: 190px;
  /* Largeurs de colonne des 3 dispositions non-justifiées (mosaïque/grille/
     carrés) — mêmes valeurs par défaut que les vrais configs Lychee
     (photo_layout_masonry_column_width=300, _grid_column_width=250,
     _square_column_width=200, vérifié en lecture sur la vraie base) mais
     codées en dur ici plutôt que lues dynamiquement, pour rester cohérent
     avec --row-height ci-dessus (déjà un défaut statique, jamais lu depuis
     lychee.configs.photo_layout_justified_row_height) — cf. CLAUDE.md. */
  --masonry-col-width: 300px;
  --grid-col-width: 250px;
  --square-col-width: 200px;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
    sans-serif;
  /* Empilement système uniquement (aucune police externe, cf. commentaire
     de tête de fichier) — réservé aux titres d'en-tête de galerie et au
     wordmark, pour un rendu éditorial inspiré de la référence 1x/pic-time
     fournie en Phase 3. */
  --font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
}

@media (min-width: 700px) {
  :root {
    --row-height: 230px;
    --gap: 8px;
  }
}

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

html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
  padding-bottom: 4rem;
}

a {
  color: inherit;
  text-decoration: none;
}

a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

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

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

/* ─────────────────────────────────────────────────────────────────────
   Mouvement — tout ce qui bouge vit ici, débrayé d'un bloc si le système
   demande moins d'animations. Les `transition:` déclarées ailleurs restent
   (états de survol quasi instantanés, pas du mouvement de page).
   ───────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: no-preference) {
  /* Transitions de page natives entre documents du même site (fondu
     croisé) — pure amélioration progressive, ignorée par les navigateurs
     qui ne connaissent pas la règle. */
  @view-transition {
    navigation: auto;
  }

  html {
    scroll-behavior: smooth;
  }

  /* ⚠️ `page-in` UNIQUEMENT là où les view transitions n'existent pas —
     sinon DEUX animations d'entrée se superposent et le relais entre elles
     se fait en plein vol, ce qui se voit comme un clignotement.

     Bug signalé par l'utilisateur et MESURÉ au navigateur le 2026-08-02
     (tools/screenshots/probe-viewtransition.mjs) : la view transition rend
     la main à ~280 ms alors que `page-in` (400 ms) n'a amené le contenu
     vivant qu'à ~0,90 d'opacité et continue de monter. L'instantané de
     transition — plein — est donc retiré AVANT que le contenu réel ne soit
     pleinement visible : d'où l'impression que l'image disparaît une
     fraction de seconde puis réapparaît. Mesuré identique sur accueil →
     album (VT finie à 294 ms, opacité pleine à 421 ms) et album → photo
     (274 ms contre 385 ms), ce qui explique que les DEUX pages clignotent.

     Régression de la passe UX du 2026-07-25 : les view transitions étaient
     déjà activées mais aucun élément ne portait de `view-transition-name`,
     donc l'effet restait discret ; en les câblant réellement (morphing
     vignette → photo), le doublon est devenu perceptible.

     Là où les view transitions existent, c'est le navigateur qui assure le
     fondu d'entrée — `page-in` n'a plus lieu d'être. Ailleurs, il reste le
     repli.

     ⚠️ `backwards` et PAS `both` (raison distincte, à conserver) : une
     animation d'opacity/transform qui REMPLIT encore après sa fin laisse
     `main` porteur d'un contexte d'empilement permanent — tout ce qui vit
     dedans se retrouve alors plafonné sous la nav sticky (z-index 100),
     quel que soit son propre z-index. Bug réel du 2026-07-25 : le panneau
     EXIF (z-index 200) passait SOUS la barre de navigation, et l'overlay
     plein écran (z-index 300) laissait la barre posée par-dessus la photo. */
  @supports not (view-transition-name: none) {
    main {
      animation: page-in 0.4s var(--ease) backwards;
    }
  }

  .album-hero::before {
    animation: hero-zoom 1.6s var(--ease) both;
  }

  .album-hero-title {
    animation: fade-up 0.7s var(--ease) 0.15s both;
  }

  .exif-toggle[open] .exif-panel {
    animation: panel-in 0.28s var(--ease) both;
  }

  /* Flèche de la page de garde (rebond infini) — déclarée ici et pas à
     côté du reste de `.landing-scroll-hint` : c'est la seule animation
     sans fin du site, donc la première que ce réglage doit couper. */
  .landing-scroll-hint {
    animation: landing-bounce 2.4s ease-in-out infinite;
  }

  /* Révélation des photos au scroll (classe posée par gallery.js). L'état
     initial (invisible) est gated `html:not(.no-js)` ET par ce bloc : sans
     JS, ou avec un système qui demande moins d'animation, les photos sont
     visibles d'emblée — jamais de galerie vide. */
  html:not(.no-js) .photo-item {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
  }

  html:not(.no-js) .photo-item.is-revealed {
    opacity: 1;
    transform: none;
  }

  /* Zoom de la visionneuse : adoucit le double-clic. Coupé pendant un
     déplacement au doigt/à la souris, où la photo doit coller au geste. */
  .photo-stage img {
    transition: transform 0.25s var(--ease);
  }

  .photo-stage.is-panning img {
    transition: none;
  }
}

@keyframes page-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes hero-zoom {
  from {
    transform: scale(1.06);
  }
  to {
    transform: scale(1);
  }
}

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes panel-in {
  from {
    opacity: 0;
    transform: translateX(28px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   Barre de navigation — sticky, verre dépoli, icônes + libellés (les
   libellés disparaissent sur écran étroit, les icônes restent).
   ───────────────────────────────────────────────────────────────────── */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1.25rem;
  background: rgba(10, 10, 11, 0.82);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}

.site-header .wordmark {
  font-family: var(--font-serif);
  font-size: 1.02rem;
  letter-spacing: 0.05em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color 0.15s ease;
}

.site-header .wordmark:hover {
  color: var(--accent);
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--ink-muted);
  padding: 0.35rem 0.65rem;
  border-radius: 999px;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.nav-link:hover {
  color: var(--ink);
  background: var(--bg-elevated);
}

.nav-link svg {
  flex: none;
}

/* Nom du membre connecté dans la nav (non cliquable — la déconnexion est le
   lien voisin). Même gabarit qu'un .nav-link, teinte accent discrète. */
.nav-member {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--accent);
  padding: 0.35rem 0.65rem;
}
.nav-member svg {
  flex: none;
}

/* Carte de connexion membre (/login) — sobre, centrée. */
.auth-card {
  max-width: 24rem;
  margin: 2.5rem auto;
  padding: 2rem 1.75rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.auth-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.35rem;
  font-size: 1.4rem;
}
.auth-sub {
  margin: 0 0 1.25rem;
  color: var(--ink-muted);
  font-size: 0.9rem;
}
.auth-form p {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0 0 1rem;
}
.auth-form input {
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--ink);
}
.auth-submit {
  width: 100%;
  padding: 0.6rem 1rem;
  border: none;
  border-radius: 8px;
  background: var(--accent);
  color: #1a1408;
  font-weight: 600;
  cursor: pointer;
}
.auth-alt {
  margin: 1.25rem 0 0;
  text-align: center;
  font-size: 0.82rem;
}
.auth-alt a {
  color: var(--ink-muted);
  text-decoration: underline;
}

.nav-sep {
  width: 1px;
  height: 1.05rem;
  margin: 0 0.35rem;
  background: var(--border-strong);
}

.nav-icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  color: var(--ink-muted);
  transition: color 0.15s ease, background-color 0.15s ease;
}

.nav-icon-link:hover {
  color: var(--accent);
  background: var(--bg-elevated);
}

@media (max-width: 640px) {
  .site-nav .nav-link span {
    display: none;
  }

  .site-nav .nav-link {
    width: 1.9rem;
    height: 1.9rem;
    justify-content: center;
    padding: 0;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   Pied de page — identité du photographe + réseaux (site_settings, cf.
   app/templates.py : context processor `site`). Absent si rien n'est
   renseigné dans /admin/settings.
   ───────────────────────────────────────────────────────────────────── */

.site-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  padding: 2.25rem 1.25rem 2.5rem;
  border-top: 1px solid var(--border);
}

.footer-name {
  margin: 0;
  font-family: var(--font-serif);
  font-size: 0.88rem;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}

.footer-social {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 50%;
  color: var(--ink-faint);
  transition: color 0.15s ease, background-color 0.15s ease, transform 0.2s var(--ease);
}

.footer-social a:hover {
  color: var(--accent);
  background: var(--bg-elevated);
  transform: translateY(-2px);
}

/* ─────────────────────────────────────────────────────────────────────
   Fil d'Ariane — séparateurs en chevrons (icône), actions à droite.
   ───────────────────────────────────────────────────────────────────── */

.breadcrumb {
  padding: 0 1.25rem;
  margin-bottom: 1.75rem;
  font-size: 0.82rem;
  color: var(--ink-muted);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45em;
}

.breadcrumb a {
  transition: color 0.15s ease;
}

.breadcrumb a:hover {
  color: var(--accent);
}

.breadcrumb .sep {
  display: inline-flex;
  align-items: center;
  color: var(--ink-faint);
}

.breadcrumb .sep svg {
  display: block;
}

.breadcrumb strong {
  color: var(--ink);
  font-weight: 500;
}

.download-all-link {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  padding: 0.3rem;
  border-radius: 50%;
  color: var(--ink-muted);
  transition: color 0.15s ease, transform 0.2s var(--ease);
}

.download-all-link:hover {
  color: var(--accent);
  transform: translateY(1px);
}

/* ─────────────────────────────────────────────────────────────────────
   Sections — étiquette discrète en petites capitales espacées, précédée
   d'un tiret accent.
   ───────────────────────────────────────────────────────────────────── */

.section {
  padding: 0 1.25rem;
  margin-bottom: 2.5rem;
}

.section-label {
  display: flex;
  align-items: center;
  gap: 0.55em;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 1rem;
}

.section-label::before {
  content: "";
  width: 1.4em;
  height: 1px;
  background: var(--accent);
  opacity: 0.65;
}

/* ─────────────────────────────────────────────────────────────────────
   Bannière d'en-tête d'album — image plein cadre (sur ::before, pour
   pouvoir l'animer en zoom lent sans toucher au contenu), dégradé sombre,
   titre serif centré. `--hero-image` posé en style inline
   (app/templates/album.html) avec l'URL déjà résolue côté serveur
   (app/services/covers.py) — jamais l'ORIGINAL, cf. son docstring. Les
   custom properties héritent aux pseudo-éléments, donc var(--hero-image)
   reste lisible depuis ::before.
   ───────────────────────────────────────────────────────────────────── */

.album-hero {
  position: relative;
  height: 44vh;
  min-height: 260px;
  max-height: 540px;
  margin-bottom: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.album-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
      180deg,
      rgba(10, 10, 11, 0.2) 0%,
      rgba(10, 10, 11, 0.35) 55%,
      rgba(10, 10, 11, 0.92) 100%
    ),
    var(--hero-image);
  background-size: cover;
  background-position: center;
}

.album-hero-title {
  position: relative;
  margin: 0;
  padding: 0 1.5rem;
  text-align: center;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.7rem, 5vw, 3rem);
  color: var(--ink);
  text-shadow: 0 2px 28px rgba(0, 0, 0, 0.55);
  letter-spacing: 0.01em;
}

.album-meta {
  padding: 0 1.25rem;
  margin: -0.5rem 0 1.5rem;
}

.album-description {
  color: var(--ink-muted);
  font-size: 0.92rem;
  line-height: 1.5;
  max-width: 60ch;
  white-space: pre-line;
}

.album-license {
  color: var(--ink-faint);
  font-size: 0.75rem;
  margin-top: 0.35rem;
}

/* ─────────────────────────────────────────────────────────────────────
   Sous-albums / page d'accueil — cartes avec vignette optionnelle
   (`.has-cover`, `--card-cover` posé en style inline). L'image vit sur
   ::after (z-index 0) pour être zoomée au survol sans déformer la carte ;
   le dégradé sur ::before (z-index 1) reste au-dessus, le titre (z-index
   2) au-dessus de tout. Repli typographique sobre pour un album sans
   vignette.
   ───────────────────────────────────────────────────────────────────── */

.albums-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
  gap: var(--gap);
}

.album-card {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: flex-end;
  padding: 1rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition:
    transform 0.35s var(--ease),
    box-shadow 0.35s var(--ease),
    border-color 0.2s ease;
}

.album-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lift);
  border-color: var(--border-strong);
}

.album-card.has-cover::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: var(--card-cover);
  background-size: cover;
  background-position: center;
  transition: transform 0.6s var(--ease);
}

.album-card.has-cover:hover::after {
  transform: scale(1.06);
}

.album-card.has-cover::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(10, 10, 11, 0) 40%, rgba(10, 10, 11, 0.88) 100%);
}

.album-card span {
  position: relative;
  z-index: 2;
  font-size: 0.88rem;
  font-weight: 500;
  line-height: 1.3;
}

/* ─────────────────────────────────────────────────────────────────────
   Galerie — 4 dispositions reproduisant les options de Lychee
   (`PhotoLayoutType` : justified/masonry/grid/square, cf. CLAUDE.md).
   Sélectionnées via `data-layout` sur `.photo-gallery` (gallery-layout.js,
   bouton + préférence localStorage) :
   - justified : layout calculé par app/static/gallery.js à partir des
     dimensions réelles (data-w/data-h) — seule disposition qui a besoin de
     JS pour le calcul lui-même (largeurs variables à hauteur de ligne
     fixe, pas exprimable en CSS pur). Invisible tant que non calculée
     (`.is-ready`), pour éviter un flash non stylé.
   - masonry : calculé par app/static/gallery.js (`layoutMasonry`), PAS par
     `column-width` CSS natif — corrigé le 2026-07-14, signalé par
     l'utilisateur : `column-width` remplit une colonne ENTIÈRE avant de
     passer à la suivante (ordre de lecture haut-bas puis gauche-droite),
     alors que l'ordre attendu est gauche-droite puis haut-bas, comme sur
     la référence pic-time.com fournie. Algorithme « colonne la plus
     courte d'abord » (même famille que Masonry.js/Isotope) : chaque photo,
     dans l'ordre du serveur, va dans la colonne actuellement la moins
     remplie (hauteur prédite depuis data-w/data-h, jamais mesurée après
     chargement) — avec des photos de hauteur similaire (cas courant), ça
     cycle les colonnes dans l'ordre et ressemble donc à un remplissage
     gauche-droite puis haut-bas, sans jamais le garantir à 100% (aucune
     disposition en colonnes de largeur fixe et hauteur variable ne le
     peut réellement). `.masonry-column` : conteneurs générés par JS,
     neutralisés en `display: contents` quand une autre disposition est
     active (même mécanique que `.justified-row`).
   - grid : cellules de largeur fixe, image ENTIÈRE visible (object-fit:
     contain — jamais recadrée), lettrboxée si l'aspect ratio diffère.
   - square : cellules carrées, recadrées (object-fit: cover).
   Si JS est désactivé, `data-layout` reste "justified" (valeur posée par
   le serveur, seul JS peut la changer) et `.no-js` bascule sur une grille
   simple en repli, comme avant — masonry ne peut de toute façon jamais
   être sélectionné sans JS (aucun repli CSS pur ne le calcule plus). */

.photo-gallery {
  padding: 0 1.25rem;
}

.photo-item {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background-size: cover;
  background-position: center;
}

.photo-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--bg-elevated);
  transition: opacity 0.45s ease, transform 0.5s var(--ease);
}

/* Fondu d'apparition au chargement (classe .is-loaded posée par le script
   de base.html). Gated sur la présence de JS : sans lui, la classe ne
   serait jamais posée — l'image reste alors visible d'emblée (.no-js). Le
   placeholder flou (blur-up) vit sur le PARENT .photo-item, pas sur
   l'img : il reste visible derrière pendant le fondu. */
html:not(.no-js) .photo-item img:not(.is-loaded) {
  opacity: 0;
}

.photo-item:hover img {
  transform: scale(1.04);
}

/* --- Justifié (défaut serveur) ----------------------------------------- */

.photo-gallery[data-layout="justified"] {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  visibility: hidden;
}

.photo-gallery[data-layout="justified"].is-ready {
  visibility: visible;
}

.justified-row {
  display: flex;
  gap: var(--gap);
}

/* Une autre disposition est active : si la page a démarré en justifié,
   gallery.js a déjà regroupé les .photo-item dans des .justified-row —
   `display: contents` neutralise ce regroupement (le conteneur disparaît
   du rendu, ses enfants deviennent visuellement directs de .photo-gallery)
   sans avoir à les déplacer dans le DOM. */
.photo-gallery:not([data-layout="justified"]) .justified-row {
  display: contents;
}

/* --- Mosaïque (masonry) : colonnes calculées en JS, ordre de lecture
   gauche-droite puis haut-bas — cf. commentaire de tête de section ------ */

.photo-gallery[data-layout="masonry"] {
  display: flex;
  align-items: flex-start;
  gap: var(--gap);
}

.masonry-column {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  flex: 1 1 0;
  min-width: 0;
}

/* Une autre disposition est active : gallery.js a déjà réparti les
   .photo-item dans des .masonry-column — `display: contents` neutralise
   ce regroupement sans avoir à déplacer les nœuds, même mécanique que
   `.justified-row` ci-dessus. */
.photo-gallery:not([data-layout="masonry"]) .masonry-column {
  display: contents;
}

.photo-gallery[data-layout="masonry"] .photo-item {
  width: 100%;
  height: auto;
}

.photo-gallery[data-layout="masonry"] .photo-item img {
  height: auto;
}

/* --- Grille (grid) : cellules fixes, photo entière (jamais recadrée) -- */

.photo-gallery[data-layout="grid"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--grid-col-width), 1fr));
  gap: var(--gap);
}

.photo-gallery[data-layout="grid"] .photo-item {
  aspect-ratio: 4 / 3;
}

.photo-gallery[data-layout="grid"] .photo-item img {
  object-fit: contain;
}

/* --- Carrés (square) : cellules carrées, recadrées --------------------- */

.photo-gallery[data-layout="square"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--square-col-width), 1fr));
  gap: var(--gap);
}

.photo-gallery[data-layout="square"] .photo-item {
  aspect-ratio: 1;
}

/* --- Barre d'outils : diaporama + sélecteur de disposition ------------- */

.gallery-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0 1.25rem;
  margin-bottom: 0.75rem;
}

.gallery-toolbar .section-label {
  margin: 0;
}

.gallery-toolbar-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

/* Bouton « Lancer le diaporama » (gallery-layout.js) — pastille assortie
   aux boutons de disposition à sa droite. */
.slideshow-launch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: rgba(19, 19, 22, 0.6);
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}

.slideshow-launch:hover {
  color: var(--accent);
  border-color: var(--border-strong);
}

.layout-switcher {
  display: flex;
  gap: 0.15rem;
  padding: 0.15rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: rgba(19, 19, 22, 0.6);
}

.layout-choice {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.layout-choice:hover {
  color: var(--ink);
}

.layout-choice.is-active {
  color: var(--accent);
  background: var(--bg-raised);
}

/* Info-bulle au survol/focus — nom de fichier + date de prise de vue,
   jamais affichée en permanence sur un appareil qui a un vrai survol
   (encombrerait la grille). Sur un appareil tactile, où :hover ne se
   déclenche pas, elle est affichée en continu (cf. le @media (hover: none)
   plus bas) — même raisonnement que .photo-nav, déjà rendue permanente
   sous 700px. */
.photo-item-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1.5rem 0.6rem 0.4rem;
  background: linear-gradient(180deg, rgba(10, 10, 11, 0) 0%, rgba(10, 10, 11, 0.85) 100%);
  color: var(--ink);
  font-size: 0.72rem;
  line-height: 1.4;
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateY(4px);
  transition:
    opacity 0.2s ease,
    transform 0.2s var(--ease);
  pointer-events: none;
}

.photo-item:hover .photo-item-caption,
.photo-item:focus-visible .photo-item-caption {
  opacity: 1;
  transform: translateY(0);
}

/* Appareil sans survol réel (téléphone, tablette) : la règle :hover
   ci-dessus ne se déclenche JAMAIS, la légende y était donc purement et
   simplement invisible. On l'affiche en permanence — le dégradé de fond
   assure déjà la lisibilité, et une légère transparence l'empêche de
   concurrencer la photo. */
@media (hover: none) {
  .photo-item-caption {
    opacity: 0.92;
    transform: none;
  }
}

.caption-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.caption-date {
  color: var(--ink-muted);
}

/* Repli sans JavaScript : simple grille, pas de justification — les
   <a class="photo-item"> sont rendus par Jinja, jamais générés en JS.
   data-layout reste "justified" sans JS (seul JS peut le changer), d'où le
   sélecteur non conditionné sur cet attribut ici. Le sélecteur de
   disposition lui-même (boutons) ne sert à rien sans JS — pas caché
   explicitement, mais sans effet, cf. gallery-layout.js. */
.no-js .photo-gallery {
  visibility: visible;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}

.no-js .photo-gallery .photo-item {
  aspect-ratio: 1;
}

.empty {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0 1.25rem;
  color: var(--ink-muted);
  font-size: 0.9rem;
}

.empty svg {
  flex: none;
  color: var(--ink-faint);
}

/* ─────────────────────────────────────────────────────────────────────
   Formulaire de déverrouillage — carte centrée, icône cadenas.
   ───────────────────────────────────────────────────────────────────── */

.unlock-view {
  max-width: 24rem;
  margin: 4rem auto 0;
  padding: 2.25rem 1.75rem;
  text-align: center;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.unlock-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.4rem;
  height: 3.4rem;
  margin-bottom: 1rem;
  border-radius: 50%;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--accent);
}

.unlock-view h1 {
  font-size: 1.05rem;
  font-weight: 500;
  color: var(--ink);
  margin: 0 0 1.5rem;
}

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

form.unlock input {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--ink);
  font-size: 0.95rem;
  padding: 0.7rem 0.85rem;
  border-radius: 6px;
  transition: border-color 0.15s ease;
}

form.unlock input:focus {
  outline: none;
  border-color: var(--accent);
}

form.unlock button {
  background: var(--ink);
  color: var(--bg);
  border: none;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 0.75rem;
  border-radius: 6px;
  cursor: pointer;
  transition: opacity 0.15s ease, transform 0.2s var(--ease);
}

form.unlock button:hover {
  opacity: 0.9;
  transform: translateY(-1px);
}

.error {
  color: #e07a6b;
  font-size: 0.85rem;
  margin: 0 0 1rem;
}

.notice {
  color: #8fbf7f;
  font-size: 0.85rem;
  margin: 0 0 1rem;
}

/* Description de photo (alt-text) — visible par tous si renseignée, éditable
   inline en session admin (photo-admin.js). */

.photo-description {
  margin: 0.3rem 0 0;
  color: var(--ink-muted);
  font-size: 0.85rem;
  max-width: 60ch;
}

.photo-description[role="button"] {
  cursor: pointer;
}

.photo-description[role="button"]:hover,
.photo-description[role="button"]:focus-visible {
  color: var(--accent);
}

.photo-description-editor {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  margin-top: 0.3rem;
}

.photo-description-editor textarea {
  flex: 1;
  min-width: 16rem;
  background: rgba(255, 255, 255, 0.06);
  color: var(--ink);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
  font: inherit;
  font-size: 0.85rem;
}

.photo-description-editor button {
  background: rgba(255, 255, 255, 0.06);
  color: var(--ink);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 6px;
  padding: 0.35rem 0.7rem;
  font: inherit;
  font-size: 0.8rem;
  cursor: pointer;
}

/* ─────────────────────────────────────────────────────────────────────
   Visionneuse photo — plein cadre, UI minimale.
   ───────────────────────────────────────────────────────────────────── */

.photo-view {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.photo-view-top {
  width: 100%;
  padding: 0 1.25rem;
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.photo-view .back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.82rem;
  color: var(--ink-muted);
  transition: color 0.15s ease;
}

.photo-view .back-link:hover {
  color: var(--accent);
}

.photo-view-top-right {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

.photo-position {
  font-size: 0.78rem;
  color: var(--ink-faint);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Bouton play/pause du diaporama (photo-viewer.js) — l'icône affichée
   (play vs pause) est basculée en JS via l'attribut `hidden`. Teinté
   accent quand la lecture est active (aria-pressed=true). */
.slideshow-toggle {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem;
  background: none;
  border: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease;
}

.slideshow-toggle:hover {
  color: var(--ink);
}

.slideshow-toggle[aria-pressed="true"] {
  color: var(--accent);
}

/* Bouton d'entrée en plein écran — même famille visuelle que
   .slideshow-toggle. JS-only (photo-viewer.js) : masqué sans JS. */
.fullscreen-toggle {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem;
  background: none;
  border: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease;
}

.fullscreen-toggle:hover {
  color: var(--ink);
}

.no-js .fullscreen-toggle {
  display: none;
}

/* --- Mode plein écran (photo-viewer.js pose html.photo-fullscreen) -----
   Immersif CSS, PAS l'API Fullscreen native : chaque photo est une vraie
   navigation pleine page, or le plein écran natif est éjecté au
   déchargement du document (il ne survivrait pas au passage à la photo
   suivante), et iOS Safari ne le supporte pas pour un élément quelconque.
   La scène devient un overlay fixe opaque au-dessus de tout (nav sticky à
   100, panneau EXIF à 200) — pas besoin de masquer le reste de la page.
   L'état vit en sessionStorage (même mécanique que le diaporama) : les
   flèches, le balayage et le diaporama continuent de fonctionner en plein
   écran à travers les navigations. */
html.photo-fullscreen {
  overflow: hidden; /* rien ne défile derrière l'overlay */
}

/* Mode immersif : la barre de navigation du site n'a rien à y faire — on la
   retire franchement plutôt que de compter sur l'empilement (le z-index de
   la scène a suffi à la masquer une fois le contexte d'empilement de `main`
   corrigé, mais laisser la nav du site flotter au-dessus d'une photo en
   plein écran n'aurait de toute façon aucun sens). */
html.photo-fullscreen .site-header {
  display: none;
}

html.photo-fullscreen .photo-stage {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: #000;
  align-items: center;
}

/* Effacement du chrome après 2,5 s d'inactivité (photo-viewer.js pose
   html.photo-chrome-hidden) — ne laisse que la photo. UNIQUEMENT en plein
   écran : ailleurs, faire disparaître la navigation enfermerait le
   visiteur. Seuls les enfants de .photo-stage sont concernés — le reste de
   la page (barre du haut, EXIF, méta) est de toute façon caché derrière
   l'overlay fixe. `pointer-events` conservés volontairement : un toucher
   n'importe où réveille l'interface (wakeChrome) avant même d'atteindre sa
   cible, donc un bouton effacé reste utilisable au premier essai.
   La transition vit sur l'état NORMAL de chaque élément (`.photo-nav` a
   déjà la sienne, `.fullscreen-exit` la reçoit plus bas) : la déclarer ici
   ne ferait fondre que la disparition, le retour serait sec. */
html.photo-fullscreen.photo-chrome-hidden .photo-nav,
html.photo-fullscreen.photo-chrome-hidden .fullscreen-exit {
  opacity: 0;
}

html.photo-fullscreen .photo-view img {
  max-width: 100vw;
  max-height: 100vh; /* repli si dvh non supporté */
  max-height: 100dvh; /* mobile : hauteur RÉELLE, barres d'outils déduites */
  border-radius: 0;
}

/* Bouton de sortie : pastille verre dépoli (même famille que .photo-nav
   svg), en haut à droite de l'overlay, TOUJOURS visible en plein écran
   (c'est la porte de sortie — jamais dépendante d'un survol). max() +
   env(safe-area-inset-*) : ne pas se loger sous l'encoche iPhone. */
.fullscreen-exit {
  display: none;
}

html.photo-fullscreen .fullscreen-exit {
  display: inline-flex;
  position: absolute;
  top: 0.9rem;
  top: max(0.9rem, env(safe-area-inset-top));
  right: 0.9rem;
  right: max(0.9rem, env(safe-area-inset-right));
  padding: 0.55rem;
  border-radius: 50%;
  background: rgba(19, 19, 22, 0.72);
  border: 1px solid var(--border);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--ink);
  cursor: pointer;
  /* `opacity` dans la liste : c'est elle que fait varier l'effacement
     automatique du chrome (html.photo-chrome-hidden, cf. plus haut) —
     sans ça le retour du bouton serait sec. */
  transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.4s ease;
}

html.photo-fullscreen .fullscreen-exit:hover {
  background: var(--bg-raised);
  border-color: var(--border-strong);
}

/* Bouton « rendre privée / publique » (vue photo, admin) — même famille
   visuelle que .slideshow-toggle ; accent quand la photo EST privée. */
.visibility-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 0.2rem;
  background: none;
  border: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease;
}

.visibility-toggle:hover {
  color: var(--ink);
}

.visibility-toggle[aria-pressed="true"] {
  color: var(--accent);
}

.visibility-toggle[disabled] {
  opacity: 0.5;
  cursor: progress;
}

.visibility-error {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 0.25rem;
  font-size: 0.7rem;
  color: #e07a6b;
  white-space: nowrap;
}

/* Suppression définitive (vue photo, admin) — même gabarit que les autres
   boutons du cluster haut-droit, teinte destructive au survol. */
.photo-delete-btn {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem;
  background: none;
  border: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease;
}

.photo-delete-btn:hover {
  color: #e07a6b;
}

.photo-delete-btn[disabled] {
  opacity: 0.5;
  cursor: progress;
}

/* ─────────────────────────────────────────────────────────────────────
   Panneau EXIF — <details>/<summary> natif (pas de JS), tiroir vertical
   plein hauteur ancré à droite (position: fixed), glissé depuis le bord
   (panel-in, cf. bloc Mouvement). Une icône vectorielle par champ, dans
   le <dt> (app/templates/photo.html + _icons.html).
   ───────────────────────────────────────────────────────────────────── */

.exif-toggle summary {
  display: flex;
  align-items: center;
  list-style: none;
  cursor: pointer;
  color: var(--ink-muted);
  transition: color 0.15s ease;
}

.exif-toggle summary::-webkit-details-marker {
  display: none;
}

.exif-toggle summary:hover,
.exif-toggle[open] summary {
  color: var(--accent);
}

.exif-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(320px, 88vw);
  overflow-y: auto;
  padding: 1.5rem;
  background: var(--bg-elevated);
  border-left: 1px solid var(--border);
  box-shadow: -12px 0 32px rgba(0, 0, 0, 0.35);
  /* Au-dessus de la barre de navigation sticky (z-index 100). */
  z-index: 200;
}

/* Le résumé (icône, dans .photo-view-top) se retrouve recouvert par ce
   panneau une fois ouvert (position: fixed, plein hauteur) — la fermeture
   passe donc par ce bouton dédié, pas en re-cliquant l'icône d'origine. */
.exif-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.5rem;
}

.exif-panel-title {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.exif-close {
  display: flex;
  align-items: center;
  padding: 0.2rem;
  background: none;
  border: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease;
}

.exif-close:hover {
  color: var(--ink);
}

.exif-panel dl {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}

.exif-field {
  min-width: 0;
}

.exif-panel dt {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0;
}

.exif-panel dt svg {
  flex: none;
  color: var(--accent);
  opacity: 0.85;
}

.exif-panel dd {
  /* Aligné sous le libellé, pas sous l'icône (15px d'icône + 0.5rem
     d'écart dans le dt). */
  margin: 0.3rem 0 0 calc(15px + 0.5rem);
  font-size: 0.85rem;
  color: var(--ink);
}

.photo-stage {
  position: relative;
  width: 100%;
  display: flex;
  justify-content: center;
}

/* Zoom actif (photo-viewer.js) : l'image agrandie est bornée à la foulée
   qu'elle occupait déjà (clampZoom), donc la scène la recadre — la mise en
   page ne bouge pas d'un pixel, l'image glisse simplement dessous. */
.photo-stage.is-zoomed {
  overflow: hidden;
}

.photo-stage.is-zoomed img {
  cursor: grab;
}

.photo-stage.is-panning img {
  cursor: grabbing;
}

/* Barre de progression du diaporama — au-dessus de l'overlay plein écran
   (300) pour rester visible en mode immersif. Pas gated sur
   prefers-reduced-motion : c'est un indicateur fonctionnel demandé par une
   action explicite (comme le diaporama lui-même), pas une décoration. */
.slideshow-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 301;
  pointer-events: none;
}

.slideshow-progress > span {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
}

.photo-view img {
  max-height: 82vh;
  width: auto;
  /* height:auto est INDISPENSABLE : l'<img> porte des attributs
     width/height (photo.html) et le height présentational resterait
     sinon appliqué — sur écran étroit, max-width:100% écrase alors la
     largeur sans recalculer la hauteur, et une photo paysage s'étire
     verticalement (bug réel constaté sur iPhone en portrait). Avec
     width:auto + height:auto, le navigateur garde le ratio dérivé des
     attributs en respectant max-width/max-height. */
  height: auto;
  margin: 0 auto;
  background-size: cover;
  background-position: center;
  border-radius: var(--radius-sm);
}

/* Flèches précédent/suivant — pastilles circulaires en verre dépoli,
   invisibles jusqu'au survol (desktop) ; toujours en place pour le
   tactile (le survol n'existe pas, donc la règle :hover ci-dessous ne
   s'applique jamais aux touchscreens au sens strict, mais les liens
   restent atteignables et le balayage tactile — cf. photo-viewer.js —
   reste le principal moyen mobile). */
.photo-nav {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  width: 4rem;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.photo-nav svg {
  box-sizing: content-box;
  padding: 0.55rem;
  border-radius: 50%;
  background: rgba(19, 19, 22, 0.72);
  border: 1px solid var(--border);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.2s var(--ease);
}

.photo-stage:hover .photo-nav {
  opacity: 1;
}

.photo-nav:hover svg {
  background: var(--bg-raised);
  border-color: var(--border-strong);
  transform: scale(1.08);
}

.photo-nav-prev {
  left: 0;
  justify-content: flex-start;
  padding-left: 0.75rem;
}

.photo-nav-next {
  right: 0;
  justify-content: flex-end;
  padding-right: 0.75rem;
}

@media (max-width: 700px) {
  /* Pas de survol sur mobile : les flèches restent visibles en permanence
     plutôt que de dépendre d'un hover qui n'arrivera jamais. */
  .photo-nav {
    opacity: 1;
  }
}

.photo-view .photo-meta {
  width: 100%;
  padding: 1rem 1.25rem 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.photo-view h1 {
  font-size: 0.95rem;
  font-weight: 500;
  margin: 0;
}

.photo-view .download-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--ink-muted);
  white-space: nowrap;
  transition: color 0.15s ease;
}

.photo-view .download-link:hover {
  color: var(--accent);
}

/* ─────────────────────────────────────────────────────────────────────
   Admin : upload en masse (modale) + suppression de photo. Visibles
   uniquement en session admin (rendu conditionnel côté template, cf.
   album.html) — pas besoin de les cacher davantage ici.
   ───────────────────────────────────────────────────────────────────── */

.upload-trigger {
  margin-left: 0.5rem;
  display: inline-flex;
  align-items: center;
  padding: 0.3rem;
  border-radius: 50%;
  color: var(--ink-muted);
  background: none;
  border: none;
  cursor: pointer;
  transition: color 0.15s ease;
}

.upload-trigger-solo {
  margin-left: auto;
}

.upload-trigger:hover {
  color: var(--accent);
}

/* Coin opposé à .photo-item-caption (qui occupe déjà le bas) — même
   logique de révélation au survol/focus, `pointer-events` réactivé
   puisque ce span doit recevoir de vrais clics, contrairement à la
   légende qui n'est que de l'information. */
.photo-delete {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  width: 1.6rem;
  height: 1.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(10, 10, 11, 0.72);
  color: var(--ink);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transform: translateY(-4px);
  transition:
    opacity 0.15s ease,
    transform 0.15s ease;
  pointer-events: none;
}

.photo-item:hover .photo-delete,
.photo-item:focus-visible .photo-delete,
.photo-delete:focus-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* Badge « privée » (admin) — coin haut-gauche, TOUJOURS visible (contrairement
   au bouton supprimer qui n'apparaît qu'au survol) : c'est un indicateur
   d'état permanent, pas une action. Un anonyme ne voit jamais ces photos. */
.photo-private-badge {
  position: absolute;
  top: 0.35rem;
  left: 0.35rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.2rem;
  border-radius: 50%;
  background: rgba(10, 10, 11, 0.72);
  color: var(--accent);
  line-height: 1;
  pointer-events: none;
}

.upload-dialog {
  width: min(420px, 90vw);
  max-height: 80vh;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  color: var(--ink);
}

.upload-dialog::backdrop {
  background: rgba(10, 10, 11, 0.6);
}

.upload-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}

.upload-dialog-title {
  font-weight: 500;
  margin: 0;
}

.upload-close {
  background: none;
  border: none;
  color: var(--ink-muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
}

.upload-close:hover {
  color: var(--ink);
}

.upload-dropzone {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 1.5rem 1rem;
  border: 1px dashed var(--border-strong);
  border-radius: 8px;
  color: var(--ink-muted);
  font-size: 0.82rem;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.upload-dropzone:hover {
  border-color: var(--accent);
  color: var(--ink);
}

.upload-rows {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  max-height: 45vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.upload-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 5rem 4rem;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
}

.upload-row-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
}

.upload-row progress {
  width: 100%;
}

.upload-row-status {
  color: var(--ink-muted);
  text-align: right;
}

.upload-row-status.is-ok {
  color: #7bc492;
}

.upload-row-status.is-error {
  color: #e07a6b;
}

/* ─────────────────────────────────────────────────────────────────────
   404.
   ───────────────────────────────────────────────────────────────────── */

.not-found {
  min-height: 55vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  text-align: center;
  color: var(--ink-muted);
  font-size: 0.9rem;
  padding: 0 1.25rem;
}

.not-found-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.6rem;
  height: 3.6rem;
  border-radius: 50%;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--ink-faint);
}

.not-found p {
  margin: 0;
}

.not-found-back {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.8rem;
  padding: 0.45rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--ink-muted);
  transition: color 0.15s ease, border-color 0.15s ease;
}

.not-found-back:hover {
  color: var(--ink);
  border-color: var(--border-strong);
}

/* ─────────────────────────────────────────────────────────────────────
   Page de garde (/, toute première visite de la session) — photo aléatoire
   de l'album par défaut en plein cadre, nom du photographe, flèche
   discrète. `--landing-image` posé en style inline (app/templates/
   home.html) avec l'URL déjà résolue côté serveur (app/routers/gallery.py
   ::_landing_context) — jamais l'ORIGINAL, même variantes que la bannière
   d'album (_HERO_COVER_TYPES). S'estompe en JS (app/static/landing.js) au
   premier geste de défilement vers le bas ; sans JS (.no-js), reste
   masquée d'emblée pour ne jamais bloquer l'accès à la grille en dessous.
   ───────────────────────────────────────────────────────────────────── */

html.landing-locked,
html.landing-locked body {
  overflow: hidden;
  height: 100%;
}

.landing-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-image: linear-gradient(
      180deg,
      rgba(10, 10, 11, 0.25) 0%,
      rgba(10, 10, 11, 0.1) 45%,
      rgba(10, 10, 11, 0.8) 100%
    ),
    var(--landing-image);
  background-size: cover;
  background-position: center;
  opacity: 1;
  transition: opacity 0.6s ease;
}

.landing-overlay-hidden {
  opacity: 0;
  pointer-events: none;
}

.landing-name {
  margin: 0;
  padding: 0 1.5rem;
  text-align: center;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.1rem, 2.6vw, 1.6rem);
  letter-spacing: 0.06em;
  color: var(--ink);
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.6);
}

.landing-scroll-hint {
  position: absolute;
  bottom: 2.25rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  background: none;
  border: none;
  padding: 0.5rem;
  margin: 0;
  color: var(--ink-muted);
  cursor: pointer;
  /* L'animation elle-même est déclarée dans le bloc
     `prefers-reduced-motion: no-preference` en tête de fichier — une
     animation infinie est exactement ce que ce réglage système existe
     pour couper. */
}

.landing-scroll-hint:hover {
  color: var(--ink);
}

@keyframes landing-bounce {
  0%,
  100% {
    transform: translate(-50%, 0);
  }
  50% {
    transform: translate(-50%, 8px);
  }
}

/* Repli sans JavaScript : landing.js ne tournant jamais, l'overlay ne
   s'estomperait jamais tout seul — mieux vaut ne jamais l'afficher que de
   bloquer la page derrière un plein écran permanent. */
.no-js .landing-overlay {
  display: none;
}

/* ── Classement des albums ─────────────────────────────────────────────
   Sélecteur rendu en LIENS (le tri est calculé côté serveur, cf.
   app/services/albums.py) — il fonctionne donc sans JavaScript. Même
   famille visuelle que `.layout-switcher`, dont il reprend le fond et le
   rayon ; les libellés disparaissent sur petit écran, comme la nav. */
.albums-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 0.9rem;
}

.albums-toolbar .section-label {
  margin: 0;
}

.sort-switcher {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  margin-left: auto;
  padding: 0.2rem;
  border-radius: var(--radius);
  background: var(--bg-elevated);
}

.sort-choice {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.65rem;
  border-radius: calc(var(--radius) - 0.2rem);
  color: var(--ink-muted);
  text-decoration: none;
  font-size: 0.82rem;
  line-height: 1;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.sort-choice:hover,
.sort-choice:focus-visible {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
}

/* L'accent doré marque l'état actif — mais jamais SEUL : le fond et
   `aria-current` portent aussi l'information (daltonisme, lecteurs
   d'écran). */
.sort-choice.is-active {
  color: var(--accent);
  background: rgba(255, 255, 255, 0.09);
}

@media (max-width: 700px) {
  .sort-choice-label {
    /* Icônes seules sur petit écran — le `title`/`aria-label` reste. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ── Vidéo ─────────────────────────────────────────────────────────────
   La grille affiche le POSTER d'une vidéo (une image comme une autre) : le
   badge est donc le SEUL marqueur visuel, et il porte aussi la durée. */
.photo-video-badge {
  position: absolute;
  left: 0.5rem;
  bottom: 0.5rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.25rem 0.45rem;
  border-radius: 999px;
  /* Verre dépoli sombre : lisible sur une vignette claire comme sombre,
     sans dépendre de la couleur de l'image en dessous. */
  background: rgba(10, 10, 11, 0.68);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  pointer-events: none;
}

.video-duration {
  letter-spacing: 0.02em;
}

/* Le lecteur occupe la scène comme le ferait l'image. `height: auto` sur les
   DEUX axes : même piège que celui rencontré en production sur <img> (une
   dimension intrinsèque + max-width sans height:auto = média déformé). */
.photo-stage-video {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 82vh;
  background: #000;
  border-radius: var(--radius-sm);
}

html.photo-fullscreen .photo-stage-video {
  max-width: 100vw;
  max-height: 100dvh;
  border-radius: 0;
}

/* Vidéo en cours de transcodage — carte visible de l'ADMIN uniquement
   (cf. _visible_photo_cards). Occupe la même boîte qu'une vignette pour que
   la mise en page justifiée reste correcte. */
.photo-pending {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  height: 100%;
  min-height: 120px;
  background: var(--bg-elevated);
  color: var(--ink-muted);
}

.photo-pending-label {
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}
