.gallery-wrapper {
  flex: 1;
  overflow-x: visible;
  overflow-y: hidden;
}

.gallery-scroller {
  overflow-x: auto;
  overflow-y: hidden;
  white-space: nowrap;
  position: relative;
  touch-action: auto;
  z-index: 0 !important;
  /* La molette verticale appartient désormais à la page : sans trackpad, on
     explore une galerie en l'attrapant. Le curseur est le seul indice de cette
     possibilité, et il ne coûte aucun pixel — le site n'a pas besoin de
     flèches par-dessus les images. */
  cursor: grab;
}

.gallery-scroller:active {
  cursor: grabbing;
}

/* Au doigt, il n'y a pas de curseur et le geste va de soi. */
@media (hover: none) {
  .gallery-scroller {
    cursor: auto;
  }
}

/* Sur grand écran, les colonnes livrées par la build s'effacent : leurs
   cadres redeviennent enfants directs de la piste et s'alignent en une seule
   rangée, comme avant. Le regroupement par trois ne concerne que le mobile. */
.gallery-slide {
  display: contents;
}

.gallery {
  display: inline-flex !important;
  gap: 20px;
  flex-shrink: 0;
  width: max-content !important;
  pointer-events: auto !important;
  /* Attraper une photo doit déplacer la bande, pas amorcer une sélection.
     Il n'y a de toute façon aucun texte à sélectionner ici. */
  user-select: none;
  -webkit-user-select: none;
}

/* Le pendant CSS du garde-fou posé dans engine-horizontal.js : sans lui, le
   navigateur propose son glisser-déposer natif sur chaque photo et le geste
   n'arrive jamais jusqu'au moteur. */
.gallery img,
.gallery picture {
  -webkit-user-drag: none;
}

/* Aucun `will-change`, `translateZ(0)` ni `backface-visibility` ici — c'est
   volontaire, et c'est une correction, pas un oubli.

   Ces trois propriétés forcent la création d'une couche de composition, et
   surtout la MAINTIENNENT en vie. Or une couche réserve une mémoire tampon
   dimensionnée en pixels physiques : sur un écran en DPR 3, la surface est
   multipliée par neuf. Appliqué aux 16 galeries de l'accueil, cela réservait
   environ 432 Mo — de quoi faire tuer l'onglet par iOS, indépendamment de la
   résolution des images, du poids de la vidéo ou de tout le reste.

   La promotion est désormais posée par le moteur sur la SEULE galerie visible,
   et retirée à sa sortie de l'écran (voir js/engine-horizontal.js). Pendant
   l'animation, le translate3d appliqué par applyTransform() suffit de toute
   façon à promouvoir la couche. */

/* ── img-frame: holds shimmer background ── */
.gallery .img-frame {
  position: relative;
  display: inline-block;
  overflow: hidden;
  border-radius: 8px;
  height: 45vh;
  width: auto;
  /* Largeur = 45vh x ratio, connue avant le moindre octet d'image. C'est ce
     qui rend scrollWidth juste dès la première mesure, dans tous les
     navigateurs. */
  aspect-ratio: var(--ratio, 16 / 9);
  background-color: #111;
  background-image: linear-gradient(
    105deg,
    transparent 40%,
    rgba(255, 255, 255, 0.04) 50%,
    transparent 60%
  );
  background-size: 200% 100%;
  background-position: -200% 0;
  animation: shimmer 3.5s infinite linear;
  flex-shrink: 0;
}

/* Shimmer keyframe */
@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

/* Once loaded: stop shimmer */
.gallery .img-frame.loaded {
  animation: none;
  background-image: none;
}

/* ── img inside frame: fade-in ── */
.gallery .img-frame img {
  /* Le cadre ayant déjà le ratio exact, l'image le remplit sans rien décider :
     plus aucune dépendance circulaire. */
  width: 100% !important;
  height: 100% !important;
  max-width: none !important;
  max-height: none !important;
  object-fit: contain !important;
  display: block;
  opacity: 0;
  transform: scale(0.97);
  /* `ease-in-out` plutôt que `ease-out` : la courbe est douce AUX DEUX bouts,
     l'image ne surgit plus d'un coup avant de se poser.

     Pas un `ease-in` pur, malgré son nom trompeur : il démarre lentement, donc
     l'image resterait noire plus longtemps avant d'arriver d'un bloc —
     l'inverse de ce qu'on cherche, où c'est justement la phase noire qu'on
     veut adoucir. */
  transition:
    opacity 1.2s ease-in-out,
    transform 1.4s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, transform;
}

.gallery .img-frame img.loaded {
  opacity: 1;
  transform: scale(1);
  /* Le fondu d'apparition est terminé : on relâche la couche. Sans cela les
     179 images de l'accueil gardaient chacune la leur, à vie. */
  will-change: auto;
}