/* ─────────────────────────────────────────
   MLinguistique : Feuille de style globale
   Chargée sur toutes les pages.
   Design tokens en :root : valeurs neutres,
   à affiner lors du cadrage graphique.
   Mobile first.
───────────────────────────────────────── */

:root {
  /* Palette de marque */
  --ml-primary:        #2e123f;   /* aubergine nuit : titres sur blanc et chrome
                                     sombre (en-tête, sections sombres, pied de
                                     page de repli). 16,5:1 sur blanc comme en
                                     blanc sur ce fond. C'est la nuit de la marque,
                                     la même teinte que --ml-accent-nuit, montée
                                     ici au rang de couleur structurante. Elle a
                                     remplacé le bleu marine d'origine, retiré du
                                     site public. */
  --ml-primary-dark:   #1e0a2a;   /* nuit violette plus profonde : fond opaque du
                                     haut de page en attente de la vidéo, calée sur
                                     la couche la plus sombre du dégradé du hero
                                     (le même rgb(30, 10, 42) que ses voiles). */
  --ml-primary-light:  #7a3d97;   /* violet moyen, marche haute de l'échelle du
                                     chrome. Sans usage aujourd'hui : le vérifier
                                     au contraste avant de le poser sous du texte. */
  --ml-accent:         #661f80;   /* violet du logo */
  --ml-accent-dark:    #4d1760;
  --ml-accent-vif:     #9e3dbe;   /* le violet du logo, éclairci : réservé aux
                                     lueurs décoratives sur fond clair (l'onde
                                     des étapes). Jamais une couleur de texte. */
  --ml-accent-soft:    #f4ecf7;   /* violet très dilué, pour les fonds d'accent */
  --ml-accent-nuit:    #2e123f;   /* violet nuit, fond des sections sombres des
                                     fiches pays et villes (maquettes validées).
                                     Plus profond que --ml-accent : c'est la nuit
                                     de la marque, la même teinte que --ml-primary,
                                     réservée ici aux fonds, jamais au texte. */
  --ml-accent-bordure: #ece0f4;   /* bordure lilas des cartes posées sur fond
                                     clair (fiches pays et villes). */
  --ml-accent-clair:   #c9a6e0;   /* le violet du logo, très éclairci, aligné
                                     sur le « bonjour » de la maquette « Hero
                                     Mascotte » (juillet 2026 ; valait #d9a8f0
                                     avant elle, et les mesures ci-dessous
                                     datent de cette valeur et du lit
                                     précédent). La SEULE teinte d'accent qui
                                     puisse porter du texte sur un fond
                                     sombre.

                                     Pourquoi si pâle : le titre du haut de page
                                     est blanc, le lit derrière lui doit donc
                                     être assez sombre pour le porter. Sur un
                                     lit aussi sombre, --ml-accent (#661f80)
                                     tombe à 2,3:1, et --ml-accent-vif (#9e3dbe)
                                     à 2,1:1 : illisibles, tous les deux. Ce
                                     n'est pas un réglage, c'est de
                                     l'arithmétique : un fond ne peut pas être
                                     sombre pour du blanc et clair pour du
                                     violet. La seule sortie est d'éclaircir le
                                     violet jusqu'à ce qu'il tienne, lui aussi,
                                     4,5:1 sur ce lit. Pire pixel mesuré dans un
                                     Chromium réel, sur une photo entièrement
                                     blanche (le pire cas absolu), de 320 à
                                     1920px de large : de 5,1 à 5,3:1. Voir le
                                     tableau au-dessus de .ml-home-hero-texte.

                                     Sur fond clair, en revanche, elle ne vaut
                                     rien : 2,2:1 sur du blanc. Elle n'a donc
                                     rien à faire ailleurs que sur le lit sombre
                                     du haut de page. */
  --ml-en-ligne:       #1a8f4c;   /* le point « en ligne » de la carte de
                                     Linguine. Il ne dit rien à lui seul : le mot
                                     est écrit à côté. */

  /* Surfaces. Des blancs chauds plutôt que des gris froids : le site parle
     de voyage et de rencontre, pas de tableau de bord. Le gris neutre
     durcit tout ce qu'il touche. */
  --ml-bg:             #ffffff;
  --ml-bg-alt:         #faf7f5;   /* crème très pâle, à peine teinté */
  --ml-bg-warm:        #f6f1ee;   /* un cran plus dense, pour les encarts */
  --ml-bg-dark:        var(--ml-primary);

  --ml-text:           #221d29;   /* presque noir, légèrement violacé */
  --ml-text-mid:       #58515f;
  --ml-text-dim:       #8b8391;
  --ml-text-on-dark:   #f7f4f9;

  --ml-border:         #ece5e8;   /* la bordure ne doit pas se voir, juste séparer */
  --ml-border-strong:  #d6ccd2;

  /* Hiérarchie typographique.
     Baloo 2 pour les titres (voir assets/css/fonts.css) : ses formes rondes
     reprennent le lettrage des bulles de la marque. Le corps de texte reste
     neutre, on lit beaucoup sur ce site. */
  --ml-font-base:      system-ui, -apple-system, "Segoe UI", sans-serif;
  --ml-font-heading:   'Baloo 2', var(--ml-font-base);
  --ml-fs-base:        1rem;
  --ml-fs-sm:          0.875rem;
  --ml-fs-lg:          1.125rem;
  --ml-fs-h1:          clamp(2.15rem, 5.2vw, 3.4rem);
  --ml-fs-h2:          clamp(1.6rem, 3.6vw, 2.4rem);
  --ml-fs-h3:          clamp(1.25rem, 2.5vw, 1.55rem);
  --ml-lh-base:        1.65;
  --ml-lh-heading:     1.15;

  /* Espacements */
  --ml-space-xs:       0.5rem;
  --ml-space-sm:       1rem;
  --ml-space-md:       1.5rem;
  --ml-space-lg:       2.5rem;
  --ml-space-xl:       4.5rem;
  --ml-header-h:       72px;
  --ml-container-w:    1200px;
  /* Le haut de page de l'accueil est le seul endroit du site où le contenu
     dépasse le conteneur commun. La barre de menu lit cette même valeur pour
     s'aligner exactement sur le titre du haut de page, plutôt que de flotter
     sur une largeur arbitraire plus étroite. */
  --ml-hero-w:         1440px;

  /* Rayons généreux : c'est là que se joue la douceur du site. */
  --ml-radius-sm:      10px;
  --ml-radius:         16px;
  --ml-radius-lg:      28px;
  --ml-radius-pill:    999px;

  /* Élévations teintées de violet plutôt que de noir : une ombre grise sur
     un fond chaud le fait virer au sale. */
  --ml-shadow-sm:      0 2px 8px rgba(34, 29, 41, 0.05);
  --ml-shadow-md:      0 10px 30px rgba(34, 29, 41, 0.09);
  --ml-shadow-lg:      0 24px 60px rgba(34, 29, 41, 0.14);

  --ml-focus-ring:     0 0 0 3px rgba(102, 31, 128, 0.35);

  /* Une courbe unique pour tout le site : deux courbes différentes sur une
     même page se remarquent, même sans savoir pourquoi. */
  --ml-ease:           cubic-bezier(0.22, 1, 0.36, 1);
}

/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* Aucun defilement horizontal, jamais. Les grands elements decoratifs
   peuvent deborder de leur section : c'est la page qui les rogne.
   `overflow-x: clip` plutot que `hidden` : `hidden` sur <html> casse
   `position: sticky` sur les descendants, `clip` ne le casse pas. */
html { overflow-x: clip; }
body { overflow-x: clip; }
img, video { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }

.screen-reader-text {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}
.screen-reader-text:focus {
  position: static;
  width: auto;
  height: auto;
  clip: auto;
}

/* ── Focus visible : accessibilité ── */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--ml-accent);
  outline-offset: 2px;
}

body.ml-site {
  background: var(--ml-bg);
  font-family: var(--ml-font-base);
  font-size: var(--ml-fs-base);
  line-height: var(--ml-lh-base);
  color: var(--ml-text);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--ml-font-heading);
  line-height: var(--ml-lh-heading);
  color: var(--ml-primary);
  margin: 0 0 var(--ml-space-sm);
  text-wrap: balance;
  /* Baloo 2 a une hauteur d'x genereuse et des jambages courts : elle tient
     seule, sans le resserrement qu'exigeait la police precedente. */
  letter-spacing: 0;
}
h1 { font-size: var(--ml-fs-h1); font-weight: 700; }
h2 { font-size: var(--ml-fs-h2); font-weight: 700; }
h3 { font-size: var(--ml-fs-h3); font-weight: 600; }

p { margin: 0 0 var(--ml-space-sm); }

/* ── Conteneur générique ── */
.ml-container {
  width: 100%;
  max-width: var(--ml-container-w);
  margin-inline: auto;
  padding-inline: var(--ml-space-sm);
}

/* ── Skip link ── */
.ml-skip-link {
  position: absolute;
  top: -100px;
  left: 0;
  background: var(--ml-primary);
  color: var(--ml-text-on-dark);
  padding: var(--ml-space-xs) var(--ml-space-sm);
  z-index: 1000;
}
.ml-skip-link:focus {
  top: 0;
}

/* Sentinelle du script de défilement (assets/js/main.js) : hors flux,
   invisible, sans dimension. Elle ne doit rien déplacer ni créer de
   défilement horizontal. */
#ml-scroll-sentinelle {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 1px;
  block-size: 1px;
  pointer-events: none;
}

/* ── Header chrome : barre flottante ──────────────────────────

   .ml-header reste le conteneur collant, invisible en lui même : c'est
   .ml-nav qui porte l'apparence de la barre (fond, rayon, ombre), pour
   pouvoir la détacher des bords avec une marge, sur une largeur confortable
   sans occuper tout l'écran.

   État par défaut, et état sans JavaScript : la barre est opaque, texte
   sombre. C'est l'état sûr, celui qui reste lisible partout, y compris sur
   les pages sans hero sombre.

   La transparence est un bonus, activé seulement si les trois conditions
   sont réunies :
   1. body porte « ml-has-dark-header » : la page a un hero sombre derrière
      l'en-tête (posée par ml_body_classes(), voir inc/template-functions.php).
   2. html porte « ml-js-scroll » : le script s'est chargé et a confirmé,
      via IntersectionObserver, que la page est encore tout en haut. Sans
      cette classe, impossible de savoir si la page a défilé : on reste donc
      opaque plutôt que de risquer un texte clair illisible plus bas.
   3. .ml-header ne porte pas encore « is-scrolled » : dès que le script
      détecte le défilement, cette classe force la barre opaque.

   La couleur du texte suit via une variable locale, --ml-header-fg, lue par
   le logo, les liens et le méga menu (assets/css/mega-menu.css). ── */
.ml-header {
  position: sticky;
  top: 0;
  z-index: 100;
  padding-block-start: var(--ml-space-sm);
  /* Le fond et la bordure vivent sur .ml-nav : .ml-header ne peint rien,
     pour laisser voir le hero autour de la barre flottante. */
}

/* LA BARRE A LA MÊME LARGEUR PARTOUT (demande de Marino, août 2026).

   Elle s'alignait sur le conteneur de la page, sauf sur les pages à haut de
   page sombre, où elle s'élargissait jusqu'à --ml-hero-w. Deux largeurs pour
   une même barre : en passant de l'accueil à une page de langue, elle se
   resserrait sans raison visible, et l'accueil montrait bien qu'elle avait la
   place. Elle prend donc la largeur large partout.

   Ce que ça coûte, et c'est assumé : le bord gauche de la pilule ne tombe plus
   exactement sur la première lettre des titres en dessous, sur les pages dont
   le conteneur est plus étroit. La barre est une pilule flottante, détachée du
   contenu, pas une colonne qui devrait s'y aligner. */
.ml-nav {
  --ml-header-fg: var(--ml-primary);

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ml-space-md);
  inline-size: min(
    100% - 2 * var(--ml-space-sm),
    calc(var(--ml-hero-w) - 2 * var(--ml-space-sm))
  );
  margin-inline: auto;
  padding: var(--ml-space-xs) var(--ml-space-md);
  min-height: var(--ml-header-h);
  color: var(--ml-header-fg);
  background: var(--ml-bg);
  border: 1px solid transparent;
  border-radius: var(--ml-radius-pill);
  box-shadow: var(--ml-shadow-sm);
  transition: background-color 240ms var(--ml-ease),
              border-color 240ms var(--ml-ease),
              box-shadow 240ms var(--ml-ease),
              color 240ms var(--ml-ease);
}

/* ── Verre dépoli, tout en haut de l'accueil seulement ──
   Deux couches de fond, dans cet ordre : une teinte sombre posée sur la photo,
   puis le blanc translucide qui donne la matière du verre.

   Cette teinte a changé de rôle. Tant que le haut de page portait un voile
   plein écran, la photo arrivait déjà assombrie sous la barre et 0,18
   suffisait. Le voile est parti (voir .ml-hero-media::after) : la barre reçoit
   maintenant une image presque nue, et c'est à elle, désormais, de fournir le
   fond de son propre texte. Elle passe donc de 0,18 à 0,50.

   Ce n'est pas un retour du voile déguisé : la pilule ne couvre que sa propre
   surface, quelques milliers de pixels tout en haut, là où il n'y a rien à
   voir de la vidéo. Assombrir la barre ne coûte rien à l'image ; assombrir
   l'image pour sauver la barre coûtait toute l'image.

   Pire cas mesuré au pixel (photo entièrement blanche, sous les glyphes des
   liens) : 4,54:1 en 1920, en 1440 et en 1024. Sous 900px les liens laissent
   la place au bouton du menu, dont les trois traits, mesurés de la même façon,
   tiennent 4,52:1.

   La bordure claire détache la pilule du fond, sinon elle s'y dissout. */
html.ml-js-scroll body.ml-has-dark-header .ml-header:not(.is-scrolled) .ml-nav {
  --ml-header-fg: var(--ml-text-on-dark);
  background:
    linear-gradient(rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.12)),
    linear-gradient(rgba(30, 10, 42, 0.50), rgba(30, 10, 42, 0.50));
  -webkit-backdrop-filter: blur(16px) saturate(140%);
          backdrop-filter: blur(16px) saturate(140%);
  border-color: rgba(255, 255, 255, 0.22);
  box-shadow: none;
}

/* Sans flou d'arrière plan (Firefox ancien, navigateurs de vieux téléphones),
   du blanc à 12 pour cent sur une photo claire ne serait qu'un voile
   invisible et le texte blanc deviendrait illisible. On retombe alors sur un
   fond sombre franchement opaque : le rendu perd la matière, jamais la
   lisibilité. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html.ml-js-scroll body.ml-has-dark-header .ml-header:not(.is-scrolled) .ml-nav {
    background: rgba(30, 10, 42, 0.62);
    border-color: rgba(255, 255, 255, 0.18);
  }
}

/* Superposition au hero sombre : sur ces pages seulement, l'en-tête ne
   réserve pas sa propre bande au dessus du hero. Une marge basse négative,
   égale à sa hauteur, ramène la section suivante juste sous le haut de
   page : c'est ce chevauchement qui rend la transparence visible et utile.
   Sur les pages sans hero sombre, l'en-tête garde sa place normale dans le
   flux (piège décrit dans la spec : une barre transparente y serait
   invisible sur fond clair, elle reste donc opaque et occupe sa place). */
body.ml-has-dark-header .ml-header {
  margin-block-end: calc(-1 * (var(--ml-header-h) + var(--ml-space-sm)));
}

/* ── La barre d'administration de WordPress ──────────────────────────────
   Visible sur le site public dès qu'une personne est connectée. WordPress
   pousse alors la page vers le bas (html { margin-top }) et pose sa propre
   barre par dessus. Trois régimes, lus dans wp-includes/css/admin-bar.css :

   - au dessus de 782px : barre fixe, 32 pixels de haut ;
   - de 601 à 782px : barre fixe, 46 pixels de haut ;
   - à 600px et moins : barre de 46 pixels, mais « position: absolute » : elle
     défile avec la page et libère le haut de la fenêtre. Un décalage fixe y
     serait faux, l'en-tête doit revenir à zéro.

   Sans ces règles, l'en-tête collant se glisse SOUS la barre noire dès le
   premier défilement, et la pilule y perd ses seize premiers pixels. */
body.admin-bar .ml-header {
  top: 32px;
}
@media (max-width: 782px) {
  body.admin-bar .ml-header {
    top: 46px;
  }
}
@media (max-width: 600px) {
  body.admin-bar .ml-header {
    top: 0;
  }
}

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

.ml-nav-logo,
.ml-nav-logo .custom-logo-link {
  display: block;
  line-height: 0;
}

.ml-nav-logo img,
.ml-nav-logo .custom-logo {
  /* La largeur commande, la hauteur suit. Le plafond de hauteur n'est pas une
     valeur choisie au jugé : c'est exactement ce que la pilule peut contenir,
     soit sa hauteur moins ses marges intérieures et son filet. Un plafond plus
     haut (56px) faisait grandir la barre à 74px alors que la marge négative qui
     la superpose au haut de page vaut, elle, --ml-header-h + --ml-space-sm :
     les deux pixels d'écart laissaient voir une bande blanche tout en haut du
     hero. Le calcul se règle donc tout seul si un token change. */
  inline-size: auto;
  max-inline-size: 180px;
  max-block-size: calc(var(--ml-header-h) - 2 * var(--ml-space-xs) - 2px);
  block-size: auto;
  object-fit: contain;
}
/* Le logo est violet, pensé pour un fond blanc. Sur le haut de page sombre,
   il disparaîtrait. Le filtre le rend blanc quel que soit le fichier choisi,
   sans demander deux versions du logo à la cliente. Il revient à sa couleur
   dès que la barre devient opaque. */
html.ml-js-scroll body.ml-has-dark-header .ml-header:not(.is-scrolled) .ml-nav-logo img,
html.ml-js-scroll body.ml-has-dark-header .ml-header:not(.is-scrolled) .ml-nav-logo .custom-logo {
  filter: brightness(0) invert(1);
}
.ml-nav-logo img,
.ml-nav-logo .custom-logo {
  transition: filter 240ms var(--ml-ease);
}
@media (prefers-reduced-motion: reduce) {
  .ml-nav-logo img,
  .ml-nav-logo .custom-logo { transition: none; }
}

.ml-nav-links {
  display: none;
  gap: var(--ml-space-md);
}
.ml-nav-burger {
  /* Les trois traits sont peints en « currentColor ». Sans cette ligne, le
     bouton prend la couleur par défaut des boutons du navigateur, un noir qui
     n'hérite pas du blanc de la barre : sur le verre dépoli, au dessus du haut
     de page sombre, les traits disparaissaient presque. « inherit » les
     raccroche à --ml-header-fg, qui suit déjà l'état de la barre. */
  color: inherit;

  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  background: none;
  border: 0;
  cursor: pointer;
  padding: var(--ml-space-xs);
  /* Cible tactile de 44px minimum (audit mobile) : les trois traits ne font
     que 24x16, le doigt vise la boîte, pas le dessin. */
  min-inline-size: 44px;
  min-block-size: 44px;
}
.ml-burger-bar {
  width: 24px;
  height: 2px;
  background: currentColor;
  transition: background-color 240ms var(--ml-ease);
}
@media (prefers-reduced-motion: reduce) {
  .ml-burger-bar { transition: none; }
}

@media (min-width: 900px) {
  .ml-nav-links { display: flex; }
  .ml-nav-burger { display: none; }
}

/* LE LOGO SE RESSERRE UN PEU SUR TÉLÉPHONE (demande de Marino). Il gardait sa
   largeur de bureau, 180px, sur une barre qui doit aussi loger la loupe et le
   bouton du menu : c'est lui qui prenait la place des deux autres. Le plafond de
   HAUTEUR n'est pas touché, seule la largeur cède : c'est la hauteur qui commande
   celle de la barre, et la faire varier ferait sauter le haut de page. */
@media (max-width: 899.98px) {
  .ml-nav-logo img,
  .ml-nav-logo .custom-logo {
    max-inline-size: 148px;
  }
}

/* ── Footer chrome ──
   Le pied de page quitte le violet. Il l'a porte un temps, et le client a vu
   ce qui n'allait pas : la section des avis est un aplat violet, le pied de
   page etait un aplat violet, l'un sous l'autre. Deux grandes surfaces de meme
   teinte qui se touchent ne font plus qu'une, et la limite entre les deux
   disparait.

   Il prend la nuit du site, --ml-primary : c'est la couleur du chrome, la
   meme que celle de l'en-tete. La page s'ouvre et se ferme sur la meme nuit,
   et le violet redevient ce qu'il doit etre, un accent qu'on ne voit qu'aux
   endroits qui comptent (le bouton, le liseré, la barre des titres de
   colonne).

   Contrastes mesures sur ce fond (#2e123f) :
   - le texte courant, --ml-text-on-dark (#f7f4f9) : 15,1:1
   - les liens au repos, blanc a 82 pour cent : 11,3:1
   - le copyright, blanc a 72 pour cent : 8,9:1 */
/* Le pied de page est CLAIR depuis juillet 2026 (demande de Marino : fond
   blanc, coins hauts arrondis, ombre d'élévation). Tout son habillage vit
   dans assets/css/footer.css, chargé après ce fichier : les valeurs posées
   ici ne sont qu'un repli si footer.css venait à manquer, et elles suivent
   la même direction claire pour ne jamais mentir sur la couleur réelle. */
.ml-footer {
  background: var(--ml-bg);
  color: var(--ml-text);
  padding: 0;
}
.ml-footer a { color: var(--ml-text-mid); }

/* ── Boutons ── */
.ml-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--ml-space-xs);
  padding: 0.8em 1.6em;
  border-radius: var(--ml-radius-pill);
  font-family: var(--ml-font-heading);
  font-weight: 600;
  font-size: var(--ml-fs-base);
  border: 2px solid transparent;
  cursor: pointer;
  transition: background-color 200ms var(--ml-ease),
              transform 200ms var(--ml-ease),
              box-shadow 200ms var(--ml-ease);
}
.ml-btn:hover {
  transform: translateY(-1px);
}
.ml-btn:active {
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .ml-btn { transition: none; }
  .ml-btn:hover, .ml-btn:active { transform: none; }
}

.ml-btn-primary {
  background: var(--ml-accent);
  color: #fff;
  box-shadow: var(--ml-shadow-sm);
}
.ml-btn-primary:hover {
  background: var(--ml-accent-dark);
  box-shadow: var(--ml-shadow-md);
}
.ml-btn-secondary {
  background: var(--ml-bg);
  border-color: var(--ml-border-strong);
  color: var(--ml-primary);
}
.ml-btn-secondary:hover {
  border-color: var(--ml-accent);
  color: var(--ml-accent);
}

/* ── Sections génériques ──────────────────────────────────────────────
   Le rythme de la page tient à trois choses, et à trois seulement : le souffle
   (l'air autour de la section), le fond, et le cadrage du titre. Sans elles,
   toutes les sections se valent et la page paraît plate, quel que soit le soin
   mis dans les cartes.

   Trois souffles, jamais davantage : le souffle courant, un souffle serré pour
   une section qui qualifie celle d'avant (les chiffres, qui n'introduisent rien,
   ils comptent), et un souffle ample pour une section qui doit être lue seule
   (l'appel final). ── */
.ml-section {
  padding: var(--ml-space-xl) var(--ml-space-sm);
}
.ml-section-serree {
  padding-block: var(--ml-space-lg);
}
.ml-section-ample {
  padding-block: calc(var(--ml-space-xl) * 1.35);
}
.ml-section-alt {
  background: var(--ml-bg-alt);
}

/* ── Section sombre ──────────────────────────────────────────────────
   Une seule dans le corps d'une page, sinon elle ne veut plus rien dire.

   Contrastes mesurés sur le fond --ml-primary (#2e123f) :
   - texte courant --ml-text-on-dark (#f7f4f9) : 15,1:1
   - titres et surtitre en blanc pur : 16,5:1
   - texte adouci, blanc à 82 pour cent sur ce fond : 11,3:1
   - repère de listing, blanc à 75 pour cent : 9,5:1
   Tout est très au dessus des 4,5:1 exigés. Le violet de marque, lui, ne peut
   pas servir de couleur de texte ici (1,6:1 sur cette nuit, illisible) : sur
   fond sombre, c'est le blanc qui porte l'accent. ── */
.ml-section-dark {
  background: var(--ml-primary);
  color: var(--ml-text-on-dark);
}
.ml-section-dark h2,
.ml-section-dark h3 {
  color: #fff;
}
.ml-section-dark p {
  color: rgba(255, 255, 255, 0.82);
}
.ml-section-dark .ml-listing-placeholder {
  border-color: rgba(255, 255, 255, 0.3);
  color: rgba(255, 255, 255, 0.75);
}
.ml-section-dark a:focus-visible,
.ml-section-dark button:focus-visible {
  outline-color: #fff;
}

/* ── Surtitre de section ─────────────────────────────────────────────
   Composant global, et non plus une règle enfermée dans la seule section
   « Marine » : c'était la raison pour laquelle cette section paraissait plus
   soignée que les autres. Le surtitre situe, le titre raconte. La cliente en
   écrit le texte depuis l'éditeur, jamais la classe. */
.ml-eyebrow {
  display: flex;
  align-items: center;
  gap: 0.6em;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--ml-fs-sm);
  font-weight: 700;
  /* Le violet sur blanc : 10,1:1. Sur le crème : 9,5:1. Sur le fond chaud de la
     section « Marine » : 9,0:1. Largement au dessus des 4,5:1, y compris à ce
     petit corps de texte. */
  color: var(--ml-accent);
  margin-bottom: var(--ml-space-xs);
}
/* LE SIGNE QUI OUVRE LE SURTITRE : UNE PETITE VAGUE, plus un trait droit.
   Décoratif : posé en CSS, jamais écrit dans le contenu, pour qu'aucun lecteur
   d'écran n'ait à l'annoncer.

   Le trait droit se retrouve sur des milliers de sites et ne dit rien de ce
   client là (constat de Marino). La vague, elle, parle de départ, de mer et de
   traversée, ce qui est le sujet même de l'agence, et elle rime avec les grands
   arrondis de la maquette.

   ELLE SE POSE EN url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 8'%3E%3Cpath d='M2 4c2-3.2 4-3.2 6 0s4 3.2 6 0 4-3.2 6 0 4 3.2 6 0' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / contain, ET C'EST CE QUI LA REND GRATUITE PARTOUT. Le fond
   reste « currentColor », donc les cinq déclinaisons du thème, qui ne
   redéfinissent que « background » pour s'adapter à un fond sombre ou violet,
   continuent de fonctionner sans y toucher : la vague prend leur teinte comme
   le trait prenait la sienne.

   Aucun fichier de plus : le tracé est un petit SVG intégré à l'URL, comme la
   coche de la page Formule et les chevrons de la fiche ville.

   Sans masque CSS, le navigateur garde le trait droit d'avant : la règle de
   base ci dessous n'est pas touchée, seul le bloc @supports la remplace. */
.ml-eyebrow::before {
  content: '';
  flex: 0 0 auto;
  inline-size: 1.6em;
  block-size: 2px;
  border-radius: var(--ml-radius-pill);
  background: currentColor;
}

@supports (mask: linear-gradient(#000, #000)) or (-webkit-mask: linear-gradient(#000, #000)) {
  .ml-eyebrow::before {
    inline-size: 1.9em;
    block-size: 0.56em;
    border-radius: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 8'%3E%3Cpath d='M2 4c2-3.2 4-3.2 6 0s4 3.2 6 0 4-3.2 6 0 4 3.2 6 0' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / contain;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 8'%3E%3Cpath d='M2 4c2-3.2 4-3.2 6 0s4 3.2 6 0 4-3.2 6 0 4 3.2 6 0' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / contain;
  }
}
/* Sur fond sombre, le violet tombe à 1,5:1 : illisible. Le surtitre passe donc
   au blanc (15,1:1), et le trait garde une teinte de marque, très pâle. */
.ml-section-dark .ml-eyebrow {
  color: #fff;
}
.ml-section-dark .ml-eyebrow::before {
  background: var(--ml-accent-soft);
}

/* ── Titre de section ────────────────────────────────────────────────
   Les titres de niveau deux flottaient sur les 1200 pixels du conteneur, sans
   mesure : une ligne de titre aussi longue ne se lit plus d'un seul regard. On
   les borne, comme le titre du haut de page l'est déjà. Les sections qui ont
   leur propre mise en page (« Marine », en deux colonnes) ne sont pas touchées :
   la règle ne vise que les enfants directs du conteneur. */
.ml-section > .ml-container > h2 {
  max-inline-size: 22ch;
}
.ml-section > .ml-container > h2 + p {
  max-inline-size: 58ch;
  color: var(--ml-text-mid);
  font-size: var(--ml-fs-lg);
}
.ml-section-dark > .ml-container > h2 + p {
  color: rgba(255, 255, 255, 0.82);
}

/* ── Placeholder de listing (zones alimentées plus tard par les CPT) ── */
.ml-listing-placeholder {
  border: 1px dashed var(--ml-border-strong);
  border-radius: var(--ml-radius);
  padding: var(--ml-space-lg);
  text-align: center;
  color: var(--ml-text-dim);
}

/* ── Grilles de cartes ── */
.ml-home-sejours-grid,
.ml-langues-grid {
  display: grid;
  gap: var(--ml-space-md);
  grid-template-columns: 1fr;
}
@media (min-width: 700px) {
  .ml-home-sejours-grid,
  .ml-langues-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
  .ml-home-sejours-grid { grid-template-columns: repeat(3, 1fr); }
  /* Quatre langues : quatre colonnes, sinon la dernière carte reste seule. */
  .ml-langues-grid { grid-template-columns: repeat(4, 1fr); }
}
/* La carte de langue ne partage plus ce vocabulaire de simple encart bordé :
   elle a le sien, plus bas, avec sa bulle de marque. */
.ml-sejour-card {
  background: var(--ml-bg);
  border: 1px solid var(--ml-border);
  border-radius: var(--ml-radius-lg);
  padding: var(--ml-space-md);
  box-shadow: var(--ml-shadow-sm);
}
.ml-sejour-card-img img {
  border-radius: var(--ml-radius);
  margin-bottom: var(--ml-space-sm);
}

/* ── Zone de listing différée (CPT à venir) ── */
.ml-listing-zone {
  margin-top: var(--ml-space-md);
}

/* ── Page contact ── */
.ml-contact-grid {
  display: grid;
  gap: var(--ml-space-lg);
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .ml-contact-grid { grid-template-columns: 3fr 2fr; }
}
.ml-contact-form-card,
.ml-contact-info {
  background: var(--ml-bg);
  border: 1px solid var(--ml-border);
  border-radius: var(--ml-radius-lg);
  padding: var(--ml-space-lg);
}
.ml-contact-form p { margin-bottom: var(--ml-space-sm); }
.ml-contact-form label { display: block; font-weight: 600; margin-bottom: 0.25em; }
.ml-contact-form input,
.ml-contact-form textarea {
  width: 100%;
  padding: 0.65em 0.85em;
  border: 1px solid var(--ml-border-strong);
  border-radius: var(--ml-radius);
  font: inherit;
}
/* Piege a robots : invisible sans creer de debordement.
   `left: -9999px` pousserait la page vers la gauche. */
.ml-field-honeypot {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ml-info-list li {
  padding: var(--ml-space-xs) 0;
  border-bottom: 1px solid var(--ml-border);
  /* Une adresse e-mail est un seul mot : autorisée à se couper plutôt que de
     déborder sur un très petit écran (audit mobile). */
  overflow-wrap: anywhere;
}
.ml-info-list li:last-child { border-bottom: none; }
.ml-info-list span {
  display: block;
  font-size: var(--ml-fs-sm);
  color: var(--ml-text-dim);
}

/* ─────────────────────────────────────────
   Éléments de marque : globe et bulles

   Le globe sert de filigrane de section, jamais de contenu : il est
   décoratif, donc invisible pour les lecteurs d'écran. Les bulles
   « Hi », « Hola », « Ciao », « Hallo » sont un rappel de l'univers
   du client, une par langue enseignée. À employer avec parcimonie :
   une ou deux par page, jamais une nuée.
───────────────────────────────────────── */

/* Filigrane du globe. Le SVG lit currentColor, donc la teinte se règle
   par `color` sur le conteneur. */
.ml-watermark {
  position: relative;
  isolation: isolate;
}
.ml-watermark::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset-block-start: 50%;
  inset-inline-end: -4%;
  translate: 0 -50%;
  inline-size: min(52vw, 460px);
  aspect-ratio: 311 / 334;
  background-color: currentColor;
  -webkit-mask: url('../img/globe.svg') no-repeat center / contain;
          mask: url('../img/globe.svg') no-repeat center / contain;
  color: var(--ml-accent);
  opacity: 0.06;
  pointer-events: none;
}

/* Sur fond sombre, le filigrane doit s'éclaircir, pas s'assombrir. */
.ml-watermark.is-on-dark::before {
  color: #fff;
  opacity: 0.08;
}

/* Les quatre bulles de salutation, une par langue enseignée. Elles sont
   purement ornementales : le markup qui les porte est aria-hidden, et le nom
   de la langue est toujours écrit à côté d'elles.

   Elles sont EN FLUX, jamais posées en ornement flottant : leur seule place
   sur le site est le haut d'une carte de langue (voir .ml-langue-card-bulle).
   La règle générique ne fait donc que dire ce qu'est l'image, et laisse la
   carte la dimensionner. Aucune opacité statique, aucun état à révéler par
   script : sans JavaScript, les bulles sont là, telles quelles.

   L'ancien jeu de règles (position absolue, apparition et flottement continu
   pilotés par la classe is-visible) est retiré : aucun gabarit ne produisait
   ce balisage, c'était du code mort. */
.ml-bubble {
  pointer-events: none;
  user-select: none;
}

/* ─────────────────────────────────────────
   Page d'accueil
   Mobile first. Aucune valeur en dur, tout
   lit les tokens du :root.
───────────────────────────────────────── */

/* ── Hero plein ecran, video de fond ──────────────────────────

   La video est purement decorative. L'affiche (poster) est ce que le
   visiteur voit d'abord, et c'est elle que Google mesure comme plus grand
   element affiche. La video ne se charge qu'ensuite, et jamais sur mobile
   ni en economie de donnees : voir assets/js/hero-video.js.
──────────────────────────────────────────────────────────── */
.ml-home-hero {
  /* L'ombre des tracés blancs du haut de page : la mascotte et la flèche de
     défilement. Elle est déclarée ici, une seule fois, parce que les deux la
     lisent et qu'un dessin au trait posé sur une photo nue a exactement le
     même besoin, qu'il soit un personnage ou un chevron.

     Quatre couches, dont trois serrées. Ce n'est pas une coquetterie : un
     tracé fin ne retient qu'une ombre serrée. Un flou large étale l'encre du
     trait sur trop de surface et ne laisse, au contact, presque rien. Mesuré :
     avec un flou de 3 puis de 24, le halo au contact du personnage plafonnait
     à 1,9:1 sur une photo blanche, il passe ici à 3,5:1 au pire. */
  /* Une seule nappe, large et douce. Les couches serrees cernaient le trace au
     lieu de l'asseoir, comme elles cernaient le texte.

     Les alphas ont monté (0,45/0,35 puis 0,55/0,42) quand le voile d'ambiance
     est passé de 0,40 à 0,30 : les tracés blancs perdaient une part de leur
     assise avec le fond, l'ombre la leur rend. */
  --ml-hero-ombre-trait:
    drop-shadow(0 3px 9px rgba(6, 14, 26, 0.62))
    drop-shadow(0 6px 22px rgba(6, 14, 26, 0.6))
    drop-shadow(0 2px 46px rgba(6, 14, 26, 0.5));

  position: relative;
  /* Plein écran d'ouverture de chapitre : remontée à 100svh (demande de
     Marino). C'est un plancher, pas une hauteur fixe : depuis la maquette
     « page de chapitre » (juillet 2026), le contenu centré (titre, paragraphe,
     actions, puis le globe et sa carte) peut dépasser cette hauteur sans que
     rien ne soit coupé, la section grandit avec lui. svh, pas vh : la barre
     d'adresse mobile ne coupe pas le bas, le plein écran reste plein. */
  min-block-size: 100svh;
  display: grid;
  place-items: center;
  /* Le rognage de la vidéo et l'isolement de ses couches en z négatif ne
     vivent plus ici : ils sont descendus sur .ml-hero-media seul (plus bas
     dans ce fichier), pour que la mascotte puisse déborder du bas du hero
     sans que la vidéo, elle, ne déborde jamais. Un « overflow: hidden » posé
     ici l'aurait tranchée aux chevilles. */
  /* La barre de menu flotte par dessus le haut de page : sans cette réserve
     en haut, le titre passerait dessous sur un écran court. En bas, la flèche
     de défilement réclame sa place. */
  padding-block: calc(var(--ml-header-h) + var(--ml-space-xl) + var(--ml-space-md)) var(--ml-space-xl);
  background-color: var(--ml-primary-dark);
}

/* Personne connectée : WordPress descend toute la page de la hauteur de sa
   barre d'administration. Un haut de page de 100svh dépasserait alors le bas
   de la fenêtre d'autant, et la flèche de défilement passerait dessous. On lui
   rend cette hauteur. Les valeurs suivent les trois régimes de la barre, décrits
   plus haut avec .ml-header. Sous 600px, la barre défile avec la page : elle ne
   mange plus rien, le haut de page reprend ses 100svh. */
body.admin-bar .ml-home-hero {
  min-block-size: calc(100svh - 32px);
}
@media (max-width: 782px) {
  body.admin-bar .ml-home-hero {
    min-block-size: calc(100svh - 46px);
  }
}
@media (max-width: 600px) {
  body.admin-bar .ml-home-hero {
    min-block-size: 100svh;
  }
}

/* Écran court : le haut de page fait 100svh, et tout doit y tenir, titre,
   texte, mascotte et sa bulle. On rend au contenu ce que les marges lui
   prenaient. */
@media (max-height: 760px) {
  .ml-home-hero {
    padding-block: calc(var(--ml-header-h) + var(--ml-space-sm)) var(--ml-space-lg);
  }
}

.ml-hero-media img,
.ml-hero-media video {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  z-index: -2;
}

/* Zoom lent (Ken Burns) sur l'affiche, en boucle douce et continue : même
   principe que .ml-pays-hero-img (assets/css/fiche-pays.css), mais en
   respiration infinie plutôt qu'un zoom d'arrivée unique, le haut de page de
   l'accueil restant affiché plus longtemps qu'une fiche destination avant
   qu'on ne défile. .ml-hero-media, plus bas, rogne déjà le débordement du
   grossissement (overflow: hidden) : le cadrage ne bouge donc jamais, seule
   l'image grossit très légèrement dans sa fenêtre. */
@keyframes ml-home-hero-zoom {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}
.ml-hero-media .ml-hero-affiche {
  animation: ml-home-hero-zoom 20s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ml-hero-media .ml-hero-affiche {
    animation: none;
  }
}

/* ── Régression corrigée : la vidéo de fond avait disparu ──────────────────
   Le rognage de la vidéo, et l'isolement de ses couches en z négatif,
   vivaient sur .ml-home-hero. Ils sont descendus ici (demande de Marino :
   Linguine doit pouvoir déborder du bas du hero pour se tenir sur la section
   des chiffres, un rognage posé sur le hero entier l'aurait tranchée aux
   chevilles). .ml-hero-media a exactement les mêmes dimensions que le hero
   (inset: 0 sur un parent en position: relative) : le cadrage de l'affiche et
   de la vidéo ne change pas d'un pixel.

   Ce déplacement a cassé la vidéo, sans qu'aucun pixel de règle ne semble en
   cause : .ml-home-hero, qui portait l'isolement avant, ne forme plus lui-
   même de contexte d'empilement (il n'a qu'un position: relative, jamais de
   z-index). Or .ml-hero-media héritait un z-index négatif du sélecteur
   groupé ci-dessus, POSÉ SUR LUI-MÊME, pas seulement sur ses enfants. Un
   élément en z négatif dépourvu d'ancêtre qui l'isole remonte chercher le
   premier contexte d'empilement disponible, potentiellement bien plus haut
   que .ml-home-hero dans l'arbre : il se peignait alors dans la couche
   « z négatif » de CET ancêtre lointain, une couche peinte AVANT celle où
   .ml-home-hero peint son propre fond opaque (--ml-primary-dark) : position:
   relative sans z-index fait de lui un élément positionné de niveau 0,
   peint plus tard, donc par-dessus. Le fond de la section masquait la vidéo,
   invisible dessous.

   La correction sépare les deux rôles. .ml-hero-media LUI-MÊME ne porte plus
   de z-index négatif : en z-index: 0 ci-dessous (un élément positionné de
   niveau 0, comme .ml-home-hero-inner qui le suit dans le document), il se
   peint dans l'ordre du document, donc au-dessus du fond de .ml-home-hero et
   en dessous de .ml-home-hero-inner (le texte, la mascotte, la carte), qui
   vient après lui dans le HTML. « isolation: isolate » n'est alors plus une
   ceinture de sécurité mais une nécessité : c'est elle seule qui rouvre,
   pour .ml-hero-media, un contexte d'empilement propre, pour que l'image
   (z: -2), la vidéo ou l'iframe (z: -3 puis -2, voir plus bas) et le dégradé
   du ::after (z: -1) restent ordonnés ENTRE EUX sans dépendre d'un z-index
   négatif qui, lui, s'échapperait vers le premier ancêtre isolé trouvé plus
   haut dans l'arbre.

   Le débordement de la mascotte au bas du hero (voir
   .ml-am-mascotte-hero .ml-am-mascotte-image dans assets/css/assistant-
   modale.css) reste intact : rien ici ne pose de overflow: hidden sur
   .ml-home-hero, seul .ml-hero-media rogne ses propres couches internes. */
.ml-hero-media {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  z-index: 0;
  overflow: hidden;
  isolation: isolate;
}

/* Un voile TRÈS léger et NEUTRE (jamais violet) : la photo Great Ocean Road est
   claire, et le texte blanc y accroche mal malgré son ombre. Ce voile n'assombrit
   vraiment que le centre, derrière le titre, le texte et le globe, et s'éteint aux
   bords pour laisser la photo respirer. Neutre, donc pas un retour de l'ancien
   voile violet. Posé au-dessus de l'image (z-index 1 dans le contexte isolé de
   .ml-hero-media), sous le texte (.ml-home-hero-inner, plus loin dans le DOM).
   Demande de Marino, juillet 2026. */
.ml-hero-media::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: radial-gradient(120% 95% at 50% 44%,
    rgba(8, 10, 18, 0.42),
    rgba(8, 10, 18, 0.22) 52%,
    rgba(8, 10, 18, 0.07) 100%);
  pointer-events: none;
}

/* Le fond animé se glisse SOUS l'image, jamais au dessus. L'image ne
   s'efface qu'une fois la vidéo réellement en train de jouer : on part donc
   de l'image, et le lecteur ne montre jamais son fond noir. */
.ml-hero-media video,
.ml-hero-media iframe {
  opacity: 0;
  z-index: -3;
}
.ml-hero-media video.is-playing,
.ml-hero-media iframe.is-playing {
  opacity: 1;
}

/* L'image s'efface, doucement, une fois la vidéo prête. Aucune transition sur
   la vidéo elle même : elle est déjà là, dessous, et l'image se retire. */
.ml-hero-media img {
  transition: opacity 600ms ease;
}
.ml-hero-media.is-video-prete img {
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .ml-hero-media img { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ml-hero-media video,
  .ml-hero-media iframe { display: none; }
}

/* Un cadre Vimeo garde son format 16/9 : « object-fit » n'a aucun effet sur
   lui. Pour couvrir le haut de page sans deformer l'image, on l'agrandit
   jusqu'à ce que sa plus petite dimension dépasse celle de la section, puis
   on le centre. Le débordement est rogné par .ml-hero-media (plus haut dans
   ce fichier), pas par la section elle même : elle, désormais, laisse
   déborder Linguine.

   Les hauteurs sont en « svh », jamais en « vh ». Le haut de page fait 88svh :
   « vh » compte la barre d'adresse du telephone, « svh » ne la compte pas, et
   les deux unites ne valent donc pas la meme chose sur mobile. Un cadre
   dimensionne en « vh » etait plus haut que la section qui le contient : la
   video ne tombait pas au meme endroit que l'affiche, et l'on voyait l'image
   sauter au moment ou la video prenait le relais. C'est ce decalage.

   Le calage se fait sur le conteneur (100 %), et non sur la fenetre, pour les
   memes raisons : la section est la reference, pas l'ecran. */
.ml-hero-media iframe {
  /* Au même niveau que l'affiche (z: -2), jamais au dessus : sans ce
     z-index, un cadre Vimeo se peint par dessus tout le reste de la
     couche, y compris le texte. */
  z-index: -2;
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  inline-size: 100%;
  block-size: 56.25vw;       /* 100 / (16/9) */
  min-inline-size: 156.44svh; /* 88 * (16/9), sur la hauteur reelle du haut de
                                  page (88svh, voir .ml-home-hero) */
  min-block-size: 100%;
  border: 0;
  pointer-events: none;
}

/* Le fond du haut de page : la photo, nue.

   Maquette « page de chapitre » (juillet 2026, demande de Marine et de
   Marino) : plus AUCUN voile plein écran sur la photo, ambiance ou lit de
   texte confondus. Le contenu (titre, paragraphe, actions) est désormais
   centré, sur une seule colonne. Une vignette radiale localisée avait un
   temps porté la lisibilité du texte juste derrière lui : retirée à son tour
   (demande de Marino, elle marquait trop la photo). La photo reste donc
   entièrement nue, sans aucune couche posée dessus : la lisibilité du texte
   blanc tient désormais uniquement à son ombre portée, douce et diffuse
   (voir .ml-home-hero-texte h1, p plus bas), jamais à un fond. Ce n'est plus
   une garantie de contraste par le calcul : sur une photo très claire et peu
   contrastée, une vérification à l'oeil reste nécessaire avant mise en
   ligne. */

.ml-container.ml-home-hero-inner {
  position: relative;
  display: grid;
  /* Une seule colonne centrée : le texte, puis en dessous la mascotte et sa
     carte. Les deux anciennes colonnes (texte à gauche, Linguine à droite,
     dès 900px) ont disparu avec le voile qui les accompagnait : le premier
     écran doit se lire comme une image de chapitre, texte au centre. */
  justify-items: center;
  gap: var(--ml-space-lg);
  inline-size: 100%;
  /* Le haut de page est le seul endroit du site où le contenu dépasse le
     conteneur commun (--ml-container-w, 1200px) : le titre et la mascotte
     ont besoin de respirer à l'échelle d'un plein écran. La barre de menu
     lit le même token pour s'aligner dessus. */
  /* La double classe donne à cette règle une spécificité supérieure à celle
     de .ml-container : elle ne dépend donc jamais de l'ordre des
     déclarations dans ce fichier. */
  max-inline-size: var(--ml-hero-w);
  margin-inline: auto;
  padding-inline: var(--ml-space-sm);
}
/* ── Le texte du haut de page, sur la photo nue ───────────────────────────
   Maquette « page de chapitre » (juillet 2026) : le texte n'est plus posé sur
   un côté de l'image assombri en pleine hauteur, il est centré. Une vignette
   radiale a un temps porté sa lisibilité (voir l'historique de ce fichier) :
   retirée à la demande de Marino, elle repeignait trop la photo derrière le
   texte. Plus aucun fond posé derrière le bloc de texte : sa lisibilité tient
   uniquement à l'ombre portée du titre et du paragraphe, ci-dessous, douce et
   diffuse plutôt que cernante. Ce n'est plus une garantie de contraste par le
   calcul : sur une photo très claire et peu contrastée, une vérification à
   l'oeil reste nécessaire avant mise en ligne. */
.ml-home-hero-texte {
  position: relative;
  max-inline-size: 60rem;
  text-align: center;
  /* Une nappe fine, au contact du trait, et une nappe large et douce en
     arrière-plan : l'ombre pose le texte sur la photo, elle ne le cerne pas
     d'un trait dur. */
  --ml-hero-ombre-texte:
    0 1px 2px rgba(0, 0, 0, 0.62),
    0 2px 6px rgba(0, 0, 0, 0.58),
    0 4px 16px rgba(0, 0, 0, 0.55),
    0 8px 44px rgba(0, 0, 0, 0.5);
}
.ml-home-hero-texte h1,
.ml-home-hero-texte p {
  color: #fff;
  text-shadow: var(--ml-hero-ombre-texte);
}
.ml-home-hero-texte h1 {
  /* Le titre du haut de page est le premier mot du site : il doit peser, sans
     écraser le reste de la colonne (paragraphe, actions, puis le globe et sa
     carte). Soixante pixels sur grand écran, ronds et gras : Baloo 2 monte
     jusqu'à 800, et c'est cette graisse là qui lui donne son air de lettrage à
     la main plutôt que de titre de journal.

     Sa mesure est bornée : au delà de quinze mots par ligne, un titre de cette
     taille cesse de se lire d'un seul regard. */
  font-size: clamp(2.5rem, 5.6vw, 4.4rem); /* jusqu'à ~70px, titre agrandi (demande de Marino, juillet 2026) */
  font-weight: 800;
  line-height: 1.06;
  margin-bottom: var(--ml-space-md);
  text-wrap: balance;
}

/* Le mot mis en couleur dans le titre. C'est un <mark> venu de l'éditeur (voir
   ml_home_pattern_content()) : il porte donc un fond jaune par défaut, qu'il
   faut éteindre, et une teinte que la cliente ne choisit pas.

   La couleur est --ml-accent-clair, et elle ne peut pas être le violet de la
   marque : sur la vignette du haut de page, #661f80 tombe bien sous 4,5:1. Le
   raisonnement complet est au-dessus du token, dans le :root. */
.ml-home-hero-texte h1 .ml-mot-accent,
.ml-home-hero-texte h1 mark {
  /* Tout surlignage dans le titre du haut de page devient le mot d'accent :
     la cliente peut donc le poser elle-même avec l'outil « Surligner » de
     l'éditeur, sans dépendre de la classe du pattern. Les !important sont
     nécessaires et assumés : l'éditeur écrit sa couleur de surlignage en
     style en ligne, qui gagnerait sinon sur toute règle de la feuille. */
  background: none !important;
  color: var(--ml-accent-clair) !important;
}

.ml-home-hero-texte p {
  /* Slogan agrandi (demande de la cliente, septembre 2026). */
  font-size: clamp(1.1rem, 1.9vw, 1.35rem);
  /* Blanc à 92 pour cent sur son ombre : voir --ml-hero-ombre-texte,
     au-dessus de .ml-home-hero-texte. C'est ce texte, et non le titre, qui
     porte le corps de l'ombre : ses traits sont fins, et une ombre y accroche
     beaucoup moins. Un gris franc, ici, tomberait sous la barre des 4,5:1. */
  color: rgba(255, 255, 255, 0.92);
}

/* ── Le badge, en tête de la section des chiffres ─────────────────────────
   Il n'est plus posé sur la photo (maquette « page de chapitre », juillet
   2026) : le même bloc éditable (voir template-home.php, qui le capture et le
   réaffiche ici) ouvre désormais la section blanche des chiffres qui suit le
   hero, comme un label de section. C'est la même section, une seule, jamais
   deux : le badge n'a plus sa propre section, il vit à l'intérieur de
   .ml-home-chiffres, avant .ml-chiffres-grille. Sur ce fond clair, sa matière
   change entièrement : fini le verre dépoli sombre, place à un fond violet
   très dilué et un trait fin, la même famille que .ml-eyebrow.

   Couleur du texte : --ml-accent-dark, jamais --ml-accent (le violet vif de
   la marque, réservé aux fonds et aux gros aplats, est trop soutenu comme
   couleur de texte à ce corps réduit sur un fond clair). Calculé :
   11,3:1 sur --ml-accent-soft. Très au-dessus des 4,5:1 exigés. */
.ml-chiffres-badge {
  text-align: left;
  margin-block-end: var(--ml-space-md);
}
.ml-chiffres-badge .ml-hero-badge {
  display: inline-block;
  margin: 0;
  padding: 0.45em 1.1em;
  background: var(--ml-accent-soft);
  border: 1px solid var(--ml-accent-bordure);
  border-radius: var(--ml-radius-pill);
  color: var(--ml-accent-dark);
  font-family: var(--ml-font-heading);
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ── Les deux actions ─────────────────────────────────────────────────────
   Deux blocs bouton de l'éditeur : la cliente en écrit le libellé et l'adresse,
   jamais l'apparence. Le premier est plein, le second est un lien souligné : deux
   boutons pleins côte à côte se disputeraient le regard, et il n'y a qu'une
   action principale.

   Le violet plein sous du texte blanc, c'est le sens d'usage de la marque
   (10,1:1). L'inverse, du violet écrit sur un fond clair, ne tiendrait pas ici. */
/* SUR TÉLÉPHONE, LE SECOND LIEN DU HAUT DE PAGE DISPARAIT (demande de Marino).
   « Quelles destinations ? » ne fait que descendre plus bas dans la même page,
   et le haut de page mobile est déjà chargé : titre, phrase, deux actions, le
   globe et la carte de Linguine. En retirer un rend de l'air au dessus du titre,
   qui est ce que le visiteur doit lire en premier.

   Masqué en CSS et non retiré du pattern : sur un écran large il garde tout son
   sens, et c'est un bloc que la cliente peut modifier dans l'éditeur. Rien n'est
   perdu, c'est une ancre vers un autre endroit de la MÊME page. */
@media (max-width: 899.98px) {
  /* LES DEUX ACTIONS DISPARAISSENT DU HAUT DE PAGE SUR TÉLÉPHONE (demande de
     Marino, en deux fois).

     « Quelles destinations ? » ne faisait que descendre plus bas dans la même
     page. « Commencer », lui, ouvre l'assistant, et la carte de Linguine juste
     dessous invite déjà à le faire, en toutes lettres et sur une bien plus
     grande surface : deux portes vers la même fenêtre, l'une sous l'autre.

     Ce qu'on y gagne est ce que Marino cherchait depuis le début : de la place,
     donc un titre qui descend vers le milieu de l'écran.

     Masquées en CSS et non retirées du pattern : sur un écran large elles
     gardent tout leur sens, et ce sont des blocs que la cliente peut modifier
     dans l'éditeur. Rien n'est perdu pour les moteurs non plus, l'une est une
     ancre vers la même page, l'autre mène au contact sans JavaScript.

     LE BLOC QUI LES ENVELOPPE DISPARAÎT AVEC ELLES (le titre restait trop
     proche du menu malgré le premier resserrement, demande de Marino).
     Masquer les deux boutons ne masquait pas le bloc wp:buttons qui les
     contient (.ml-hero-actions) : vide de tout contenu visible, il ne pesait
     plus rien en hauteur, mais gardait sa marge du dessus, qui se fondait
     avec celle du paragraphe précédent au lieu de s'effacer avec elle (le
     navigateur ne retient que la plus grande des deux marges adjacentes). Ce
     reliquat poussait le titre vers le haut. Le poser en display: none avec
     ses deux boutons efface la marge en même temps qu'eux, à toutes les
     largeurs où ils sont masqués, pas seulement sur téléphone. */
  .ml-home-hero-texte .ml-hero-action-secondaire,
  .ml-home-hero-texte .ml-hero-action-principale,
  .ml-home-hero-texte .ml-hero-actions {
    display: none;
  }
}

.ml-home-hero-texte .ml-hero-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--ml-space-sm) var(--ml-space-md);
  margin-block-start: var(--ml-space-lg);
}
.ml-home-hero-texte .ml-hero-actions .wp-block-button__link {
  display: inline-block;
  font-family: var(--ml-font-heading);
  font-weight: 700;
  font-size: var(--ml-fs-base);
  line-height: 1.3;
  border: 0;
  transition: background-color 200ms var(--ml-ease),
              box-shadow 200ms var(--ml-ease),
              transform 200ms var(--ml-ease);
}
.ml-home-hero-texte .ml-hero-action-principale .wp-block-button__link {
  background: var(--ml-accent);
  color: #fff;
  font-size: 1.125rem;
  padding: 0.85em 1.7em;
  border-radius: var(--ml-radius-pill);
  /* L'ombre violette de la maquette : elle fait flotter le bouton sur la
     vidéo, là où une ombre neutre le faisait juste dépasser. */
  box-shadow: 0 12px 26px rgba(102, 31, 128, 0.42);
}
.ml-home-hero-texte .ml-hero-action-principale .wp-block-button__link:hover {
  /* La maquette ÉCLAIRCIT le bouton au survol (elle vit sur un fond sombre),
     elle ne l'assombrit pas. La teinte est un mélange des deux violets de la
     palette ; le repli, pour un navigateur sans color-mix(), reste l'accent
     vif, un cran plus clair lui aussi. */
  background: var(--ml-accent-vif);
  background: color-mix(in srgb, var(--ml-accent) 70%, var(--ml-accent-vif) 30%);
  transform: translateY(-1px);
}
.ml-home-hero-texte .ml-hero-action-principale .wp-block-button__link:active {
  transform: translateY(0);
}
.ml-home-hero-texte .ml-hero-action-secondaire .wp-block-button__link {
  background: none;
  color: #fff;
  padding: 0.85em 0;
  border-radius: 0;
  text-decoration: underline;
  text-underline-offset: 0.28em;
  text-decoration-thickness: 2px;
  /* Le soulignement à demi éteint de la maquette : il s'allume au survol. */
  text-decoration-color: rgba(255, 255, 255, 0.5);
  text-shadow: var(--ml-hero-ombre-texte);
}
.ml-home-hero-texte .ml-hero-action-secondaire .wp-block-button__link:hover {
  text-decoration-thickness: 3px;
  text-decoration-color: #fff;
}
.ml-home-hero-texte .ml-hero-actions .wp-block-button__link:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .ml-home-hero-texte .ml-hero-actions .wp-block-button__link {
    transition: none;
  }
  .ml-home-hero-texte .ml-hero-action-principale .wp-block-button__link:hover,
  .ml-home-hero-texte .ml-hero-action-principale .wp-block-button__link:active {
    transform: none;
  }
}

/* SUR TÉLÉPHONE, LE TITRE REMONTE VERS LE MILIEU DE L'ÉCRAN (demande de
   Marino) : « le titre h1 est beaucoup trop proche du menu, réduis les
   espaces inutiles pour idéalement que le titre soit pratiquement au centre
   verticalement de l'écran ».

   .ml-home-hero centre déjà son contenu (place-items: center), mais cette
   mise en page ne peut centrer que s'il reste de l'espace libre une fois le
   contenu posé. Sur un petit écran, deux réserves cumulées mangeaient cet
   espace avant même que le centrage n'ait une chance d'agir :

   1. le remplissage du haut ajoutait, PAR DESSUS la hauteur de la barre de
      menu flottante, deux espacements pensés pour respirer sur grand écran
      (--ml-space-xl puis --ml-space-md, 72px + 24px) : une réserve généreuse
      qui n'a pas sa place sur un écran déjà court ;
   2. l'écart entre le bloc de texte et le globe (.ml-container.ml-home-hero-inner,
      --ml-space-lg, 40px).

   Ni l'une ni l'autre ne sert le lecteur sur téléphone : les resserrer libère
   de la hauteur, que le centrage automatique répartit ensuite lui même au
   dessus et en dessous du titre. La hauteur du haut de page elle même
   (min-block-size sur .ml-home-hero) n'est pas touchée : ce n'est pas elle
   qui posait problème, c'est ce qui se disputait la place à l'intérieur.

   Une troisième réserve vivait ici, la marge au dessus des deux actions :
   déjà resserrée une fois (de --ml-space-lg à --ml-space-md), jamais
   supprimée. Les deux boutons étaient pourtant déjà masqués sur téléphone,
   mais pas le bloc qui les enveloppe, qui gardait donc cette marge pour rien
   (voir le bloc à 899.98px plus haut, où le bloc entier passe maintenant en
   display: none avec eux). Il n'y a donc plus rien à resserrer ici pour
   cette réserve, la ligne a disparu de ce bloc. */
@media (max-width: 599px) {
  .ml-home-hero {
    padding-block: calc(var(--ml-header-h) + var(--ml-space-sm)) var(--ml-space-md);
  }
  /* L'ÉCART ENTRE LE TEXTE ET LINGUINE, ENCORE RESSERRÉ (demande de Marino,
     qui l'a nommé précisément : « réduis l'espace entre Une agence suisse, une
     seule interlocutrice, aucun frais d'agence, et Linguine »).

     Il valait quarante pixels, puis vingt-quatre, il en vaut huit. C'est peu, et
     c'est voulu : le globe porte déjà son propre halo, qui fait de l'air autour
     de lui sans rien coûter en hauteur. L'espace gagné va au titre, qui descend
     d'autant vers le milieu de l'écran.

     Sur un écran plus large, l'écart reste celui d'origine : la place n'y manque
     pas, et un haut de page trop serré y paraîtrait à l'étroit. */
  .ml-container.ml-home-hero-inner {
    gap: var(--ml-space-xs);
  }
}

/* L'ancienne disposition asymétrique (texte à gauche, Linguine et sa carte à
   droite, à partir de 900px) a disparu avec la maquette « page de chapitre » :
   une seule colonne centrée, à toutes les largeurs (voir .ml-home-hero-inner
   et .ml-home-hero-texte plus haut). Rien à réoccuper ici : c'est
   .ml-am-mascotte-hero (assets/css/assistant-modale.css) qui règle
   l'agencement interne du globe et de sa carte. */

/* Fleche de defilement, purement indicative. Elle se tient au dessus de la
   carte des chiffres, qui remonte sur le bas du haut de page : plus bas, la
   carte lui passerait dessus.

   Elle est blanche sur une photo qui ne l'est plus : elle porte donc l'ombre
   commune aux tracés blancs (--ml-hero-ombre-trait), plus une passe serrée à
   elle. C'est le trait le plus fin de la page, deux pixels : une ombre y
   retient moins d'encre que sur le personnage, qui en fait cinq.

   Son opacité de 0,8 est retirée, et ce n'est pas un caprice : l'opacité
   s'applique à l'élément ET à son ombre. Elle rabotait d'un cinquième la seule
   chose qui rendait la flèche visible. Sa discrétion tient à sa taille, pas à
   sa transparence.

   Mesurée au pixel sur photo entièrement blanche, entre son blanc et le halo
   qui la borde : voir le tableau des contrastes du haut de page. Sans ombre,
   sur ce fond, elle était à 1,3:1, autant dire absente. */
.ml-hero-scroll {
  position: absolute;
  inset-block-end: var(--ml-space-xl);
  inset-inline: 0;
  margin-inline: auto;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  display: grid;
  place-items: center;
  color: #fff;
  filter: var(--ml-hero-ombre-trait);
  animation: ml-hero-bob 1.8s ease-in-out infinite;
}
/* Le rythme et l'amplitude de la maquette « Hero Mascotte » : la flèche
   respire aussi en opacité, pas seulement en position. */
@keyframes ml-hero-bob {
  0%, 100% { translate: 0 0; opacity: 0.55; }
  50%      { translate: 0 9px; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ml-hero-scroll { animation: none; }
}

/* ── Les deux bulles de salutation du haut de page ─────────────────────────
   « Hi » et « Ciao », voulues par Marino sur la maquette validée : deux PNG à
   la craie posés dans le ciel de la vidéo, purement décoratifs (leur conteneur
   est aria-hidden dans template-home.php). Deux, jamais plus : la parcimonie
   reste la règle. La consigne « Hola et Hallo seulement » ne s'applique pas
   ici : elle ne vaut que pour les bulles qui désignent le personnage, et
   celles-ci ne pointent vers rien, elles flottent.

   Elles montent et descendent chacune sur sa propre durée : deux durées
   étrangères l'une à l'autre, pour que le mouvement ne batte jamais la mesure.
   L'ombre portée les détache du fond, même sur une vidéo en pleine lumière
   (demande explicite). « prefers-reduced-motion » les immobilise sans les
   cacher.

   Sous 900px, elles disparaissent : en colonne unique le haut de l'écran
   appartient au lit du texte et au titre, deux bulles par-dessus feraient la
   nuée que la charte interdit. */
/* Les bulles flottantes « Hi » et « Ciao » du haut de page vivaient ici.
   Retirées à la demande de Marino : le ciel de la vidéo reste nu, seule la
   carte de Linguine parle. Leur balisage est parti de template-home.php et
   leur accroche de assets/js/assistant-modale.js. Les bulles des cartes de
   langue (.ml-bubble, plus bas dans la page) ne sont pas concernées. */

/* Les anciennes règles .ml-mascotte et .ml-mascotte-bulle vivaient ici. Plus
   aucun gabarit ni script ne produit ce balisage depuis que la mascotte est
   passée dans inc/assistant-modale.php : elles sont retirées plutôt que
   gardées « au cas où ». Son style vit désormais dans
   assets/css/assistant-modale.css. */

/* ── La mascotte du haut de page, elle aussi, porte son ombre ──────────
   Le personnage est un tracé blanc (currentColor, forcé en blanc sur le haut
   de page par assets/css/assistant-modale.css). Tant que la photo passait sous
   un voile, il se détachait tout seul. La photo est nue désormais : sur un ciel
   clair, un mur blanc, une plage, il s'effaçait purement et simplement.

   L'ombre est posée ici, et non dans le fichier de la mascotte : elle n'existe
   que sur le haut de page de l'accueil, sur le fond photographique. La mascotte
   fixe des autres pages vit sur du blanc, en violet, et n'en a aucun besoin.
   Le filtre s'applique au SVG lui même, jamais au conteneur : sa bulle de
   question, qui est son voisine dans la même boîte, a un fond blanc opaque et
   n'a rien à faire d'une ombre portée.

   Elle lit --ml-hero-ombre-trait, l'ombre commune aux tracés blancs du haut de
   page (déclarée sur .ml-home-hero, avec le pourquoi de ses quatre couches).
   Pire cas mesuré au pixel sur photo entièrement blanche, entre le blanc du
   personnage et le halo qui le borde : 3,51:1 en 390 de large, 4,39:1 en 1920.
   Au dessus des 3:1 attendus d'un élément graphique. Sans ombre, sur ce même
   fond, il était à 1,00:1, c'est à dire invisible. ── */
.ml-home-hero .ml-am-mascotte-image svg {
  filter: var(--ml-hero-ombre-trait);
}

/* ── L'assistant de recherche ── */
.ml-assistant {
  background: var(--ml-bg);
  border: 1px solid var(--ml-border);
  border-radius: var(--ml-radius-lg);
  box-shadow: var(--ml-shadow-md);
  padding: var(--ml-space-lg);
}
.ml-assistant-champ {
  margin-bottom: var(--ml-space-sm);
}
.ml-assistant-champ label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.25em;
}
.ml-assistant-champ select {
  width: 100%;
  padding: 0.7em 0.85em;
  border: 1px solid var(--ml-border-strong);
  border-radius: var(--ml-radius);
  font: inherit;
  background: var(--ml-bg);
}
.ml-assistant button[type="submit"] {
  width: 100%;
  margin-top: var(--ml-space-xs);
}

.ml-assistant-resultats:not(:empty) {
  margin-top: var(--ml-space-md);
  padding-top: var(--ml-space-md);
  border-top: 1px solid var(--ml-border);
}
.ml-assistant-resultats h2 {
  font-size: var(--ml-fs-h3);
}
.ml-assistant-resultats h3 {
  font-size: var(--ml-fs-base);
  color: var(--ml-text-mid);
  margin-bottom: var(--ml-space-xs);
}
.ml-assistant-villes li {
  display: inline;
}
.ml-assistant-villes li:not(:last-child)::after {
  content: ', ';
}
.ml-assistant-formules a {
  color: var(--ml-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Apparition des résultats, uniquement quand le script anime. */
.ml-assistant-resultats--apparait {
  animation: ml-fade-up 400ms ease-out;
}
@keyframes ml-fade-up {
  from { opacity: 0; translate: 0 8px; }
  to   { opacity: 1; translate: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ml-assistant-resultats--apparait { animation: none; }
}

/* ── Les chiffres : section blanche unique avec le badge du hero ─────────
   Retour de Marino en direct sur l'accueil (juillet 2026) : il ne doit y
   avoir qu'une seule section entre le hero et les quatre langues, jamais
   deux, et elle doit rester blanche. L'ancien label du badge et la bande
   violette des chiffres, deux sections distinctes, n'en font donc plus
   qu'une : .ml-home-chiffres, entièrement --ml-bg. Elle ouvre avec le badge
   du pattern (« Séjours linguistiques sur mesure », capturé en tête du hero
   dans template-home.php et rendu ici, dans .ml-chiffres-badge, jamais
   dupliqué), puis garde sa composition à deux colonnes sur grand écran (le
   texte d'introduction tient sa colonne, les trois nombres la leur), chaque
   nombre porté par une jauge qui se remplit pendant qu'il monte.

   Ce qui disparaît avec le violet : le dégradé de fond, le chevauchement en
   marge négative qui calait la bande sous l'ancien voile du haut de page
   (parti lui aussi), les coins arrondis du haut et le z-index qui la
   faisait passer devant la vidéo. La section suit simplement le hero dans
   le fil du document, comme toutes les autres.

   Les couleurs internes reprennent les tokens sombres du reste du site,
   pensés pour du blanc plutôt que pour un aplat violet : les nombres en
   --ml-accent (10,1:1 sur blanc, la même mesure que le surtitre .ml-eyebrow),
   l'accroche et les libellés en --ml-text-mid (le gris violacé du corps de
   texte, très au dessus des 4,5:1 exigés), et la jauge en --ml-accent sur un
   rail --ml-accent-soft, décorative, aria-hidden. ── */
.ml-home-chiffres {
  background: var(--ml-bg);
  padding-block: var(--ml-space-lg);
}
@media (min-width: 900px) {
  .ml-home-chiffres {
    padding-block: calc(var(--ml-space-lg) * 1.2);
  }
}

.ml-chiffres-grille {
  display: grid;
  gap: var(--ml-space-md);
  align-items: center;
}
@media (min-width: 900px) {
  .ml-chiffres-grille {
    /* Le texte tient la colonne large, les nombres la colonne serrée : trois
       nombres courts n'ont pas besoin de la moitié de la page. C'est aussi ce
       qui les ramène vers le centre optique, au lieu de les envoyer se coller
       au bord droit. */
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: var(--ml-space-lg);
  }
}
@media (min-width: 1100px) {
  .ml-chiffres-grille {
    gap: var(--ml-space-xl);
  }
}

/* Le texte d'introduction, saisi dans l'éditeur. Il n'est plus un chapeau
   centré au dessus d'une frise, il est la colonne de gauche : il parle, les
   nombres répondent. Sur ce fond blanc, un surtitre, un titre ou un lien
   éventuels n'ont plus besoin d'aucune couleur forcée : ils héritent des
   règles par défaut de la page (.ml-eyebrow en --ml-accent, h2/h3 en
   --ml-primary, focus en --ml-accent), les mêmes que partout ailleurs sur
   fond clair. */
.ml-chiffres-intro > :first-child { margin-block-start: 0; }
.ml-chiffres-intro > :last-child  { margin-block-end: 0; }
.ml-chiffres-intro p {
  margin-block: 0 var(--ml-space-xs);
  max-inline-size: 42ch;
  font-size: var(--ml-fs-lg);
  color: var(--ml-text-mid);
}
.ml-chiffres-intro p:last-child { margin-block-end: 0; }

/* Les trois nombres. Sur mobile ils tiennent sur une seule ligne, en trois
   colonnes égales : ce sont trois nombres courts, ils n'ont pas besoin de
   s'empiler. */
.ml-chiffres-liste {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ml-space-sm);
}
@media (min-width: 900px) {
  .ml-chiffres-liste {
    gap: var(--ml-space-md);
  }
}

.ml-chiffre {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* La valeur de remplissage de la jauge, de 0 à 1. Elle vaut 1 par défaut :
     sans JavaScript, la jauge est pleine et le nombre est écrit. Le script ne
     la ramène à 0 qu'au moment précis où il s'apprête à compter, jamais avant.
     Aucun état masqué n'est posé en CSS statique. */
  --ml-jauge: 1;
}

.ml-chiffre-valeur {
  display: block;
  font-family: var(--ml-font-heading);
  /* Le nombre porte enfin la section : il n'a plus à tenir dans un bandeau.
     Il monte jusqu'à 4,5 rem, l'échelle d'un titre de niveau un. */
  font-size: clamp(2.4rem, 6vw, 4.5rem);
  font-weight: 700;
  line-height: 1;
  /* Le violet de marque, sur blanc : 10,1:1, la même mesure que le
     surtitre .ml-eyebrow. */
  color: var(--ml-accent);
  /* Réserve la place du chiffre final : sans cela, le compteur qui monte de
     0 à 33 élargit sa colonne et décale toute la ligne à chaque dizaine. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* La jauge. Deux couches sur un seul élément : le rail, toujours entier, et le
   remplissage, qui suit le compteur. Elle est décorative, elle ne dit rien que
   le nombre ne dise déjà : le gabarit la marque aria-hidden. */
.ml-chiffre-jauge {
  display: block;
  block-size: 4px;
  inline-size: 100%;
  border-radius: var(--ml-radius-pill);
  /* Le rail : un violet très dilué, il se voit sans peser. */
  background: var(--ml-accent-soft);
  overflow: hidden;
}
.ml-chiffre-jauge::before {
  content: '';
  display: block;
  block-size: 100%;
  inline-size: 100%;
  border-radius: inherit;
  /* Le remplissage reprend le violet des nombres qu'elle accompagne. */
  background: var(--ml-accent);
  /* Le remplissage. transform, jamais width : une largeur animée à chaque
     image recalculerait la mise en page soixante fois par seconde. */
  transform: scaleX(var(--ml-jauge));
  transform-origin: left center;
}

.ml-chiffre-libelle {
  display: block;
  font-size: var(--ml-fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  /* Le gris violacé du corps de texte, sur blanc : largement au dessus des
     4,5:1 exigés. */
  color: var(--ml-text-mid);
}

/* ── Les cartes de langue ─────────────────────────────────────────────
   Chaque carte porte la bulle dessinée à la craie de sa langue. C'est le seul
   endroit du site où ces quatre images ont un sens : une par langue enseignée,
   en contenu de carte. Elles ne flottent pas, elles ne sont pas une nuée
   d'ornements, elles disent la langue.

   Le vocabulaire est celui des cartes de destination et de formule (fond
   teinté, ombre à l'accent, flèche qui avance au survol, carte entièrement
   cliquable), pour que la page cesse d'avoir deux dialectes de carte. ── */
.ml-langues-grid {
  margin-top: var(--ml-space-lg);
}
.ml-langue-card {
  position: relative;
  display: flex;
  flex-direction: column;
  block-size: 100%;
  padding: 0 var(--ml-space-md) var(--ml-space-md);
  background: var(--ml-bg-alt);
  border-radius: var(--ml-radius-lg);
  overflow: hidden;
  box-shadow: 0 8px 28px -14px rgba(102, 31, 128, 0.35);
  transition: box-shadow 320ms var(--ml-ease), scale 320ms var(--ml-ease);
}
.ml-langue-card:hover,
.ml-langue-card:focus-within {
  box-shadow: 0 24px 60px -18px rgba(102, 31, 128, 0.55);
  scale: 1.02;
}
@media (prefers-reduced-motion: reduce) {
  .ml-langue-card { transition: none; }
  .ml-langue-card:hover,
  .ml-langue-card:focus-within { scale: 1; }
}

/* Le bandeau de la bulle : un aplat de marque très dilué, sur lequel la craie
   blanche du dessin se détache. Sur le crème de la carte, elle disparaîtrait. */
.ml-langue-card-bulle {
  display: grid;
  place-items: center;
  /* Le bandeau a sa propre hauteur, la même pour les quatre cartes : les
     quatre bulles n'ont pas le même format, sans cela chaque carte aurait
     commencé à une hauteur différente, et la grille aurait boité. */
  aspect-ratio: 16 / 11;
  margin-inline: calc(-1 * var(--ml-space-md));
  margin-block-end: var(--ml-space-md);
  padding: var(--ml-space-sm);
  background: linear-gradient(160deg, var(--ml-accent) 0%, var(--ml-primary) 100%);
}
.ml-langue-card-bulle .ml-bubble {
  /* La bulle est ici en flux, pas en ornement posé en absolu. Ses dimensions
     sont écrites dans le HTML : la place est réservée avant même que l'image
     n'arrive, donc aucun saut de mise en page. Elle tient dans le bandeau en
     largeur ET en hauteur : la queue de la bulle ne peut jamais être rognée,
     c'est justement pour la préserver que les sources ont une marge de quatre
     pour cent. */
  position: static;
  inline-size: auto;
  max-inline-size: min(100%, 200px);
  max-block-size: 100%;
  rotate: -2deg;
  transition: rotate 320ms var(--ml-ease), scale 320ms var(--ml-ease);
}
.ml-langue-card:hover .ml-bubble,
.ml-langue-card:focus-within .ml-bubble {
  rotate: 0deg;
  scale: 1.05;
}
@media (prefers-reduced-motion: reduce) {
  .ml-langue-card-bulle .ml-bubble { transition: none; }
  .ml-langue-card:hover .ml-bubble,
  .ml-langue-card:focus-within .ml-bubble { rotate: -2deg; scale: 1; }
}

/* Le nombre de villes est toujours sur la dernière ligne de la carte, à hauteur
   de la flèche, quelle que soit la longueur de l'accroche : sans cela les
   quatre cartes n'auraient pas la même ligne de base. */
.ml-langue-card-texte {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
}
.ml-langue-card-accroche {
  flex: 1 1 auto;
  color: var(--ml-text-mid);
}
.ml-langue-card-titre {
  margin-bottom: var(--ml-space-xs);
}
/* Le lien couvre toute la carte par un pseudo élément, comme la carte de
   destination : le vrai <a> reste sur le nom de la langue, pour le texte et le
   focus clavier. Aucun gestionnaire de clic. */
.ml-langue-card-titre a::after {
  content: '';
  position: absolute;
  inset: 0;
}
.ml-langue-card-meta {
  margin: 0;
  /* Le texte laisse la place à la flèche, en bas à droite. */
  padding-inline-end: 3rem;
  font-size: var(--ml-fs-sm);
  /* --ml-text-dim (#8b8391) sur le crème de la carte ne tiendrait que 3,4:1,
     sous la barre des 4,5:1. Le ton moyen (#58515f) y tient 7,1:1. */
  color: var(--ml-text-mid);
}

/* La flèche, en bas à droite. Décorative : le nom de la langue porte déjà le
   lien et son intitulé. Elle n'a jamais à capter le clic. */
.ml-langue-card-fleche {
  position: absolute;
  pointer-events: none;
  inset-inline-end: var(--ml-space-md);
  inset-block-end: var(--ml-space-md);
  display: grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border-radius: 50%;
  background: var(--ml-bg);
  color: var(--ml-primary);
  box-shadow: var(--ml-shadow-sm);
  transition: background-color 260ms var(--ml-ease),
              color 260ms var(--ml-ease),
              translate 260ms var(--ml-ease);
}
.ml-langue-card:hover .ml-langue-card-fleche,
.ml-langue-card:focus-within .ml-langue-card-fleche {
  background: var(--ml-accent);
  color: #fff;
  translate: 3px 0;
}
@media (prefers-reduced-motion: reduce) {
  .ml-langue-card-fleche { transition: none; }
  .ml-langue-card:hover .ml-langue-card-fleche,
  .ml-langue-card:focus-within .ml-langue-card-fleche { translate: none; }
}

/* ─────────────────────────────────────────────────────────────────────────
   CARTE DE FORMULE (section accueil) : inc/section-formules.php

   Meme mecanique que la carte de destination (image en fond, voile, fleche
   circulaire qui pivote au survol, ombre teintee qui s'ouvre, carte
   entierement cliquable), mais plus sobre : pas de pastille de langue, pas
   de sous-titre, pas de drapeau. Format plus paysage : ces cartes portent
   moins d'information qu'une carte de ville.
───────────────────────────────────────────────────────────────────────── */

.ml-grille-formules {
  display: grid;
  gap: var(--ml-space-md);
  grid-template-columns: 1fr;
  margin-top: var(--ml-space-lg);
}
@media (min-width: 560px) {
  .ml-grille-formules {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1000px) {
  /* Trois au plus : au dela, les cartes s'etirent en bandeaux trop maigres et
     l'image de fond perd sa presence. La fenetre « moins de 18 ans » garde sa
     contrainte a deux colonnes (selecteur plus specifique dans
     popup-formules.css, il l'emporte). */
  .ml-grille-formules {
    grid-template-columns: repeat(3, 1fr);
  }
}

.ml-carte-formule {
  position: relative;
  border-radius: var(--ml-radius-lg);
  overflow: hidden;
  background: var(--ml-bg-alt);
  box-shadow: 0 8px 28px -14px rgba(102, 31, 128, 0.35);
  transition: box-shadow 320ms var(--ml-ease), scale 320ms var(--ml-ease);
}
.ml-carte-formule:hover,
.ml-carte-formule:focus-within {
  box-shadow: 0 24px 60px -18px rgba(102, 31, 128, 0.55);
  scale: 1.02;
}
@media (prefers-reduced-motion: reduce) {
  .ml-carte-formule {
    transition: none;
  }
  .ml-carte-formule:hover,
  .ml-carte-formule:focus-within {
    scale: 1;
  }
}

.ml-carte-formule-media {
  position: relative;
  /* Plus paysage que la carte de destination (4/5) : ces cartes portent
     moins d'information, elles n'ont pas besoin de hauteur pour respirer. */
  aspect-ratio: 4 / 3;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.ml-carte-formule-img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: scale 320ms var(--ml-ease);
}
.ml-carte-formule:hover .ml-carte-formule-img,
.ml-carte-formule:focus-within .ml-carte-formule-img {
  scale: 1.04;
}
@media (prefers-reduced-motion: reduce) {
  .ml-carte-formule-img {
    transition: none;
  }
  .ml-carte-formule:hover .ml-carte-formule-img,
  .ml-carte-formule:focus-within .ml-carte-formule-img {
    scale: 1;
  }
}

/* Fond de repli, sans photo : degrade de la marque, jamais une carte cassee. */
.ml-carte-formule-img--repli {
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, var(--ml-accent) 0%, var(--ml-primary) 100%);
}

.ml-carte-formule-voile {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(30, 10, 42, 0.78) 0%,
    rgba(30, 10, 42, 0.3) 55%,
    rgba(30, 10, 42, 0) 80%
  );
  pointer-events: none;
}

/* L'ancien badge « moins de 18 » du coin droit a été retiré (demande de
   Marino, juillet 2026) : il doublonnait la pastille d'âge du coin gauche
   (.ml-carte-formule-age, popup-formules.css) depuis que tous les listings la
   passent. Un seul marquage d'âge par carte, en haut à gauche. */

.ml-carte-formule-texte {
  /* Au dessus de l'image et du voile, posés en absolu. Enfant flex de
     .ml-carte-formule-media : z-index s'y applique sans position: relative, et
     c'est voulu. Voir le commentaire détaillé sur .ml-carte-destination-texte,
     plus bas dans ce fichier : un ancêtre positionné réduisait le pseudo
     élément du lien à la taille de ce bloc, et la carte cessait d'être
     entièrement cliquable. */
  z-index: 1;
  padding: var(--ml-space-sm);
  padding-inline-end: 3.6rem;
  color: #fff;
}

.ml-carte-formule-titre {
  margin: 0;
  font-family: var(--ml-font-heading);
  font-size: var(--ml-fs-base);
  color: #fff;
}

/* Le lien couvre toute la carte par un pseudo element pose en absolu. Le vrai
   <a> reste sur le titre, pour le texte et le focus clavier. Aucun gestionnaire
   de clic JavaScript : la carte reste focusable, annoncable, ouvrable dans un
   nouvel onglet et copiable comme n'importe quel lien.

   Boite de reference : .ml-carte-formule-media, plus proche ancetre positionne
   du lien, et qui occupe toute la carte. Ne jamais positionner le bloc de texte
   entre les deux, il deviendrait la boite de reference et la zone cliquable se
   reduirait a lui. */
.ml-carte-formule-lien {
  position: static;
  color: inherit;
}
.ml-carte-formule-lien::after {
  /* Sous le texte (monte a z-index: 1), au dessus de l'image et du voile. */
  z-index: 0;
  content: '';
  position: absolute;
  inset: 0;
}
.ml-carte-formule-lien:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.ml-carte-formule-fleche {
  position: absolute;
  pointer-events: none;
  inset-inline-end: var(--ml-space-md);
  inset-block-end: var(--ml-space-md);
  display: grid;
  place-items: center;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  border-radius: 50%;
  background: #fff;
  color: var(--ml-primary);
  box-shadow: var(--ml-shadow-sm);
  transition: background-color 260ms var(--ml-ease),
              color 260ms var(--ml-ease),
              rotate 260ms var(--ml-ease);
}
.ml-carte-formule:hover .ml-carte-formule-fleche,
.ml-carte-formule:focus-within .ml-carte-formule-fleche {
  background: var(--ml-accent);
  color: #fff;
  rotate: 45deg;
}
@media (prefers-reduced-motion: reduce) {
  .ml-carte-formule-fleche { transition: none; }
  .ml-carte-formule:hover .ml-carte-formule-fleche,
  .ml-carte-formule:focus-within .ml-carte-formule-fleche { rotate: none; }
}

/* ── Marine : mise en scene du portrait, des arguments et du filigrane ──

   La section porte le fond chaud de l'agence (--ml-bg-warm, un cran plus
   dense que le fond alterne generique) et le filigrane du globe
   (.ml-watermark, pose dans template-home.php). Le portrait deborde de sa
   colonne sur grand ecran, avec un rayon genereux et une ombre teintee pour
   avoir de la presence plutot que rester une vignette. Les trois arguments
   deviennent des blocs distincts avec leur propre marque visuelle plutot
   qu'une liste a puces : voir .ml-marine-arguments plus bas. ── */
.ml-home-marine {
  background: var(--ml-bg-warm);
}
.ml-marine-grid {
  display: grid;
  gap: var(--ml-space-lg);
}
@media (min-width: 900px) {
  .ml-marine-grid {
    grid-template-columns: 5fr 7fr;
    align-items: center;
    gap: var(--ml-space-xl);
  }
}
.ml-marine-portrait {
  position: relative;
}
.ml-marine-portrait img {
  border-radius: var(--ml-radius-lg);
  /* Ombre teintee de l'accent, plus large et plus douce qu'une simple carte :
     c'est elle qui donne au portrait sa presence, pas seulement le rayon. */
  box-shadow: 0 30px 70px -24px rgba(102, 31, 128, 0.45);
}
@media (min-width: 900px) {
  /* Le portrait deborde legerement de sa colonne sur grand ecran : il gagne
     en presence sans jamais chevaucher le texte, qui garde sa propre
     colonne dans la grille. */
  .ml-marine-portrait {
    margin-inline-start: calc(-1 * var(--ml-space-lg));
  }
  .ml-marine-portrait img {
    border-radius: var(--ml-radius-lg) 6vw var(--ml-radius-lg) 6vw;
  }
}
.ml-marine-texte .ml-eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--ml-fs-sm);
  font-weight: 700;
  color: var(--ml-accent);
  margin-bottom: var(--ml-space-xs);
}

/* Les trois arguments : chacun devient un bloc distinct, avec une marque
   visuelle carree a l'accent plutot qu'une puce ronde. Rendu par
   template-home.php a partir du contenu de la liste de l'editeur. */
.ml-marine-arguments {
  display: grid;
  gap: var(--ml-space-sm);
  margin: var(--ml-space-md) 0;
}
.ml-marine-argument {
  display: flex;
  align-items: flex-start;
  gap: var(--ml-space-sm);
  padding: var(--ml-space-sm);
  background: var(--ml-bg);
  border-radius: var(--ml-radius);
  border-inline-start: 3px solid var(--ml-accent);
  box-shadow: var(--ml-shadow-sm);
}
.ml-marine-argument-marque {
  flex-shrink: 0;
  margin-block-start: 0.35em;
  inline-size: 0.6em;
  block-size: 0.6em;
  border-radius: 3px;
  background: var(--ml-accent);
  rotate: 45deg;
}

/* ── Les trois étapes ─────────────────────────────────────────────────
   Un parcours, pas trois encarts posés côte à côte. Deux choses le disent :
   le chiffre, qui devient le plus grand élément de sa carte (il était plus
   petit que le corps du texte, un numéro d'étape ne peut pas être ça), et le
   trait de liaison qui va d'une pastille à la suivante. La dernière n'en a
   pas : après elle, il n'y a plus rien à rejoindre.

   Le trait est en CSS, jamais dans le contenu : ce n'est pas une information,
   c'est un geste. ── */
.ml-etapes-liste {
  display: grid;
  gap: var(--ml-space-lg);
  margin-top: var(--ml-space-lg);
}
.ml-etape {
  position: relative;
  padding: var(--ml-space-md);
  /* LE TEXTE NE TOUCHE PLUS LA PASTILLE (demande de Marino). La pastille
     commence à une marge du bord et fait 3,5rem : le texte commençait donc
     exactement où elle finit, sans un pixel entre les deux. Un espace de plus,
     et le chiffre redevient un repère plutôt qu'une lettrine collée au mot.
     Au dessus de 800px la pastille remonte en tête de carte, cette marge n'a
     plus lieu d'être et la règle est remplacée. */
  padding-inline-start: calc(3.5rem + var(--ml-space-md) + var(--ml-space-sm));
  background: var(--ml-bg);
  border-radius: var(--ml-radius-lg);
  box-shadow: var(--ml-shadow-sm);
}
.ml-etape-numero {
  position: absolute;
  inset-block-start: var(--ml-space-md);
  inset-inline-start: var(--ml-space-md);
  z-index: 1;
  display: grid;
  place-items: center;
  inline-size: 3.5rem;
  block-size: 3.5rem;
  border-radius: 50%;
  background: var(--ml-accent);
  /* Blanc sur le violet de marque : 10,1:1. Le sens d'usage du violet est
     respecté, texte clair sur fond violet, jamais l'inverse. */
  color: #fff;
  font-family: var(--ml-font-heading);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1;
}
/* Le trait de liaison. Sur une colonne, il descend de la pastille vers celle
   de l'étape suivante, en traversant l'écart de la grille.

   Deux couches de fond sur le même pseudo élément, jamais un élément de plus
   dans la page : le trait n'est pas une information, c'est un geste, il n'a
   rien à faire dans le contenu.

   Couche du dessous : le trait lui même, plein près de la pastille, qui
   s'éteint vers l'étape suivante. Il occupe toute la longueur, il ne bouge
   jamais, il ne disparaît jamais.

   Couche du dessus : l'onde. Une tête vive, suivie d'une traîne qui s'éteint
   derrière elle, sur un peu moins de la moitié de la longueur du trait. Elle
   parcourt le trait de la pastille vers l'étape suivante.

   Sa teinte est le violet de la marque éclairci (--ml-accent-vif), pas une
   couleur nouvelle : c'est ce qui la rend visible aussi bien sur la partie
   pleine du trait, près de la pastille, que sur sa partie éteinte, plus loin.
   Une onde blanche aurait recouvert le trait sur son passage, donc l'aurait
   fait disparaître, ce qui est précisément le défaut de l'ancienne animation.
   Ici, le trait reste toujours entier : une lueur le traverse. ── */
.ml-etape:not(:last-child)::before {
  content: '';
  position: absolute;
  inset-block-start: calc(var(--ml-space-md) + 3.5rem);
  inset-block-end: calc(-1 * var(--ml-space-lg));
  inset-inline-start: calc(var(--ml-space-md) + 1.75rem - 1px);
  inline-size: 2px;
  background-image:
    linear-gradient(
      to bottom,
      rgba(158, 61, 190, 0) 0%,
      rgba(158, 61, 190, 0.4) 60%,
      var(--ml-accent-vif) 82%,
      var(--ml-accent-vif) 88%,
      rgba(158, 61, 190, 0) 100%
    ),
    linear-gradient(
      to bottom,
      var(--ml-accent) 0%,
      rgba(102, 31, 128, 0.18) 100%
    );
  background-repeat: no-repeat, no-repeat;
  background-size: 100% 45%, 100% 100%;
  /* Au repos, l'onde attend juste avant la pastille, hors du trait. */
  background-position: 0 -85%, 0 0;
}

/* L'onde en mouvement.

   Seule la position de la couche du dessus varie : celle du trait est répétée
   telle quelle à chaque étape de l'animation, donc le trait ne bouge pas d'un
   pixel et ne s'efface à aucun moment. L'onde entre par la pastille, court
   jusqu'à l'étape suivante, sort, puis un temps mort la fait attendre avant de
   repartir. Sans ce temps mort, elle tournerait sans répit et se lirait comme
   un chargement en cours, pas comme un chemin.

   Les trois traits sont décalés : l'onde descend l'escalier au lieu de
   clignoter partout en même temps.

   Vitesse constante (linear) : une courbe d'accélération ferait ralentir l'onde
   en cours de route, ce qui n'a pas de sens pour un signal qui se propage.

   Cette règle est déclarée avant le passage à trois colonnes, où le nom de
   l'animation bascule sur sa variante horizontale : une déclaration raccourcie
   posée après aurait remis ce nom à zéro. ── */
.ml-etapes-liste:not(.ml-no-anim) .ml-etape:not(:last-child)::before {
  animation: ml-etape-onde-verticale 3.2s linear infinite;
}
.ml-etape:nth-child(2)::before { animation-delay: 0.45s; }
.ml-etape:nth-child(3)::before { animation-delay: 0.9s; }

.ml-etape-titre {
  /* Le titre respire sous la pastille. Colle a elle, le chiffre avait l'air de
     lui appartenir, au lieu de numeroter l'etape entiere. */
  margin-block-start: var(--ml-space-sm);
  margin-bottom: var(--ml-space-xs);
}
.ml-etape-desc {
  margin: 0;
  color: var(--ml-text-mid);
}

@media (min-width: 800px) {
  .ml-etapes-liste {
    grid-template-columns: repeat(3, 1fr);
  }
  /* Sur trois colonnes, la pastille remonte en tête de carte et le trait
     devient horizontal : le parcours se lit de gauche à droite. */
  .ml-etape {
    padding-inline-start: var(--ml-space-md);
    padding-block-start: calc(3.5rem + var(--ml-space-md));
  }
  /* Le trait devient horizontal, et l'onde le parcourt de gauche à droite.
     Les deux couches basculent avec lui : l'onde reste un tiers de la
     longueur, le trait garde son dégradé, plein à gauche, éteint à droite. */
  .ml-etape:not(:last-child)::before {
    inset-block-start: calc(var(--ml-space-md) + 1.75rem - 1px);
    inset-block-end: auto;
    inset-inline-start: calc(var(--ml-space-md) + 3.5rem);
    inset-inline-end: calc(-1 * var(--ml-space-lg));
    inline-size: auto;
    block-size: 2px;
    background-image:
      linear-gradient(
        to right,
        rgba(158, 61, 190, 0) 0%,
        rgba(158, 61, 190, 0.4) 60%,
        var(--ml-accent-vif) 82%,
        var(--ml-accent-vif) 88%,
        rgba(158, 61, 190, 0) 100%
      ),
      linear-gradient(
        to right,
        var(--ml-accent) 0%,
        rgba(102, 31, 128, 0.18) 100%
      );
    background-size: 45% 100%, 100% 100%;
    background-position: -85% 0, 0 0;
  }
  .ml-etapes-liste:not(.ml-no-anim) .ml-etape:not(:last-child)::before {
    animation-name: ml-etape-onde-horizontale;
  }
}

@keyframes ml-etape-onde-verticale {
  0%   { background-position: 0 -85%, 0 0; }
  /* L'onde a fini sa course aux deux tiers du cycle : le tiers restant est le
     temps mort avant qu'elle ne reparte. */
  66%  { background-position: 0 185%, 0 0; }
  100% { background-position: 0 185%, 0 0; }
}
@keyframes ml-etape-onde-horizontale {
  0%   { background-position: -85% 0, 0 0; }
  66%  { background-position: 185% 0, 0 0; }
  100% { background-position: 185% 0, 0 0; }
}

/* Le mouvement est coupé pour qui a demandé moins d'animation : le trait reste,
   entier, avec son dégradé d'origine, et l'onde attend hors du trait. Rien
   n'est perdu, le dégradé porte déjà le sens du parcours.

   Le sélecteur reprend exactement celui qui pose l'animation, y compris le
   :not(.ml-no-anim) : sur trois colonnes, c'est un `animation-name` de même
   poids qui nomme la variante horizontale, et un sélecteur plus court n'aurait
   pas pesé assez lourd pour l'annuler. L'animation aurait continué de tourner
   pour qui avait demandé qu'elle s'arrête. ── */
@media (prefers-reduced-motion: reduce) {
  .ml-etapes-liste:not(.ml-no-anim) .ml-etape:not(:last-child)::before {
    animation: none;
  }
}

/* ── Avis et appel final ──────────────────────────────────────────────
   La section des avis est la seule section colorée du corps de la page, et
   c'est voulu : la preuve sociale doit arrêter l'oeil. Elle porte le violet
   de la marque, --ml-accent, et pas un violet plus foncé inventé pour
   l'occasion : le site n'a qu'un seul violet, c'est celui du logo.

   Elle garde la classe .ml-section-dark, qui lui donne déjà tout son
   vocabulaire de fond sombre (titres blancs, texte adouci, anneau de focus
   blanc, filigrane clair). Seul le fond change ici.

   Contrastes mesurés sur --ml-accent (#661f80) :
   - titres et surtitre en blanc pur : 10,1:1
   - texte courant, blanc à 82 pour cent : 7,3:1
   - repère de la zone d'avis, blanc à 75 pour cent : 6,4:1
   Tous très au dessus des 4,5:1 exigés. Le sens d'usage du violet est
   respecté : du texte clair sur du violet, jamais l'inverse. ── */
/* Les deux classes, pour ne dépendre ni de l'ordre des règles ni du fait que
   .ml-section-dark soit déclarée plus haut dans ce fichier. */
.ml-section-dark.ml-home-avis {
  background: var(--ml-accent);
}

.ml-avis-zone { margin-top: var(--ml-space-lg); }

/* ── L'appel final ────────────────────────────────────────────────────
   Il était centré, sur rien, entre deux aplats sombres : le violet des avis
   au dessus, le bleu nuit du pied de page en dessous. Trois textes au milieu
   d'un vide n'arrêtent personne, et c'est pourtant le dernier moment de la
   page où l'on décide de prendre rendez-vous.

   Il devient un objet, et non une surface : une carte claire posée sur le
   crème, coupée en deux, la promesse à gauche, la décision à droite. Le
   liseré violet en tête de carte est la seule reprise de la couleur de marque,
   et il suffit : trois aplats violets sur une même page ne diraient plus rien.

   Sa place dans l'enchaînement est ce qui le sauve : sombre, clair, sombre.
   La carte blanche entre les deux nuits est le seul endroit du bas de page où
   l'oeil se pose.

   Derrière la carte, les bulles de la marque, en champ clairsemé
   (assets/img/bulles-fond.png). Elles arrivent en image de fond CSS, et non
   en balise : la section vit sous la ligne de flottaison, et un fond CSS
   n'est téléchargé que si l'élément est réellement peint. Elles ne portent
   aucune information, elles ne sont donc dans aucun texte alternatif.

   Contrastes mesurés au pixel dans un Chromium réel, sous les glyphes : la
   carte est un aplat blanc opaque, aucune bulle ne passe dessous, et les
   valeurs ne bougent pas d'un centième par rapport à l'ancien fond crème nu.
   - le titre, --ml-primary (#2e123f) : 16,5:1
   - le texte et les points de réassurance, --ml-text-mid (#58515f) : 7,6:1
   - le surtitre et les coches, --ml-accent (#661f80) : 7,6:1
   - le bouton principal, blanc sur --ml-accent : 10,1:1 ── */
.ml-home-cta {
  position: relative;
  isolation: isolate;
  background: var(--ml-bg-warm);
}

/* Le champ de bulles.

   Le cadrage est tout le sujet : l'image est un paysage très large (deux mille
   pixels sur huit cents) dont la moitié basse est vide.

   `background-size: 100% auto` la fait tenir toute la largeur de la section :
   sa hauteur vaut alors 40 pour cent de cette largeur, et sur un grand écran,
   où la section est large et courte, la bande peuplée tombe pile sur sa
   hauteur. Les bulles se répartissent d'un bord à l'autre et du haut au bas :
   deux coiffent la carte, deux tiennent les marges, une passe sous elle. Le
   vide du bas de l'image, lui, tombe sous la section et n'est jamais peint.

   Le cadrage à la hauteur (`auto 186%`, la bande peuplée exactement haute
   comme la section) a été essayé et refusé : il grossit l'image à mesure que
   la section grandit, si bien qu'à 1280 il ne restait que deux bulles
   énormes, coupées par la carte. Le cadrage à la largeur garde les bulles à
   leur échelle.

   Limite assumée : entre 900 et 1400 pixels de large, la carte occupe presque
   toute la largeur du conteneur, et les bulles n'ont plus que la bande du haut
   pour se montrer. Elles y forment une couronne au dessus de la carte, et le
   bas de la section reste calme. C'est le dessin de l'image qui le veut, ses
   bulles vivent toutes dans sa moitié haute : les répartir autrement
   demanderait un second visuel, pas une règle de style.

   Sur un écran étroit, la carte prend toute la largeur : on grossit l'image
   pour que deux ou trois bulles coiffent la carte à une taille lisible, plutôt
   que d'en semer douze minuscules derrière un aplat blanc.

   L'opacité est basse par nécessité : la carte est opaque, rien ne passe
   derrière un texte, mais un champ trop présent volerait l'oeil au dernier
   appel à l'action de la page. */
.ml-home-cta::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: url('../img/bulles-fond.png');
  background-repeat: no-repeat;
  background-position: 58% 0;
  background-size: 190% auto;
  opacity: 0.3;
  pointer-events: none;
}
@media (min-width: 900px) {
  .ml-home-cta::before {
    background-position: center 0;
    background-size: 100% auto;
  }
}

.ml-cta-carte {
  position: relative;
  overflow: hidden;
  display: grid;
  gap: var(--ml-space-lg);
  padding: var(--ml-space-lg) var(--ml-space-md);
  border-radius: var(--ml-radius-lg);
  background: var(--ml-bg);
  box-shadow: var(--ml-shadow-lg);
}

/* Le liseré de marque, en tête de carte. Décoratif : posé en CSS, jamais écrit
   dans le contenu. */
.ml-cta-carte::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 4px;
  background: linear-gradient(
    to right,
    var(--ml-accent) 0%,
    var(--ml-accent-vif) 55%,
    var(--ml-accent) 100%
  );
}

/* Sur un écran étroit, les deux blocs sont l'un sous l'autre : le trait qui les
   sépare passe au dessus de la colonne d'action, là où il sépare vraiment.
   Vertical, il ne séparerait plus rien. */
@media (max-width: 859px) {
  .ml-cta-action {
    padding-block-start: var(--ml-space-md);
    border-block-start: 1px solid var(--ml-border);
  }
}

@media (min-width: 860px) {
  .ml-cta-carte {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    align-items: center;
    gap: var(--ml-space-xl);
    padding: calc(var(--ml-space-lg) * 1.2) var(--ml-space-lg);
  }
  .ml-cta-action {
    padding-inline-start: var(--ml-space-lg);
    border-inline-start: 1px solid var(--ml-border);
  }
}

/* La mesure du titre et du texte : la règle générique
   (.ml-section > .ml-container > h2) ne les atteint pas, ils sont dans la
   carte. Ils gardent malgré tout leur mesure de lecture. */
.ml-cta-texte > h2 {
  max-inline-size: 18ch;
  margin-bottom: var(--ml-space-sm);
}
.ml-cta-texte > p {
  max-inline-size: 46ch;
  margin-bottom: 0;
  color: var(--ml-text-mid);
  font-size: var(--ml-fs-lg);
}

/* Les deux boutons l'un sous l'autre, et non cote a cote : la colonne d'action
   est etroite par construction, deux boutons y auraient de toute facon fini par
   passer a la ligne, au petit bonheur de la longueur des libelles. Empiles, ils
   se lisent comme un choix, la prise de rendez-vous d'abord. Ils prennent toute
   la largeur de la colonne : deux boutons de largeurs differentes, l'un sous
   l'autre, font un escalier. */
.ml-cta-boutons {
  display: grid;
  gap: var(--ml-space-sm);
  justify-items: stretch;
}
.ml-cta-boutons .ml-btn {
  justify-content: center;
  text-align: center;
}

/* Les points de réassurance : ce que la cliente a écrit dans sa liste. Ils sont
   sous les boutons, jamais au dessus : on lit l'action, puis ce qui lève le
   dernier doute. */
.ml-cta-reassurance {
  display: grid;
  gap: 0.55em;
  margin-block-start: var(--ml-space-md);
  font-size: var(--ml-fs-sm);
  color: var(--ml-text-mid);
}
.ml-cta-reassurance li {
  display: flex;
  align-items: center;
  gap: 0.6em;
}
.ml-cta-coche {
  flex: 0 0 auto;
  color: var(--ml-accent);
}

/* ─────────────────────────────────────────
   Repère « humain » des appels à l'action (ml_cta_humain(),
   inc/template-functions.php)

   Le même composant partout : le visage de Marine en médaillon, un point
   « en ligne » emprunté au langage de la carte de Linguine, son prénom et le
   délai de réponse. Il tient sur DEUX familles de fonds : les appels violets
   (langue, pays, ville, texte blanc) via .ml-cta-humain--sombre, et les appels
   clairs (accueil, formule) par défaut. Une variable de fond, --ml-cth-fond,
   sert au liseré du médaillon et au cerne du point, pour qu'ils se découpent du
   fond quel qu'il soit.

   Contrastes mesurés (le prénom et le délai portent le sens, ils doivent
   passer AA) :
   - fond clair (carte blanche de l'accueil #ffffff, crème de la formule
     #faf7f5) : prénom en --ml-text (#221d29) à 15:1 et plus, délai en
     --ml-text-mid (#58515f) à 7:1 et plus.
   - fond violet (dégradé #661f80 vers #4d1760) : prénom blanc plein à 10:1 sur
     l'extrémité claire, délai blanc à 0,82 d'opacité à plus de 7:1.
   Le point « en ligne » est décoratif (aria-hidden) : il lui suffit de se
   distinguer (cible 3:1). Vert de marque #1a8f4c à 4,1:1 sur le blanc ; sur le
   violet, un vert plus lumineux (#34c974) le fait ressortir à 4,7:1, cerné du
   violet du fond. ── */

.ml-cta-humain {
  --ml-cth-fond: var(--ml-bg);
  --ml-cth-nom: var(--ml-text);
  --ml-cth-delai: var(--ml-text-mid);
  --ml-cth-point: var(--ml-en-ligne);

  /* Bloc de largeur ajustée à son contenu, jamais une ligne inline : sur la
     fiche ville, le composant et le bouton (tous deux des boîtes inline) se
     rangeraient sinon côte à côte. En bloc, il occupe sa propre ligne et
     laisse le bouton dessous. Il se cale à gauche en flux normal (accueil,
     ville, formule) et se centre dans les appels centrés (langue, pays) où la
     grille pose justify-items: center. */
  display: flex;
  inline-size: fit-content;
  max-inline-size: 100%;
  align-items: center;
  gap: 0.7rem;
  /* Séparation du bouton qui suit dans les appels à fond clair (flux de bloc) ;
     dans les appels centrés (langue, pays), la grille gère l'écart, on annule
     cette marge plus bas. */
  margin-block-end: var(--ml-space-md);
}
.ml-cta-humain--sombre {
  --ml-cth-fond: var(--ml-accent-dark);
  --ml-cth-nom: #ffffff;
  --ml-cth-delai: rgba(255, 255, 255, 0.82);
  --ml-cth-point: #34c974;
}

/* Le médaillon : la photo en cercle, un liseré fin clair qui l'assoit aussi
   bien sur le violet que sur le clair, et une ombre douce (jamais lourde) pour
   la détacher d'un fond clair où le liseré blanc se fond. */
.ml-cta-humain-medaillon {
  position: relative;
  flex: none;
  inline-size: 56px;
  block-size: 56px;
}
.ml-cta-humain-photo {
  inline-size: 100%;
  block-size: 100%;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 2px 8px rgba(34, 29, 41, 0.18);
}

/* Le point « en ligne » : posé sur le bord bas droit du médaillon, cerné de la
   couleur du fond pour paraître découpé dedans, comme sur la carte de Linguine.
   Statique, comme elle : aucun mouvement, donc rien à figer sous
   prefers-reduced-motion. */
.ml-cta-humain-point {
  position: absolute;
  inset-block-end: 1px;
  inset-inline-end: 1px;
  inline-size: 12px;
  block-size: 12px;
  border-radius: 50%;
  background: var(--ml-cth-point);
  border: 2px solid var(--ml-cth-fond);
}

.ml-cta-humain-texte {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  line-height: 1.25;
}
/* Chaque ligne reste entière : le prénom et le délai ne se coupent jamais, le
   composant garde sa forme compacte sur une seule ligne (médaillon + texte). */
.ml-cta-humain-nom {
  font-family: var(--ml-font-heading);
  font-weight: 700;
  font-size: var(--ml-fs-base);
  color: var(--ml-cth-nom);
  white-space: nowrap;
}
.ml-cta-humain-delai {
  font-size: var(--ml-fs-sm);
  color: var(--ml-cth-delai);
  white-space: nowrap;
}

/* Dans les appels centrés (langue, pays), le composant est une cellule de
   grille : l'écart au bouton vient déjà du gap de la grille, on retire la marge
   de flux pour ne pas doubler l'espace. */
.ml-langue-cta-inner .ml-cta-humain,
.ml-pays-cta-inner .ml-cta-humain {
  margin-block-end: 0;
}

/* ─────────────────────────────────────────
   Page d'information (visas, conditions)
   Un sommaire, un contenu long. Rien a vendre.
───────────────────────────────────────── */

.ml-info-grid {
  display: grid;
  gap: var(--ml-space-lg);
}
@media (min-width: 900px) {
  .ml-info-grid {
    grid-template-columns: 16rem 1fr;
    align-items: start;
    gap: var(--ml-space-xl);
  }
  /* Le sommaire suit la lecture. `top` tient compte de l'en-tete collant. */
  .ml-info-sommaire {
    position: sticky;
    top: calc(var(--ml-header-h) + var(--ml-space-md));
  }
}

.ml-info-sommaire {
  border-inline-start: 2px solid var(--ml-border);
  padding-inline-start: var(--ml-space-md);
}
.ml-info-sommaire-titre {
  font-size: var(--ml-fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ml-text-dim);
  margin-bottom: var(--ml-space-sm);
}
.ml-info-sommaire ol {
  counter-reset: sommaire;
}
.ml-info-sommaire li {
  counter-increment: sommaire;
  margin-bottom: var(--ml-space-xs);
}
.ml-info-sommaire a {
  color: var(--ml-text-mid);
  text-decoration: none;
}
.ml-info-sommaire a:hover,
.ml-info-sommaire a:focus-visible {
  color: var(--ml-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Le contenu long : une mesure confortable, jamais toute la largeur. */
.ml-info-contenu {
  max-inline-size: 68ch;
}
.ml-info-contenu h2 {
  margin-block-start: var(--ml-space-lg);
  /* Le titre vise par une ancre ne doit pas se coller sous l'en-tete. */
  scroll-margin-top: calc(var(--ml-header-h) + var(--ml-space-md));
}
.ml-info-contenu h2:first-child {
  margin-block-start: 0;
}
.ml-info-contenu ul,
.ml-info-contenu ol {
  margin-block: var(--ml-space-sm);
  padding-inline-start: 1.4em;
  list-style: revert;
}
.ml-info-contenu li {
  margin-bottom: var(--ml-space-xs);
}
.ml-info-contenu a {
  color: var(--ml-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.ml-info-aide {
  text-align: center;
}
.ml-info-aide a {
  color: var(--ml-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ─────────────────────────────────────────
   Fil d'Ariane
   Sur toutes les pages sauf l'accueil, juste sous l'en-tete.
───────────────────────────────────────── */

.ml-fil-ariane {
  max-inline-size: var(--ml-container-w);
  margin-inline: auto;
  padding: var(--ml-space-sm);
  font-size: var(--ml-fs-sm);
  color: var(--ml-text-dim);
  /* Un fil long defile dans son propre conteneur, jamais la page entiere. */
  overflow-x: auto;
  scrollbar-width: none;
}
.ml-fil-ariane::-webkit-scrollbar { display: none; }

.ml-fil-ariane ol {
  display: flex;
  align-items: center;
  gap: var(--ml-space-xs);
  white-space: nowrap;
}

/* Le separateur est pose en CSS, jamais ecrit dans le HTML : un lecteur
   d'ecran ne doit pas lire « barre oblique » entre chaque niveau. */
.ml-fil-ariane li + li::before {
  content: '';
  display: inline-block;
  inline-size: 0.42em;
  block-size: 0.42em;
  margin-inline-end: var(--ml-space-xs);
  border-block-start: 1.5px solid currentColor;
  border-inline-end: 1.5px solid currentColor;
  rotate: 45deg;
  opacity: 0.5;
  vertical-align: middle;
}

.ml-fil-ariane a {
  color: var(--ml-text-mid);
  border-radius: var(--ml-radius-sm);
  transition: color 160ms var(--ml-ease);
}
.ml-fil-ariane a:hover,
.ml-fil-ariane a:focus-visible {
  color: var(--ml-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .ml-fil-ariane a { transition: none; }
}

/* Le dernier niveau est la page courante : ni lien, ni cliquable. */
.ml-fil-ariane li[aria-current="page"] {
  color: var(--ml-text);
  font-weight: 600;
}

/* ─────────────────────────────────────────────────────────────────────────
   CARTE DE DESTINATION : composant partagé (inc/carte-destination.php)

   À DÉPLACER PAR LE BOSS DANS assets/css/main.css : cette carte sert
   partout (page d'accueil, pages de pays, ici dans la fenêtre), elle n'a
   rien de spécifique à la fenêtre de l'assistant. Le fichier de mission ne
   permettait pas de toucher à main.css, ce bloc est donc posé ici en
   attendant. functions.php enqueue déjà assets/css/assistant-modale.css
   sur toutes les pages (dépendant de mlinguistique-style), donc la carte
   fonctionne dès maintenant partout où elle est appelée : mais son bon
   endroit est main.css, aux côtés des autres composants de carte
   (.ml-sejour-card, .ml-langue-card).
───────────────────────────────────────────────────────────────────────── */

.ml-grille-destinations {
  display: grid;
  gap: var(--ml-space-md);
  grid-template-columns: 1fr;
}
@media (min-width: 560px) {
  .ml-grille-destinations {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 900px) {
  .ml-grille-destinations {
    grid-template-columns: repeat(3, 1fr);
  }
}
/* Grille compacte (utilisée dans la fenêtre de l'assistant) : cartes plus
   petites, davantage de colonnes disponibles dès une largeur modeste. */
.ml-grille-destinations--compacte {
  grid-template-columns: repeat(2, 1fr);
  gap: var(--ml-space-sm);
}
@media (min-width: 640px) {
  .ml-grille-destinations--compacte {
    grid-template-columns: repeat(3, 1fr);
  }
}

.ml-groupe-pays + .ml-groupe-pays {
  margin-block-start: var(--ml-space-md);
}
.ml-groupe-pays-titre {
  display: flex;
  align-items: center;
  gap: 0.4em;
  font-size: var(--ml-fs-base);
  font-family: var(--ml-font-heading);
  color: var(--ml-text-mid);
  margin-block: 0 0.5em;
}
.ml-groupe-pays-drapeau {
  font-size: 1.1em;
  line-height: 1;
}

.ml-carte-destination {
  position: relative;
  border-radius: var(--ml-radius-lg);
  overflow: hidden;
  background: var(--ml-bg-alt);
  /* Une ombre teintée de l'accent plutôt que grise : sur les fonds chauds du
     site, une ombre neutre fait virer la carte au sale. Elle est déjà
     présente au repos, discrète, et s'ouvre au survol. */
  box-shadow: 0 8px 28px -14px rgba(102, 31, 128, 0.35);
  transition: box-shadow 320ms var(--ml-ease), scale 320ms var(--ml-ease);
}
.ml-carte-destination:hover,
.ml-carte-destination:focus-within {
  box-shadow: 0 24px 60px -18px rgba(102, 31, 128, 0.55);
  scale: 1.02;
}
@media (prefers-reduced-motion: reduce) {
  .ml-carte-destination {
    transition: none;
  }
  .ml-carte-destination:hover,
  .ml-carte-destination:focus-within {
    scale: 1;
  }
}

.ml-carte-destination-media {
  position: relative;
  aspect-ratio: 4 / 5;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.ml-carte-destination-img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: scale 320ms var(--ml-ease);
}
.ml-carte-destination:hover .ml-carte-destination-img,
.ml-carte-destination:focus-within .ml-carte-destination-img {
  scale: 1.04;
}
@media (prefers-reduced-motion: reduce) {
  .ml-carte-destination-img {
    transition: none;
  }
  .ml-carte-destination:hover .ml-carte-destination-img,
  .ml-carte-destination:focus-within .ml-carte-destination-img {
    scale: 1;
  }
}

/* Fond de repli, sans photo : dégradé de la couleur d'accent, jamais une
   carte cassée (image manquante, cadre vide). */
.ml-carte-destination-img--repli {
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, var(--ml-accent) 0%, var(--ml-primary) 100%);
}

/* Voile dégradé du bas vers le haut : garantit la lisibilité du texte quel
   que soit le contenu de la photo. */
.ml-carte-destination-voile {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(30, 10, 42, 0.82) 0%,
    rgba(30, 10, 42, 0.35) 55%,
    rgba(30, 10, 42, 0) 80%
  );
  pointer-events: none;
}

.ml-carte-destination-langue {
  position: relative;
  align-self: flex-start;
  margin: var(--ml-space-sm) var(--ml-space-sm) auto;
  background: var(--ml-bg);
  border-radius: var(--ml-radius-pill);
  padding: 0.3em 0.8em;
  font-size: var(--ml-fs-sm);
  font-weight: 600;
  color: var(--ml-primary);
}

/* Badge « moins de 18 » : coin haut DROIT de la carte (le haut gauche porte la
   langue). Posé sur la formule ouverte aux mineurs qu'une ville propose (voir
   ml_ville_badge_jeunes(), inc/popup-formules.php). Fond accent plein, texte
   blanc, le sens d'usage de la marque (jamais l'inverse) : #661f80 porte du
   blanc pur à 10,1:1, très au dessus de 4,5:1, et le fond opaque garantit ce
   contraste sur n'importe quelle photo, là où un fond translucide ne le
   pourrait pas.

   Posé en absolu, il ne perturbe pas le flux flex du média (langue à gauche,
   texte en bas). Sans z-index, il reste sous le pseudo élément du lien qui
   couvre la carte (.ml-carte-destination-lien::after, monté dans le contexte
   du bloc texte) : le clic sur le badge ouvre donc la ville, comme partout
   ailleurs sur la carte. */
.ml-carte-destination-jeunes {
  position: absolute;
  inset-block-start: var(--ml-space-sm);
  inset-inline-end: var(--ml-space-sm);
  padding: 0.3em 0.8em;
  background: var(--ml-accent);
  color: #fff;
  border-radius: var(--ml-radius-pill);
  font-size: var(--ml-fs-sm);
  font-weight: 700;
  /* Garde-fou des cartes étroites (audit mobile) : la pastille de langue occupe
     le coin gauche de la même bande, sans budget de largeur partagé. Le badge
     réel (« Dès 14 ans ») tient partout, mais un libellé long de repli
     (« Moins de 18 ans ») mordrait sur le nom de langue vers 320px : il se
     tronque proprement plutôt que de recouvrir. */
  max-inline-size: 55%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Cartes compactes de la fenêtre de l'assistant (~156px de large sur deux
   colonnes) : aucune bande n'a la place de deux pastilles, le badge s'efface
   par défaut. Il reparaît dans les résultats de l'assistant en tranche « moins
   de 18 ans » (conteneur .ml-am-resultats-villes.is-mineur, posé par
   ml_am_resultats()), en réassurance explicite voulue par Marino : c'est le
   MÊME badge que le listing de l'accueil (fond accent, texte blanc, « Dès X
   ans »). La place vient de la pastille de langue, masquée dans les résultats
   car la langue est déjà choisie dans le parcours (assets/css/assistant-modale.css).
   En tranche majeur, le badge reste masqué, même pour une ville qui accueille
   aussi des mineurs : la pilule n'existe que pour la tranche mineur. */
.ml-carte-destination--compacte .ml-carte-destination-jeunes {
  display: none;
}
.ml-am-resultats-villes.is-mineur .ml-carte-destination--compacte .ml-carte-destination-jeunes {
  display: block;
}

.ml-carte-destination {
  --ml-carte-fleche-taille: 2.5rem;
  --ml-carte-fleche-retrait: var(--ml-space-md);
}

.ml-carte-destination-texte {
  /* LE COULOIR DE LA FLECHE EST RESERVÉ : sans cette marge, un nom de ville
     long passe dessous (voir .ml-carte-destination-fleche plus bas). L'écart de
     sécurité vaut une petite espace, pour que la dernière lettre ne vienne pas
     lécher le rond. */
  padding-inline-end: calc(
    var(--ml-carte-fleche-retrait) + var(--ml-carte-fleche-taille) + var(--ml-space-xs)
  );

  /* Au dessus de l'image et du voile, qui sont posés en absolu : sans cela le
     texte passerait dessous et disparaîtrait.

     Ce bloc est un enfant de .ml-carte-destination-media, qui est en flex : un
     élément flex accepte z-index SANS être positionné. C'est exactement ce
     qu'il faut ici, et c'est la raison pour laquelle position: relative a été
     retiré : un ancêtre positionné devient la boîte de référence du pseudo
     élément du lien, et celui-ci ne couvrait alors plus que ce bloc de texte,
     soit 71px sur une carte de 460. Mesuré : 5 points de clic sur 25
     atteignaient le lien, tout le haut de la carte ne cliquait nulle part.
     La carte n'était entièrement cliquable que sur le papier. */
  z-index: 1;
  padding: var(--ml-space-sm);
  color: #fff;
}

.ml-carte-destination-pays {
  display: flex;
  align-items: center;
  gap: 0.35em;
  margin: 0 0 0.2em;
  font-size: var(--ml-fs-sm);
  color: rgba(255, 255, 255, 0.85);
}
.ml-carte-destination-drapeau {
  font-size: 1.05em;
  line-height: 1;
}

.ml-carte-destination-ville {
  margin: 0;
  font-family: var(--ml-font-heading);
  font-size: var(--ml-fs-lg);
  color: #fff;
}

/* Le lien couvre toute la carte par un pseudo élément posé en absolu. Le vrai
   <a> reste sur le nom de la ville, pour le texte et le focus clavier. C'est le
   seul <a> de la carte : aucun gestionnaire de clic JavaScript n'est posé sur
   l'article, pour que la carte reste focusable, annonçable, ouvrable dans un
   nouvel onglet et copiable comme n'importe quel lien.

   La boîte de référence du pseudo élément est .ml-carte-destination-media, le
   plus proche ancêtre positionné du lien, et ce média occupe toute la carte :
   inset: 0 couvre donc bien la carte entière. Ne JAMAIS positionner un élément
   intermédiaire entre ce média et le lien (le bloc de texte, par exemple) : il
   deviendrait la nouvelle boîte de référence, et la zone cliquable se
   réduirait à lui. C'est très exactement le défaut qui a été corrigé ici. */
.ml-carte-destination-lien {
  position: static;
  color: inherit;
}
.ml-carte-destination-lien::after {
  /* Il passe SOUS le texte, qui est monté à z-index: 1 : sans cela le nom de la
     ville disparaîtrait sous la zone cliquable. Le clic, lui, traverse le texte
     sans encombre : un bloc sans fond ne capte rien. */
  z-index: 0;
  content: '';
  position: absolute;
  inset: 0;
}
.ml-carte-destination-lien:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Taille compacte, utilisée dans la fenêtre de l'assistant : moins de
   padding, texte légèrement réduit, la carte reste lisible à petite
   échelle. */
.ml-carte-destination--compacte .ml-carte-destination-media {
  aspect-ratio: 1 / 1;
}
.ml-carte-destination--compacte {
  /* Une flèche plus petite sur une carte plus petite, et le couloir réservé
     suit tout seul : les deux valeurs ne sont écrites qu'ici. */
  --ml-carte-fleche-taille: 2rem;
  --ml-carte-fleche-retrait: var(--ml-space-sm);
}
.ml-carte-destination--compacte .ml-carte-destination-texte {
  /* Un cran plus d'air qu'avant (demande de Marino : le texte collait aux
     bords de la carte dans les résultats de l'assistant). */
  padding: var(--ml-space-sm);
  padding-inline-end: calc(
    var(--ml-carte-fleche-retrait) + var(--ml-carte-fleche-taille) + var(--ml-space-xs)
  );
}
.ml-carte-destination--compacte .ml-carte-destination-ville {
  font-size: var(--ml-fs-base);
}
.ml-carte-destination--compacte .ml-carte-destination-langue {
  margin: 0.5em 0.5em auto;
  padding: 0.2em 0.6em;
  font-size: 0.75rem;
}
.ml-carte-destination--compacte .ml-carte-destination-jeunes {
  /* Mêmes retraits du bord que la carte pleine taille (demande de Marino :
     à 0.5em de 0.75rem, la pilule collait littéralement au coin). */
  inset-block-start: var(--ml-space-sm);
  inset-inline-end: var(--ml-space-sm);
  padding: 0.25em 0.7em;
  font-size: 0.75rem;
}
.ml-carte-destination--compacte .ml-carte-destination-pays {
  font-size: 0.75rem;
}

/* Carte de repli, sans fiche ml_ville correspondante (silo figé non encore
   importé) : un simple encart avec le nom de la ville, jamais une carte
   cassée ni un lien mort. */
.ml-carte-destination--repli {
  background: var(--ml-bg-alt);
  border: 1px dashed var(--ml-border-strong);
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--ml-space-sm);
  text-align: center;
}
.ml-carte-destination-repli-nom {
  margin: 0;
  font-weight: 600;
  color: var(--ml-text-mid);
}

/* La flèche circulaire, en bas à droite. Purement décorative : le nom de la
   ville porte déjà le lien et son intitulé. pointer-events: none pour
   qu'elle ne capte jamais le clic à la place du pseudo élément du lien qui
   couvre toute la carte (elle vient après lui dans le DOM, donc gagnerait
   sinon le clic dans sa zone). */
/* LA FLECHE DECLARE SA PLACE, ET LE TEXTE LA RESPECTE. Elle est posée en
   absolu, donc elle ne pousse rien : un nom de ville long passait dessous, et
   « Bournemouth » disparaissait à moitié sous le rond blanc (constat de Marino
   sur téléphone). Sa taille et son retrait vivent dans deux variables, lues par
   le bloc de texte pour se réserver le couloir : deux valeurs écrites deux fois
   auraient fini par se contredire. */
.ml-carte-destination-fleche {
  position: absolute;
  pointer-events: none;
  inset-inline-end: var(--ml-carte-fleche-retrait);
  inset-block-end: var(--ml-carte-fleche-retrait);
  display: grid;
  place-items: center;
  inline-size: var(--ml-carte-fleche-taille);
  block-size: var(--ml-carte-fleche-taille);
  border-radius: 50%;
  background: #fff;
  color: var(--ml-primary);
  box-shadow: var(--ml-shadow-sm);
  transition: background-color 260ms var(--ml-ease),
              color 260ms var(--ml-ease),
              rotate 260ms var(--ml-ease);
}
.ml-carte-destination:hover .ml-carte-destination-fleche,
.ml-carte-destination:focus-within .ml-carte-destination-fleche {
  background: var(--ml-accent);
  color: #fff;
  rotate: 45deg;
}
@media (prefers-reduced-motion: reduce) {
  .ml-carte-destination-fleche { transition: none; }
  .ml-carte-destination:hover .ml-carte-destination-fleche,
  .ml-carte-destination:focus-within .ml-carte-destination-fleche { rotate: none; }
}

/* Le bloc de texte laisse la place à la flèche. */
.ml-carte-destination-texte {
  padding-inline-end: 3.6rem;
}

/* La section des destinations de l'accueil est la cible du lien « Quelles
   destinations ? » du haut de page. La barre de menu est collante : sans cette
   réserve, elle recouvrirait le titre de la section à l'arrivée du lien, et le
   visiteur atterrirait sur un texte décapité. */
.ml-home-destinations {
  scroll-margin-block-start: calc(var(--ml-header-h) + var(--ml-space-md));
}

/* ─────────────────────────────────────────────────────────────────────────
   FILTRE DE DESTINATIONS (section accueil) : inc/section-destinations.php,
   assets/js/filtre-destinations.js

   Les onglets sont de vrais liens sans JavaScript (page de langue). Le
   script transforme la barre en role="tablist" et filtre les cartes avec
   l'attribut hidden. Rien ici ne dépend du script pour rester lisible :
   sans lui, tous les onglets et toutes les cartes restent visibles.
───────────────────────────────────────────────────────────────────────── */

.ml-destinations-onglets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ml-space-sm) var(--ml-space-lg);
  margin: 0 0 var(--ml-space-md);
  padding-block-end: var(--ml-space-xs);
  border-block-end: 1px solid var(--ml-border);
}

.ml-destinations-onglet {
  position: relative;
  padding-block-end: 0.6em;
  font-family: var(--ml-font-heading);
  font-weight: 600;
  font-size: var(--ml-fs-base);
  color: var(--ml-text-mid);
  transition: color 200ms var(--ml-ease);
}
.ml-destinations-onglet:hover {
  color: var(--ml-accent);
}
@media (prefers-reduced-motion: reduce) {
  .ml-destinations-onglet { transition: none; }
}

/* Le trait sous l'onglet actif : c'est lui qui porte l'état, jamais la
   seule couleur. Présent dès le rendu PHP sur « Tout », déplacé par le
   script quand un autre onglet devient actif. */
.ml-destinations-onglet.is-actif {
  color: var(--ml-primary);
}
.ml-destinations-onglet.is-actif::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: -1px;
  block-size: 3px;
  border-radius: var(--ml-radius-pill);
  background: var(--ml-accent);
}

.ml-destinations-annonce {
  /* Repris par .screen-reader-text côté PHP : les changements de résultat
     sont annoncés aux lecteurs d'écran, jamais affichés visuellement en
     double du décompte déjà lisible dans la grille. */
}

/* ─────────────────────────────────────────────────────────────────────────
   PISTE DES DESTINATIONS : carrousel ou grille (inc/section-destinations.php)

   Une seule piste, deux présentations, décidées par le nombre de cartes
   réellement montrées après filtrage :

   - .is-carrousel (4 cartes ou plus) : une piste qui défile, avec accrochage
     (scroll-snap). Le défilement au doigt, à la molette et au clavier est du
     CSS natif : il fonctionne sans une ligne de script. Le script n'ajoute
     que les flèches et les points, et bascule la classe au filtrage.
   - .is-grille (3 cartes ou moins) : la grille ordinaire. Faire défiler deux
     cartes n'aurait aucun sens.
───────────────────────────────────────────────────────────────────────── */

.ml-destinations-zone {
  position: relative;
}

/* ── La barre des langues, et le bouton qui la ferme ──
   Le bouton se pose au bout de la ligne des langues, pas sous la liste : il
   repond a la question « et les autres ? » a l'endroit meme ou l'on filtre.
   Sur petit ecran, il passe sous les onglets, ou il tient toute la largeur :
   un bouton coince a droite d'une ligne d'onglets qui defile serait
   inatteignable au doigt. ── */
.ml-destinations-barre {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--ml-space-sm);
  margin-block-end: var(--ml-space-md);
}

.ml-destinations-barre .ml-destinations-onglets {
  margin-block-end: 0;
}

/* LA BARRE N'A PLUS D'OBJET quand la section ne montre qu'une seule selection :
   une page langue, ou la liste explicite d'une page formule. Un onglet « Tout »
   tout seul est un filtre qui ne filtre rien, et qui laisse croire qu'il existe
   d'autres onglets ailleurs (constat de Marino sur les pages de formule).

   Elle est masquee, jamais retiree du DOM : le script y lit l'onglet unique dont
   il a besoin pour armer le carrousel (voir inc/section-destinations.php et
   assets/js/filtre-destinations.js, qui sort si aucun onglet n'est trouve).

   CETTE REGLE VIT ICI, AVEC LE COMPOSANT, et non dans la feuille d'un gabarit :
   elle etait dans page-langue.css, donc absente partout ailleurs, et la page
   formule a herite de la barre le jour ou elle a repris le carrousel. */
.ml-destinations-filtre--langue .ml-destinations-barre {
  display: none;
}

/* « auto » le pousse au bout de la ligne quand il y a de la place, et le laisse
   passer dessous quand il n'y en a plus. */
.ml-destinations-tout {
  margin-inline-start: auto;
  flex-shrink: 0;
}
/* .ml-btn pose display: inline-flex, qui l'emportait sur l'attribut hidden :
   le bouton sans libellé restait visible, pastille blanche vide. */
.ml-destinations-tout[hidden] {
  display: none;
}

@media (max-width: 700px) {
  .ml-destinations-tout {
    margin-inline-start: 0;
    inline-size: 100%;
    justify-content: center;
  }
}

/* ── La piste laisse respirer le survol ──────────────────────────────────────

   Au survol, la carte grandit d'un cinquantième et son ombre violette porte
   loin. C'est LA PISTE qui rogne tout cela, elle et elle seule : c'est elle qui
   porte overflow-x: auto (indispensable pour défiler) et overflow-y: hidden.
   Un rembourrage posé sur la zone parente n'y peut rien : il ne fait que
   déplacer la piste vers l'intérieur, le rognage a lieu dans la piste.

   CES TROIS VALEURS SONT MESURÉES, PAS CALCULÉES. C'est tout le sujet, et le
   piège y a coûté deux passages :

   Le calcul de tête donne, pour « 0 24px 60px -18px », une ombre qui porte à
   moitié du flou, soit 30px, moins l'étalement de 18, plus le décalage de 24 :
   36px sous la carte, 12px sur les côtés. C'est FAUX. Une ombre floutée est une
   gaussienne, et une gaussienne ne s'arrête pas à son écart type : le navigateur
   continue de peindre bien au delà, faiblement, mais visiblement.

   Ce que Chromium peint vraiment, relevé au pixel (on rend la carte au repos,
   puis réellement survolée, et l'on regarde jusqu'où les deux images diffèrent),
   à partir du bord de la carte DÉJÀ AGRANDIE :

     à gauche  42px      à droite  42px
     au dessus 18px      en dessous 66px

   Soit, en bas, presque le double des 36px calculés. C'est exactement ce que le
   client voyait : l'ombre s'arrêtait net sur une ligne droite, à 44px, là où le
   rembourrage s'achevait.

   À cela s'ajoute l'agrandissement lui même, que le rembourrage doit absorber
   en plus, puisqu'il pousse la carte vers l'extérieur avant que l'ombre ne
   commence : un centième de la largeur de chaque côté (3,8px sur la plus grande
   carte, 381px) et un centième de la hauteur en haut comme en bas (4,8px sur
   476px). D'où : 42 + 4 sur les côtés, 18 + 5 en haut, 66 + 5 en bas, arrondis
   au multiple de quatre supérieur. Les baisser recoupe l'ombre.

   Le rembourrage est posé SUR LA PISTE, et des marges négatives de même valeur
   rendent cette place au reste de la page : les cartes restent alignées sur le
   titre de la section, comme si de rien n'était.

   La grille reçoit exactement le même couple (elle ne rogne rien, overflow y
   est visible) : sans lui, elle serait plus courte que le carrousel de la
   hauteur du rembourrage, et la section sauterait au filtrage.

   Deux conséquences, tenues plus bas :
   - l'accrochage (scroll-snap) repart du bord du CONTENU, pas du bord de la
     piste : c'est le rôle de scroll-padding-inline-start, sans quoi la première
     carte s'arrêterait à l'intérieur du rembourrage ;
   - la piste déborde du conteneur de 28px de chaque côté sur un écran étroit
     (le conteneur n'a que 16px de marge intérieure). Ce débord est du vide
     transparent, et html/body sont en overflow-x: clip : mesuré à 390, 768,
     1280 et 1600px, le document ne défile pas d'un pixel à l'horizontale. Sur
     un écran étroit, le bord de la fenêtre rogne donc la frange extérieure de
     l'ombre de la première et de la dernière carte : aucune mise en page ne
     peut peindre en dehors de l'écran, et il n'y a rien à y voir. ── */
.ml-destinations-zone {
  --ml-destinations-air-cote: 48px;
  --ml-destinations-air-haut: 24px;
  --ml-destinations-air-bas: 76px;
}

.ml-destinations-piste {
  padding-block: var(--ml-destinations-air-haut) var(--ml-destinations-air-bas);
  padding-inline: var(--ml-destinations-air-cote);
  margin-block: calc(-1 * var(--ml-destinations-air-haut)) calc(-1 * var(--ml-destinations-air-bas));
  margin-inline: calc(-1 * var(--ml-destinations-air-cote));
}

/* ── Grille : trois cartes ou moins, rien à faire défiler ── */
.ml-destinations-piste.is-grille {
  display: grid;
  gap: var(--ml-space-md);
  grid-template-columns: 1fr;
  /* Aucune barre de défilement, aucun accrochage : la grille tient dans la
     page. On neutralise explicitement l'état carrousel, la même piste passant
     de l'un à l'autre sans rechargement. */
  overflow: visible;
  scroll-snap-type: none;
}
@media (min-width: 560px) {
  .ml-destinations-piste.is-grille {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 900px) {
  .ml-destinations-piste.is-grille {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ── Carrousel : quatre cartes ou plus ── */
.ml-destinations-piste.is-carrousel {
  /* L'accrochage se cale sur le bord du CONTENU, pas sur celui de la piste :
     la piste porte un rembourrage latéral (l'air rendu à l'ombre du survol,
     voir plus haut), et sans cette ligne chaque carte s'accrocherait au bord de
     la piste, donc à l'intérieur du rembourrage. La première carte paraîtrait
     rentrée, et le défilement au doigt calerait 20px trop tôt. Une seule valeur
     à tenir : celle du rembourrage lui même. */
  scroll-padding-inline-start: var(--ml-destinations-air-cote);

  display: flex;
  gap: var(--ml-space-md);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* Élan naturel sur iOS. */
  -webkit-overflow-scrolling: touch;
  /* La barre de défilement est masquée, à la demande du client : les flèches et
     les points la remplacent. Seule la barre disparaît, jamais le défilement :
     la piste continue de défiler au doigt, à la molette et au clavier, puisque
     overflow-x reste auto. Il faut les deux déclarations, Firefox ne connaît
     que la première, WebKit et Chromium que la seconde. */
  scrollbar-width: none;
}
.ml-destinations-piste.is-carrousel::-webkit-scrollbar {
  display: none;
}

/* La piste est focusable au clavier quand elle défile (tabindex posé par le
   PHP puis par le script) : son focus doit se voir, sinon on ne sait plus où
   l'on est avant d'appuyer sur une flèche. */
.ml-destinations-piste:focus-visible {
  outline: 2px solid var(--ml-accent);
  outline-offset: 4px;
  border-radius: var(--ml-radius);
}

.ml-destinations-piste.is-carrousel .ml-destinations-carte-piste {
  flex: 0 0 auto;
  /* Un peu moins large que la piste : la carte suivante dépasse du bord, et
     c'est ce débord, mieux qu'une flèche, qui dit qu'il y a une suite. La
     hauteur, elle, ne bouge pas pour autant : voir --ml-dest-h plus bas. */
  inline-size: min(80%, 300px);
  scroll-snap-align: start;
}
@media (min-width: 560px) {
  .ml-destinations-piste.is-carrousel .ml-destinations-carte-piste {
    inline-size: calc((100% - var(--ml-space-md)) / 2);
  }
}
@media (min-width: 900px) {
  .ml-destinations-piste.is-carrousel .ml-destinations-carte-piste {
    /* Trois par vue au dessus de 900px, comme la grille, jamais quatre : une
       quatrième colonne rétrécissait les cartes, et donc les raccourcissait. */
    inline-size: calc((100% - 2 * var(--ml-space-md)) / 3);
  }
}

/* ── La hauteur des cartes, constante ──────────────────────────────────────

   Demande du client : les cartes gardent toujours la même hauteur, en grille
   comme en carrousel, avant comme après un filtrage.

   Le défaut venait de la carte elle même : sa hauteur naissait de sa largeur,
   par l'aspect-ratio 4/5 de son média. Or une carte de carrousel n'a pas la
   largeur d'une carte de grille (elle laisse dépasser la suivante sur les
   écrans étroits), donc elle n'avait pas non plus sa hauteur. Mesuré à 1280px
   avant correction : 460px en grille, 337px en carrousel. Cent vingt-trois
   pixels qui sautaient dès que le carrousel apparaissait.

   La sortie n'est pas de figer une hauteur en pixels, qui serait fausse à la
   première largeur d'écran venue, mais de couper le lien entre la largeur de la
   CARTE et sa hauteur, pour la rattacher à la largeur de la ZONE, qui est la
   même dans les deux présentations. C'est à cela que servent les unités de
   requête de conteneur (cqw) : 1cqw vaut un centième de la largeur de la zone,
   que la carte soit large ou étroite. --ml-dest-col recalcule donc la largeur
   qu'aurait une colonne de grille à cette largeur d'écran, et --ml-dest-h la
   hauteur que le rapport 4/5 en tire. Les deux présentations lisent la même
   valeur : la hauteur ne peut plus différer de l'une à l'autre. La barre de
   commandes, elle, s'ajoute sous la piste, elle ne prend rien aux cartes.

   Un pourcentage n'aurait pas pu faire ce travail : sur une hauteur, il se lit
   par rapport à la hauteur du parent, jamais à sa largeur. C'est le piège de ce
   calcul, mesuré : une carte de 112px de haut au lieu de 460.

   Une seule déclaration par palier, posée sur la zone : la grille et le
   carrousel en héritent tous les deux, il n'y a pas deux vérités à tenir
   d'accord. ── */
.ml-destinations-zone {
  container-type: inline-size;
  /* Une seule colonne, pleine largeur, sous 560px. */
  --ml-dest-col: 100cqw;
  --ml-dest-h: calc(var(--ml-dest-col) * 5 / 4);
}
@media (min-width: 560px) {
  .ml-destinations-zone {
    --ml-dest-col: calc((100cqw - var(--ml-space-md)) / 2);
  }
}
@media (min-width: 900px) {
  .ml-destinations-zone {
    --ml-dest-col: calc((100cqw - 2 * var(--ml-space-md)) / 3);
  }
}

/* La hauteur est portée par le média, seul élément de la carte qui avait un
   rapport de forme, et l'article s'y ajuste. Le rapport de forme est neutralisé
   ici, sans quoi il continuerait de disputer la hauteur à la largeur de la
   carte. Ailleurs (pages de pays, fenêtre de l'assistant), la carte garde son
   rapport 4/5 d'origine : cette règle ne vaut que dans la zone du filtre. */
.ml-destinations-zone .ml-carte-destination-media {
  aspect-ratio: auto;
  block-size: var(--ml-dest-h);
}

/* Moins de mouvement : le défilement se fait d'un coup, sans glissement. Les
   flèches et les points continuent de fonctionner, ils ne font que sauter à
   la carte demandée. */
@media (prefers-reduced-motion: reduce) {
  .ml-destinations-piste.is-carrousel {
    scroll-behavior: auto;
  }
}

/* ── Les commandes : flèches et points, ajoutées par le script ──────────────

   Sans script, ce conteneur reste vide : il ne prend alors aucune place
   (:empty), et la piste défile quand même au doigt et à la molette.

   Avec le script, c'est l'inverse : une fois les flèches construites, cette
   barre ne quitte plus jamais la mise en page. Quand elle n'a rien à commander
   (le filtrage est passé sous le seuil et la piste est redevenue une grille, ou
   bien toutes les cartes tiennent sur une seule vue), elle est masquée SANS
   céder sa place. La retirer faisait perdre d'un coup 88px à la section
   (44px de flèches, 24 de marge, 20 de rembourrage de piste) : mesuré à 1280px,
   870,4px de haut avec le carrousel, 782,4 après un filtrage qui ne laissait que
   deux cartes. La page sautait sous le doigt à chaque clic sur une langue.

   visibility: hidden, et non une simple transparence : les flèches et les
   points sortent aussi de l'ordre de tabulation et de l'arbre d'accessibilité.
   Une commande qu'on ne voit pas ne doit pas s'atteindre au clavier. */
.ml-destinations-commandes:empty {
  display: none;
}
.ml-destinations-commandes.is-masquee {
  visibility: hidden;
}

.ml-destinations-commandes {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ml-space-sm);
  /* Les marges négatives de la piste ramènent cette marge à 40px comptés depuis
     le bas des cartes. L'ombre du survol, elle, s'éteint sur 66px : sa frange
     la plus faible (moins de quatre pour cent d'opacité, mesurés) passe donc
     derrière les flèches, qui sont opaques et peintes après elle. Sa partie
     dense, la seule qui se voie, s'arrête bien au dessus. Descendre les flèches
     de 66px pour l'éviter n'aurait rien réglé qui se voie, et aurait creusé un
     trou dans la section. */
  margin-block-start: var(--ml-space-lg);
}

.ml-destinations-fleche {
  display: grid;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  flex: 0 0 auto;
  border: 1px solid var(--ml-border-strong);
  border-radius: 50%;
  background: var(--ml-bg);
  color: var(--ml-primary);
  cursor: pointer;
  transition: background-color 200ms var(--ml-ease),
              color 200ms var(--ml-ease),
              border-color 200ms var(--ml-ease);
}
.ml-destinations-fleche:hover {
  background: var(--ml-accent);
  border-color: var(--ml-accent);
  color: #fff;
}
.ml-destinations-fleche:focus-visible {
  outline: 2px solid var(--ml-accent);
  outline-offset: 2px;
}
/* Bout de piste atteint : la flèche reste là, désactivée, plutôt que de
   disparaître et de faire sauter les points d'un cran. */
.ml-destinations-fleche[disabled] {
  opacity: 0.4;
  cursor: default;
}
.ml-destinations-fleche[disabled]:hover {
  background: var(--ml-bg);
  border-color: var(--ml-border-strong);
  color: var(--ml-primary);
}
@media (prefers-reduced-motion: reduce) {
  .ml-destinations-fleche { transition: none; }
}

/* Les points de position, un par vue, sous la piste. Toujours des pastilles,
   jamais un compteur « 3 sur 9 » : quand les vues sont nombreuses (dix cartes
   font neuf vues sur un écran étroit), elles passent à la ligne (flex-wrap)
   plutôt que de déborder, et restent chacune une cible que le pouce peut viser.
   Partagé par l'accueil et les pages langue. */
.ml-destinations-points {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

/* Le point est un vrai bouton : sa cible tactile fait 24px, le rond visible
   n'en fait que 8. Un rond de 8px est une cible impossible au doigt. */
.ml-destinations-point {
  display: grid;
  place-items: center;
  inline-size: 24px;
  block-size: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.ml-destinations-point::before {
  content: '';
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--ml-border-strong);
  transition: background-color 200ms var(--ml-ease), scale 200ms var(--ml-ease);
}
.ml-destinations-point:hover::before {
  background: var(--ml-text-mid);
}
.ml-destinations-point[aria-current="true"]::before {
  background: var(--ml-accent);
  scale: 1.35;
}
.ml-destinations-point:focus-visible {
  outline: 2px solid var(--ml-accent);
  outline-offset: 0;
  border-radius: 50%;
}
@media (prefers-reduced-motion: reduce) {
  .ml-destinations-point::before { transition: none; }
}

/* SUR TÉLÉPHONE, DES POINTS PLUS SERRÉS (demande de Marino). Dix cartes font
   neuf vues sur un écran étroit : les cibles rétrécissent et se rapprochent
   pour tenir sur une ligne le plus souvent possible, deux au pire. Le retour
   à la ligne reste possible (flex-wrap: wrap, hérité de la règle de base) :
   avec beaucoup de vues, forcer une seule ligne faisait déborder les points à
   gauche et à droite de l'écran, invisibles au delà du bord (constat de
   Marino sur une page à de nombreuses destinations).

   La cible perd de la largeur, jamais de la hauteur : 18 sur 24, elle reste
   confortable au pouce dans le sens où il vise. Et le glissement du doigt reste
   le vrai moyen de parcourir la piste, les points ne font qu'indiquer où l'on
   est. */
@media (max-width: 599px) {
  .ml-destinations-points {
    gap: 0.15rem;
  }
  .ml-destinations-point {
    inline-size: 18px;
  }
  .ml-destinations-point::before {
    inline-size: 7px;
    block-size: 7px;
  }
}

/* BEAUCOUP DE VUES : LES POINTS DEVIENNENT UNE RÈGLE GRADUÉE (constat de
   Marino sur la page Angleterre). Neuf points tenaient sur une ligne, vingt non,
   et vingt ronds alignés ne se comptent de toute façon pas : à ce nombre, on ne
   cherche plus « la troisième vue », on cherche « où j'en suis ».

   À partir de neuf vues, chaque point devient donc un TIRET, et l'ensemble se
   lit comme une barre de progression segmentée : la position saute aux yeux,
   et l'objet dit de lui même qu'il y a une piste à parcourir. En dessous, les
   ronds restent : ils sont plus précis, et à six vues on vise vraiment une vue.

   Les tirets sont réduits et resserrés pour tenir sur une ligne le plus
   souvent possible, mais rien n'empêche plus le retour à la ligne
   (flex-wrap: wrap, hérité de la règle de base) : avec un très grand nombre
   de destinations, sur un écran étroit, forcer une seule ligne faisait
   déborder les tirets hors de l'écran plutôt que de les faire tenir. Mieux
   vaut deux lignes lisibles qu'une ligne coupée dans le vide.

   « :has » choisit la présentation sur le NOMBRE RÉEL d'enfants, sans une ligne
   de script : un navigateur qui ne le connaît pas garde les ronds, qui passent
   à la ligne comme avant. Le repli est l'ancien comportement, jamais un écran
   cassé. */
.ml-destinations-points:has(> :nth-child(9)) {
  gap: 2px;
}
.ml-destinations-points:has(> :nth-child(9)) .ml-destinations-point {
  inline-size: 14px;
}
.ml-destinations-points:has(> :nth-child(9)) .ml-destinations-point::before {
  inline-size: 8px;
  block-size: 4px;
  border-radius: 2px;
}
/* La vue courante s'allonge au lieu de grossir : sur une règle graduée, c'est
   la longueur qui se lit, pas le diamètre. */
.ml-destinations-points:has(> :nth-child(9)) .ml-destinations-point[aria-current="true"]::before {
  inline-size: 14px;
  scale: 1;
}

/* ─────────────────────────────────────────────────────────────────────────
   ACCROCHE ET MISE EN AVANT SUR LA CARTE DE DESTINATION
   inc/carte-destination.php : ml_carte_destination()

   Le prix n'apparaît plus nulle part sur le site (décision de la cliente,
   juillet 2026) : il ne reste donc plus, à cet endroit, que l'accroche
   (_ml_intro, révélée au survol) et le label de mise en avant
   (_ml_accroche_avant, toujours visible, voir plus bas). Aucun des deux ne
   s'affiche s'il n'est pas renseigné (côté PHP), jamais de ligne vide.
───────────────────────────────────────────────────────────────────────── */

.ml-carte-destination-accroche {
  margin: 0 0 0.4em;
  font-size: var(--ml-fs-sm);
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.9);
  /* Deux lignes au plus : une accroche est un argument court, pas un
     paragraphe. Le texte complet reste sur la fiche de la ville. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Dans la fenêtre de l'assistant, la place est comptée : la petite carte va
   à l'essentiel (pays, ville), l'accroche s'efface. */
.ml-carte-destination--compacte .ml-carte-destination-accroche {
  display: none;
}

/* Label de mise en avant (_ml_accroche_avant) : coin haut DROIT du média,
   même emplacement que l'ancien badge « moins de 18 » (voir
   .ml-carte-destination-jeunes plus haut). Aucun conflit possible : ce badge
   d'âge ne s'affiche plus que dans les cartes compactes de l'assistant (voir
   l'option `badge_age`), où le label de mise en avant s'efface ci-dessous.
   Même traitement visuel que le badge d'âge (fond accent plein, texte blanc :
   le sens d'usage de la marque, jamais l'inverse), pour une seule famille de
   pastilles sur la carte. Toujours visible, contrairement à l'ancien prix
   qui n'apparaissait qu'au survol : c'est tout l'intérêt de ce label. */
.ml-carte-destination-avant {
  position: absolute;
  inset-block-start: var(--ml-space-sm);
  inset-inline-end: var(--ml-space-sm);
  padding: 0.3em 0.8em;
  background: var(--ml-accent);
  color: #fff;
  border-radius: var(--ml-radius-pill);
  font-size: var(--ml-fs-sm);
  font-weight: 700;
  max-inline-size: 55%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Cartes compactes de l'assistant : la place est comptée, ce label cède la
   place au badge d'âge (mode mineur) quand les deux pourraient coïncider. */
.ml-carte-destination--compacte .ml-carte-destination-avant {
  display: none;
}

/* Révélation au survol : au repos, le titre (et le pays) se tiennent au BAS de
   la carte, l'accroche est repliée à hauteur nulle. Au survol ou au focus
   clavier, le conteneur replié se déploie et POUSSE le titre vers le haut
   pendant que son contenu apparaît en fondu (demande de Marino : pas
   d'espace réservé qui reste vide au repos).

   Mécanique : .ml-carte-destination-revele est une grille dont l'unique rangée
   anime de 0fr à 1fr (les navigateurs animent les rangées fractionnaires
   depuis 2022-2023 ; avant cela, la rangée saute de fermé à ouvert sans
   transition, rien ne casse). L'enfant -inner porte min-height: 0 et overflow
   hidden, indispensables pour que 0fr replie vraiment. La hauteur de la carte
   est imposée par le média (aspect-ratio) : le déploiement pousse le texte À
   L'INTÉRIEUR de la carte, la grille de cartes ne se recompose jamais.

   Accessibilité : le contenu replié est aussi retiré de l'arbre
   d'accessibilité (hauteur nulle), mais il se déploie au :focus-within (le
   lien couvrant est focusable) : un parcours clavier le révèle comme la
   souris. Sur écran tactile, tout est déployé en permanence.

   Réservé aux cartes normales : la carte compacte de l'assistant garde son
   comportement (accroche masquée), elle est exclue par :not(). */
.ml-carte-destination:not(.ml-carte-destination--compacte) .ml-carte-destination-revele {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 280ms var(--ml-ease);
}
.ml-carte-destination:not(.ml-carte-destination--compacte) .ml-carte-destination-revele-inner {
  min-block-size: 0;
  overflow: hidden;
  opacity: 0;
  transition: opacity 280ms var(--ml-ease);
}
.ml-carte-destination:not(.ml-carte-destination--compacte):hover .ml-carte-destination-revele,
.ml-carte-destination:not(.ml-carte-destination--compacte):focus-within .ml-carte-destination-revele {
  grid-template-rows: 1fr;
}
.ml-carte-destination:not(.ml-carte-destination--compacte):hover .ml-carte-destination-revele-inner,
.ml-carte-destination:not(.ml-carte-destination--compacte):focus-within .ml-carte-destination-revele-inner {
  opacity: 1;
}

/* Écrans tactiles (aucun survol possible) : tout reste affiché en permanence,
   comme avant cette révélation. Un contenu qui n'existe qu'au survol
   n'existerait pas sur un téléphone, et masquer l'accroche à la majorité
   mobile serait une régression. */
@media (hover: none) {
  .ml-carte-destination:not(.ml-carte-destination--compacte) .ml-carte-destination-revele {
    grid-template-rows: 1fr;
  }
  .ml-carte-destination:not(.ml-carte-destination--compacte) .ml-carte-destination-revele-inner {
    opacity: 1;
  }
}

/* Moins de mouvement : pas d'animation de dépliage ni de fondu, la bascule est
   nette. Le contenu reste révélé au survol et au focus. */
@media (prefers-reduced-motion: reduce) {
  .ml-carte-destination:not(.ml-carte-destination--compacte) .ml-carte-destination-revele,
  .ml-carte-destination:not(.ml-carte-destination--compacte) .ml-carte-destination-revele-inner {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   FICHE PAYS : template-pays.php
   En-tête (image, titre, accroche, prix « à partir de ») puis liste des villes.
   Base sobre, à affiner au cadrage graphique de la page pays.
───────────────────────────────────────────────────────────────────────── */

.ml-pays-hero-inner {
  display: grid;
  gap: var(--ml-space-lg);
  align-items: center;
}
@media (min-width: 800px) {
  .ml-pays-hero-inner {
    grid-template-columns: 1fr 1fr;
  }
}

.ml-pays-titre {
  font-size: var(--ml-fs-h1);
  line-height: var(--ml-lh-heading);
  margin: 0 0 var(--ml-space-sm);
}

.ml-pays-accroche {
  font-size: var(--ml-fs-lg);
  color: var(--ml-text-mid);
  margin: 0 0 var(--ml-space-sm);
}

/* Le prix en pastille d'accent : texte blanc sur violet, le sens d'usage de la
   marque (jamais l'inverse). C'est l'argument fort de l'en-tête. */
.ml-pays-prix {
  display: inline-block;
  margin: 0;
  padding: 0.35em 0.9em;
  border-radius: var(--ml-radius-pill);
  background: var(--ml-accent);
  color: #fff;
  font-family: var(--ml-font-heading);
  font-weight: 700;
  font-size: var(--ml-fs-lg);
}

.ml-pays-hero-media {
  margin: 0;
}
.ml-pays-hero-img {
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--ml-radius-lg);
  box-shadow: var(--ml-shadow-md);
}

.ml-pays-villes-titre {
  font-size: var(--ml-fs-h2);
  font-family: var(--ml-font-heading);
  margin: 0 0 var(--ml-space-md);
}


/* ══ LE HUB DES FORMULES : DES CARTES QUI SE COMPARENT ════════════════════════
   Troisième tentative, et la bonne raison est dans le mot « comparer ».

   La première rendait ces pages comme les cartes du hub des langues : une
   vignette de 300 pixels, un titre, un extrait. La deuxième les déroulait en
   bandes alternées pleine largeur. Marino n'a été convaincu par aucune des
   deux, et il a écarté d'emblée les cartes de destination du site : elles sont
   faites pour donner envie d'une VILLE, photo plein cadre et texte par dessus.

   Une formule ne se contemple pas, elle se choisit. Le visiteur arrive ici
   avec une question précise, « laquelle est pour moi », et il repart dès qu'il
   a de quoi trancher. D'où cet objet, différent des deux autres :

   - une photo en bandeau paysage EN HAUT, qui donne le ton du voyage, et non
     une photo plein cadre qui avalerait le texte ;
   - un corps BLANC et structuré : l'âge, le titre, l'accroche, et surtout les
     points clés lus dans la page de la formule elle même. C'est ce qui permet
     de comparer deux formules sans les ouvrir l'une après l'autre ;
   - un pied qui dit combien de destinations la proposent.

   La photo reste au dessus du texte, jamais derrière : aucun contraste à
   surveiller, et l'image peut être n'importe quoi sans rendre le texte
   illisible. */
.ml-hub-offres {
  display: grid;
  gap: var(--ml-space-md);
  margin-block-start: var(--ml-space-lg);
  /* Chaque tuile garde SA hauteur : sans cela, ouvrir « Lire plus » sur l'une
     étirait sa voisine de la même rangée, qui n'avait rien demandé. */
  align-items: start;
}
@media (min-width: 700px) {
  .ml-hub-offres {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.ml-hub-offre {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--ml-bg);
  border: 1px solid var(--ml-accent-bordure);
  border-radius: var(--ml-radius-lg);
  box-shadow: var(--ml-shadow-sm);
  transition: border-color 240ms var(--ml-ease),
              box-shadow 240ms var(--ml-ease),
              transform 240ms var(--ml-ease);
}
.ml-hub-offre:hover,
.ml-hub-offre:focus-within {
  border-color: var(--ml-accent);
  box-shadow: var(--ml-shadow-lg);
  transform: translateY(-3px);
}

/* Bandeau paysage : 16/9, franchement différent du 4/5 portrait des cartes de
   destination. Deux objets qui ne se ressemblent pas ne se confondent pas. */
.ml-hub-offre-media {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--ml-accent-soft);
}
.ml-hub-offre-img {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: transform 700ms var(--ml-ease);
}
.ml-hub-offre-img--repli {
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, var(--ml-accent) 0%, var(--ml-primary) 100%);
}
.ml-hub-offre:hover .ml-hub-offre-img {
  transform: scale(1.04);
}

/* L'âge, posé sur la photo : c'est le premier filtre du visiteur, avant même
   le nom de la formule. Il doit se lire sans lire. */
.ml-hub-offre-age {
  position: absolute;
  inset-block-start: var(--ml-space-sm);
  inset-inline-start: var(--ml-space-sm);
  z-index: 1;
  padding: 0.35em 0.9em;
  background: var(--ml-accent);
  color: #fff;
  font-size: var(--ml-fs-sm);
  font-weight: 600;
  border-radius: var(--ml-radius-pill);
  box-shadow: var(--ml-shadow-sm);
}

.ml-hub-offre-corps {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--ml-space-xs);
  padding: var(--ml-space-md);
}
.ml-hub-offre-titre {
  margin: 0;
  font-size: var(--ml-fs-h3);
}
.ml-hub-offre-lien {
  color: inherit;
  text-decoration: none;
}
/* LA CARTE N'EST PLUS CLIQUABLE EN ENTIER (septembre 2026) : un recouvrement
   invisible empêcherait d'ouvrir le dépli « Lire plus » et le bouton qu'il
   contient. Le titre reste un lien, et la carte se soulève au focus-within. */
.ml-hub-offre-accroche {
  margin: 0;
  color: var(--ml-text-mid);
}

/* Les points clés : des puces courtes, cochées, qui se lisent en diagonale.
   La coche est décorative et vient du style, jamais du contenu : le texte de
   la puce reste celui écrit sur la page de la formule. */
.ml-hub-offre-points {
  margin: var(--ml-space-xs) 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.4em;
  font-size: var(--ml-fs-sm);
  color: var(--ml-text-mid);
}
.ml-hub-offre-points li {
  position: relative;
  padding-inline-start: 1.6em;
}
.ml-hub-offre-points li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.42em;
  inline-size: 0.6em;
  block-size: 0.32em;
  border-inline-start: 2px solid var(--ml-accent);
  border-block-end: 2px solid var(--ml-accent);
  transform: rotate(-45deg);
}

/* Le pied, poussé en bas de la carte : deux cartes côte à côte finissent sur
   la même ligne, même si leurs textes n'ont pas la même longueur. */
.ml-hub-offre-pied {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--ml-space-xs);
  margin-block-start: auto;
  padding-block-start: var(--ml-space-sm);
}
.ml-hub-offre-meta {
  margin: 0;
  font-size: var(--ml-fs-sm);
  font-weight: 600;
  color: var(--ml-text-mid);
}
.ml-hub-offre-action {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  color: var(--ml-accent-dark);
  font-weight: 700;
  font-size: var(--ml-fs-sm);
}

@media (prefers-reduced-motion: reduce) {
  .ml-hub-offre,
  .ml-hub-offre-img {
    transition: none;
  }
  .ml-hub-offre:hover,
  .ml-hub-offre:focus-within,
  .ml-hub-offre:hover .ml-hub-offre-img {
    transform: none;
  }
}

/* ══ LES TRANSITIONS ENTRE PAGES ONT ETE RETIREES (aout 2026) ════════════════
   Elles faisaient glisser l'image d'une carte vers le haut de page de sa
   destination. C'etait juste quand on venait d'une carte, et faux le reste du
   temps : le nom de transition etait pose EN DUR sur l'image de haut de page de
   chaque fiche, donc en arrivant depuis le MENU aucun element de depart ne lui
   repondait, et le navigateur animait l'image d'arrivee toute seule. Le haut de
   page sursautait (constat de Marino).

   Le defaut ne tenait pas au nommage de la carte, mais au fait que la page
   d'ARRIVEE se nommait toujours, quelle que soit la provenance. Le rendre
   conditionnel supposait de retenir la provenance d'une page a l'autre puis de
   poser le nom au bon instant du cycle de transition : beaucoup de mecanique
   pour un agrement.

   Sont partis d'un bloc : la regle @view-transition, les trois declarations
   view-transition-name sur .ml-pays-hero-img, .ml-ville-hero-img et
   .ml-formule-hero-photo, les durees, et le garde « moins de mouvement » qui
   n'avait plus rien a garder. Cote script, le nommage de la carte cliquee est
   parti de assets/js/ml-motion.js. Tout est dans ce commit si l'on y revient.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ══ VISA ET FORMALITES ══════════════════════════════════════════════════════
   L'encart hérité du pays (inc/visa-pays.php), posé sur la fiche du pays et sur
   chacune de ses villes. Il vit dans main.css et non dans l'un des deux CSS de
   fiche : c'est un composant partagé, le dupliquer serait accepter qu'il
   diverge d'un gabarit à l'autre.

   PREMIERE VERSION : un cadre clair sur fond blanc, trop nu (constat de
   Marino). Il ne suffit pas qu'une information utile soit lisible, il faut
   qu'elle ait l'air d'appartenir à la page. D'où le panneau teinté, la colonne
   d'illustration, et le passeport tamponné qui donne à la section un sujet
   plutôt qu'un simple filet. */
.ml-visa-encart {
	display: grid;
	grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
	align-items: center;
	gap: var(--ml-space-md) var(--ml-space-lg);
	max-inline-size: 60rem;
	margin-inline: auto;
	padding: var(--ml-space-lg);
	border: 1px solid var(--ml-accent-bordure);
	border-radius: var(--ml-radius-lg);
	background:
		radial-gradient(120% 120% at 0% 0%, #fff 0%, transparent 55%),
		linear-gradient(140deg, var(--ml-accent-soft) 0%, #fbf7fd 100%);
	box-shadow: var(--ml-shadow-sm);
}

@media (max-width: 780px) {
	.ml-visa-encart {
		grid-template-columns: minmax(0, 1fr);
		padding: var(--ml-space-md);
		text-align: center;
	}
}

.ml-visa-pastille {
	display: inline-block;
	margin: 0 0 0.5rem;
	padding: 0.3em 0.9em;
	border-radius: var(--ml-radius-pill);
	background: var(--ml-accent);
	color: #fff;
	font-size: 0.72rem;
	font-weight: 700;
	letter-spacing: 0.09em;
	text-transform: uppercase;
}

.ml-visa-titre {
	margin: 0 0 0.75rem;
	font-size: var(--ml-fs-h3);
	color: var(--ml-primary);
}

.ml-visa-texte > p { margin: 0 0 0.75rem; }
.ml-visa-texte > p:last-child { margin-bottom: 0; }
.ml-visa-lien { margin: 1.25rem 0 0; }

/* ── L'illustration ─────────────────────────────────────────────────────────
   Un passeport que l'on tamponne, survolé d'un avion qui suit sa trajectoire.
   Tout est en SVG et en CSS : ni script, ni image, ni police d'icônes. Elle se
   recolorise par la propriété `color` de son conteneur, comme le globe de la
   marque, et tout son mouvement est coupé par « moins de mouvement ». */
.ml-visa-visuel {
	display: flex;
	justify-content: center;
	color: var(--ml-accent);
}

.ml-visa-illu {
	inline-size: 100%;
	max-inline-size: 14rem;
	block-size: auto;
	overflow: visible;
}

.ml-visa-illu-halo { fill: #fff; opacity: 0.72; }

.ml-visa-illu-couverture { fill: currentColor; }
.ml-visa-illu-page { fill: #fff; }
.ml-visa-illu-globe { fill: none; stroke: currentColor; stroke-width: 2.4; }
.ml-visa-illu-meridiens {
	fill: none;
	stroke: currentColor;
	stroke-width: 1.6;
	opacity: 0.75;
}
.ml-visa-illu-lignes {
	fill: none;
	stroke: currentColor;
	stroke-width: 3.2;
	stroke-linecap: round;
	opacity: 0.28;
}

.ml-visa-illu-tampon > rect {
	fill: none;
	stroke: var(--ml-accent-dark);
	stroke-width: 3;
}
.ml-visa-illu-coche {
	fill: none;
	stroke: var(--ml-accent-dark);
	stroke-width: 3.4;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.ml-visa-illu-trajet {
	fill: none;
	stroke: currentColor;
	stroke-width: 2;
	stroke-linecap: round;
	stroke-dasharray: 3 8;
	opacity: 0.45;
}

.ml-visa-illu-avion { fill: currentColor; }

/* Le passeport respire, le tampon tombe, l'avion passe, les pointillés
   défilent. Une seule durée pour les quatre : c'est ce qui les fait raconter la
   même scène plutôt que quatre boucles qui se croisent au hasard. */
.ml-visa-illu-passeport {
	transform-box: fill-box;
	transform-origin: center;
	animation: ml-visa-flotte 7s ease-in-out infinite;
}

.ml-visa-illu-tampon {
	transform-box: fill-box;
	transform-origin: center;
	opacity: 0;
	animation: ml-visa-tampon 7s ease-out infinite;
}

.ml-visa-illu-trajet { animation: ml-visa-pointilles 7s linear infinite; }

/* L'avion suit EXACTEMENT le tracé dessiné en SVG : les deux courbes doivent
   rester identiques, sinon il vole à côté de sa trajectoire. Sans offset-path,
   il reste simplement au bout du trajet, immobile et juste. */
.ml-visa-illu-avion { opacity: 0; }

@supports (offset-path: path("M0 0")) {
	.ml-visa-illu-avion {
		offset-path: path("M18 158C48 96 152 110 186 38");
		offset-rotate: auto;
		animation: ml-visa-vol 7s ease-in-out infinite;
	}
}

@keyframes ml-visa-flotte {
	50% { translate: 0 -5px; }
}

@keyframes ml-visa-vol {
	0%        { offset-distance: 0%;   opacity: 0; }
	8%        { opacity: 1; }
	46%, 100% { offset-distance: 100%; opacity: 0; }
	40%       { offset-distance: 100%; opacity: 1; }
}

@keyframes ml-visa-pointilles {
	to { stroke-dashoffset: -110; }
}

@keyframes ml-visa-tampon {
	0%, 44%  { opacity: 0; rotate: -26deg; scale: 1.7; }
	52%      { opacity: 1; rotate: -10deg; scale: 0.94; }
	57%      { rotate: -12deg; scale: 1; }
	92%      { opacity: 1; rotate: -12deg; scale: 1; }
	100%     { opacity: 0; rotate: -12deg; scale: 1; }
}

@media (prefers-reduced-motion: reduce) {
	.ml-visa-illu-passeport,
	.ml-visa-illu-tampon,
	.ml-visa-illu-trajet,
	.ml-visa-illu-avion {
		animation: none;
	}

	/* La scène reste lisible, simplement arrêtée à sa fin. */
	.ml-visa-illu-tampon { opacity: 1; rotate: -12deg; }
	.ml-visa-illu-avion { opacity: 1; offset-distance: 100%; }
}

/* L'école concernée d'une ville à plusieurs écoles (page formule, septembre
   2026). Un vrai lien, qui mène à la section de l'école sur la fiche ville :
   monté au dessus du lien couvrant de la carte (.ml-carte-destination-lien::after,
   z-index 0), sans positionner le bloc de texte (voir la règle plus haut). */
.ml-carte-destination-ecole {
  position: relative;
  z-index: 2;
  margin: 0.25em 0 0;
  font-size: var(--ml-fs-sm);
  font-weight: 600;
}
.ml-carte-destination-ecole a {
  color: #fff;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.ml-carte-destination-ecole a:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* ── Hub des formules : filigrane, introduction et tuiles dépliables ────────
   (demandes de la cliente, septembre 2026) */

/* Le globe de fond dérive doucement, jamais plus : c'est un filigrane, pas
   une animation. Coupé pour qui demande moins de mouvement. */
.ml-hub-hero.ml-watermark::before {
  animation: ml-hub-filigrane 26s ease-in-out infinite;
}
@keyframes ml-hub-filigrane {
  0%,
  100% {
    translate: 0 -50%;
    rotate: -1.5deg;
  }
  50% {
    translate: -2% -54%;
    rotate: 1.5deg;
  }
}
@media (prefers-reduced-motion: reduce) {
  .ml-hub-hero.ml-watermark::before {
    animation: none;
  }
}

/* L'introduction, centrée au dessus de la liste : elle annonce la section,
   elle n'est plus une section à elle seule. */
.ml-hub-intro {
  max-inline-size: 78ch;
  margin-inline: auto;
  text-align: center;
}
.ml-hub-intro > :first-child {
  margin-block-start: 0;
}

/* Le résumé : trois lignes, puis un fondu vers le fond de la carte. La hauteur
   est bornée en em (donc en lignes), pour que le fondu tombe toujours au même
   endroit quelle que soit la taille du texte. */
.ml-hub-offre-resume {
  position: relative;
  overflow: hidden;
  max-block-size: calc(3 * 1.6em);
}
.ml-hub-offre-resume::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 1.7em;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--ml-bg));
  pointer-events: none;
}
/* Ouvert : le texte entier, sans fondu. */
.ml-hub-offre-corps:has(.ml-hub-offre-details[open]) .ml-hub-offre-resume {
  max-block-size: none;
}
.ml-hub-offre-corps:has(.ml-hub-offre-details[open]) .ml-hub-offre-resume::after {
  content: none;
}

/* « Lire plus » : une vraie commande, pas un triangle de navigateur. */
.ml-hub-offre-details {
  margin-block-start: auto;
}
.ml-hub-offre-plus {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding-block: 0.4em;
  color: var(--ml-accent);
  font-size: var(--ml-fs-sm);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}
.ml-hub-offre-plus::-webkit-details-marker {
  display: none;
}
.ml-hub-offre-plus:hover {
  color: var(--ml-accent-dark);
}
.ml-hub-offre-chevron {
  transition: rotate 220ms var(--ml-ease);
}
.ml-hub-offre-details[open] .ml-hub-offre-chevron {
  rotate: 180deg;
}
.ml-hub-offre-plus-fermer,
.ml-hub-offre-details[open] .ml-hub-offre-plus-ouvrir {
  display: none;
}
.ml-hub-offre-details[open] .ml-hub-offre-plus-fermer {
  display: inline;
}
@media (prefers-reduced-motion: reduce) {
  .ml-hub-offre-chevron {
    transition: none;
  }
}

/* Le pied déplié : le compte de destinations, puis le bouton vers la page. */
.ml-hub-offre-details .ml-hub-offre-pied {
  flex-direction: column;
  align-items: flex-start;
  gap: var(--ml-space-xs);
}
/* Devenu un vrai bouton : la règle plus haut lui donnait le violet en couleur
   de TEXTE, illisible sur le fond violet du bouton principal. */
.ml-hub-offre-action.ml-btn {
  color: #fff;
  text-decoration: none;
}

/* ── Le haut de page d'accueil sur téléphone ───────────────────────────────
   Quatre retouches demandées par Marino (septembre 2026), toutes sous 700px
   de large, et aucune au dessus : sur grand écran, le haut de page ne bouge
   pas d'un pixel. */

/* 1. De l'air entre la barre de menu et le titre. La réserve du haut était
      calée sur le grand écran, où le menu est plus haut que le titre ; sur un
      téléphone, le titre montait le toucher. La condition de hauteur garde
      l'ancienne valeur sur un écran court, où chaque pixel sert à faire tenir
      le globe et la carte de Linguine (voir la règle max-height: 760px). */
@media (max-width: 700px) and (min-height: 700px) {
  .ml-home-hero {
    /* Deuxième passe (Marino) : le titre descend encore d'un cran, la barre de
       menu avait toujours l'air posée dessus. */
    padding-block-start: calc(var(--ml-header-h) + var(--ml-space-xl) + var(--ml-space-lg) + var(--ml-space-md));
  }
}

@media (max-width: 700px) {
  /* 2. Un titre un peu plus sage : à pleine largeur d'un téléphone, la borne
        basse du clamp (2,5rem) restait trop grosse et mangeait deux lignes de
        plus que nécessaire. */
  .ml-home-hero-texte h1 {
    font-size: clamp(1.9rem, 7.6vw, 2.5rem);
  }

  /* 3. Moins d'écart entre le texte et l'assistant juste en dessous : le
        gap de la colonne, et la marge des deux actions qui la ferment. */
  .ml-container.ml-home-hero-inner {
    gap: var(--ml-space-xs);
  }
  .ml-home-hero-texte .ml-hero-actions {
    margin-block-start: var(--ml-space-xs);
  }
  /* Le paragraphe touche presque les boutons : c'est le même bloc de parole,
     l'écart qui compte est celui d'avant Linguine, resserré juste au dessus. */
  .ml-home-hero-texte h1 {
    margin-bottom: var(--ml-space-sm);
  }

  /* 4. Une ombre plus légère derrière le texte. Les quatre nappes du grand
        écran sont calées sur un titre de 70px ; sur un titre deux fois plus
        petit, elles font une tache sombre au lieu de poser le texte. Trois
        nappes plus claires suffisent, le contraste du blanc sur la photo
        reste tenu par la nappe de contact. */
  .ml-home-hero-texte {
    --ml-hero-ombre-texte:
      0 1px 2px rgba(0, 0, 0, 0.5),
      0 3px 10px rgba(0, 0, 0, 0.4),
      0 6px 26px rgba(0, 0, 0, 0.32);
  }
}
