/* ═══════════════════════════════════════════════════════════
   Bordeluche — Design System · Motion
   Conciergerie Airbnb Premium · Bordeaux Métropole

   Socle unique pour toutes les animations du site.

   POURQUOI CE FICHIER
   Avant lui, le même fade-up existait en ~6 variantes, chacune avec ses
   propres durées et easings écrits en dur : `dotPulse`/`scrollPulse`
   (style.css), `cp-fadeup`/`cp-pop` (comparer.astro), `profil-in`
   (partenaires.astro), `simFadeUp`/`simFadeIn` (SimulateurLCD.jsx),
   `geo-pop`/`geo-fade` (Optimiseur*.jsx), `.reveal-up` (article.css,
   blog.css). Résultat : le site n'avait pas un rythme de mouvement, il
   en avait six.

   RÈGLE
   Comme pour les couleurs : ne jamais écrire une durée ou une courbe en
   dur. Toujours passer par les tokens ci-dessous.

   CHARGEMENT
   À charger APRÈS /css/style.css, pour que le bloc
   prefers-reduced-motion de fin de fichier puisse neutraliser les
   animations déclarées ailleurs.

   NOMMAGE
   Tout est préfixé `.m-`, y compris la classe d'état `.m-visible`.
   Cette dernière s'appelait `.is-visible` : abandonné car HomeClosing
   .astro utilise déjà `.urgency-toast.is-visible`, piloté par son propre
   setTimeout. Il n'y avait pas de collision réelle (toutes les règles
   ici sont composées avec une classe `.m-`), mais un nom d'état partagé
   entre deux systèmes indépendants est une bombe à retardement.

   COEXISTENCE
   Les `.reveal` / `.reveal-up` historiques restent pilotées par
   /js/main.js avec leur classe `.visible`. Migration page par page.
════════════════════════════════════════════════════════════ */

:root {
  /* ── Courbes ──────────────────────────────────────────────
     Les quatre courbes réellement présentes dans le code, promues en
     tokens pour que la migration soit SANS PERTE. Sans --ease-overshoot,
     migrer `cp-pop` (le rebond du gain net dans comparer.astro) l'aurait
     aplati : on aurait perdu une intention de design en croyant faire du
     rangement.
       --ease-out-soft   : entrées. Déjà la courbe la plus utilisée du
                           site (blog.css, article.css, services.css,
                           partenaires.astro, Optimiseur*).
       --ease-in-out-soft: ce qui bouge dans les deux sens.
       --ease-overshoot  : léger dépassement, pour ce qui doit attirer
                           l'œil une fois (un chiffre qui tombe).
       --ease-material   : nav et carrousels, déjà en place. */
  --ease-out-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out-soft: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-material: cubic-bezier(0.4, 0, 0.2, 1);

  /* ── Durées ──────────────────────────────────────────────
     Trois paliers, calés sur ce que le site fait DÉJÀ — pas sur une
     préférence théorique :
       --dur-fast ≈ les 0.28s–0.3s des hovers de partenaires.astro
       --dur-base ≈ les 0.35s–0.45s des carrousels et vignettes
       --dur-slow  = les 0.6s des reveals de article.css et blog.css
     C'est --dur-slow qui pilote les reveals. Un socle à 320ms aurait
     rendu les entrées deux fois plus rapides qu'aujourd'hui : la
     migration aurait changé le ressenti du site en prétendant être un
     simple refactor. */
  --dur-fast: 200ms;
  --dur-base: 380ms;
  --dur-slow: 600ms;

  /* ── Reveals ──────────────────────────────────────────────
     24px = la valeur déjà utilisée par `.reveal-up` dans article.css et
     blog.css. Les autres allaient de 6px (geo-fade) à 28px (simFadeUp) ;
     s'aligner sur la plus répandue évite un écart visible entre une page
     migrée et une page qui ne l'est pas encore. */
  --reveal-shift: 24px;
  --reveal-scale: 0.96;

  /* Décalage entre les enfants d'une même grille (voir .m-stagger). */
  --stagger: 70ms;

  /* Élévation au survol des cards et CTA. Sobre : la marque reste
     discrète, jamais lourde. */
  --lift: -2px;

  /* ── Mouvement ambiant ────────────────────────────────────
     Durées en dizaines de secondes, volontairement. Une boucle ambiante
     qui se remarque est une boucle ratée : le visiteur doit sentir que
     la page est vivante sans jamais pouvoir dire ce qui bouge. En
     dessous de ~8s, l'effet devient une distraction. */
  --dur-ambient: 18s;
  --dur-ambient-slow: 34s;
  --ambient-shift: 10px;
  --ambient-rotate: 1.2deg;

  /* Amplitude du parallaxe et du zoom pilotés par le scroll. Garder
     faible : au-delà, le décalage se voit et fait « site de démo ». */
  --scroll-parallax: 32px;
  --scroll-zoom: 1.06;
}

/* ═══════════════════════════════════════════════════════════
   1 · REVEALS À L'ENTRÉE DANS LE VIEWPORT
   État initial en CSS, classe .m-visible posée par /js/motion.js.

   `--delay` est supporté : c'est la convention déjà utilisée par
   article.css et blog.css (`transition-delay: var(--delay, 0s)`), donc
   le HTML existant peut migrer sans réécrire ses styles inline.

   NOTE JS-OFF : si le JavaScript ne tourne pas, .m-visible n'arrive
   jamais et le contenu resterait invisible. Le bloc @media
   (scripting: none) en fin de section rétablit la visibilité.
════════════════════════════════════════════════════════════ */

.m-reveal,
.m-reveal-up,
.m-reveal-scale {
  opacity: 0;
  transition:
    opacity var(--dur-slow) var(--ease-out-soft),
    transform var(--dur-slow) var(--ease-out-soft);
  transition-delay: var(--delay, 0s);
}

.m-reveal-up { transform: translateY(var(--reveal-shift)); }
.m-reveal-scale { transform: scale(var(--reveal-scale)); }

.m-reveal.m-visible,
.m-reveal-up.m-visible,
.m-reveal-scale.m-visible {
  opacity: 1;
  transform: none;
}

/* Variantes de tempo, à poser en plus de la classe de reveal. */
.m-reveal-fast { transition-duration: var(--dur-base); }
.m-reveal-delay-1 { --delay: calc(var(--stagger) * 1); }
.m-reveal-delay-2 { --delay: calc(var(--stagger) * 2); }
.m-reveal-delay-3 { --delay: calc(var(--stagger) * 3); }

/* ── Stagger ────────────────────────────────────────────────
   À poser sur le CONTENEUR d'une grille (services, tarifs, avis,
   logements). Les enfants entrent en cascade plutôt que d'apparaître
   d'un seul bloc — c'est ce détail qui fait la différence entre « ça
   s'affiche » et « ça se pose ».
   Au-delà de 12 enfants, les suivants entrent avec le délai du 12e :
   inutile de faire attendre le visiteur davantage. */
.m-stagger > * {
  opacity: 0;
  transform: translateY(var(--reveal-shift));
  transition:
    opacity var(--dur-slow) var(--ease-out-soft),
    transform var(--dur-slow) var(--ease-out-soft);
}

.m-stagger.m-visible > * {
  opacity: 1;
  transform: none;
}

.m-stagger.m-visible > *:nth-child(1)  { transition-delay: calc(var(--stagger) * 0); }
.m-stagger.m-visible > *:nth-child(2)  { transition-delay: calc(var(--stagger) * 1); }
.m-stagger.m-visible > *:nth-child(3)  { transition-delay: calc(var(--stagger) * 2); }
.m-stagger.m-visible > *:nth-child(4)  { transition-delay: calc(var(--stagger) * 3); }
.m-stagger.m-visible > *:nth-child(5)  { transition-delay: calc(var(--stagger) * 4); }
.m-stagger.m-visible > *:nth-child(6)  { transition-delay: calc(var(--stagger) * 5); }
.m-stagger.m-visible > *:nth-child(7)  { transition-delay: calc(var(--stagger) * 6); }
.m-stagger.m-visible > *:nth-child(8)  { transition-delay: calc(var(--stagger) * 7); }
.m-stagger.m-visible > *:nth-child(9)  { transition-delay: calc(var(--stagger) * 8); }
.m-stagger.m-visible > *:nth-child(10) { transition-delay: calc(var(--stagger) * 9); }
.m-stagger.m-visible > *:nth-child(11) { transition-delay: calc(var(--stagger) * 10); }
.m-stagger.m-visible > *:nth-child(n + 12) { transition-delay: calc(var(--stagger) * 11); }

/* Filet de sécurité : sans JavaScript, tout est visible immédiatement. */
@media (scripting: none) {
  .m-reveal,
  .m-reveal-up,
  .m-reveal-scale,
  .m-stagger > * {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════
   2 · MICRO-INTERACTIONS
   Le glow reprend --gold : c'est le repère visuel du chemin vers la
   réservation, pas une décoration.

   Ces trois utilitaires sont désactivés sur écran tactile (media
   hover: none) : un :hover sur mobile reste « collé » après le tap et
   donne l'impression d'un bouton bloqué.
════════════════════════════════════════════════════════════ */

.m-lift {
  transition:
    transform var(--dur-fast) var(--ease-out-soft),
    box-shadow var(--dur-fast) var(--ease-out-soft),
    border-color var(--dur-fast) var(--ease-out-soft);
}

.m-glow {
  transition:
    box-shadow var(--dur-fast) var(--ease-out-soft),
    border-color var(--dur-fast) var(--ease-out-soft);
}

.m-arrow {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-out-soft);
}

@media (hover: hover) {
  .m-lift:hover { transform: translateY(var(--lift)); }
  .m-glow:hover { box-shadow: 0 0 0 1px rgba(232, 201, 122, 0.45); }
  .m-arrow-parent:hover .m-arrow { transform: translateX(3px); }
}

/* ═══════════════════════════════════════════════════════════
   3 · MOUVEMENT AMBIANT

   Boucles lentes et permanentes, réservées aux éléments DÉCORATIFS :
   halos, dégradés, formes floutées, pastilles. Jamais sur du texte, un
   prix, un CTA ou un champ de formulaire — du contenu qui bouge tout
   seul est illisible et donne une impression d'instabilité, l'inverse
   de l'effet cherché.

   PERF — deux garde-fous
   1. Uniquement `transform`, `opacity` et `filter` : composés sur le
      GPU, sans recalcul de mise en page. Jamais `top`, `height`,
      `box-shadow` ou `background-position` sur une boucle infinie.
   2. Les boucles sont EN PAUSE par défaut et ne tournent que lorsque
      /js/motion.js pose `.m-visible`, donc uniquement quand la section
      est à l'écran. Une boucle de 34s qui tourne dans le pied de page
      pendant que le visiteur lit le hero, c'est de la batterie brûlée
      pour rien.

   Pas de `will-change` ici, volontairement : le compositeur promeut
   déjà les éléments dont le transform est animé, alors qu'un
   `will-change` écrit en dur maintient une couche en mémoire en
   permanence — y compris pendant que l'animation est en pause, ce qui
   est exactement le coût qu'on cherche à éviter.

   CLASSES REQUISES
     .m-ambient       — rend l'élément observable (pause / reprise)
     .m-ambient-decor — neutralise les clics. À poser sur les halos et
                        décors de fond. Séparée volontairement de
                        .m-ambient : `pointer-events: none` sur la classe
                        générique aurait rendu inerte tout bloc de
                        contenu animé, à commencer par la card de
                        réservation via .m-ambient-sheen.
════════════════════════════════════════════════════════════ */

.m-ambient { animation-play-state: paused; }
.m-ambient.m-visible { animation-play-state: running; }

.m-ambient-decor {
  pointer-events: none;
  user-select: none;
}

/* ── Respiration ────────────────────────────────────────────
   Un halo qui gonfle et se dissipe très lentement. À poser sur une div
   de fond en position absolue avec un radial-gradient (--sauge ou
   --gold très dilué). C'est l'effet le plus utile du lot : il donne de
   la profondeur à une section plate sans qu'on sache pourquoi. */
@keyframes m-breathe {
  0%, 100% { transform: scale(1); opacity: 0.55; }
  50%      { transform: scale(1.12); opacity: 0.8; }
}

.m-ambient-breathe {
  animation: m-breathe var(--dur-ambient-slow) var(--ease-in-out-soft) infinite;
}

/* ── Flottement ─────────────────────────────────────────────
   Léger va-et-vient vertical, pour une pastille ou un badge détaché. */
@keyframes m-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(calc(var(--ambient-shift) * -1)); }
}

.m-ambient-float {
  animation: m-float var(--dur-ambient) var(--ease-in-out-soft) infinite;
}

/* ── Dérive ─────────────────────────────────────────────────
   Translation + rotation infime, en boucle longue. Pour les formes
   organiques floutées de fond : deux dérives avec des durées
   différentes ne se resynchronisent jamais à l'œil, ce qui suffit à
   faire vivre une section entière. */
@keyframes m-drift {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  33%      { transform: translate3d(var(--ambient-shift), calc(var(--ambient-shift) * -0.6), 0) rotate(var(--ambient-rotate)); }
  66%      { transform: translate3d(calc(var(--ambient-shift) * -0.7), var(--ambient-shift), 0) rotate(calc(var(--ambient-rotate) * -1)); }
}

.m-ambient-drift {
  animation: m-drift var(--dur-ambient-slow) var(--ease-in-out-soft) infinite;
}

/* Décalage de phase, à combiner avec les variantes ci-dessus quand
   plusieurs décors cohabitent. Un délai négatif démarre la boucle en
   cours de route, sans temps mort au chargement. */
.m-ambient-phase-1 { animation-delay: -6s; }
.m-ambient-phase-2 { animation-delay: -13s; }
.m-ambient-phase-3 { animation-delay: -21s; }

/* ── Reflet ─────────────────────────────────────────────────
   Balayage lumineux très lent traversant un bloc glassmorphique.
   Un seul par page, sur le bloc qu'on veut voir remarqué.

   ATTENTION — cette classe pose `overflow: hidden`. Ne pas l'appliquer
   à un conteneur dont un enfant doit déborder : modale, menu déroulant,
   embed Cal.com. Sur la card de réservation, la poser sur un wrapper
   intérieur dédié, jamais sur la racine de la card.

   Cette variante est observée directement par motion.js : elle n'a pas
   besoin de `.m-ambient` (sans quoi un oubli l'aurait laissée en pause
   pour toujours, donc invisible, sans aucune erreur pour le signaler). */
@keyframes m-sheen {
  0%   { transform: translateX(-120%); }
  100% { transform: translateX(120%); }
}

.m-ambient-sheen { position: relative; overflow: hidden; }

.m-ambient-sheen::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 20%,
    rgba(241, 239, 234, 0.16) 50%,
    transparent 80%
  );
  transform: translateX(-120%);
  animation: m-sheen var(--dur-ambient) var(--ease-in-out-soft) infinite;
  animation-play-state: paused;
}

.m-ambient-sheen.m-visible::after { animation-play-state: running; }

/* ═══════════════════════════════════════════════════════════
   4 · ANIMATIONS PILOTÉES PAR LE SCROLL

   Ici l'animation n'avance pas avec le temps mais avec la position de
   scroll : le visiteur « joue » lui-même l'animation en descendant.

   POURQUOI CE N'EST PAS DU JAVASCRIPT
   Tout repose sur `animation-timeline` (view() et scroll()), calculé
   par le compositeur, hors du thread principal. Aucun écouteur de
   scroll n'est ajouté : l'ancien handler de parallaxe du hero avait
   justement été supprimé pour cause de coût, il n'est pas question de
   le réintroduire sous une autre forme.

   PORTÉE VOLONTAIREMENT LIMITÉE — à lire avant d'utiliser
   Une animation liée à une timeline est RÉVERSIBLE : en remontant, elle
   se rejoue à l'envers. C'est agréable sur un décor, pénible sur du
   texte, qui se remet à disparaître quand on relit un paragraphe.
   Donc : `.m-scroll-*` pour les visuels et les décors, `.m-reveal-*`
   pour le contenu. C'est aussi pourquoi les reveals n'ont PAS été
   « upgradés » automatiquement en version scrubbée dans @supports.
   Ne jamais cumuler les deux familles sur un même élément.

   REPÈRES `animation-range`
     entry 0%   → l'élément commence à entrer par le bas du viewport
     entry 100% → il vient d'entrer entièrement
     cover      → toute la traversée du viewport
     exit       → il commence à sortir par le haut

   DÉGRADATION
   Tout est enfermé dans @supports (Chrome / Edge / Safari récents ;
   Firefox selon version). Sans support : aucun effet, contenu à son
   état final et lisible. Rien ne dépend de ces règles pour être visible.
════════════════════════════════════════════════════════════ */

@supports (animation-timeline: view()) {
  /* Entrée progressive — pour un visuel ou un bloc décoratif. */
  @keyframes m-scroll-in {
    from { opacity: 0; transform: translateY(var(--reveal-shift)); }
    to   { opacity: 1; transform: none; }
  }

  .m-scroll-in {
    animation: m-scroll-in linear both;
    animation-timeline: view();
    animation-range: entry 5% entry 85%;
  }

  /* Parallaxe — le décor monte moins vite que la page, ce qui crée un
     plan de profondeur. Images et décors uniquement, jamais du texte. */
  @keyframes m-scroll-parallax {
    from { transform: translateY(var(--scroll-parallax)); }
    to   { transform: translateY(calc(var(--scroll-parallax) * -1)); }
  }

  .m-scroll-parallax {
    animation: m-scroll-parallax linear both;
    animation-timeline: view();
    animation-range: cover;
  }

  /* Zoom lent — une photo qui se rapproche imperceptiblement pendant sa
     traversée. Le parent doit avoir overflow: hidden. */
  @keyframes m-scroll-zoom {
    from { transform: scale(1); }
    to   { transform: scale(var(--scroll-zoom)); }
  }

  .m-scroll-zoom {
    animation: m-scroll-zoom linear both;
    animation-timeline: view();
    animation-range: cover;
  }

  /* Trait qui se dessine — pour un process en étapes : la ligne
     verticale reliant les étapes se remplit à mesure qu'on descend.
     C'est l'effet « on avance avec vous » : il raconte un déroulé au
     lieu de le lister. Poser sur le trait lui-même. */
  @keyframes m-scroll-draw {
    from { transform: scaleY(0); }
    to   { transform: scaleY(1); }
  }

  .m-scroll-draw {
    transform-origin: top center;
    animation: m-scroll-draw linear both;
    animation-timeline: view();
    animation-range: entry 40% cover 70%;
  }

  /* Barre de progression de lecture — un filet --gold en haut des
     articles. Poser sur une div fine en position: fixed; top: 0.
     Sur une page plus courte que le viewport il n'y a pas de plage de
     scroll : la barre reste à zéro, donc invisible. C'est le
     comportement voulu. */
  @keyframes m-scroll-progress {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }

  .m-scroll-progress {
    transform-origin: left center;
    animation: m-scroll-progress linear both;
    animation-timeline: scroll(root block);
  }

  /* Même mécanique restreinte à une section (avancement dans un
     comparatif, un simulateur). */
  .m-scroll-progress-section {
    transform-origin: left center;
    animation: m-scroll-progress linear both;
    animation-timeline: view();
    animation-range: cover;
  }
}

/* ═══════════════════════════════════════════════════════════
   5 · PREFERS-REDUCED-MOTION — bloc unique du site

   Avant : le réglage système n'était respecté qu'à un tiers. Bien géré
   pour la vidéo hero (main.js met en pause sur la première image) et
   présent dans style.css et Layout.astro, mais totalement absent de
   comparer.astro, partenaires.astro, SimulateurLCD.jsx et des
   Optimiseur*.jsx. Un visiteur sensible au mouvement subissait quand
   même les animations de ces pages.

   Le sélecteur universel couvre tout le site d'un coup, pages migrées
   ou non, y compris les <style> inline des composants React. C'est
   aussi pour cela que ce fichier doit être chargé en dernier.

   0.01ms plutôt que 0 : la durée quasi nulle laisse les événements
   transitionend / animationend se déclencher. Aucun script du repo n'en
   dépend aujourd'hui — c'est une précaution pour la suite, pas un
   correctif.

   ATTENTION — les sections 3 et 4 ne peuvent PAS compter sur ce filet.
   Une boucle infinie ramenée à 0.01ms reste une boucle, et une
   animation attachée à une timeline de scroll ignore purement et
   simplement animation-duration. Les deux familles sont donc coupées
   explicitement avec animation: none.
════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  html { scroll-behavior: auto !important; }

  /* Les reveals doivent finir visibles, pas figés à opacity: 0. */
  .m-reveal,
  .m-reveal-up,
  .m-reveal-scale,
  .m-stagger > * {
    opacity: 1 !important;
    transform: none !important;
  }

  /* Pas de déplacement au survol. */
  .m-lift:hover { transform: none !important; }
  .m-arrow-parent:hover .m-arrow { transform: none !important; }

  /* Ambiant : aucune boucle, dans aucun état. Le décor reste affiché,
     simplement immobile. */
  .m-ambient,
  .m-ambient-breathe,
  .m-ambient-float,
  .m-ambient-drift {
    animation: none !important;
    transform: none !important;
  }

  .m-ambient-sheen::after { display: none !important; }

  /* Scroll : timelines détachées et état final imposé, pour que rien ne
     reste à moitié transparent ou décalé. */
  .m-scroll-in,
  .m-scroll-parallax,
  .m-scroll-zoom,
  .m-scroll-draw,
  .m-scroll-progress,
  .m-scroll-progress-section {
    animation: none !important;
    animation-timeline: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}
