/* ==========================================================================
   DuplexWeb — feuille de style unique
   ==========================================================================
   Ordre du fichier, à respecter quand on ajoute :
     1. Police               (@font-face)
     2. Variables            (:root)
     3. Remise à zéro        (reset)
     4. Base document        (body, titres, liens)
     5. Éléments partagés    (boutons, décor de fond)
     6. Blocs                (un commentaire de section par bloc, dans
                              l'ordre où ils apparaissent dans la page)
   ========================================================================== */


/* 1. Police =============================================================== */

/* Manrope en fichier VARIABLE : une seule requête de 25 Ko couvre toutes les
   graisses de 200 à 800, là où des fichiers séparés en coûteraient une par
   graisse utilisée. */
@font-face {
  font-family: 'Manrope';
  src: url('/assets/fonts/Manrope.var.woff2') format('woff2-variations');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* Bebas Neue pour les titres. Sous-ensemble « latin » de Google Fonts (8,6 Ko),
   hébergé avec le site : aucun appel à un CDN tiers au chargement.
   Il couvre U+0000–00FF, donc tous les accents français (é, è, à, ç, œ).
   ⚠️ Bebas Neue n'a QU'UNE graisse (400) et QUE des capitales : le bas de
   casse est dessiné en capitales. Tout titre écrit avec sortira en majuscules,
   quoi qu'on tape — ce n'est pas un réglage, c'est la police. */
@font-face {
  font-family: 'Bebas Neue';
  src: url('/assets/fonts/BebasNeue-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}


/* 2. Variables ============================================================ */

:root {
  /* --- Fond ---------------------------------------------------------- */
  /* ⚠️ EXACTEMENT LE FOND D'UBLO, valeur comprise. Charles, le 2026-08-16 :
     « on va mettre le background exactement pareil que celui de ublo.ai ».
     Un noir légèrement bleuté plutôt que #000 : le noir pur écrase les
     dégradés et fait ressortir le banding sur les écrans OLED.
     Les colonnes verticales, la lueur mauve d'angle et le grain qui étaient
     ici ont été RETIRÉS avec cette décision — ne pas les réintroduire sans le
     lui demander. Le décor, c'est le ciel d'étoiles et rien d'autre. */
  --fond: #05060A;
  --texte: #FFFFFF;
  /* Le texte secondaire passe par l'opacité plutôt que par un gris fixe :
     il reste juste quel que soit ce qu'il y a derrière. */
  --texte-doux: rgba(255, 255, 255, 0.62);

  /* --- Mauve de marque ----------------------------------------------- */
  /* ⚠️ LES BOUTONS SONT EN MAUVE PLEIN, PLUS EN DÉGRADÉ. Charles, le
     2026-08-16 : « pour les boutons je veux juste mauve, genre un ti peu plus
     foncé ». Le dégradé bleu → mauve → magenta a été retiré.
     `--mauve` est descendu de #7C5CFF à #6A45E8 : même teinte, un cran plus
     sombre et un peu moins lumineux. */
  --mauve: #6A45E8;
  /* Le survol ÉCLAIRCIT plutôt qu'il n'assombrit : sur un fond presque noir,
     un bouton qui s'assombrit au survol a l'air de s'éteindre. */
  --mauve-survol: #7B58F0;
  /* Gardé pour le liseré de focus et l'accent du hero — il ne sert plus aux
     boutons. */
  --mauve-vif: #B15CFF;

  /* --- Verre --------------------------------------------------------- */
  /* Les trois ingrédients du « liquid glass », regroupés ici pour que
     l'en-tête, les boutons et les pastilles restent cohérents entre eux. */
  --verre-fond: rgba(255, 255, 255, 0.055);
  --verre-bord: rgba(255, 255, 255, 0.11);
  --verre-reflet: inset 0 1px 0 rgba(255, 255, 255, 0.10);
  --verre-flou: blur(18px) saturate(150%);

  /* --- Typographie ---------------------------------------------------- */
  /* Deux polices, deux rôles, et ils ne se mélangent pas : Bebas Neue pour
     les titres (capitales condensées), Manrope pour tout ce qui se lit en
     continu — Bebas est illisible en paragraphe. */
  --police-titre: 'Bebas Neue', 'Manrope', system-ui, sans-serif;
  --police-texte: 'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;

  /* --- Formes --------------------------------------------------------- */
  /* ⚠️ 14 px, PAS UNE PILULE. Charles, le 2026-08-16, capture d'ublo.ai à
     l'appui : « les boutons pas arrondis trop, je veux comme ublo.ai ». Le
     `border-radius: 999px` d'avant en faisait des gélules. C'est la valeur
     exacte d'Ublo (`--radius-btn`). */
  --radius-btn: 14px;

  /* --- Mise en page --------------------------------------------------- */
  --largeur-max: 1320px;
  /* ⚠️ SUR TÉLÉPHONE C'EST LE MINIMUM QUI COMMANDE, pas le `vw`. À 414 px de
     large, 4vw ne fait que 16 px : la valeur retenue est donc le premier
     terme. C'est lui qu'il fallait monter — Charles trouvait le contenu collé
     aux bords. 1,5rem = 24 px de chaque côté. */
  /* ⚠️ CE RÉGLAGE TIENT LES REBORDS DE TOUT LE SITE. À 4vw, un écran de
     1280 px ne laissait que 51 px de marge : trop maigre pour du bureau, et
     ça se voyait surtout sur les sections en deux colonnes. 5vw avec un
     plafond à 4rem donne des rebords francs de 1024 px jusqu'aux très grands
     écrans, sans jamais étouffer le contenu. */
  --gouttiere: clamp(1.5rem, 5vw, 4rem);

  /* --- Jetons portés du vitrine Ublo avec l'en-tête -------------------- */
  --nav-h: 84px;
  --trait: rgba(255, 255, 255, .10);
  --verre: rgba(255, 255, 255, .10);
  --verre-sombre: rgba(10, 12, 18, .55);
  --verre-trait: rgba(255, 255, 255, .26);

  /* Deux familles de mouvement, et elles ne sont pas interchangeables :
     `--t` pour ce qu'on survole, `--t-panneau` pour ce qui se transforme. */
  --ease: cubic-bezier(.22, .9, .28, 1);
  --t: .35s var(--ease);
  --t-panneau: .42s cubic-bezier(.16, 1, .3, 1);
}


/* 3. Remise à zéro ======================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body,
h1, h2, h3, h4, p,
figure, blockquote,
dl, dd { margin: 0; }

ul[role="list"],
ol[role="list"] {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Les médias sont des blocs : sans ça, ils traînent l'espace sous la ligne de
   base du texte et décalent tout ce qui les entoure de quelques pixels. */
img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select { font: inherit; color: inherit; }

/* Un mot très long (une URL collée, un courriel) ne doit pas élargir la page
   sur mobile. */
p, h1, h2, h3, h4 { overflow-wrap: break-word; }


/* 4. Base document ======================================================== */

html { scroll-behavior: smooth; }

/* Respecter le réglage système : une animation de défilement peut déclencher
   des nausées chez les personnes sensibles au mouvement. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  min-height: 100vh;
  background: var(--fond);
  color: var(--texte);
  font-family: var(--police-texte);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.6;
  /* Aucune barre de défilement horizontale, quoi qu'on ajoute dedans. */
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* Tous les titres passent en Bebas Neue. `font-weight: 400` explicite : la
   police n'a que cette graisse, et un `700` hérité ferait fabriquer au
   navigateur un faux gras baveux. */
h1, h2, h3, h4 {
  font-family: var(--police-titre);
  font-weight: 400;
}

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

/* ⚠️ C'EST CETTE RÈGLE QUI GARDE LE CIEL DERRIÈRE TOUT — étoiles ET filantes.
   `.ciel` est un canvas positionné en `z-index: 0`. Or, dans l'ordre de
   peinture du CSS, un élément NON positionné se peint SOUS un élément
   positionné de z-index 0 : une section qui n'aurait ni `position` ni
   `z-index` disparaîtrait donc derrière le ciel.
   En posant la règle une fois sur `main`, toute section ajoutée par la suite
   est au-dessus par construction, sans avoir à y penser. Avant, `.hero` et
   `.defile` portaient chacun leur `z-index: 1` — ça marchait pour eux deux et
   piégeait le troisième. */
main {
  position: relative;
  z-index: 1;
}

/* Le focus doit rester visible : c'est la seule façon de naviguer au clavier.
   Ne jamais poser `outline: none` sans remplaçant. */
:focus-visible {
  outline: 2px solid var(--mauve-vif);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Lien d'évitement : invisible tant qu'on ne l'atteint pas au clavier. */
.saut-contenu {
  position: absolute;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  border-radius: 10px;
  background: var(--mauve);
  transform: translateY(-150%);
  transition: transform 0.2s ease;
}
.saut-contenu:focus-visible { transform: translateY(0); }


/* 5. Éléments partagés ==================================================== */

/* --- Ciel : le champ d'étoiles ------------------------------------------

   PORTÉ TEL QUEL du vitrine Ublo, à la demande de Charles : « le background
   exactement pareil que celui de ublo.ai ». Un seul canvas, peint par
   `js/main.js`, posé juste après `<body>`. Les étoiles et les filantes sont
   donc partout dans le site, pas seulement dans le hero.

   ⚠️ `position: fixed` : les étoiles restent en place pendant qu'on défile,
   comme un ciel lointain. Un ciel qui défilerait avec la page demanderait un
   canvas de la hauteur du document — quelques milliers de pixels à repeindre
   à chaque image, pour un parallaxe que personne ne remarque sur des points
   de 1 px.

   ⚠️ AUCUNE SECTION NE DOIT PORTER DE FOND OPAQUE, sinon elle masque le ciel.
   La couleur de base reste sur `body`, donc SOUS le canvas — c'est elle qui
   évite le flash blanc avant la première peinture.

   ⚠️ `z-index: 0`, et surtout PAS un z-index NÉGATIF : au négatif le canvas
   passerait derrière le fond de `body` et disparaîtrait complètement. */
.ciel {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

/* --- Boutons ------------------------------------------------------------ */

.bouton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /* Rembourrage repris d'Ublo (15 px / 28 px), converti en rem. */
  padding: 0.9375rem 1.75rem;
  border-radius: var(--radius-btn);
  font-weight: 600;
  font-size: 0.9375rem;
  letter-spacing: -0.01em;
  white-space: nowrap;
  transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
}

.bouton--grand {
  padding: 1.0625rem 2rem;
  font-size: 1rem;
}

/* Le bouton mauve et sa matière liquide.

   ⚠️ LE MAUVE PLEIN RESTE LE FOND DE L'ÉLÉMENT, et l'effet n'est qu'une couche
   par-dessus. Si les animations sont gelées — onglet en arrière-plan, réglage
   « mouvement réduit », navigateur ancien — le bouton reste un bouton mauve
   parfaitement lisible. Rien d'essentiel ne dépend de l'animation. C'est la
   leçon payée sur le vitrine Ublo, où un contenu masqué en attendant son
   animation restait invisible pour de bon. */
.bouton--principal {
  position: relative;
  /* Découpe la matière aux coins arrondis du bouton. */
  overflow: hidden;
  /* Enferme les couches dans le bouton : sans ça, le `z-index: -1` des
     pseudo-éléments les ferait passer derrière le bouton lui-même. */
  isolation: isolate;

  background: var(--mauve);
  color: #fff;
  box-shadow:
    0 10px 30px rgba(106, 69, 232, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

/* ---- Couche 1 : la matière qui coule ----
   Trois taches de lumière mauve, floutées, qui dérivent lentement. Le flou est
   ce qui fait « liquide » : sans lui on voit trois disques se promener.

   ⚠️ `z-index: -1` : d'après l'ordre de peinture du CSS, un descendant à
   z-index négatif se peint AU-DESSUS du fond de l'élément mais EN DESSOUS de
   son contenu en ligne. Le texte et la flèche restent donc nets par-dessus,
   sans avoir à les positionner un par un.

   ⚠️ `inset: -40%` : la couche déborde largement du bouton pour qu'aucun bord
   de tache n'entre jamais dans le cadre pendant la dérive. `overflow: hidden`
   la recoupe. On parle d'une centaine de pixels, pas d'un décor géant. */
.bouton--principal::before {
  content: '';
  position: absolute;
  inset: -40%;
  z-index: -1;
  background:
    radial-gradient(38% 55% at 25% 40%, rgba(173, 143, 255, 0.95), transparent 62%),
    radial-gradient(34% 50% at 72% 66%, rgba(118, 66, 255, 0.90), transparent 64%),
    radial-gradient(30% 44% at 52% 18%, rgba(206, 184, 255, 0.55), transparent 66%);
  filter: blur(12px);
  /* `alternate` évite le saut de la fin au début : le mouvement repart en
     sens inverse au lieu de se téléporter. */
  animation: bouton-liquide 9s ease-in-out infinite alternate;
}

/* ---- Couche 2 : le reflet qui balaie ----
   Un éclat diagonal qui traverse le bouton, puis une longue pause. C'est la
   pause qui fait l'effet : un balayage continu deviendrait un gyrophare. */
.bouton--principal::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    105deg,
    transparent 38%,
    rgba(255, 255, 255, 0.30) 50%,
    transparent 62%);
  transform: translateX(-130%);
  animation: bouton-reflet 7s ease-in-out infinite;
}

@keyframes bouton-liquide {
  0%   { transform: translate3d(-6%, -4%, 0) scale(1.06) rotate(0deg); }
  50%  { transform: translate3d(5%,  3%, 0)  scale(1.16) rotate(9deg); }
  100% { transform: translate3d(-3%, 5%, 0)  scale(1.09) rotate(-7deg); }
}

@keyframes bouton-reflet {
  /* Immobile hors cadre les deux tiers du temps, puis il traverse. */
  0%, 62%  { transform: translateX(-130%); }
  100%     { transform: translateX(130%); }
}

.bouton--secondaire {
  background: var(--verre-fond);
  border: 1px solid var(--verre-bord);
  -webkit-backdrop-filter: var(--verre-flou);
          backdrop-filter: var(--verre-flou);
  box-shadow: var(--verre-reflet);
}

/* Le survol ne s'applique qu'aux appareils qui en ont un : sur tactile, un
   `:hover` reste collé après le tap. */
@media (hover: hover) {
  .bouton--principal:hover {
    background: var(--mauve-survol);
    transform: translateY(-1px);
    box-shadow:
      0 14px 38px rgba(106, 69, 232, 0.45),
      inset 0 1px 0 rgba(255, 255, 255, 0.30);
  }
  .bouton--secondaire:hover {
    background: rgba(255, 255, 255, 0.10);
    transform: translateY(-1px);
  }
}

.fleche {
  width: 1em;
  height: 1em;
  flex: none;
}


/* 6. Blocs ================================================================ */

/* --- En-tête -------------------------------------------------------------

   PORTÉ DU VITRINE UBLO, à la demande de Charles : « le header pareil que
   ublo.ai ». Logo à gauche, liens au centre, action à droite — et AUCUN fond
   en haut de page : le contenu passe dessous sans couture.

   Dès qu'on descend, la barre se RESSERRE en une pastille de verre flottante,
   détachée des bords. Ce n'est jamais une bande d'un bord à l'autre avec un
   trait au travers — cette version-là a existé sur Ublo et Charles l'a
   refusée. Si l'on remet un fond un jour, il doit rester une BOÎTE DÉTACHÉE.

   ⚠️ `fixed` est voulu. Ne pas le « corriger » en `absolute`.
   ------------------------------------------------------------------------- */

.nav-wrap {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  /* ⚠️ ELLE NE DOIT PAS INTERCEPTER LES CLICS. Elle couvre toute la largeur de
     l'écran en permanence : sans ça, une bande invisible de 84 px avalerait
     tous les clics en haut de chaque section pendant tout le défilement.
     Seule la boîte, elle, reprend les événements. */
  pointer-events: none;
  /* Même raison que la gouttière : à 414 px, 3vw ne fait que 12 px. */
  padding: 10px clamp(16px, 3vw, 28px) 0;
}

/* ⚠️ UNE GRILLE `1fr auto 1fr`, ET SURTOUT PAS `space-between`. Les deux
   rendent la même image quand les côtés pèsent pareil — sauf qu'ils ne pèsent
   jamais pareil. Avec `space-between`, le groupe du milieu se retrouve poussé
   de plusieurs pixels à côté du centre de la page.
   ⚠️ ET C'EST CE QUI EMPÊCHE LES LIENS DE BOUGER PENDANT LA TRANSFORMATION :
   la boîte est centrée dans les deux états, donc le centre de sa colonne du
   milieu est le centre de l'écran, à toute largeur. Elle peut se resserrer :
   les liens ne se déplacent pas d'un pixel, seuls le logo et le bouton
   glissent vers l'intérieur. */
.nav-boite {
  pointer-events: auto;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  height: var(--nav-h);
  max-width: 100%;
  margin-inline: auto;
  margin-top: 0;
  /* ⚠️ 6 px NE SUFFISAIENT PAS. Sur une fenêtre étroite, le bouton d'appel
     arrivait au ras du bord arrondi et sa lueur débordait de la pastille : le
     logo avait 17 px d'air à gauche, le bouton zéro à droite. */
  padding-inline: clamp(12px, 1.8vw, 22px);
  border: 1px solid transparent;
  border-radius: 22px;
  /* ⚠️ LA LARGEUR EST LA SEULE PROPRIÉTÉ DE MISE EN PAGE QUI BOUGE, et c'est
     délibéré. Tout le reste (fond, liseré, arrondi, ombre) est de la peinture,
     qui ne coûte rien. Ne pas ajouter d'animation sur les marges des enfants :
     on paierait un recalcul de mise en page à chaque image du défilement. */
  transition:
    max-width var(--t-panneau),
    height var(--t-panneau),
    margin-top var(--t-panneau),
    padding var(--t-panneau),
    background-color var(--t-panneau),
    border-color var(--t-panneau),
    box-shadow var(--t-panneau);
}

/* Les trois colonnes. Le hamburger partage la troisième avec les actions :
   ils ne sont jamais visibles en même temps. */
.nav-boite > .nav-logo    { grid-column: 1; justify-self: start; }
.nav-boite > .nav-liens   { grid-column: 2; justify-self: center; }
.nav-boite > .nav-actions { grid-column: 3; justify-self: end; }
.nav-boite > .nav-burger  { grid-column: 3; justify-self: end; }

.nav-boite.est-condense {
  /* ⚠️ 970 ET NON 940 : les 30 px rendus compensent le rembourrage passé de 7 à
     22 px, pour que la zone de contenu ne rétrécisse pas. */
  max-width: min(970px, 100%);
  height: 62px;

  /* ⚠️ MÊME AIR DES DEUX CÔTÉS QU'À L'ÉTAT OUVERT. À 7 px, le bouton d'appel
     n'avait que 8 px avant le bord contre 23 pour le logo — et dans le coin
     arrondi la garde réelle tombait à 4 px, sa lueur violette débordant du
     verre. C'est ce que Charles voyait. */
  padding-inline: 22px;

  /* ⚠️ CES 11 px EMPÊCHENT LES LIENS DE MONTER, et le compte est exact.
     La boîte perd 22 px de hauteur (84 → 62) : sans compensation, son centre —
     donc le milieu du texte — remonterait de 11 px pendant la transformation.
       ouvert   : 10 (rembourrage de `.nav-wrap`) + 84 ÷ 2 = 52
       condensé : 10 + 11 + 62 ÷ 2                          = 52
     ⚠️ ET ÇA TIENT PENDANT TOUTE LA TRANSITION : les deux propriétés s'animent
     avec la même durée et la même courbe, donc à tout instant t le centre vaut
     10 + 11t + (84 − 22t) ÷ 2 = 52. La hauteur et la marge doivent rester
     ensemble dans la liste des transitions — en retirer une remettrait le
     glissement. */
  margin-top: 11px;

  /* ⚠️ PAS DE `--verre-relief` ICI. Ce reflet interne clair en haut donne
     l'épaisseur du matériau sur un BOUTON de 48 px ; sur une barre large il
     longe tout le bord supérieur et l'œil le lit comme une deuxième ligne
     collée à la première — donc comme un cadre épais. Sur Ublo, Charles l'a
     fait retirer pour cette raison. Le floutage reste : c'est lui qui fait le
     verre. Ce qu'on retire, c'est le CADRE, pas la matière. */
  background: rgba(11, 13, 19, .62);
  border-color: var(--trait);
  -webkit-backdrop-filter: var(--verre-flou);
          backdrop-filter: var(--verre-flou);
  /* Une ombre portée simple, sans reflet interne : elle décolle la pastille du
     contenu qui passe dessous, ce qu'un `inset` ne fait pas. */
  box-shadow: 0 10px 34px rgba(0, 0, 0, .42);
}

/* Le logo a besoin d'un peu d'air à gauche une fois la boîte refermée sur lui.
   Le rembourrage de la boîte, lui, reste SYMÉTRIQUE : le centre de la zone de
   contenu vaut (largeur + gauche − droite) ÷ 2, donc un rembourrage asymétrique
   décalerait la colonne du milieu. L'air en plus du logo passe par SA marge. */
/* ⚠️ PLUS DE MARGE ICI. Elle rendait au logo les 15 px que le rembourrage
   réduit lui prenait — mais le bouton d'appel, lui, ne récupérait rien, d'où le
   déséquilibre. Le rembourrage étant redevenu symétrique, les deux côtés sont
   servis par la même règle. */

/* ⚠️ LE BOUTON CHANGE DE VERRE UNE FOIS DEDANS. Le bouton principal garde son
   dégradé, mais un éventuel bouton de verre poserait le même problème que sur
   Ublo : deux surfaces identiques superposées ne se distinguent plus.
   ⚠️ ET IL PERD SON PROPRE FLOUTAGE : la pastille floute déjà le fond pour
   tout le monde, et un `backdrop-filter` imbriqué dans un autre est payé deux
   fois pour un résultat qu'on ne voit pas. */
.nav-boite.est-condense .nav-burger {
  background: var(--verre);
  border-color: rgba(255, 255, 255, .16);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  box-shadow: none;
}

/* ⚠️ 68 px, PAS LES 26 px D'UBLO. Le mot-symbole d'Ublo est court et compact,
   celui de DuplexWeb fait huit lettres capitales sur un rapport de 2,42
   (320 × 132) : à hauteur égale il paraît beaucoup plus petit et le dégradé de
   ses barres disparaît. Passé de 26 à 34 puis à 68 px sur deux demandes de
   Charles. C'est la HAUTEUR qui se règle, jamais la largeur.
   68 px dans une barre de 84 laisse 8 px au-dessus et en dessous. */
.nav-logo img {
  height: 68px;
  width: auto;
  /* Il rétrécit avec la barre — voir la règle suivante. La transition partage
     la durée et la courbe de `.nav-boite`, sinon le logo finirait sa course
     avant ou après elle et on verrait deux mouvements au lieu d'un. */
  transition: height var(--t-panneau);
}

/* ⚠️ LE LOGO DOIT RÉTRÉCIR AVEC LA PASTILLE, contrairement à Ublo où il ne
   bouge pas. Là-bas 26 px tiennent dans les deux états ; ici 68 px ne rentrent
   pas dans une pastille de 62 px de haut — le logo dépasserait de la boîte.
   46 px dans 62 laisse les mêmes 8 px de part et d'autre que l'état ouvert :
   l'écart au bord se lit identique avant et après la transformation. */
.nav-boite.est-condense .nav-logo img { height: 46px; }

/* ⚠️ LE BOUTON AUSSI SE MET À 46 px, ET IL PERD SA LUEUR. À 54 px dans une
   pastille de 62, il ne laissait que 4 px de part et d'autre — la moitié des
   8 px que le logo respecte, et la clairance dans le coin arrondi tombait à
   4 px elle aussi. Sa lueur de 30 px de flou débordait alors forcément du
   verre, quel que soit l'air horizontal. Même raison que le floutage du burger,
   neutralisé ici pour le même motif : ce qui déborde d'une pastille se voit. */
.nav-boite.est-condense .nav-actions .bouton {
  height: 46px;
  padding-block: 0;
  box-shadow: none;
}

.nav-actions { display: flex; align-items: center; gap: 8px; }

/* ---------- Liens de navigation ---------- */

.nav-liens {
  display: flex;
  align-items: center;
  gap: 2px;
}

/* ⚠️ `> a` ET NON `a`. Le panneau déroulant contient lui aussi des liens : sans
   l'enfant direct, ils héritaient du style des onglets de la barre. */
.nav-liens > a,
.nav-deroulant__bouton {
  padding: 12px 16px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -.01em;
  line-height: 1;
  color: rgba(255, 255, 255, .70);
  /* Invisible sur le fond spatial, décisive si une lueur passe derrière. */
  text-shadow: 0 1px 10px rgba(0, 0, 0, .75);
  transition: color var(--t), background-color var(--t);
}

@media (hover: hover) {
  .nav-liens > a:hover,
  .nav-deroulant__bouton:hover { color: var(--texte); background: rgba(255, 255, 255, .10); }
}

/* --- Le déroulant des services ----------------------------------------------

   ⚠️ IL S'APPELLE `nav-deroulant` PARCE QUE `nav-menu` EST DÉJÀ PRIS par le
   menu mobile. Deux choses différentes, deux noms.

   ⚠️ NI FLOU NI `backdrop-filter` : le panneau est un aplat opaque. C'est la
   règle de ce projet, et elle vaut aussi pour une barre fixe posée sur un fond
   d'étoiles qui défile — un flou s'y repeindrait à chaque image du site. */
.nav-deroulant { position: relative; }

.nav-deroulant__bouton {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 0;
  background: transparent;
  /* ⚠️ `font-family`, PAS le raccourci `font`. Un `<button>` n'hérite pas de la
     police par défaut, d'où cet `inherit` — mais le raccourci remettait AUSSI
     `font-size`, `font-weight` et `line-height` à leurs valeurs héritées
     (17px/400), écrasant le 14px/600 posé plus haut avec les liens de la
     barre : « Services » sortait énorme à côté de « Contact ». */
  font-family: inherit;
  cursor: pointer;
}

.nav-deroulant__chevron {
  width: 11px;
  height: 11px;
  opacity: 0.7;
  transition: transform 0.34s var(--ease);
}

.nav-deroulant.est-ouvert .nav-deroulant__chevron {
  transform: rotate(180deg);
  opacity: 1;
}

/* ⚠️ OUVERT, LE BOUTON N'EST PAS « SURVOLÉ » : il est ACTIF. Le gris du survol
   lui donnait l'air d'un bloc collé sous le curseur ; ce fond violet le
   rattache au panneau qu'il vient d'ouvrir. */
.nav-deroulant.est-ouvert .nav-deroulant__bouton {
  color: var(--texte);
  background: rgba(124, 92, 255, 0.16);
}

.nav-deroulant__panneau {
  position: absolute;
  top: calc(100% + 13px);
  left: 50%;
  z-index: 5;
  width: 322px;
  margin-left: -161px;
  padding: 8px;
  border: 1px solid var(--trait);
  border-radius: 18px;
  background: rgba(12, 12, 18, 0.97);
  box-shadow: 0 28px 70px rgba(0, 0, 0, 0.65);
  text-align: left;

  /* Fermé : ramassé vers le haut et effacé. `transform-origin` en haut, c'est
     ce qui le fait se DÉPLIER depuis le bouton au lieu de grandir en l'air. */
  opacity: 0;
  transform: translateY(-12px) scale(0.94);
  transform-origin: 50% 0;
  pointer-events: none;

  /* ⚠️ `visibility`, ET PAS SEULEMENT L'OPACITÉ. Un panneau à opacité zéro reste
     dans l'ordre de tabulation : au clavier, on traversait trois liens
     invisibles après « Services ». `visibility: hidden` les en sort.
     ⚠️ ET SA TRANSITION EST À DURÉE NULLE AVEC UN RETARD. C'est ce qui la rend
     compatible avec un fondu : à l'ouverture elle bascule tout de suite, à la
     fermeture elle attend que le fondu soit fini — sinon le panneau
     disparaîtrait d'un coup au lieu de s'effacer. */
  visibility: hidden;
  transition: opacity 0.2s var(--ease), transform 0.34s var(--ease),
              visibility 0s linear 0.34s;
}

/* Le filet violet du haut : la même couleur que le bouton d'appel, pour que le
   panneau appartienne visiblement à cette barre. */
.nav-deroulant__panneau::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 22%;
  right: 22%;
  height: 1px;
  border-radius: 999px;
  background: linear-gradient(to right,
    rgba(124, 92, 255, 0), rgba(124, 92, 255, 0.85), rgba(124, 92, 255, 0));

  /* Il se déploie du centre vers les bords, un souffle après le panneau. */
  opacity: 0;
  transform: scaleX(0.25);
  transition: transform 0.42s var(--ease) 0.05s, opacity 0.3s var(--ease);
}

.nav-deroulant.est-ouvert .nav-deroulant__panneau::before {
  opacity: 1;
  transform: none;
}

.nav-deroulant.est-ouvert .nav-deroulant__panneau {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  visibility: visible;
  transition: opacity 0.2s var(--ease), transform 0.34s var(--ease), visibility 0s;
}

.nav-deroulant__item {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 14px;
  padding: 12px 14px;
  border-radius: 13px;
  text-decoration: none;

  opacity: 0;
  transform: translateY(9px);
  transition: opacity 0.24s var(--ease), transform 0.36s var(--ease);
}

/* Le filet qui pousse sur le côté au survol. Il est en `::before` et il grandit
   en hauteur, donc rien ne se recalcule : ni marge, ni bordure. */
.nav-deroulant__item::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 50%;
  width: 2px;
  height: 0;
  border-radius: 999px;
  background: #7C5CFF;
  transform: translateY(-50%);
  transition: height 0.26s var(--ease);
}

/* ⚠️ LE DÉCALAGE N'EXISTE QU'À L'OUVERTURE. Posé sur l'item lui-même, il
   s'appliquerait aussi à la fermeture : les trois lignes partiraient l'une
   après l'autre et le panneau traînerait. Ici elles s'effacent ensemble. */
.nav-deroulant.est-ouvert .nav-deroulant__item { opacity: 1; transform: none; }
.nav-deroulant.est-ouvert .nav-deroulant__item:nth-child(1) { transition-delay: 0.06s; }
.nav-deroulant.est-ouvert .nav-deroulant__item:nth-child(2) { transition-delay: 0.11s; }
.nav-deroulant.est-ouvert .nav-deroulant__item:nth-child(3) { transition-delay: 0.16s; }

/* ⚠️ ALIGNÉ SUR LA PREMIÈRE LIGNE, PAS CENTRÉ SUR LES DEUX. Centré, le numéro
   tombait entre le nom et sa note : il flottait au lieu de numéroter. */
.nav-deroulant__num {
  grid-row: 1 / 3;
  align-self: start;
  padding-top: 2px;
  color: rgba(255, 255, 255, 0.44);
  font-family: var(--police-titre);
  font-size: 13px;
  letter-spacing: 0.12em;
  transition: color var(--t);
}

.nav-deroulant__nom {
  color: var(--texte);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
}

.nav-deroulant__note {
  margin-top: 2px;
  color: rgba(255, 255, 255, 0.42);
  font-size: 12px;
  line-height: 1.3;
}

/* Le survol est instantané : le décalage d'ouverture porte déjà sur l'opacité
   et la position, l'ajouter au fond ferait traîner le pointeur. */
@media (hover: hover) {
  .nav-deroulant__item:hover { background: rgba(255, 255, 255, 0.055); }
  .nav-deroulant__item:hover .nav-deroulant__num { color: #7C5CFF; }
  .nav-deroulant__item:hover::before { height: 22px; }
}

.nav-deroulant__item:focus-visible {
  outline: 2px solid #7C5CFF;
  outline-offset: 2px;
}

/* Sous-liens du menu mobile : décalés, plus discrets que leur parent. */
.nav-menu-sous {
  padding-left: 18px;
  color: rgba(255, 255, 255, 0.55);
  font-size: 15px;
  text-decoration: none;
}

@media (prefers-reduced-motion: reduce) {
  .nav-deroulant__panneau,
  .nav-deroulant__item { transition-duration: 0.01ms; transform: none; }
  .nav-deroulant__chevron { transition: none; }
}

.nav-liens a[aria-current="page"] {
  color: var(--texte);
  background: rgba(255, 255, 255, .16);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .18);
}

/* ---------- Menu mobile ---------- */

.nav-burger {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  padding: 0 11px;
  border-radius: 12px;
  background: var(--verre-sombre);
  border: 1px solid var(--verre-trait);
  -webkit-backdrop-filter: var(--verre-flou);
          backdrop-filter: var(--verre-flou);
  cursor: pointer;
}

.nav-burger span {
  display: block;
  height: 2px;
  width: 100%;
  border-radius: 2px;
  background: var(--texte);
  transition: transform var(--t), opacity var(--t);
}

.nav-burger.ouvert span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-burger.ouvert span:nth-child(2) { opacity: 0; }
.nav-burger.ouvert span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ⚠️ Le panneau est masqué par `visibility`, PAS par `display: none`.
   `display` ne s'anime pas : avec lui, le panneau apparaîtrait d'un coup.
   `visibility` le retire des clics et du parcours au clavier tout en laissant
   l'opacité et le glissement s'animer. */
.nav-menu {
  position: fixed;
  inset: var(--nav-h) 16px auto 16px;
  z-index: 99;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  border-radius: 24px;
  background: rgba(11, 13, 19, .92);
  border: 1px solid var(--verre-trait);
  -webkit-backdrop-filter: var(--verre-flou);
          backdrop-filter: var(--verre-flou);
  box-shadow: 0 30px 60px rgba(0, 0, 0, .6);

  visibility: hidden;
  opacity: 0;
  transform: translateY(-14px);
  transition: opacity var(--t), transform var(--t), visibility var(--t);
}

.nav-menu.ouvert {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
}

.nav-menu-lien {
  padding: 12px 14px;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--texte);
  transition: background-color var(--t);
}
.nav-menu-lien:hover { background: rgba(255, 255, 255, .08); }

/* Le bouton d'appel à l'action ferme la pile : c'est une autre nature de
   commande que les liens, on ne les lit pas dans le même bloc. */
.nav-menu .bouton { margin-top: 8px; justify-content: center; }

/* --- Hero --------------------------------------------------------------- */

.hero {
  max-width: var(--largeur-max);
  margin-inline: auto;
  padding-inline: var(--gouttiere);
  /* Le haut dégage l'en-tête flottant ; le bas laisse la place au bloc
     suivant (l'arc de réalisations), qui viendra chevaucher cette zone. */
  /* Le rythme vertical est relevé sur la référence (fenêtre de 1281 × 776) :
     badge à 25 % de la hauteur, bas des boutons à 78 %, l'arc démarrant juste
     en dessous et débordant sous la ligne de flottaison. Tout est exprimé en
     `vh` pour que ces proportions tiennent quelle que soit la fenêtre. */
  padding-block: calc(var(--nav-h) + clamp(3.5rem, 16.2vh, 18rem)) 0;
  text-align: center;
}

.hero__titre {
  /* Cette marge était l'écart entre la pastille et le titre. La pastille a été
     retirée le 2026-08-16 (Charles) et le titre est devenu le premier élément
     du hero : la marge reste, mais comme simple décalage sous l'en-tête. */
  margin-top: clamp(1.25rem, 4.2vh, 6rem);
  /* Le titre est l'élément le plus gros de la page : il se cale sur la
     largeur de la fenêtre, pas sur une taille fixe.
     Bebas est condensée et sans jambages descendants : à taille égale elle
     paraît plus petite qu'une grotesque, d'où une échelle plus généreuse.
     6.4vw est relevé sur la référence (≈82 px pour une fenêtre de 1281). Le
     plafond en `rem` est très haut à dessein : à 6.25rem il entrait en action
     dès 1300 px de large et bridait le titre sur tous les grands écrans. */
  /* Le `min(…, 13vh)` est un garde-fou : sur une fenêtre large mais basse,
     une taille purement en `vw` fabrique un titre si haut qu'il repousse les
     boutons hors de l'écran. */
  /* ⚠️ Le plafond n'est pas arbitraire : il vaut 6,4 % de `--largeur-max`
     (1320px → 84px). Le conteneur du hero est plafonné à cette largeur ; si la
     taille du titre continuait de suivre la fenêtre au-delà, il déborderait de
     son propre conteneur et passerait sur trois lignes. Constaté à 2560px de
     large avec un plafond trop haut. */
  font-size: clamp(3rem, min(6.4vw, 13vh), 5.4rem);
  /* Des capitales n'ont ni hampes ni jambages : l'interligne peut descendre
     sous 1 sans que les lignes se touchent. */
  line-height: 0.92;
  /* Un chouïa d'écart, à l'inverse d'une grotesque : des capitales condensées
     serrées deviennent un mur illisible. */
  letter-spacing: 0.012em;
  /* Répartit les lignes au lieu de remplir la première au maximum : c'est ce
     qui évite un dernier mot tout seul. Ignoré par les navigateurs qui ne le
     connaissent pas, sans conséquence. */
  text-wrap: balance;
}

.hero__soustitre {
  margin-top: clamp(1rem, 4.9vh, 6rem);
  margin-inline: auto;
  max-width: 34rem;
  text-wrap: pretty;
  color: var(--texte-doux);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem);
}

.hero__actions {
  margin-top: clamp(1.5rem, 4.6vh, 5.5rem);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
}




/* --- Défilé des logos de clients ----------------------------------------

   Un carrousel EN PROFONDEUR, pas une rangée plate. Le logo du centre est
   grand, net et devant ; ses voisins reculent, pivotent vers lui, rétrécissent
   et s'effacent. C'est la mise en évidence demandée par Charles.

   ⚠️ LA MISE EN PAGE 3D N'EXISTE QUE SI LE SCRIPT TOURNE. Par défaut les
   logos sont une simple rangée en ligne, donc VISIBLES même sans JavaScript ;
   c'est `js/main.js` qui pose la classe `est-3d` et prend la main sur les
   positions. Ne pas mettre `position: absolute` par défaut : les quatorze
   logos se superposeraient en un tas illisible si le script ne s'exécutait
   pas.
   ------------------------------------------------------------------------- */

.defile {
  /* ⚠️ LES DEUX ESPACES SONT DISSYMÉTRIQUES À DESSEIN. Le libellé appartient
     aux logos, pas aux boutons : il doit être nettement plus près de ce qu'il
     annonce que de ce qui le précède. Réglé sur demande de Charles, qui le
     trouvait collé aux boutons et détaché des logos. */
  padding-top: clamp(3rem, 8vh, 6rem);
  padding-bottom: clamp(1.5rem, 4vh, 3rem);
}

/* Le libellé au-dessus des logos. En Manrope et NON en Bebas Neue : à cette
   taille, des capitales condensées deviennent un pâté. L'interlettrage élargi
   compense le fait que ce soit écrit tout petit — c'est ce qui le fait lire
   comme une étiquette et non comme un titre raté. */
.defile__titre {
  /* ⚠️ CE LIBELLÉ EST UN `h2` DEPUIS LE 2026-08-26 — la section n'avait
     aucun titre et restait donc absente du plan de la page pour Google.
     La règle `h1,h2,h3,h4` le passerait en Bebas Neue : on le ramène à
     Manrope, sinon le rendu change. Le `font-weight: 600` ci-dessous
     l'emporte déjà sur le 400 de cette même règle. */
  font-family: var(--police-texte);
  /* Petit écart : le cadre du défilé porte déjà une trentaine de pixels d'air
     au-dessus des logos (il est plus haut qu'eux pour laisser passer le logo
     agrandi du centre). Une marge généreuse ici s'ajouterait à cet air et
     éloignerait le libellé de ce qu'il annonce. */
  margin-bottom: clamp(0.25rem, 0.8vh, 0.6rem);
  text-align: center;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.42);
}

.defile__cadre {
  --defile-largeur: min(1100px, 92vw);

  /* ⚠️ L'ÉCART ENTRE DEUX LOGOS DOIT RESTER PLUS GRAND QUE LEUR LARGEUR, sinon
     ils se chevauchent en bouillie. Et la largeur d'un logo ne rétrécit PAS
     comme l'écran : le pas était à 22 % de la largeur du cadre, ce qui donne
     242 px sur un écran de bureau (correct) mais 79 px sur un téléphone —
     alors que le plus large des logos fait ~200 px une fois agrandi au centre.
     C'est exactement ce que Charles a vu : un tas illisible.
     Le pourcentage monte donc sur les petits écrans (voir les adaptations en
     bas de fichier), et les logos y rétrécissent en même temps. */
  /* ⚠️ UN RATIO SANS UNITÉ, PAS UN `calc()`. `getComputedStyle` NE RÉSOUT PAS
     les `calc()` d'une variable personnalisée : il rend la chaîne telle quelle,
     donc `parseFloat` y répond `NaN` et le script retombait silencieusement sur
     sa valeur de repli — le réglage mobile ci-dessous n'aurait servi à rien.
     Un nombre nu se lit sans ambiguïté, et c'est le script qui multiplie. */
  --pas-ratio: 0.22;

  /* Ces trois-là sont LUES PAR `js/main.js`. Les décisions de mise en page
     restent ainsi dans la feuille de style, y compris leurs variantes par
     taille d'écran — le script ne fait que les appliquer. */
  --defile-grand: 1.5;        /* échelle du logo mis en évidence */
  --defile-profondeur: 120;   /* px de recul par cran */

  width: var(--defile-largeur);
  margin-inline: auto;
  overflow: hidden;

  /* La distance de l'œil. En `vw` et non en pixels : les logos sont
     dimensionnés en fonction du cadre, donc si l'œil restait à distance fixe
     la déformation enflerait sur les grands écrans. Leçon déjà payée sur
     l'arc de cartes. */
  perspective: clamp(700px, 60vw, 1500px);

  /* Le fondu sur les côtés : les logos ne disparaissent pas d'un coup au bord,
     ils s'effacent. C'est un masque, donc il marche quelle que soit la couleur
     derrière — contrairement à un dégradé opaque posé par-dessus, qui devrait
     connaître le fond et masquerait aussi les étoiles. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 20%, #000 80%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 20%, #000 80%, transparent 100%);

  cursor: grab;
}
.defile__cadre.est-attrape { cursor: grabbing; }

.defile__piste {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3rem;
  user-select: none;
  -webkit-user-select: none;
}

/* --- L'état 3D, posé par le script --- */

.defile__cadre.est-3d {
  height: 150px;
  position: relative;
}

.defile__cadre.est-3d .defile__piste {
  display: block;
  height: 100%;
  transform-style: preserve-3d;
}

/* Chaque logo est placé à la main par le script : sa position, sa profondeur,
   sa rotation, sa taille et son opacité dépendent de sa distance au centre. */
.defile__cadre.est-3d .defile__item {
  position: absolute;
  left: 50%;
  top: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Le script écrit `transform` à chaque image : aucune transition ici, sinon
     chaque écriture déclencherait une interpolation par-dessus l'autre et le
     mouvement deviendrait pâteux. */
  will-change: transform, opacity;
}

.defile__item img {
  width: auto;
  -webkit-user-drag: none;
}

/* ⚠️ LA HAUTEUR DÉPEND DU RAPPORT DE CHAQUE LOGO, ET CE N'EST PAS UN CAPRICE.
   Les quatorze logos vont d'un carré (JackFit, rapport 1,00) à une bande très
   allongée (Leroux, 4,32). À hauteur égale, le carré paraît trois fois plus
   gros que la bande ; à largeur égale, c'est l'inverse. Les hauteurs
   ci-dessous suivent `hauteur = 62 / racine(rapport)`, ce qui égalise la
   SURFACE d'encre — c'est elle que l'œil compare, pas la hauteur.
   Le script les agrandit ensuite selon la distance au centre. */
.defile__item--carre       img { height: 60px; }
.defile__item--moyen       img { height: 51px; }
.defile__item--allonge     img { height: 46px; }
.defile__item--large       img { height: 40px; }
.defile__item--tres-large  img { height: 32px; }

/* Le sceau SHTS est une forme PLEINE (70 % de son cadre est de l'encre, contre
   9 % pour un mot-symbole filaire). À surface égale il écrase ses voisins :
   il descend donc sous la règle générale. */
.defile__item--sceau       img { height: 48px; }


/* --- Résultat en vedette -------------------------------------------------

   UNE réussite client à la fois, posée entre deux filets d'un pixel.

   ⚠️ RIEN NE DOIT AVOIR DE FOND. Charles, le 2026-08-19, capture à l'appui :
   « aucun fond de carte, aucun gros rectangle arrondi, fond transparent afin
   de conserver le fond noir étoilé existant ». Toute la composition tient donc
   dans des filets : deux horizontaux, deux verticaux entre les colonnes. Si un
   jour quelque chose doit être mis en valeur ici, ça se fait par l'espace et la
   couleur du texte, JAMAIS par un panneau.

   ⚠️ LA MINUTERIE EST DANS LE POINT ACTIF, PAS DANS LE FILET DU BAS. Charles a
   refusé le filet qui se remplissait sur toute la largeur (« je ne veux pas le
   loading effect sur la barre d'en bas ») puis a demandé la même idée en tout
   petit : le point courant s'étire en capsule et se remplit en mauve. Ne pas
   remettre de remplissage sur un élément qui traverse la section.
   La cadence vit dans `DUREE` (`js/main.js`) : trois secondes par client.
   ------------------------------------------------------------------------- */

.vedette {
  padding-block: clamp(3rem, 9vh, 5.5rem);

  /* Une seule mesure pour l'écart entre les colonnes ET pour la position des
     filets verticaux : ils sont posés au MILIEU de cet écart, et un filet
     décalé d'un demi-espace se voit tout de suite. */
  --espace: clamp(1.5rem, 4vw, 3.5rem);
}

/* La bande reprend exactement la boîte du reste du site : les filets
   commencent donc sur la même marge que le titre de l'en-tête et que la
   section équipe, et non sur le bord de l'écran. */
.vedette__bande {
  max-width: var(--largeur-max);
  padding-inline: var(--gouttiere);
  margin-inline: auto;
}

.vedette__titre {
  /* ⚠️ CE LIBELLÉ EST UN `h2` DEPUIS LE 2026-08-26 — la section n'avait
     aucun titre et restait donc absente du plan de la page pour Google.
     La règle `h1,h2,h3,h4` le passerait en Bebas Neue : on le ramène à
     Manrope, sinon le rendu change. Le `font-weight: 600` ci-dessous
     l'emporte déjà sur le 400 de cette même règle. */
  font-family: var(--police-texte);
  text-align: center;
  margin-bottom: clamp(1rem, 2.6vh, 1.6rem);
  color: var(--mauve-vif);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
}

/* Les deux filets qui encadrent la composition. Rien d'autre ne la dessine. */
.vedette__scene {
  border-top: 1px solid var(--trait);
  border-bottom: 1px solid var(--trait);
}

.vedette__cas {
  display: grid;
  /* `1fr auto 1fr` : les deux colonnes de côté sont égales, donc le chiffre
     tombe au MILIEU de la bande — pas au milieu de ce qui reste une fois le
     logo placé. C'est ce qui tient la composition d'un client à l'autre,
     malgré des logos de largeurs très différentes. */
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--espace);
  padding-block: clamp(1.6rem, 4vh, 2.4rem);
}

/* Sans script, chaque cas a besoin de son propre filet pour ne pas se coller
   au suivant. Le script les empile : ce filet disparaît alors. */
.vedette__cas + .vedette__cas { border-top: 1px solid var(--trait); }

.vedette__logo { justify-self: center; }

/* ⚠️ LA HAUTEUR EST PROPRE À CHAQUE LOGO, posée en `--logo-h` dans le HTML.
   Même règle que le défilé : `hauteur = 110 / racine(rapport)`, ce qui égalise
   la SURFACE d'encre. À hauteur égale, la bande d'Aestorm (rapport 3,45)
   écraserait le bloc de Ton Prochain Cell (1,93).
   110 et non 80 : à 80, le logo ne pesait plus rien face au chiffre et la
   colonne de gauche paraissait vide — ce n'est pas l'équilibre de la capture
   de Charles, où le logo tient tête au chiffre. */
.vedette__logo img {
  display: block;
  height: var(--logo-h, 48px);
  width: auto;
  -webkit-user-drag: none;
}

.vedette__chiffre {
  font-family: var(--police-titre);
  font-size: clamp(2.4rem, 5.4vw, 4rem);
  line-height: 1;
  letter-spacing: 0.012em;
  text-align: center;
  /* Le chiffre ne se casse jamais en deux lignes : c'est LE point de la
     section, et « +52 000 » au-dessus de « $ » ne veut plus rien dire. */
  white-space: nowrap;
}

/* ⚠️ LA PHRASE EST LA SUITE DU CHIFFRE, PAS UNE LÉGENDE. « +52 000 $ » puis
   « de contrats signés en 1 seul mois » se lit d'un trait. La première version
   répétait ce que le chiffre disait déjà (« ROI 1800 % » / « Retour sur
   investissement ») et Charles l'a vu tout de suite : « les textes à droite
   font pas vraiment de sens ». Le chiffre porte l'unité, la phrase porte le
   complément — jamais les deux. */
.vedette__phrase {
  color: var(--texte-doux);
  /* Plus gros qu'au premier jet : à 16 px à côté d'un chiffre de 64, ça
     paraissait une note de bas de page. La pente est calée pour qu'un portable
     de 1250 px soit déjà à la taille d'un grand écran — même raison que
     `--e` dans la section équipe. */
  font-size: clamp(1.05rem, 1.6vw, 1.35rem);
  line-height: 1.45;
  /* 34ch : « Contrat signé en seulement 1 mois » tient alors sur UNE ligne,
     comme dans la capture. À 24, il se cassait après « seulement » et la
     colonne de droite prenait deux lignes pour trois mots. */
  max-width: 34ch;
}

.vedette__accent {
  margin-top: 0.85rem;
  color: var(--mauve-vif);
  font-weight: 600;
  font-size: clamp(1.05rem, 1.65vw, 1.4rem);
  letter-spacing: 0.01em;
}

/* Le court trait mauve au-dessus de l'accent, repris de la capture. */
.vedette__accent::before {
  content: '';
  display: block;
  width: 26px;
  height: 2px;
  margin-bottom: 0.7rem;
  background: var(--mauve);
  border-radius: 999px;
}

/* Les deux filets verticaux, posés au milieu de l'écart entre colonnes. */
.vedette__chiffre,
.vedette__detail { position: relative; }

.vedette__chiffre::before,
.vedette__detail::before {
  content: '';
  position: absolute;
  left: calc(var(--espace) / -2);
  top: 0.2rem;
  bottom: 0.2rem;
  width: 1px;
  background: var(--trait);
}

/* --- Les trois points --- */

/* Cachés tant que le script n'a pas pris la main : sans lui, les trois cas
   sont déjà tous affichés et ces boutons ne mèneraient nulle part. */
.vedette__points { display: none; }

.est-vedette .vedette__points {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.15rem;
  margin-top: clamp(0.9rem, 2.2vh, 1.4rem);

  /* Longueur de la capsule du point actif. Une seule valeur : la piste et son
     remplissage la partagent, et un écart entre les deux se verrait comme un
     débordement de mauve. */
  --capsule: 26px;
}

/* ⚠️ LE BOUTON N'EST PAS LE POINT. Le point fait 6 px ; un bouton de 6 px est
   intouchable au pouce. Le rembourrage porte la cible à 22 px sans que rien
   ne se voie. */
.vedette__point {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: none;
  padding: 8px;
  line-height: 0;
  cursor: pointer;
  position: relative;
}

/* Le point au repos, et la PISTE de la capsule quand il est actif : c'est le
   même élément qui s'étire, donc le passage de l'un à l'autre est continu. */
.vedette__point::before {
  content: '';
  display: block;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  transition: width var(--t), background var(--t);
}

.vedette__point:hover::before { background: rgba(255, 255, 255, 0.42); }

.vedette__point.est-actif::before { width: var(--capsule); }

/* Le remplissage. ⚠️ IL EST PILOTÉ PAR `--part`, QUE LE SCRIPT ÉCRIT À CHAQUE
   IMAGE : aucune transition ici, sinon chaque écriture lancerait une
   interpolation par-dessus la précédente et le remplissage traînerait derrière
   la minuterie. Et c'est un `scaleX`, pas une largeur : une largeur animée
   soixante fois par seconde force un recalcul de mise en page à chaque image. */
.vedette__point::after {
  content: '';
  position: absolute;
  left: 8px;
  top: 50%;
  margin-top: -3px;
  width: var(--capsule);
  height: 6px;
  border-radius: 999px;
  background: var(--mauve-vif);
  transform-origin: left center;
  transform: scaleX(var(--part, 0));
  opacity: 0;
}

.vedette__point.est-actif::after { opacity: 1; }

.vedette__point:focus-visible {
  outline: 2px solid var(--mauve-vif);
  outline-offset: -2px;
  border-radius: 999px;
}


/* --- L'état carrousel, posé par le script --- */

.est-vedette .vedette__scene {
  display: grid;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  /* Le glisser horizontal nous appartient, le défilement vertical reste à la
     page — même règle que le carrousel de l'équipe. */
  touch-action: pan-y;
  outline-offset: 8px;
}

/* Les trois cas occupent la MÊME case : la scène prend donc la hauteur du plus
   grand et ne saute pas d'un client à l'autre. */
.est-vedette .vedette__cas {
  grid-area: 1 / 1;
  transition: opacity var(--t), transform var(--t);
}
.est-vedette .vedette__cas + .vedette__cas { border-top: 0; }

.est-vedette .vedette__cas:not(.est-actif) {
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
}

/* ⚠️ AU PREMIER PLACEMENT, AUCUNE TRANSITION. Dans un onglet d'arrière-plan
   l'horloge d'animation est suspendue : les deux cas qu'on éteint resteraient
   visibles par-dessus le premier, et la section s'afficherait en bouillie. Le
   script retire cette classe une fois l'état posé. */
.vedette--sans-transition .vedette__cas { transition: none !important; }

@media (prefers-reduced-motion: reduce) {
  .est-vedette .vedette__cas:not(.est-actif) { transform: none; }
  /* Rien qui se remplit : la capsule du point actif est pleine, un point. */
  .vedette__point::after { transform: scaleX(1); }
}


/* --- Notre équipe --------------------------------------------------------

   Texte à gauche, carrousel de photos à droite. La personne du centre est
   nette et grande, les deux autres reculent, rétrécissent et s'assombrissent.

   ⚠️ MÊME PRINCIPE QUE LE DÉFILÉ DE LOGOS, MAIS PAS LE MÊME TEMPÉRAMENT. Les
   logos filent vite et se lancent d'un geste ; ici Charles veut « que ça bouge
   tranquillement et qu'il y ait une genre de résistance ». Le mouvement passe
   donc par un ressort amorti, et le glisser est volontairement freiné.

   ⚠️ SANS JAVASCRIPT, les trois photos restent alignées côte à côte et
   lisibles. C'est le script qui pose `est-carrousel` et prend la main.
   ------------------------------------------------------------------------- */

.equipe {
  /* Moins d'air en haut qu'avant : le texte étant centré sur une colonne de
     photos plus haute que lui, la mise en page en ajoute déjà d'elle-même.
     Et moins encore EN DESSOUS depuis que la scène épinglée suit : son premier
     écran est à moitié vide par construction, l'air s'y ajoutait. */
  padding-top: clamp(3rem, 8vh, 6rem);
  padding-bottom: clamp(1.5rem, 4vh, 3rem);

  /* ⚠️ UNE SEULE SOURCE POUR LA HAUTEUR DES PHOTOS. Le cadre s'en déduit :
     s'ils étaient réglés séparément, une valeur changée d'un côté rognerait
     les photos ou laisserait un trou de l'autre. */
  /* ⚠️ UNE SEULE MESURE PILOTE TOUTE LA SECTION, et c'est la seule façon de
     la garder homogène. Chaque taille était réglée séparément, avec son propre
     `clamp()` et son propre plafond : sur un grand écran le titre avait grandi
     jusqu'à passer sur trois lignes pendant que le nom restait bloqué à 32 px.
     Tout ce qui suit est maintenant un multiple de `--e`, donc les proportions
     ne peuvent plus diverger — quelle que soit la largeur.
     ⚠️ LA PENTE COMPTE AUTANT QUE LE PLAFOND. À `1.28vw`, le plafond n'était
     atteint que vers 1810 px : sur un portable de 1250 px la section tournait
     à 16 px d'échelle contre 23 sur le moniteur de Charles — tout y était plus
     petit ET plus étalé, puisque les photos rapetissent avec l'échelle pendant
     que le cadre, lui, garde sa largeur. Charles, le 2026-08-20 : « sur laptop
     un peu plus rapproché et le texte un peu plus gros, genre comme sur mon
     moniteur ». Passé à `1.6vw`, puis redescendu d'un cran à `1.5vw` le même
     jour — « un petit peu plus petit, juste pour dire » : sur un portable de
     1280 px, ça fait 19,2 px d'échelle au lieu de 20,5, et le plafond tombe
     vers 1550 px. Le regroupement des trois photos suit tout seul
     (`mesurerScene` resserre le pas quand les photos grandissent). */
  --e: clamp(1rem, 1.5vw, 1.45rem);
  --photo-h: calc(var(--e) * 22);

  /* ⚠️ PLUS AUCUN RÉGLAGE D'ÉCARTEMENT ICI, ET C'EST VOULU. L'écart entre
     les personnes n'est pas un nombre qu'on choisit : c'est le plus grand qui
     tienne dans le cadre sans couper personne, et `js/main.js` le calcule à
     partir des silhouettes (voir `mesurerScene`). Un nombre écrit ici ne
     pouvait valoir que pour une largeur d'écran — et il coupait partout
     ailleurs. */
}

.equipe__grille {
  /* ⚠️ EXACTEMENT LA MÊME BOÎTE QUE `.hero`, et c'est ce qui manquait.
     Le hero est `max-width` + `padding-inline`, donc son texte commence à la
     gouttière EN DEDANS du conteneur. Cette grille était `width: min(…, 100%
     − 2×gouttière)` centrée : son contenu commençait donc une gouttière plus à
     GAUCHE que tout le reste du site. Charles : « mets-le sur la marge du
     titre du site ». Les deux formulations font la même largeur totale mais
     pas la même marge de texte. */
  max-width: var(--largeur-max);
  padding-inline: var(--gouttiere);
  margin-inline: auto;
  display: grid;
  /* ⚠️ PAS `1fr 1fr`. Le texte fait environ 400 px de large ; dans une colonne
     de moitié d'écran, les 200 px restants devenaient un trou entre lui et les
     photos. `auto` donne à la colonne de gauche la largeur de son contenu
     (plafonnée par `.equipe__intro`), et les photos récupèrent le reste. */
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  gap: clamp(1.5rem, 3vw, 3.5rem);
  align-items: center;
}

/* La largeur du texte est bornée ici, pas dans la grille : c'est elle qui fixe
   la colonne. Au-delà, une ligne de sous-titre devient trop longue à lire. */
/* La largeur suit l'échelle : un titre plus grand a besoin de plus de place,
   sinon il se casse en trois lignes — c'est précisément ce qui arrivait. */
/* 24 et non 27 : chaque unité rendue ici passe à la colonne des photos, qui en
   a besoin pour écarter les trois sans les faire couper par le bord du cadre.
   En dessous de 23, « CHEZ DUPLEXWEB » ne tient plus sur une ligne. */
.equipe__intro { max-width: calc(var(--e) * 24); }

.equipe__titre {
  font-size: calc(var(--e) * 3.7);
  line-height: 0.98;
  letter-spacing: 0.012em;
}

.equipe__soustitre {
  margin-top: calc(var(--e) * 0.9);
  color: var(--texte-doux);
  font-size: calc(var(--e) * 1.05);
}

/* --- La scène --- */

.equipe__scene { position: relative; }

.equipe__cadre {
  /* Repère de la lueur, qui est en `absolute` dans les deux états. */
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 1rem;
}

.equipe__cadre.est-carrousel {
  display: block;
  /* Le carrousel se pilote aussi aux flèches du clavier : il lui faut donc un
     liseré de focus visible, comme tout élément qu'on peut atteindre au
     clavier. */
  outline-offset: 6px;
  position: relative;
  /* La hauteur est imposée : les membres passent en `absolute` et ne
     porteraient donc plus la scène. */
  height: calc(var(--photo-h-reel, var(--photo-h)) * 1.04);

  /* ⚠️ CE `overflow` EMPÊCHE LA PAGE DE S'ÉLARGIR. Les photos de côté sont
     décalées puis mises à l'échelle ; la plus large des trois (Charles, rapport
     0,98) débordait du cadre et poussait la largeur du document. Sur téléphone,
     ça se voyait comme un site qui se redimensionne à chaque changement de
     personne. `overflow-x: hidden` sur `body` ne suffit pas toujours à
     rattraper ça : on coupe donc à la source. */
  overflow: hidden;
  /* Le carrousel se prend à la souris ET au doigt.
     ⚠️ `touch-action: pan-y` EST CE QUI REND LE GLISSER AU DOIGT POSSIBLE
     sans casser la page : le navigateur garde les gestes VERTICAUX pour
     faire défiler et nous laisse les gestes HORIZONTAUX. Sans cette ligne,
     soit le carrousel vole le défilement de la page, soit la page vole le
     geste du carrousel. */
  touch-action: pan-y;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
.equipe__cadre.est-carrousel.est-attrape { cursor: grabbing; }

.equipe__membre { flex: 0 1 auto; }

/* Les photos de côté s'annoncent cliquables ; celle du centre, non — elle est
   déjà là où on veut l'amener. Le script ignore d'ailleurs un clic sur elle. */
.est-carrousel .equipe__membre { cursor: pointer; }
.est-carrousel .equipe__membre.est-au-centre { cursor: default; }

.est-carrousel .equipe__membre {
  position: absolute;
  /* Ancrées EN BAS et non au centre : les trois photos sont cadrées à
     hauteurs différentes, mais leurs sujets reposent tous sur la même ligne
     d'épaules. Les aligner par le bas les met d'aplomb. */
  bottom: 0;
  left: 50%;
  will-change: transform, opacity;
}

/* La photo elle-même : la couleur vient d'un JPEG, la découpe d'un masque PNG.
   ⚠️ LES DEUX DOIVENT PARTAGER `size` ET `position`, sinon le masque glisse
   par rapport à l'image et rogne la silhouette de travers. */
/* ⚠️ HAUTEUR IMPOSÉE, LARGEUR DÉDUITE DU RAPPORT DE CHAQUE PHOTO. Avec une
   largeur commune et un `contain`, les trois cadrages (0,70 / 0,94 / 0,98) se
   seraient ajustés différemment — l'un par sa hauteur, l'autre par sa largeur —
   et les personnes n'auraient pas eu la même taille à l'écran. En fixant la
   hauteur, elles sont toutes à la même échelle. */
.equipe__photo {
  /* ⚠️ `--photo-h-reel` EST POSÉE PAR LE SCRIPT, et c'est elle qui l'emporte.
     Sur un cadre trop étroit pour montrer trois personnes à la taille demandée
     ici, le script rapetissit les photos plutôt que de les laisser couper.
     `--photo-h` reste l'idéal : la valeur souhaitée quand la place existe. */
  height: var(--photo-h-reel, var(--photo-h));
  aspect-ratio: var(--ratio);

  background-image: var(--photo);
  background-size: contain;
  background-position: bottom center;
  background-repeat: no-repeat;

  -webkit-mask-image: var(--masque);
          mask-image: var(--masque);
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: bottom center;
          mask-position: bottom center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* --- Le nom, le rôle, la jauge --- */

.equipe__legende {
  margin-top: calc(var(--e) * 1.4);
  text-align: center;
  /* Réserve la hauteur de la légende : sans ça, elle change de taille d'une
     personne à l'autre et la jauge sautille. Deux lignes suffisent depuis que
     les rôles sont des intitulés courts (« Directeur des ventes ») et non des
     descriptions de trois lignes.
     ⚠️ La réserve tient compte du PIRE CAS : sur écran étroit, « Responsable
     développement web » passe sur deux lignes. Trop juste, et la jauge —
     donc tout ce qui suit — remonte d'une ligne à chaque changement de
     personne. C'est le texte que Charles voyait bouger. */
  min-height: calc(var(--e) * 7);
}

.equipe__nom {
  font-family: var(--police-titre);
  font-size: calc(var(--e) * 2.15);
  /* ⚠️ Interligne resserré. Bebas Neue n'a que des capitales, sans jambages :
     l'interligne de 1,6 hérité du corps de texte leur laisse une marge vide
     au-dessus et en dessous, qui se voyait comme un décrochage entre le nom
     et son sous-titre. */
  line-height: 1.15;
  letter-spacing: 0.02em;
  transition: opacity var(--t);
}

.equipe__role {
  margin-top: calc(var(--e) * 0.5);
  margin-inline: auto;
  max-width: calc(var(--e) * 30);
  color: var(--texte-doux);
  font-size: calc(var(--e) * 1.05);
  line-height: 1.5;
  transition: opacity var(--t);
}

/* Pendant le changement de personne, le texte s'efface puis revient. */
.equipe__legende.est-en-transition .equipe__nom,
.equipe__legende.est-en-transition .equipe__role { opacity: 0; }

.equipe__jauge {
  margin: calc(var(--e) * 1.15) auto 0;
  width: calc(var(--e) * 10);
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  overflow: hidden;
}

.equipe__jauge-barre {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: inherit;
  background: var(--mauve);
  /* Remplie par `transform: scaleX()` et non par `width` : une largeur
     animée soixante fois par seconde force un recalcul de mise en page à
     chaque image, une transformation non. */
  transform-origin: left center;
  transform: scaleX(0);
}


/* --- Ce qu'on fait : une scène épinglée -----------------------------------

   Quatre étapes qui se remplacent sur place pendant que la page défile. La
   scène reste collée en haut de l'écran le temps que le rail extérieur défile,
   puis la page repart normalement.

   ⚠️ CHAQUE SERVICE SE DÉROULE EN TROIS TEMPS : le titre seul, puis le
   sous-titre le rejoint, puis les deux s'effacent et les projets passent UN PAR
   UN à leur place. C'est la demande de Charles, et c'est ce qui explique la
   grille ci-dessous : le bloc de projets couvre TOUTE la largeur du volet et se
   superpose au texte, puisque les deux ne sont jamais visibles ensemble.

   ⚠️ TOUT EST LISIBLE SANS LE SCRIPT. Par défaut, les volets sont des blocs
   empilés, sans épinglage ni superposition : c'est la classe `est-epingle`,
   posée par `js/main.js`, qui les met l'un sur l'autre. Même chose sous
   `prefers-reduced-motion`.

   ⚠️ LE SCRIPT N'ÉCRIT QUE DES NOMBRES : `--vol` (volet), `--txt` (texte du
   service), `--sst` (sous-titre), `--prj` (projet) ; le CSS en déduit opacité,
   flou, échelle et voile. Noms longs à dessein : `--t` est DÉJÀ le jeton de
   durée de transition du site, et un `opacity: var(--t)` héritant d'une durée
   ne produit aucune erreur — juste un rendu faux qu'on cherche longtemps.

   ⚠️ PAS D'INDICATEUR DE PROGRESSION. Un filet mauve courait dans la marge de
   gauche : Charles n'en veut pas, comme il n'a pas voulu du filet qui se
   remplissait dans la section vedette.
   ------------------------------------------------------------------------- */

.services {
  /* Même pente d'échelle que la section équipe : un portable doit y être à la
     même taille apparente qu'un grand écran. */
  --s: clamp(1rem, 1.5vw, 1.45rem);
  /* ⚠️ C'EST LE SEUL BOUTON DU RYTHME. Le rail fait N fois la hauteur de
     l'écran ; tous les temps se partagent cette distance. Le monter ralentit
     TOUT (il faut scroller plus longtemps pour passer d'un temps au suivant),
     le baisser accélère tout. Aucun autre réglage n'agit sur la vitesse : le
     script ne fait que lire où en est la page.
     800vh, puis 1200, puis 1800 le 2026-08-20 — « les sections passent trop
     vite ». Au pavé tactile, un seul geste parcourt facilement deux écrans :
     il faut donc compter environ deux écrans par temps pour qu'un geste ne
     saute pas trois états. */
  --course: 1759vh;
  /* ⚠️ UNE SEULE DÉFINITION DES COLONNES, réutilisée par le volet ET par chaque
     projet. Le texte du service et celui d'un projet tombent donc exactement au
     même endroit : c'est ce qui fait que le contenu se remplace au lieu de
     bouger. */
  --col1: minmax(0, 0.85fr);
  --col2: minmax(0, 1.15fr);
  --gap: clamp(2rem, 5vw, 5rem);

  /* ⚠️ LA SCÈNE REMONTE SUR LA SECTION PRÉCÉDENTE, ET C'EST VOULU. Son contenu
     est centré dans un écran entier : à l'arrivée, on voit donc d'abord sa
     moitié haute, qui est VIDE — près de la moitié d'un écran pour l'accroche,
     qui ne fait qu'une ligne. C'est tout cet air que Charles voyait entre
     l'équipe et « Ce qu'on fait. ».
     Ce retrait ne fait rien chevaucher : ce qui remonte, c'est précisément ce
     vide. Et il n'a aucun effet une fois la scène épinglée — elle est alors
     collée en haut de l'écran et son contenu est centré, point.
     ⚠️ NE PAS DÉPASSER LA MOITIÉ D'UN ÉCRAN : au-delà, ce n'est plus du vide
     qu'on remonte mais l'accroche elle-même, qui viendrait alors chevaucher la
     légende de l'équipe. Le vide au-dessus de l'accroche vaut ici ~44vh
     (l'écran moins le titre et son logo, divisé par deux) : à -28vh il reste
     donc une quinzaine de vh de sécurité. */
  margin-top: -28vh;
}

.services__cadre {
  max-width: var(--largeur-max);
  padding-inline: var(--gouttiere);
  margin-inline: auto;
  /* ⚠️ LA BANDE FAIT TROIS FOIS LA LARGEUR DU CADRE : sans cette coupe, elle
     élargit le DOCUMENT. Sur téléphone, une page plus large que l'écran décale
     tout et coupe l'en-tête — c'est ce que Charles décrivait comme « la barre
     du haut est buguée ». La scène épinglée coupe déjà, mais elle ne coupe que
     dans l'état épinglé : ici on protège aussi l'état sans script et le
     mouvement réduit. */
  overflow: hidden;
}

.services__volet { padding-block: clamp(2.5rem, 8vh, 5rem); }

.services__nom {
  font-family: var(--police-titre);
  font-size: calc(var(--s) * 3.4);
  line-height: 0.96;
  letter-spacing: 0.012em;
}

/* L'accroche est plus grande que les titres de service : elle est seule à
   l'écran et c'est elle qui ouvre la séquence. */
.services__nom--grand { font-size: calc(var(--s) * 4.6); }

/* Le logo tient la ligne suivante du titre. Sa hauteur suit l'échelle de la
   section, comme tout le reste : posée en pixels, elle aurait été énorme sur un
   téléphone et minuscule sur un moniteur.
   ⚠️ C'EST UNE VERSION RECADRÉE SUR L'ENCRE (`logo-duplexweb-mot.png`). Le
   fichier de l'en-tête a 132 px de haut pour 35 px de mot : les trois quarts
   sont du vide. Posé à la hauteur voulue, le mot n'en faisait plus que le
   quart et paraissait timide sous un titre de 107 px. Ne pas remplacer par le
   fichier d'origine sans refaire le calcul. */
.services__marque {
  display: block;
  height: calc(var(--s) * 1.5);
  width: auto;
  margin: calc(var(--s) * 0.9) auto 0;
}

.services__desc {
  margin-top: calc(var(--s) * 1.1);
  max-width: 26ch;
  color: var(--texte-doux);
  font-size: calc(var(--s) * 1.05);
  line-height: 1.5;
}

/* --- Le mot-clé qui termine le titre ---

   ⚠️ IL DOIT RESTER LISIBLE AVANT D'ÊTRE BEAU. La lueur est faite de trois
   ombres de plus en plus larges et de plus en plus faibles ; le glyphe, lui,
   reste plein et d'un mauve clair. Un mot en mauve saturé avec une lueur de la
   même couleur derrière se met à vibrer et devient pénible à lire — c'est le
   piège classique du néon.
   ⚠️ ET LA LUEUR NE S'ANIME PAS. Une ombre de texte qui pulse est repeinte à
   chaque image, sur un texte de 100 px : c'est exactement ce qu'on vient de
   retirer partout ailleurs dans cette scène. Ce qui bouge, c'est l'opacité et
   l'échelle du mot. */
/* ⚠️ LA PERSPECTIVE EST SUR LE CONTENEUR, PAS SUR LE MOT. Posée sur l'élément
   qui tourne, chaque mot aurait son propre point de fuite et la bascule
   paraîtrait plate. Ici les trois mots partagent le même œil. */
.services__mots {
  display: grid;
  margin-top: calc(var(--s) * 0.35);
  perspective: 900px;
  perspective-origin: center top;
}

.services__mot {
  grid-area: 1 / 1;
  justify-self: center;
  position: relative;
  font-family: var(--police-titre);
  font-size: calc(var(--s) * 3.4);
  line-height: 0.96;
  letter-spacing: 0.012em;

  /* ⚠️ LA MATIÈRE DU BOUTON, DANS LES LETTRES. Ce sont les mêmes taches
     violettes que `.bouton--principal`, découpées par le texte au lieu de
     remplir un rectangle. Le violet plein reste dessous en `color` : si
     `background-clip` n'est pas supporté, le mot s'affiche en violet uni et
     reste lisible — jamais de texte transparent sur fond noir.

     ⚠️ C'EST LE VIOLET DU BOUTON, PAS `--mauve-vif`. Première version faite
     avec #B15CFF et ses voisins : un mauve qui tire sur le ROSE, à côté d'un
     bouton franchement bleuté. Charles, les deux côte à côte : « la couleur est
     totalement différente, c'est horrible ».
     La LUMINOSITÉ, elle, reste plus haute que sur le bouton : là-bas le violet
     est un FOND avec du texte blanc dessus, ici il EST le texte sur fond noir.
     D'où un plancher à #7C5CFF plutôt que le #6A45E8 du bouton.

     ⚠️ `background-size` À 180 % ET POSITION ANIMÉE : c'est ce qui fait
     respirer la couleur DANS les lettres. Les taches sont plus grandes que le
     mot et se promènent lentement à l'intérieur — même principe que la matière
     liquide du bouton, mais découpée par le texte. */
  color: #7C5CFF;
  background-image:
    radial-gradient(38% 120% at 22% 40%, #CFC2FF 0%, transparent 62%),
    radial-gradient(34% 110% at 72% 66%, #6E4CF0 0%, transparent 64%),
    radial-gradient(45% 130% at 52% 15%, #A08FFF 0%, transparent 70%);
  background-color: #7C5CFF;
  background-size: 180% 180%;
  background-position: 18% 34%;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: mot-respire 9s ease-in-out infinite alternate;

  /* ⚠️ LA CHARNIÈRE EST EN HAUT DU MOT. C'est une trappe qui bascule vers le
     haut, demande de Charles, croquis à l'appui : le mot arrive replié VERS le
     lecteur, pivote autour de son bord supérieur et vient se poser à plat dans
     le plan de l'écran ; en partant, il continue sa course et bascule derrière.
     Une charnière en bas donnerait l'effet inverse — une trappe de plancher. */
  transform-origin: center top;
  backface-visibility: hidden;

  will-change: opacity, transform;
}

/* La respiration : les taches se déplacent de quelques dizaines de pour cent,
   lentement, en alternance. ⚠️ `alternate` évite le saut de la fin au début —
   le mouvement repart en sens inverse au lieu de se téléporter. L'amplitude
   reste faible : au-delà, ce n'est plus une respiration, c'est un gyrophare. */
@keyframes mot-respire {
  from { background-position: 18% 34%; }
  to   { background-position: 74% 66%; }
}

@media (prefers-reduced-motion: reduce) {
  .services__mot { animation: none; }
}

/* ⚠️ LA LUEUR EST DERRIÈRE, PAS EN `text-shadow`. Un texte dont le remplissage
   est transparent (`background-clip`) ne projette plus d'ombre de texte : elle
   se dessinerait sous des lettres vides et on ne verrait qu'un halo flou. Le
   halo est donc une copie du mot, floutée, posée dessous — même mot, même
   position, `aria-hidden` par construction puisque c'est un pseudo-élément. */
.services__mot::before {
  content: attr(data-mot);
  position: absolute;
  inset: 0;
  z-index: -1;
  /* La lueur reprend la couleur de l'ombre portée du bouton, rgba(106,69,232). */
  color: #6A45E8;
  -webkit-text-fill-color: #6A45E8;
  filter: blur(18px);
  opacity: 0.75;
}

.services__projets { display: grid; gap: clamp(0.8rem, 1.4vw, 1.4rem); }

/* --- Un projet --- */

/* ⚠️ NEUTRE SUR ORDINATEUR, ET ÇA DOIT LE RESTER. `display: contents` fait
   disparaître cette boîte de la mise en page : les trois wagons redeviennent
   des enfants directs du flex de `.services__bande`, aux mêmes pixels près.
   `mesurerBandes()` lit des `getBoundingClientRect()` — rien ne bouge.
   Ne jamais lui donner un `display` réel ici : le tapis roulant se dérèglerait
   et c'est la géométrie la plus délicate du site. */
.services__wagons { display: contents; }

/* Les mentions « ce qui est inclus » : téléphone seulement — sur ordinateur
   elles allongeraient le wagon de texte au moment où le tapis le centre. */
.services__inclus { display: none; }

.services__projet { text-decoration: none; color: inherit; }

.services__projet-nom {
  font-family: var(--police-titre);
  font-size: calc(var(--s) * 2.4);
  line-height: 1;
  letter-spacing: 0.015em;
}

.services__projet-desc {
  margin-top: calc(var(--s) * 0.7);
  max-width: 28ch;
  color: var(--texte-doux);
  font-size: calc(var(--s) * 1.05);
  line-height: 1.5;
}

.services__projet-lien {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  margin-top: calc(var(--s) * 1.1);
  color: var(--mauve-vif);
  font-weight: 600;
  font-size: calc(var(--s) * 1);
}

.services__projet-lien .fleche { width: 1em; height: 1em; }

a.services__projet:hover .services__projet-lien,
a.services__projet:focus-visible .services__projet-lien {
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* --- La vignette --- */

.services__vignette {
  position: relative;
  display: block;
  border: 1px solid var(--trait);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.018);
  overflow: hidden;
}

.services__vignette img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform 0.5s var(--ease);
}

/* ⚠️ SUR LE CADRE, PAS SUR LA CARTE. Le survol était posé sur le lien entier,
   qui couvre une large zone vide autour de l'image : le voile apparaissait
   alors que la souris était encore à côté de la capture. */
.services__vignette:hover img,
a.services__projet:focus-visible .services__vignette img { transform: scale(1.03); }

/* Le voile « Voir le projet ». ⚠️ IL NE REMPLACE PAS LE LIEN ÉCRIT : au doigt,
   il n'y a pas de survol, et rien ne dirait que la capture est cliquable. Le
   lien « Voir le site » sous la description reste donc visible en permanence ;
   ici, c'est le bonus de la souris. */
.services__voir {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* ⚠️ PAS DE `backdrop-filter` ICI. Il en avait un : un flou d'arrière-plan
     crée une couche de composition qui est repayée à chaque image MÊME quand
     l'élément est à `opacity: 0`. Sur une section qui bouge à chaque pixel de
     défilement, c'est de la saccade offerte. Un aplat suffit. */
  background: rgba(6, 5, 12, 0.62);
  color: #fff;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity var(--t);
}

.services__vignette:hover .services__voir,
a.services__projet:focus-visible .services__voir { opacity: 1; }

/* --- Le résultat de publicité : même grille, pas de vignette --- */

.services__logo { display: block; height: var(--logo-h, 56px); width: auto; }

.services__chiffre {
  font-family: var(--police-titre);
  font-size: calc(var(--s) * 3.4);
  line-height: 1;
  white-space: nowrap;
}

.services__ligne {
  margin-top: calc(var(--s) * 0.6);
  max-width: 26ch;
  color: var(--texte-doux);
  font-size: calc(var(--s) * 1.05);
  line-height: 1.45;
}

.services__accent {
  margin-top: calc(var(--s) * 0.8);
  color: var(--mauve-vif);
  font-weight: 600;
  font-size: calc(var(--s) * 1.15);
}

/* --- Les deux bandes du catalogue -------------------------------------------

   Toutes les vidéos réalisées, sur deux rangées qui traversent l'écran en sens
   contraire. Le mur vertical qui était ici a été retiré à la demande de
   Charles : « juste une bande défilante, ça passe de chaque côté de l'écran ».

   ⚠️ LES RANGÉES SONT PLUS LARGES QUE L'ÉCRAN, ET C'EST LA SCÈNE QUI COUPE.
   Ne pas mettre d'`overflow: hidden` ici : la découpe se fait déjà sur
   `.services__scene`, et un deuxième cadre de découpe ferait perdre le
   débordement voulu sur les côtés.

   ⚠️ AUCUN `loading="lazy"` SUR CES VIGNETTES. Elles vivent derrière un
   `transform` : le report de chargement se déclenche mal dans ce cas, et huit
   des vingt-six ne partaient JAMAIS — on gardait des rectangles noirs jusqu'au
   bout. Vingt-six affiches font 1 Mo au total, et elles ne se chargent qu'une
   fois. */
/* ⚠️ QUATRE NIVEAUX DE SÉLECTEUR, ET C'EST NÉCESSAIRE. La règle générale
   `.est-epingle .services__volet:not([data-projets="glisse"])` pèse le même
   poids que `.est-epingle .services__volet[data-projets="bandes"]` et se trouve
   PLUS BAS dans le fichier : c'est donc elle qui gagnait, avec son
   `max-width: 1320px` et son `margin-inline: auto`. Mesuré : le volet ne faisait
   que 1688 px sur un écran de 2071, centré — d'où 368 px de noir à gauche des
   bandes, exactement ce que Charles voyait. Les bandes doivent aller d'un bord
   à l'autre, quoi qu'il arrive. */
.est-epingle .services__cadre .services__volet[data-projets="bandes"] {
  display: block;
  position: absolute;

  /* ⚠️ `grid-area: auto` EST LA LIGNE QUI COMPTE. Un enfant ABSOLU d'une grille
     ne se repère pas sur le cadre mais sur SA CASE — et les volets ont tous
     `grid-area: 1 / 1`, une case centrée et de hauteur nulle. Mesuré : le volet
     se posait 654 px trop bas. Sans placement, la case redevient la boîte de
     rembourrage du cadre, c'est-à-dire l'écran entier. */
  grid-area: auto;
  align-self: stretch;
  inset: 0;
  width: auto;
  max-width: none;
  padding-inline: 0;
}

/* Le bloc entier — titre et rangées — se centre dans l'écran. */
.mur {
  position: absolute;
  left: 0;
  right: 0;
  /* ⚠️ CENTRÉ SUR L'ÉCRAN UTILE, PAS SUR L'ÉCRAN BRUT. La barre de navigation
     mange les 94 premiers pixels : le milieu de ce qui reste est à 47 px sous
     le milieu de la fenêtre. À 24 px, mesuré sur 1440 × 800, le haut du titre
     tombait à 67 px — soit 27 px DERRIÈRE la barre, et il était coupé. */
  top: calc(50% + 47px);
  transform: translateY(-50%);
  display: grid;
  /* ⚠️ `minmax(0, 1fr)` ET NON UNE COLONNE IMPLICITE. Les rangées font
     `width: max-content` — plus de 3300 px : la colonne se dimensionnait
     dessus, et le titre, centré dans cette largeur fantôme, partait se coller au
     bord droit de l'écran, coupé. Même piège que sur le cadre de la scène. */
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(0.8rem, 2vh, 1.4rem);
}

/* ⚠️ LE TITRE EST PLUS PETIT ICI, ET C'EST DE LA PLACE ACHETÉE. Voir le calcul
   sur `.video.bande__vue` : sur un portable de 800 px, tout doit tenir sous la
   barre de navigation. Ce titre-ci passe donc sur une ligne et d'un cran plus
   bas en taille. */
.mur__titre { font-size: clamp(1.9rem, 3.6vw, 3.1rem); }

.mur__entete { padding-bottom: clamp(0.6rem, 1.6vh, 1.2rem); }

.bandes {
  display: grid;
  min-width: 0;
  gap: clamp(0.5rem, 0.85vw, 0.85rem);
}

/* ⚠️ LE DÉFILEMENT EST AUTONOME, IL N'ÉCOUTE PLUS LA MOLETTE. Charles : « je
   veux pas qu'elle interagisse avec ma souris, je veux qu'elle défile
   automatiquement ». C'est donc une animation CSS, pas le moteur de la scène —
   la seule de tout le site qui parte d'elle-même.
   ⚠️ −50 % ET PAS AUTRE CHOSE. La rangée contient son contenu EN DOUBLE : à
   −50 % de sa propre largeur, la seconde copie occupe exactement la place qu'
   occupait la première au départ. Le raccord est invisible et la boucle ne
   laisse jamais de trou sur les côtés. Changer la copie ou ce pourcentage sans
   changer l'autre fait sauter la bande à chaque tour.
   ⚠️ ET ELLE NE TOURNE QUE QUAND LA SECTION EST À L'ÉCRAN : `est-actif` est
   posée par le script sur le volet courant. Une animation qui tourne en
   permanence sur une page de vingt-cinq écrans se paie à chaque image. */
/* ⚠️ AUCUN `gap` ICI, ET C'EST TOUT LE SUJET DE LA BOUCLE. Avec un `gap`, la
   rangée mesure 26 vignettes + 25 écarts : la moitié de cette largeur vaut donc
   13 vignettes + 12,5 écarts, alors qu'une copie en compte 13. Le −50 % tombait
   une DEMI-GOUTTIÈRE trop court, et ce décalage s'accumulait à chaque tour —
   c'est le trou que Charles voyait apparaître au bout d'un moment. L'écart est
   donc porté par les vignettes elles-mêmes : la rangée vaut exactement
   26 × (vignette + écart), sa moitié exactement une copie, et le raccord tombe
   au pixel, indéfiniment. */
.bande {
  display: flex;
  width: max-content;
  will-change: transform;
  animation: bande-defile 64s linear infinite;
  animation-play-state: paused;
}

/* Deux vitesses légèrement différentes : à durée égale, les deux rangées se
   croisent toujours au même endroit et l'œil y voit un motif. */
.bande[data-sens="1"] {
  animation-direction: reverse;   /* de la gauche vers la droite */
  animation-duration: 58s;
}

/* ⚠️ `est-eveille` ET NON `est-actif`. `est-actif` n'arrive qu'à mi-croisement ;
   le catalogue était donc déjà pleinement visible avec ses deux rangées à
   l'arrêt. `est-eveille` suit l'opacité du volet : ça démarre au premier pixel
   vu, et ça se remet en pause dès que le volet n'est plus là. */
.est-epingle .services__volet.est-eveille .bande { animation-play-state: running; }

/* ⚠️ QUATRE NIVEAUX POUR BATTRE LA RÈGLE DU DESSUS, qui en compte trois. Une
   rangée dont une vidéo joue s'arrête : on ne regarde pas une vidéo qui glisse
   sous le curseur. Elle repart d'elle-même dès que plus rien n'y joue. */
.est-epingle .services__volet.est-eveille .bande.est-arretee {
  animation-play-state: paused;
}

@keyframes bande-defile {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .bande { animation-play-state: paused !important; }
}

/* ⚠️ LA LARGEUR EST LE SEUL RÉGLAGE, ET ELLE COMMANDE TOUT LE RESTE. À 16 vw on
   voit un peu moins de six vignettes par rangée sur un portable — Charles en
   demandait cinq au maximum. Passer à 18,5 vw en donne exactement cinq, MAIS
   les deux rangées ne tiennent alors plus dans la hauteur d'un écran de
   portable : 5 × 288 px de large font 360 px de haut, soit 738 px pour les deux
   rangées, plus le titre. C'est l'arbitrage à connaître avant d'y toucher. */
/* ⚠️ DEUX CLASSES POUR LE RAPPORT. `.video` porte déjà `aspect-ratio: 9/16` et
   se trouve plus bas dans le fichier : à poids égal c'est elle qui gagne, et les
   vignettes restaient en 9/16 — 427 px de haut, deux rangées ne tenaient plus
   dans un écran de portable.
   ⚠️ ET 5/6 N'EST PAS UN CAPRICE. C'est l'arbitrage entre les deux demandes de
   Charles : cinq vignettes par rangée sur un portable, et deux rangées visibles
   en même temps. À 17,5 vw sur 1440 px, une vignette fait 252 px de large —
   soit 5,4 par rangée — et 302 de haut, donc 621 px pour les deux rangées, plus
   le titre : ça tient dans 800 px de haut. En 9/16 il en faudrait 900. */
/* ⚠️ CARRÉ, ET C'EST LE SEUL RAPPORT QUI SATISFASSE LES DEUX DEMANDES. Le
   calcul, sur le portable de référence (1440 × 800) :
     hauteur utile sous la barre de navigation .......... ~680 px
     titre sur une ligne + sous-titre ................... ~140 px
     reste pour les DEUX rangées ........................ ~540 px
     donc une rangée ....................................  266 px de haut
     et cinq vignettes par rangée ....................... (1440 − 4×12) ÷ 5 = 278
   266 de haut pour 278 de large : c'est un carré. En 9/16 il faudrait 494 px de
   haut par rangée, soit 1130 pour les deux — une fois et demie l'écran.
   Le recadrage prend le centre de l'image, là où est le sujet. */
.video.bande__vue {
  flex: 0 0 auto;
  /* L'écart entre vignettes — voir l'avertissement sur `.bande`. */
  margin-right: clamp(0.5rem, 0.85vw, 0.85rem);
  /* ⚠️ LE PLAFOND EST HAUT ET C'EST VOULU. À 290 px, un moniteur de 2071
     affichait huit vignettes par rangée au lieu des cinq ou six demandées : la
     borne haute se déclenchait trop tôt. À 380, la largeur suit la fenêtre
     jusqu'à 2170 px, donc le compte reste le même partout.
     ⚠️ ET 17,5 vw N'EST PAS NÉGOCIABLE SANS REFAIRE LE CALCUL. Sur le portable
     de référence (1440 × 800) : 252 px de vignette → 516 px pour les deux
     rangées, plus ~190 px de titre = 706, soit exactement la hauteur utile sous
     la barre. Un point de vw de plus et le titre repasse derrière la barre. */
  width: clamp(140px, 17.5vw, 380px);
  aspect-ratio: 1 / 1;
}

@media (max-width: 780px) {
  .video.bande__vue { width: 42vw; }
  .mur__entete { padding-bottom: 0.4rem; }
}

/* --- L'état épinglé, posé par le script --- */

.est-epingle .services__piste { height: var(--course); }

.est-epingle .services__scene {
  position: sticky;
  top: 0;
  /* `svh` et non `vh` : sur mobile, `vh` compte la barre d'adresse rétractée,
     donc la scène dépasse d'un cran et le contenu se fait rogner. */
  height: 100svh;
  overflow: hidden;
  display: flex;
  align-items: center;
}

/* ⚠️ `minmax(0, 1fr)` ET NON UNE COLONNE IMPLICITE. Sans ça la colonne se
   dimensionne sur le CONTENU : les captures font 1200 px en natif et la bande
   des sites plusieurs écrans de large, donc la colonne devenait plus large que
   le cadre. Tout ce qu'elle contient — à commencer par l'accroche — se
   retrouvait centré dans cette largeur fantôme et débordait à droite. Invisible
   sur grand écran, où la colonne restait plus étroite que le cadre ; sur
   téléphone, Charles voyait « tout décalé ».
   Le `min-width: 0` sur les volets dit la même chose du côté des enfants : un
   élément de grille refuse par défaut de devenir plus étroit que son contenu. */
/* Le cadre sert de repère au volet du mur, qui est en absolu. */
.est-epingle .services__cadre {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  width: 100%;

  /* ⚠️ LE CADRE PREND TOUTE LA HAUTEUR DE LA SCÈNE, et son contenu se centre
     dedans. Il s'arrêtait à la hauteur de son contenu : comme c'est LUI qui
     coupe (`overflow: hidden`), le volet qui monte vers le service suivant se
     faisait trancher en plein vol, à quelques centaines de pixels du haut de
     l'écran — Charles : « je ne veux pas que le titre coupe, ça continue à
     monter vers le header ». Avec la pleine hauteur, la coupe tombe au bord de
     l'écran : le contenu file derrière l'en-tête, comme il se doit. */
  height: 100%;
  align-content: center;

  /* ⚠️ EN PLEINE LARGEUR UNE FOIS ÉPINGLÉ, et c'est tout le sujet. Le cadre
     gardait la boîte du site (1320 px) : sur un grand écran, la bande était
     donc tranchée en PLEIN MILIEU de l'écran, avec du noir de chaque côté.
     Charles : « sur un gros écran ça coupe, je trouve ça plate ». Le tapis
     court maintenant jusqu'aux bords ; ce sont les volets qui ne glissent pas
     qui reprennent la boîte du site, juste en dessous. */
  max-width: none;
  padding-inline: 0;

  /* Et il ne coupe plus net : les wagons se dissolvent sur les sept derniers
     pour cent de chaque côté. Le masque est FIXE — rien à repeindre pendant le
     défilement, contrairement à un dégradé qu'on animerait. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
}

/* Les volets sans tapis gardent la boîte du site : leur texte reste aligné sur
   la même marge que le reste de la page. */
.est-epingle .services__volet:not([data-projets="glisse"]) {
  width: 100%;
  max-width: var(--largeur-max);
  padding-inline: var(--gouttiere);
  margin-inline: auto;
}

/* Les volets occupent LA MÊME CASE : les titres se remplacent donc à la même
   place au lieu de faire monter la mise en page. */
.est-epingle .services__volet {
  grid-area: 1 / 1;
  min-width: 0;

  /* ⚠️ CHAQUE VOLET SE CENTRE SUR LUI-MÊME, il ne s'étire pas. Ils partagent
     une seule case de grille : par défaut, chacun est étiré à la hauteur du
     PLUS GRAND, et son contenu se pose alors en haut de cette hauteur. Le titre
     de la publicité paraissait donc trop haut d'une centaine de pixels — pas
     parce qu'il était mal centré, mais parce que sa boîte était plus grande que
     lui. */
  align-self: center;
  padding-block: 0;
  display: grid;
  grid-template-columns: var(--col1) var(--col2);
  gap: var(--gap);
  align-items: center;

  will-change: opacity;
  /* Un volet effacé ne doit pas intercepter les clics de celui qui est
     visible : ils sont tous les uns sur les autres. */
  pointer-events: none;
}

.est-epingle .services__volet.est-actif { pointer-events: auto; }

/* ⚠️ APRÈS LES RÈGLES GÉNÉRALES DES VOLETS, ET PLUS SPÉCIFIQUE QU'ELLES. Placée
   plus haut dans le fichier et à égalité de poids, elle perdait : le volet du
   mur restait centré, plafonné à la boîte du site et pourvu de ses gouttières.
   Le mur en a besoin de l'exact contraire — un volet étiré sur toute la hauteur
   de l'écran, sans largeur maximale, qui lui sert de repère pour glisser. */
/* ⚠️ HORS DE LA GRILLE, PAS SIMPLEMENT ÉTIRÉ DEDANS. Les volets partagent une
   case dont la hauteur est celle du PLUS GRAND d'entre eux, et cette case est
   CENTRÉE dans la scène. Le volet du mur commençait donc à une centaine de
   pixels du haut de l'écran, et le mur — calé sur `top: 0` de son volet —
   héritait de ce décalage : le titre atterrissait dans le bas de l'écran au
   lieu du milieu. En absolu sur le cadre, son zéro est le haut de l'écran, et
   il ne pèse plus sur la hauteur de la case. */
.est-epingle .services__volet[data-projets="mur"] {
  display: block;
  position: absolute;

  /* ⚠️ `grid-area: auto` EST LA LIGNE QUI COMPTE, ET ELLE N'EST PAS DÉCORATIVE.
     Un enfant ABSOLU d'une grille ne se repère pas sur le cadre : il se repère
     sur SA CASE. Les volets ont tous `grid-area: 1 / 1`, et cette case est
     centrée dans le cadre (`align-content: center`). Résultat mesuré : le volet
     se posait à 654 px du haut avec une hauteur de ZÉRO, `inset: 0` n'y pouvait
     rien, et le mur héritait des 654 px — le titre atterrissait sous le bord
     bas de l'écran. Sans placement, la case redevient la boîte de rembourrage
     du cadre, c'est-à-dire l'écran entier. */
  grid-area: auto;
  align-self: stretch;
  inset: 0;
  width: auto;
  max-width: none;
  padding-inline: 0;
}

/* L'accroche est seule à l'écran : elle est donc centrée, dans les deux sens. */
.est-epingle .services__volet--intro {
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}

/* ⚠️ LE TEXTE D'UN SERVICE EST CENTRÉ DANS L'ÉCRAN, comme l'accroche — demande
   de Charles. Il tenait la colonne de gauche, celle où arrive ensuite le texte
   des projets ; mais pendant ses deux temps il est SEUL à l'écran, et un titre
   seul collé à gauche laissait tout le reste vide. Le bloc de projets, lui,
   garde la grille en deux colonnes : les deux ne sont jamais visibles ensemble,
   donc rien ne se chevauche. */
.est-epingle .services__texte {
  grid-column: 1 / -1;
  grid-row: 1;
  justify-self: center;
  text-align: center;
}

.est-epingle .services__desc { margin-inline: auto; }

.est-epingle .services__projets {
  grid-column: 1 / -1;
  grid-row: 1;
}

/* ⚠️ UN GROUPE PREND TOUTE LA LARGEUR, comme un projet solo. Il était logé
   dans la colonne de droite, celle des projets : mais quand il paraît, le texte
   du service s'est déjà effacé et la moitié gauche de l'écran restait vide pour
   rien. */
.est-epingle [data-projets="groupe"] .services__projets { grid-column: 1 / -1; }

/* --- La publicité numérique, en quatre temps --------------------------------

   Un en-tête qui reste (surtitre, titre, logos des plateformes), puis quatre
   étapes qui se remplacent AU MÊME ENDROIT — même principe que les mots-clés,
   même raison : c'est ce qui évite que la mise en page saute.

   ⚠️ NI FLOU NI OMBRE ANIMÉE ICI. La demande parlait d'un léger flou à
   l'entrée ; on l'a retiré de toute cette scène après que les collègues de
   Charles l'aient trouvée saccadée — un flou est repeint à chaque image. Le
   passage se fait à l'opacité, à l'échelle et au voile `clip-path`, que le
   compositeur applique sans repeindre. Ne pas le réintroduire sans mesurer.
   ------------------------------------------------------------------------- */

/* ⚠️ `minmax(0, 1fr)` PARTOUT OÙ IL Y A UNE GRILLE. Sans colonne déclarée, la
   colonne implicite se dimensionne sur le CONTENU : le bloc entier se tassait à
   gauche et le texte des étapes tombait dans une colonne de 150 px. C'est
   exactement le piège qui avait décalé toute la scène sur téléphone. */
.pub {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.4rem, 4vh, 2.6rem);
  justify-items: center;
  width: 100%;
}

/* ⚠️ L'EN-TÊTE ET LES ÉTAPES OCCUPENT LA MÊME CASE — une fois épinglé
   seulement. Le titre est d'abord seul, centré dans l'écran ; les étapes
   viennent ENSUITE à sa place. Sans le script, les deux se suivent normalement
   et tout reste lisible. */
.est-epingle .pub { gap: 0; }

.est-epingle .pub__entete,
.est-epingle .pub__bloc {
  grid-area: 1 / 1;
  align-self: center;
  width: 100%;
}

/* ⚠️ LES ÉTAPES SE RESSERRENT AU MILIEU DE L'ÉCRAN. Le volet reprend la boîte
   du site (1320 px) parce que les autres volets en ont besoin ; sur un écran de
   portable, l'étape occupait donc toute la fenêtre : le texte collé à la marge
   gauche, le visuel perdu à droite, avec un trou au centre. Charles : « je veux
   ça au milieu de mon écran ». On plafonne DONC la seule boîte des étapes, plus
   étroite que le volet, et elle se centre (`.pub` a `justify-items: center`).
   Ne pas déplacer ce plafond sur `.services__volet` : le tapis roulant des
   sites, lui, doit rester pleine largeur. */
.est-epingle .pub__bloc { max-width: 880px; }

.pub__bloc {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  gap: clamp(1.2rem, 3vh, 2rem);
  width: 100%;
}

.pub__entete {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  text-align: center;
  width: 100%;
}

/* Au calibre d'un sous-titre, sous le titre. L'interlettrage baisse avec la
   taille : les 0,24 em qu'il fallait à 12,8 px pour tenir la ligne deviennent
   une ligne éparpillée à 18 px. */
.pub__kicker {
  margin-top: calc(var(--s) * 0.7);
  color: #7C5CFF;
  font-size: calc(var(--s) * 1.15);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* --- Les quatre étapes --- */

/* ⚠️ SANS LE SCRIPT, LES QUATRE ÉTAPES SE SUIVENT NORMALEMENT. C'est la règle
   du site : le contenu est lisible d'abord, l'animation est un bonus. Ce n'est
   qu'en mode épinglé qu'elles se superposent — sinon elles s'empileraient les
   unes sur les autres et la section serait illisible sans JavaScript, et sous
   `prefers-reduced-motion`. */
.pub__etapes {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(2rem, 6vh, 3.5rem);
  width: 100%;
}

/* ⚠️ EMPILÉE ET CENTRÉE, PLUS EN DEUX COLONNES. Charles : « je veux que ce
   soit centré, le titre et sous-titre et ensuite les 3 vidéos une à côté de
   l'autre en dessous », puis « et les autres 3 sections aussi doivent être
   centrées ». Le texte à gauche et le visuel à droite laissaient un trou au
   milieu de l'écran dès que le visuel était petit — c'est ce qu'il voyait sur
   l'étape 03, dont le visuel est vide. */
.pub__etape { display: block; text-align: center; }

.est-epingle .pub__etapes { gap: 0; }

.est-epingle .pub__etapes {
  /* ⚠️ LA HAUTEUR EST POSÉE PAR LE SCRIPT, ÉTAPE PAR ÉTAPE, et c'est tout
     l'objet de cette transition. Les quatre étapes partagent une case de
     grille : la case prenait donc la hauteur de la PLUS HAUTE (l'étape 01 et
     ses trois vidéos, 552 px) et les autres traînaient un vide derrière elles —
     345 px sous « Vous avancez ». Charles : « c'est vraiment plate qu'il y a un
     gros espace entre les logos et les étapes en bas ». Le script règle donc la
     case sur l'étape courante ; le filet des quatre segments la suit.
     La transition n'est pas cosmétique : sans elle le filet SAUTE d'un coup à
     chaque changement d'étape. Elle ne coûte de la mise en page que pendant les
     trois passages, jamais pendant le reste du défilement. */
  transition: height 0.45s var(--ease);

  /* ⚠️ ET LA RANGÉE DOIT SUIVRE LA HAUTEUR POSÉE, SANS QUOI RIEN NE BOUGE. Une
     rangée implicite se dimensionne sur son plus grand contenu : régler la
     hauteur de la GRILLE laissait la rangée à 566 px, qui débordait dessous, et
     les étapes — centrées dans cette rangée — se retrouvaient 121 px plus bas
     que leur case. Le filet passait alors DERRIÈRE les logos. `1fr` sur une
     grille de hauteur définie donne une rangée exactement de cette hauteur. */
  grid-template-rows: minmax(0, 1fr);
}

.est-epingle .pub__etape {
  /* Toutes dans la même case : elles se remplacent sans rien déplacer. */
  grid-area: 1 / 1;

  /* ⚠️ CENTRÉE, DONC JAMAIS ÉTIRÉE. Deux raisons. D'abord une étape plus haute
     que la case (pendant un passage, quand la case s'est déjà réglée sur la
     suivante) déborde alors des DEUX côtés au lieu de descendre sur le filet.
     Ensuite c'est ce qui rend la mesure possible : une case étirée renvoie la
     hauteur de la case, pas celle du contenu. */
  align-self: center;

  will-change: opacity, transform;

  /* ⚠️ UNE ÉTAPE INVISIBLE NE DOIT PAS INTERCEPTER LA SOURIS. `opacity: 0`
     n'enlève PAS un élément du chemin des clics : les quatre étapes étant
     empilées dans la même case, la DERNIÈRE DU HTML recouvrait les autres.
     Les boutons de lecture des vidéos de l'étape 01 ne recevaient donc aucun
     clic : `elementFromPoint`, au centre d'un bouton, renvoyait un élément
     d'une étape invisible. */
  pointer-events: none;
}

.est-epingle .pub__etape.est-actif { pointer-events: auto; }

.pub__texte { text-align: center; }

.pub__num {
  color: rgba(255, 255, 255, 0.32);
  font-family: var(--police-titre);
  font-size: calc(var(--s) * 1.15);
  letter-spacing: 0.14em;
}

.pub__titre {
  margin-top: calc(var(--s) * 0.3);
  font-family: var(--police-titre);
  font-size: calc(var(--s) * 2.6);
  line-height: 1;
  letter-spacing: 0.012em;
}

.pub__desc {
  margin-top: calc(var(--s) * 0.8);
  margin-inline: auto;
  max-width: 32ch;
  color: var(--texte-doux);
  font-size: calc(var(--s) * 1.05);
  line-height: 1.5;
}

.pub__visuel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1.5rem, 4vw, 3rem);
  margin-top: calc(var(--s) * 1.4);
  min-height: calc(var(--s) * 6);
}

/* --- Le parcours de l'étape 03 ---------------------------------------------

   Trois jalons — nouveau prospect, appel réservé, nouveau client — reliés par
   deux flèches. Rien d'autre : ni chiffre, ni graphique, ni promesse.

   ⚠️ REPRODUCTION DE LA CAPTURE DE CHARLES, PAS DE SON TEXTE. Sa consigne écrite
   dit « un cercle coloré » pour les trois icônes ; son image montre DEUX CARRÉS
   ARRONDIS (message, agenda) et UN CERCLE (le crochet). C'est l'image qui a été
   suivie — il a demandé deux fois la fidélité à la capture. */
.pub__visuel--parcours {
  margin-top: calc(var(--s) * 1.6);
  min-height: 0;
  gap: 0;
}

.parcours {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.5rem, 1.1vw, 0.85rem);
}

/* ⚠️ `min-width: 0` ET `flex: 0 1 auto` : sans quoi les trois cartes refusent de
   se resserrer et la rangée déborde en largeur sur les écrans intermédiaires —
   le document s'élargit alors et l'en-tête se décale, panne déjà payée sur
   cette page. */
.parcours__jalon {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: 0 1 auto;
  min-width: 0;
  width: 145px;
  height: 65px;
  padding: 0 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.075);
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.038);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
}

.parcours__pastille {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  color: #fff;
}

.parcours__pastille svg { width: 20px; height: 20px; display: block; }

/* Les creux du dessin reprennent la couleur de la pastille : c'est ce qui
   dessine les lignes de la bulle et les cases de l'agenda sans deuxième calque.*/
.parcours__creux { fill: var(--pastille); }

.parcours__pastille--message { background: #2F6BFF; --pastille: #2F6BFF; }
.parcours__pastille--agenda  { background: #7C5CFF; --pastille: #7C5CFF; }
.parcours__pastille--signe   { background: #1EA34F; --pastille: #1EA34F; border-radius: 50%; }

.parcours__nom {
  min-width: 0;
  color: #fff;
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  text-align: left;
}

.parcours__fleche {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  color: rgba(255, 255, 255, 0.3);
}

/* Écrans intermédiaires : on resserre avant d'empiler, plutôt que de basculer
   d'un coup. À 641 px la rangée fait encore 462 px pour 515 disponibles. */
@media (max-width: 900px) {
  .parcours__jalon { width: 128px; height: 60px; padding: 0 0.55rem; gap: 0.5rem; }
  .parcours__pastille { width: 32px; height: 32px; }
  .parcours__pastille svg { width: 18px; height: 18px; }
  .parcours__nom { font-size: 0.66rem; }
  .parcours__fleche { width: 17px; height: 17px; }
}

/* En dessous, la rangée ne tient plus : elle se met debout et les flèches
   pointent vers le bas — la progression descend au lieu d'aller à droite. */
@media (max-width: 640px) {
  .parcours { flex-direction: column; gap: 0.4rem; }
  .parcours__jalon { width: min(210px, 74vw); height: 58px; }
  .parcours__fleche { transform: rotate(90deg); }
}


/* --- Les quatre plateformes, en apesanteur ---------------------------------

   ⚠️ CE BLOC A REMPLACÉ UNE ORBITE. Les logos tournaient sur une ellipse ;
   Charles l'a retirée — « mets les 4 logos un à côté de l'autre et fais-les
   léviter, comme s'ils étaient dans l'espace ». Ne pas la réintroduire sans
   qu'il le demande.

   ⚠️ RIEN NE BOUGE QUAND L'ÉTAPE N'EST PAS À L'ÉCRAN : les animations sont en
   `paused` par défaut et ne tournent que sur `.pub__etape.est-actif`. C'est la
   règle qu'on s'est donnée après la passe de fluidité.
   ⚠️ ET LE DÉPLACEMENT RESTE MINUSCULE — dix pixels au plus. Dans un onglet
   d'arrière-plan l'horloge est suspendue : le logo se fige à un point
   quelconque de sa dérive, et il faut que ce point soit indiscernable de sa
   place normale. Aucune position ne dépend de l'animation. */
.pub__visuel--marques {
  gap: clamp(1.6rem, 4vw, 3.6rem);
  margin-top: calc(var(--s) * 1.3);
  min-height: 0;
}

/* ⚠️ UNE TAILLE DE BOÎTE IDENTIQUE NE DONNE PAS QUATRE LOGOS DE MÊME POIDS.
   Chaque marque remplit sa boîte différemment : le carré d'Instagram l'occupe
   ENTIÈREMENT, le disque de Facebook n'en couvre que 79 %, et le « A » de
   Google Ads, qui n'est qu'une diagonale, paraît maigre à côté. À boîte égale,
   Instagram écrase les autres — c'est ce que Charles voyait. Chaque logo reçoit
   donc un facteur OPTIQUE, appliqué à la largeur et non au `transform`, qui est
   déjà pris par la lévitation. Ces valeurs se règlent à l'œil, pas au calcul :
   si un logo change, il faut les reprendre. */
.marque {
  display: block;
  flex: 0 0 auto;
  --taille: clamp(40px, 6.6vw, 64px);
  width: calc(var(--taille) * var(--optique, 1));
  aspect-ratio: 1;
  animation: flotte-a 7.4s ease-in-out infinite;
}

.marque:nth-child(1) { --optique: 1;    }  /* Google — anneau épais */
.marque:nth-child(2) { --optique: 0.94; }  /* Facebook — disque plein */
.marque:nth-child(3) { --optique: 0.86; }  /* Instagram — carré plein */
.marque:nth-child(4) { --optique: 1.06; }  /* Google Ads — diagonale ajourée */

.marque__logo {
  display: block;
  width: 100%;
  height: 100%;
}

/* Deux dérives différentes, quatre durées différentes, quatre départs
   différents : sans ça les quatre logos montent et descendent ensemble, et ça
   ne flotte plus, ça respire en chœur. */
/* ⚠️ EN PROPRIÉTÉS SÉPARÉES, PAS EN RACCOURCI `animation`. Le raccourci
   remet TOUTES les sous-propriétés à leur valeur initiale, dont
   `animation-play-state: running` — et comme ces règles sont plus spécifiques
   que la mise en pause plus bas, les trois derniers logos auraient tourné en
   permanence, y compris hors de l'étape. */
.marque:nth-child(2) { animation-name: flotte-b; animation-duration: 8.9s; animation-delay: -2.7s; }
.marque:nth-child(3) { animation-name: flotte-a; animation-duration: 6.6s; animation-delay: -4.1s; }
.marque:nth-child(4) { animation-name: flotte-b; animation-duration: 9.8s; animation-delay: -1.4s; }

@keyframes flotte-a {
  from { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
  32%  { transform: translate3d(2px, -9px, 0) rotate(1.6deg) scale(1.03); }
  68%  { transform: translate3d(-2px, -3px, 0) rotate(-1.3deg) scale(0.99); }
  to   { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
}

@keyframes flotte-b {
  from { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
  28%  { transform: translate3d(-3px, -5px, 0) rotate(-1.8deg) scale(0.985); }
  61%  { transform: translate3d(2px, -10px, 0) rotate(1.1deg) scale(1.035); }
  to   { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
}

.marque { animation-play-state: paused; }
.pub__etape.est-actif .marque { animation-play-state: running; }

@media (prefers-reduced-motion: reduce) {
  .marque { animation-play-state: paused !important; }
  /* Le filet se replace d'un coup plutôt que de glisser. */
  .est-epingle .pub__etapes { transition: none; }
}

/* --- Les créations : trois vidéos de clients -------------------------------

   Elles remplacent l'emplacement « créations à venir » de l'étape 01.

   ⚠️ TROIS CARTES 9/16 DANS LA COLONNE DE DROITE, qui fait environ 460 px. La
   largeur d'une carte commande sa hauteur (267 px pour 150) : au-delà, l'étape
   dépasserait la hauteur d'écran et on ne verrait plus les trois d'un coup. */
.pub__visuel--videos {
  align-items: start;
  gap: clamp(0.5rem, 1.2vw, 0.9rem);
  min-height: 0;
}

.video-carte { flex: 1 1 0; max-width: 150px; }

.video {
  position: relative;
  display: block;
  aspect-ratio: 9 / 16;
  border: 1px solid var(--trait);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.03);
  overflow: hidden;
}

.video__affiche {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Le bouton couvre toute la carte : on clique n'importe où sur l'affiche.
   ⚠️ ET IL REVIENT DÈS QUE LA VIDÉO EST EN PAUSE. Il ne partait qu'une fois,
   au premier clic : les trois vidéos lancées puis arrêtées, il ne restait que
   le minuscule ▶ des contrôles natifs sur une carte de 145 px, et Charles ne
   pouvait plus les relancer. C'est `est-en-lecture` — posée par le script sur
   `play`, retirée sur `pause` et `ended` — qui le cache, pas `est-lancee`. */
.video__lecture {
  position: absolute;
  inset: 0;
  /* Au-dessus du lecteur, qui arrive APRÈS lui dans le HTML. */
  z-index: 2;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  cursor: pointer;
}

/* Une fois le lecteur en place, le bas de la carte appartient aux contrôles
   natifs — sans quoi le voile les avalerait et on ne pourrait plus ni régler le
   son ni passer en plein écran. */
.video.est-lancee .video__lecture { bottom: 40px; }

/* La pastille, elle, est au centre. Le `padding-left` compense le triangle,
   dont la masse est à droite de son centre géométrique. */
.video__lecture svg {
  box-sizing: content-box;
  width: 18px;
  height: 18px;
  padding: 0.85em;
  padding-left: 1em;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  background: rgba(10, 10, 14, 0.45);
  transition: transform var(--t), background var(--t);
}

.video__lecture:hover svg,
.video__lecture:focus-visible svg {
  transform: scale(1.08);
  background: rgba(10, 10, 14, 0.7);
}

.video.est-en-lecture .video__lecture { display: none; }

/* Le lecteur, fabriqué au clic, prend la place de l'affiche. */
/* ⚠️ SANS FOND. Il était noir : au clic, l'affiche disparaissait derrière un
   rectangle vide pendant tout le téléchargement — plusieurs secondes sur une
   vidéo de 4 Mo. Transparent, le lecteur laisse voir l'affiche en dessous
   jusqu'à ce que sa première image soit décodée. */
.video__flux {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ⚠️ EN PLEIN ÉCRAN, `contain` ET NON `cover`. Dans la carte, `cover` est juste :
   la carte est en 9/16 comme la vidéo, rien n'est rogné. Mais en plein écran le
   cadre devient celui de l'ÉCRAN, en 16/9 : `cover` remplit alors la largeur et
   coupe le haut et le bas, si bien qu'on ne voyait plus qu'une tranche
   horizontale du personnage. `contain` garde la vidéo entière, centrée, avec du
   noir de chaque côté — ce qu'on attend d'une vidéo verticale.
   Les deux préfixes sont nécessaires : Safari ne connaît que le sien, et un
   sélecteur inconnu invalide TOUTE la règle, d'où deux blocs séparés. */
/* ⚠️ `pointer-events: auto` EST LA LIGNE QUI REND LE PLEIN ÉCRAN UTILISABLE.
   Les volets de la scène portent `pointer-events: none` tant qu'ils ne sont pas
   le volet courant — c'est ce qui empêche un volet invisible d'intercepter les
   clics. Mais passer en plein écran REDIMENSIONNE la fenêtre : la scène
   recalcule sa progression, le volet peut cesser d'être le courant, et la vidéo
   — toujours son descendant dans le document — héritait alors du blocage. Plus
   rien ne répondait, pas même le bouton de lecture natif.
   La règle est limitée à l'état plein écran : hors de lui, le blocage doit
   continuer de s'appliquer normalement. */
.video__flux:fullscreen {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: auto;
}

.video__flux:-webkit-full-screen {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: auto;
}

.video__client {
  margin-top: calc(var(--s) * 0.55);
  color: var(--texte-doux);
  font-size: calc(var(--s) * 0.85);
  text-align: center;
}

/* --- La lévitation ---

   ⚠️ ELLE NE TOURNE QUE SUR L'ÉTAPE COURANTE. `est-actif` est posée par le
   script sur l'étape visible ; ailleurs, aucune animation ne tourne — c'est la
   règle qu'on s'est donnée après la passe de fluidité.
   ⚠️ ET ELLE PART DE L'IMAGE 0, qui est la position normale de la carte. Dans
   un onglet d'arrière-plan l'horloge est suspendue : rien ne doit dépendre de
   l'animation pour être à sa place.
   Trois durées et trois décalages différents, sinon les cartes montent et
   descendent ensemble — ça ne flotte plus, ça respire en chœur. */
@keyframes levite {
  from { transform: translate3d(0, 0, 0) rotate(0deg); }
  50%  { transform: translate3d(0, -9px, 0) rotate(-0.5deg); }
  to   { transform: translate3d(0, 0, 0) rotate(0deg); }
}

.pub__etape.est-actif .video { animation: levite 6.4s ease-in-out infinite; }

.pub__etape.est-actif .video-carte:nth-child(2) .video {
  animation-duration: 7.6s;
  animation-delay: -2.9s;
}

.pub__etape.est-actif .video-carte:nth-child(3) .video {
  animation-duration: 5.8s;
  animation-delay: -1.2s;
}

/* Une vidéo qu'on regarde ne bouge pas. */
.video.est-lancee { animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  .video { animation: none !important; }
}

/* --- Où l'on en est ---

   ⚠️ UN SEGMENT PAR ÉTAPE, PAS UNE BARRE QUI SE REMPLIT. Charles a refusé deux fois
   un filet qui se remplit au défilement. Celui-ci ne se remplit pas : le
   segment de l'étape courante s'allume, les autres restent des traits sourds.*/
.pub__progres {
  display: flex;
  gap: 6px;
  margin-top: calc(var(--s) * 0.6);
}

.pub__progres span {
  display: block;
  width: clamp(22px, 3vw, 40px);
  height: 2px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
  transition: background var(--t);
}

.pub__progres span.est-actif { background: #7C5CFF; }

/* --- Le tapis roulant des sites -------------------------------------------

   ⚠️ LE TEXTE DU SERVICE EST LE PREMIER WAGON. Titre, mots-clés et les trois
   sites sont posés sur une seule bande ; le script ne déplace QUE cette bande,
   de façon linéaire. Deux conséquences voulues :
     · les sites POUSSENT le titre vers la gauche quand ils arrivent, sans qu'il
       y ait deux mouvements à accorder ;
     · le déplacement est constant — pas de « site par site » avec un arrêt à
       chaque fois. Charles : « un peu de résistance, mais linéaire ».
   ⚠️ ET C'EST UNE SEULE TRANSFORMATION PAR IMAGE. Ne rajouter ni fondu ni
   échelle par projet : on repasserait à quatre éléments animés et on reperdrait
   la fluidité que ce tapis a précisément apportée.
   ------------------------------------------------------------------------- */

/* ⚠️ CES VOLETS-LÀ NE SONT PAS DES GRILLES À DEUX COLONNES. Le volet générique
   l'est (texte à gauche, projets à droite) ; ceux qui portent leur propre mise
   en page — le tapis des sites, la séquence publicité — doivent être de simples
   blocs, sinon leur contenu se loge dans la colonne de GAUCHE et se retrouve
   comprimé sur un tiers de l'écran. */
.est-epingle [data-projets="glisse"],
.est-epingle [data-projets="etapes"] { display: block; }

.est-epingle .services__bande {
  display: flex;
  align-items: center;
  /* ⚠️ L'ÉCART FAIT PARTIE DU PAS. Le script lit la distance réelle entre les
     wagons (`offsetLeft`), donc changer cette valeur ne dérègle rien. */
  gap: clamp(1.5rem, 4vw, 4rem);
  width: 100%;
  will-change: transform;
}

/* Le wagon de texte fait toute la largeur du cadre : il est donc centré quand
   la bande est à zéro, exactement comme les autres volets. */
.est-epingle .services__bande .services__texte {
  flex: 0 0 100%;
  text-align: center;
  /* Le wagon fait toute la largeur de l'écran ; son texte, lui, reste dans la
     boîte du site. */
  padding-inline: var(--gouttiere);
}

.est-epingle .services__bande .services__projet {
  /* ⚠️ 64 % ET NON 100 % : les sites doivent SE SUIVRE. À pleine largeur on
     n'en voit qu'un à la fois et le glissement ne raconte rien.
     Le plafond en pixels évite qu'un site devienne démesuré sur un très grand
     écran, maintenant que la bande court d'un bord à l'autre. */
  flex: 0 0 min(64%, 820px);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(1rem, 2.5vh, 1.8rem);

  /* ⚠️ LE LIEN COUVRE TOUT LE WAGON, MAIS N'EST CLIQUABLE QUE SUR SON CONTENU.
     Sans ça, un clic dans le noir ouvrirait le site du client et le survol
     allumerait le voile de loin. */
  pointer-events: none;
}

.est-epingle .services__bande .services__projet-texte,
.est-epingle .services__bande .services__vignette { pointer-events: auto; }

/* ⚠️ HAUTEUR DE TEXTE RÉSERVÉE, sinon les captures ne s'alignent pas. Les
   descriptions n'ont pas toutes le même nombre de lignes : sans réserve, la
   carte la plus courte remonte son image et Charles voyait « la photo d'Ublo
   même pas à la même hauteur que les autres ». */
.services__projet-texte { min-height: calc(var(--s) * 7.2); }

.services__projet-accent {
  margin-top: calc(var(--s) * 0.45);
  color: #7C5CFF;
  font-weight: 600;
  font-size: calc(var(--s) * 0.95);
  letter-spacing: 0.02em;
}

.est-epingle .services__bande .services__vignette {
  width: min(100%, calc(38svh * 1.987));
}

.est-epingle [data-projets="solo"] .services__projet {
  grid-area: 1 / 1;
  display: grid;
  grid-template-columns: var(--col1) var(--col2);
  gap: var(--gap);
  align-items: center;
}

/* ⚠️ UN PROJET DE SITE EST CENTRÉ, ET TOUT Y ARRIVE ENSEMBLE — nom,
   description et capture. La capture a eu son propre temps pendant une version
   (le nom seul d'abord, l'image « de loin » ensuite) ; Charles l'a fait
   retirer. Le décalage vertical du bloc a disparu avec ce temps-là : il
   n'existait que pour recentrer le nom pendant qu'il était seul à l'écran.
   (Le sélecteur reprend `[data-projets="solo"]` uniquement pour peser plus lourd
   que la règle en deux colonnes juste au-dessus — sans ça, `display: grid` y
   gagnait et la mise en page centrée était ignorée sans la moindre erreur.) */
.est-epingle [data-projets="solo"] .services__projet--site {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: clamp(1.2rem, 3.5vh, 2.4rem);

  /* ⚠️ LA CARTE NE FAIT QUE LA LARGEUR DE LA CAPTURE. Elle occupait toute la
     colonne : un clic dans le noir, à cinquante centimètres de l'image,
     ouvrait quand même le site du client. La largeur est donc celle de la
     capture, et c'est la vignette qui prend 100 % de ce bloc. */
  width: min(60%, calc(42svh * 1.987), 780px);
  justify-self: center;
}

.est-epingle [data-projets="solo"] .services__projet--site .services__projet-desc { margin-inline: auto; }

/* La capture « de loin » : petite, floue et transparente, puis pleine taille.
   ⚠️ LA LARGEUR EST BORNÉE PAR LA HAUTEUR DISPONIBLE (`42svh × le rapport de
   l'image`) : sur un portable bas, une capture calculée en pourcentage de la
   largeur dépasse l'écran et se fait couper par la scène. */
.est-epingle [data-projets="solo"] .services__projet--site .services__vignette {
  width: 100%;
  /* L'opacité et l'échelle sont posées par le script. Le flou qu'il y avait
     ici — 8 px sur une image de 700 px de large, recalculé à chaque image —
     était le plus gros poste de saccade de la page. La capture arrive de loin
     par l'échelle et le fondu, ça suffit largement. */
  will-change: transform, opacity;
}

/* ⚠️ PLUS AUCUN `filter`, `clip-path` NI `scale` SUR DU TEXTE dans cette
   scène — et c'est LA raison pour laquelle elle saccadait. Un flou est repeint
   à chaque image ; un `clip-path` aussi ; une mise à l'échelle de texte force
   une re-rastérisation à chaque pas. Multiplié par le nombre d'éléments et par
   soixante images par seconde, ça ne tient pas.
   Il ne reste que `opacity` et `transform`, que le compositeur sait faire sans
   repeindre. C'est le script qui les pose directement (voir `poser` dans
   `js/main.js`) : écrire une VARIABLE CSS sur le volet invalidait en plus le
   style de tous ses descendants à chaque image. */

.est-epingle .services__projet {
  will-change: opacity, transform;
  pointer-events: none;
}

.est-epingle .services__projet.est-actif { pointer-events: auto; }

@media (prefers-reduced-motion: reduce) {
  .est-epingle .services__volet { transform: none; }
}


/* La scène épinglée sur petit écran.
   ⚠️ TOUT EST PLUS PETIT ICI, ET C'EST NÉCESSAIRE. À l'échelle du bureau, le
   titre de l'accroche tombait sur trois lignes dont une avec le seul mot
   « CHEZ », et les captures devenaient des timbres. Charles : « regarde les
   dimensions, ça ne va pas du tout ».
   ⚠️ La course est raccourcie : sur un téléphone, un rail trop long donne
   l'impression que la page a cessé de répondre. */
@media (max-width: 780px) {
  .services {
    --course: 1433vh;
    /* Un retrait plus franc : un écran de téléphone est haut et étroit, donc la
       moitié vide de la scène y pèse bien plus lourd qu'ailleurs. */
    margin-top: -30svh;
  }

  /* L'accroche : deux lignes équilibrées au lieu de trois bancales. */
  .services__nom--grand { font-size: calc(var(--s) * 3.4); }
  .services__marque { height: calc(var(--s) * 1.25); }

  .services__nom { font-size: calc(var(--s) * 2.9); }
  .services__mot { font-size: calc(var(--s) * 2.9); }

  .est-epingle .services__volet {
    grid-template-columns: 1fr;
    gap: clamp(1.1rem, 3.5vh, 2rem);
    align-content: center;
  }

  .est-epingle .services__texte,
  .est-epingle .services__projets,
  .est-epingle [data-projets="groupe"] .services__projets { grid-column: 1; }

  /* Le tapis : un site occupe presque tout l'écran, le suivant déborde juste
     assez pour dire qu'il est là. */
  .est-epingle .services__bande { gap: 1.2rem; }
  .est-epingle .services__bande .services__projet { flex-basis: 86%; }

  /* La séquence publicité sur téléphone : une colonne, le visuel sous le
     texte, et des logos un peu plus petits. */
  /* Le centrage est désormais celui de tous les écrans : il ne reste ici que
     ce qui dépend vraiment de la largeur. */
  .pub__visuel { min-height: calc(var(--s) * 4); }
  .pub__titre { font-size: calc(var(--s) * 2.2); }

  .services__projet-nom { font-size: calc(var(--s) * 2); }
  .services__projet-texte { min-height: calc(var(--s) * 8); }

  .est-epingle [data-projets="solo"] .services__projet {
    grid-template-columns: 1fr;
    gap: clamp(1rem, 3vh, 1.6rem);
  }

  .services__desc,
  .services__projet-desc,
  .services__ligne { max-width: 34ch; }

  .services__projet-desc { margin-top: calc(var(--s) * 0.5); }

}


/* Le résultat en vedette sur petit écran. Trois colonnes deviennent trois
   lignes : à moins de ~780 px, le chiffre — qui ne se casse jamais — pousse le
   logo et la phrase dans des colonnes trop étroites, et la phrase tombe en
   colonne de deux mots. Les filets verticaux n'ont plus de sens une fois
   empilé : ce sont les filets du haut et du bas qui tiennent la composition. */
@media (max-width: 780px) {
  .vedette__cas {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    gap: clamp(1rem, 3vh, 1.6rem);
  }

  .vedette__chiffre::before,
  .vedette__detail::before { display: none; }

  .vedette__phrase { max-width: 30ch; }
  .vedette__accent::before { margin-inline: auto; }
}


/* La section équipe sur petit écran. En deux colonnes, celle des photos ne fait
   plus que ~170 px de large alors que la plus large des trois photos en demande
   plus de 230 : elle débordait et élargissait la page. On empile. */
/* ⚠️ LE MENU MOBILE N'APPARAISSAIT JAMAIS. Le hamburger, son panneau et tout
   son script étaient là — portés depuis le vitrine Ublo — mais AUCUNE media
   query ne le sortait de son `display: none`. Sur téléphone, c'étaient donc les
   quatre liens de bureau qui restaient dans la barre, et ils débordaient :
   « Contact » se faisait couper au milieu. Charles : « la barre du haut est
   vraiment laide, on dirait qu'elle est buguée ». C'est ce bloc qui manquait.
   ⚠️ 900 px et non 780 : ce n'est pas une question de téléphone, c'est le point
   où logo + quatre liens + bouton cessent de tenir sur une rangée. */
@media (max-width: 900px) {
  .nav-liens { display: none; }
  .nav-burger { display: flex; }
  /* Le bouton d'appel cède la place au hamburger : ils partagent la troisième
     colonne, et l'action reste offerte dans le panneau qui s'ouvre. */
  .nav-actions { display: none; }
}


@media (max-width: 860px) {
  .equipe {
    /* Les photos peuvent occuper une bonne part de la largeur, puisqu'elles
       ne partagent plus la ligne avec le texte. */
    /* La même échelle, recalibrée : sur un écran étroit, `1.28vw` ne vaut
       plus rien et c'est le minimum qui commanderait — un titre de 59 px qui
       ne tient pas sur la largeur. */
    --e: clamp(0.8rem, 3.4vw, 1.15rem);
    --photo-h: calc(var(--e) * 20);

    /* Rien à resserrer ici : l'écart et, s'il le faut, la taille des photos
       se déduisent de la largeur du cadre (`mesurerScene` dans `js/main.js`).
       Un téléphone n'est donc plus un cas particulier, juste un cadre étroit. */

    /* ⚠️ MOINS D'AIR AU-DESSUS QU'EN DESSOUS. Le `12vh` général valait ~96 px
       sur un téléphone, auxquels s'ajoutaient le bas du défilé de logos et
       l'air que son cadre porte déjà sous les logos : le vide entre les deux
       sections devenait béant. Un écran de téléphone est haut et étroit, les
       respirations verticales y coûtent bien plus cher que sur un écran large.
       Le bas reste généreux, lui, pour détacher la section de ce qui suivra. */
    padding-top: clamp(1.5rem, 5vh, 3rem);
    /* Presque plus d'air en dessous : la scène épinglée qui suit apporte déjà
       la sienne, et elle en apporte beaucoup. */
    padding-bottom: clamp(1rem, 2.5vh, 2rem);
  }

  .equipe__grille {
    grid-template-columns: 1fr;
    gap: clamp(2rem, 5vh, 3rem);
  }

  .equipe__intro { text-align: center; }
  .equipe__soustitre { margin-inline: auto; max-width: 26rem; }
}


/* Le défilé sur petit écran. Trois réglages bougent ensemble, et il FAUT les
   trois : un écart relatif plus grand, des logos plus petits, et une mise en
   évidence moins forte. Changer l'un sans les autres ramène le chevauchement.
   ⚠️ La portée (combien de voisins sont affichés) est calculée par le script à
   partir de l'écart : elle se resserre donc toute seule ici. */
@media (max-width: 700px) {
  /* --- Le hero sur téléphone ---

     ⚠️ LE TITRE A SON PROPRE CALIBRE ICI, et il le faut. La règle générale
     part de `clamp(3rem, …)` : sur un écran de 414 px, c'est ce minimum de
     48 px qui l'emporte, alors que la largeur disponible n'en autorise que
     ~37. Résultat, chacune des deux phrases se recassait en deux et le titre
     tombait sur quatre lignes avec des mots orphelins — « CLIENTS. » et
     « WEB. » seuls sur leur ligne. C'est ce que Charles a vu.

     ⚠️ CE RAISONNEMENT NE VAUT PLUS DEPUIS LE 2026-08-26 : le titre est
     devenu « Agence web / au Québec », soit dix et neuf capitales au lieu de
     deux phrases. Il a donc infiniment plus de place, et la valeur ci-dessous
     est écrasée plus bas dans ce même bloc. */
  .hero__titre { font-size: clamp(2rem, 9vw, 3rem); }

  /* Une barre plus basse : à 84 px elle mange un sixième d'un écran de
     téléphone avant même que le contenu commence. */
  :root { --nav-h: 68px; }

  .hero {
    /* ⚠️ LE HERO OCCUPE PRESQUE TOUT L'ÉCRAN, ET C'EST CE QUI RÈGLE LE
       PROBLÈME DES LOGOS. Sans hauteur imposée, la bande de logos tombait à
       cheval sur le bas de l'écran : on en voyait la moitié, ce qui se lit
       comme un affichage cassé plutôt que comme une invitation à défiler. En
       calant le hero sur 84 % de la hauteur, seul le libellé « Ceux qui nous
       font confiance » dépasse — assez pour donner envie de descendre, pas
       assez pour montrer des logos coupés.

       ⚠️ `svh` ET NON `vh`. Sur mobile, `vh` correspond à l'écran BARRE
       D'ADRESSE RÉTRACTÉE : en calculant avec, tout le bas passe sous la barre
       tant qu'elle est déployée — c'est-à-dire à l'arrivée sur le site,
       précisément le moment qu'on essaie de soigner. `svh` prend la version la
       plus petite, donc ça tient dans tous les cas.
       La ligne en `vh` juste avant sert de repli aux navigateurs qui ignorent
       `svh`. */
    /* ⚠️ 72 %, PAS 84 % (2026-08-26). À 84 %, seul le libellé « Ceux qui nous
       font confiance » dépassait et les logos restaient entièrement sous le
       pli — Parker : « rapproche les logos des compagnies en bas pour qu'on
       puisse les voir un peu au pire ». À 72 %, la première rangée de logos
       apparaît en entier plutôt que d'être coupée en deux : un logo à moitié
       visible se lit comme un affichage cassé, un logo entier se lit comme
       une invitation à descendre. C'est tout l'arbitrage de ce nombre. */
    min-height: 72vh;
    min-height: 72svh;

    /* Le contenu se centre dans cette hauteur : l'espace gagné se répartit
       au-dessus et en dessous au lieu de s'accumuler d'un seul côté. */
    display: flex;
    flex-direction: column;
    justify-content: center;

    padding-top: calc(var(--nav-h) + clamp(1rem, 2vh, 3rem));
  }

  /* ⚠️ TITRE NETTEMENT PLUS GROS DEPUIS LE 2026-08-26 (Parker : « regarde le
     titre, il me semble qu'il est petit »). La cause : le `h1` est passé de
     deux phrases à deux mots le même jour, et la taille n'avait pas suivi —
     à 9,6 vw il occupait moins du tiers de la largeur disponible.
     Mesuré : en Bebas Neue, capitale condensée à ~0,40 em de large, « AGENCE
     WEB » tient jusqu'à 86 px de police sur un écran de 390. À 16 vw on est à
     62 px, soit ~28 % de marge — assez pour absorber une police de repli plus
     large sans que le mot touche les bords.
     ⚠️ SI LE TITRE CHANGE ENCORE, REFAIRE CE CALCUL. Une valeur en vw calée
     sur un texte court déborde dès qu'on rallonge le texte. */
  .hero__titre { font-size: clamp(3rem, 16vw, 5rem); }
  .hero__soustitre { font-size: 1.09rem; }

  .defile {
    /* En `svh` pour la même raison que le hero : c'est ce qui décide où tombe
       le libellé par rapport au bas de l'écran. Resserré de 5 à 2,5 avec le
       passage du hero à 72 % : les deux réglages travaillent ensemble pour
       remonter les logos. */
    padding-top: 2.5vh;
    padding-top: 2.5svh;
  }

  .defile__cadre {
    --pas-ratio: 0.34;
    --defile-grand: 1.25;
    --defile-profondeur: 80;
  }

  .defile__cadre.est-3d { height: 110px; }

  .defile__item--carre       img { height: 44px; }
  .defile__item--moyen       img { height: 37px; }
  .defile__item--allonge     img { height: 34px; }
  .defile__item--large       img { height: 29px; }
  .defile__item--tres-large  img { height: 23px; }
  .defile__item--sceau       img { height: 35px; }
}

@media (max-width: 480px) {
  /* ⚠️ PAS DE `width: 100%` ICI. Les boutons s'étalaient d'un bord à l'autre —
     Charles : « les boutons sont trop larges ». Ils reprennent la largeur de
     leur texte et se centrent ; comme `.hero__actions` passe à la ligne, ils
     s'empilent proprement l'un sous l'autre. */
  .hero__actions { flex-direction: column; align-items: center; }

  /* Un rembourrage un peu plus serré : à 2rem de chaque côté, « Céduler une
     analyse gratuite » frôlait quand même les bords. */
  .bouton--grand {
    padding: 1rem 1.65rem;
    font-size: 1rem;
  }
}


/* --- Appel final --------------------------------------------------------- */
/* Un titre, seul, au milieu de l'écran. Charles n'a demandé que ça : pas de
   sous-titre, pas de bouton. Le bouton d'appel est déjà dans l'en-tête, qui
   flotte au-dessus de cette section — en remettre un ici ferait doublon à
   trente centimètres d'écart. */

.appel {
  /* ⚠️ LA MARGE NÉGATIVE EST CE QUI FAIT DU FONDU UN VRAI CROISEMENT. Sans
     elle, le mur de vidéos finissait de se dissoudre pile à la fin du rail, et
     il restait ensuite un demi-écran d'étoiles vides avant que le titre ne
     monte : un fondu, puis un trou, puis une arrivée. En remontant cette
     section de 45 vh, elle chevauche la fin du rail — elle est donc déjà en
     train d'arriver PENDANT que le mur s'efface, et le titre franchit le bas de
     la fenêtre au moment où il n'en reste presque plus rien.
     ⚠️ ELLE NE TOUCHE PAS AUX CALCULS DE LA SCÈNE : ceux-ci ne lisent que la
     hauteur du rail, qui ne bouge pas d'un pixel.
     ⚠️ ET `z-index: 2` VA AVEC. La scène est `position: sticky`, donc
     positionnée ; une section qui ne l'est pas se peint SOUS elle, et le titre
     serait passé derrière le mur au lieu de par-dessus. */
  /* ⚠️ LA REMONTÉE EST POSÉE PAR `js/main.js`, PAS ICI, ET C'EST DÉLIBÉRÉ.
     Elle valait `-100svh` tant que cette section faisait un écran de haut. Mais
     une section d'un écran avec son bloc centré laisse mécaniquement un demi-
     écran de VIDE en dessous — plus de 500 px sur un grand moniteur — et le
     calendrier se retrouvait loin dessous. Rattraper ce vide côté `.rdv` avec
     une fraction d'écran devinée ne marchait sur aucune taille en particulier.
     La section fait donc maintenant la hauteur de son contenu, et le script
     calcule la remontée exacte pour que le bloc tombe au centre de l'écran à la
     fin du rail : un demi-écran plus une demi-hauteur de bloc. Plus de vide à
     rattraper, plus rien à deviner. */
  position: relative;
  z-index: 2;
  /* Pendant qu'elle chevauche le rail, cette section couvre la scène : sans ça,
     elle intercepterait les clics sur les vidéos du mur alors qu'elle est
     encore transparente et vide. Elle ne redevient cliquable qu'une fois
     révélée — et il n'y a alors plus rien derrière. */
  pointer-events: none;
  /* `svh` et non `vh` : sur téléphone, `100vh` compte la barre d'adresse comme
     si elle n'existait pas, et le titre se retrouve décentré vers le bas de la
     hauteur de cette barre. `svh` mesure l'écran réellement disponible. */
  display: grid;
  place-items: center;
  /* ⚠️ `align-content: center`, ET CE N'EST PAS UN DOUBLON DE `place-items`.
     `place-items` centre CHAQUE élément dans SA rangée ; `align-content` place
     l'ensemble des rangées dans la grille. Sans lui, les deux rangées (le titre
     et le sous-titre) s'étirent pour remplir l'écran et le sous-titre part
     130 px trop bas — mesuré. Exactement le même piège que dans les colonnes du
     bas de page : une grille dont la hauteur est imposée répartit ses rangées
     sur toute cette hauteur. */
  align-content: center;
  padding-inline: var(--gouttiere);
  /* Le haut dégage l'en-tête flottant, qui passe par-dessus cette section
     quand elle occupe l'écran. */
  /* ⚠️ LES DEUX MARGES SONT ÉGALES, ET C'EST CE QUI REND LE CALAGE EXACT. Avec
     un haut plus épais que le bas (pour dégager l'en-tête), le titre tombait à
     57 % de la hauteur de sa section au lieu de 50 — et le décalage variait
     avec la taille de la fenêtre, donc aucune valeur de `margin-top` ne pouvait
     le rattraper partout. Marges égales : le titre est au centre exact, quelle
     que soit la fenêtre, et les −100 svh le déposent au centre de l'écran
     pile quand le rail se termine. */
  /* Aucune marge verticale : l'écart avec le calendrier est porté par `.rdv`,
     en un seul endroit. */
  padding-block: 0;
  text-align: center;
}

.appel.est-revele { pointer-events: auto; }

/* ⚠️ PAS DE LUEUR DERRIÈRE CE TITRE. Une lueur mauve radiale y était ; Charles,
   le 2026-08-23, capture annotée à l'appui : « enlève le glow en arrière de
   "Prêt à réaliser votre projet ?" ». Le fond de cette section, c'est le ciel
   d'étoiles et rien d'autre — comme partout ailleurs sur le site. Ne pas la
   réintroduire, sous aucune forme (dégradé posé, flou, ombre de texte). */

.appel__titre {
  /* Même échelle que le hero, un cran au-dessus : ce titre est seul à l'écran,
     il peut se permettre ce que le hero ne peut pas (le hero doit laisser la
     place à son sous-titre et à ses deux boutons).
     Le `min(…, 15vh)` est le même garde-fou qu'au hero : sur une fenêtre large
     mais basse, une taille purement en `vw` fabrique deux lignes plus hautes
     que l'écran. */
  font-size: clamp(2.6rem, min(7.4vw, 15vh), 6.5rem);
  line-height: 0.92;
  letter-spacing: 0.012em;
  text-wrap: balance;
}

/* La deuxième ligne porte la couleur. Deux raisons de ne pas colorer le titre
   entier : le violet sur fond noir est moins lisible que le blanc à cette
   taille, et la question tombe sur ces deux mots.

   ⚠️ CE SONT LES PROPRIÉTÉS DE `.services__mot` REPRISES À L'IDENTIQUE —
   « PERFORMENT / VENDENT / CONVERTISSENT ». Demande de Charles, le 2026-08-23,
   capture de « CONVERTISSENT » à l'appui. Toute retouche faite ici doit être
   faite là-bas aussi, sinon les deux violets divergent — et deux violets
   voisins qui ne sont pas les mêmes, ça se voit tout de suite.

   ⚠️ ET LE HALO EN FAIT PARTIE. Première tentative avec la matière seule : sans
   le halo derrière les lettres, le mot paraît délavé à côté de son jumeau —
   Charles : « c'est genre fucking pâle ». Ce halo-là (une copie floutée du
   texte, collée sous les lettres) n'a rien à voir avec la LUEUR D'AMBIANCE qu'il
   a fait retirer : celle-là était un nuage mauve de la taille de l'écran,
   derrière tout le titre. Ne pas confondre les deux en relisant ce bloc. */
.appel__mauve {
  /* `inline-block` + `relative` : le halo est posé en absolu sur cette boîte,
     et un élément inline n'en fournit pas une qui tienne. */
  display: inline-block;
  position: relative;
  color: #7C5CFF;
  background-image:
    radial-gradient(38% 120% at 22% 40%, #CFC2FF 0%, transparent 62%),
    radial-gradient(34% 110% at 72% 66%, #6E4CF0 0%, transparent 64%),
    radial-gradient(45% 130% at 52% 15%, #A08FFF 0%, transparent 70%);
  background-color: #7C5CFF;
  background-size: 180% 180%;
  background-position: 18% 34%;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: mot-respire 9s ease-in-out infinite alternate;
  /* ⚠️ EN PAUSE PAR DÉFAUT. C'est la seule différence assumée avec les mots-clés
     de la section services : ce titre est en bas de page, et une animation de
     peinture qui tourne en boucle vingt écrans plus haut se paie sans que
     personne ne la voie. `js/main.js` pose `est-en-vue` à l'approche. */
  animation-play-state: paused;
}

.appel.est-en-vue .appel__mauve { animation-play-state: running; }

/* Le fondu d'apparition. Demande de Charles, le 2026-08-23.
   ⚠️ IL PORTE SUR LE TITRE ENTIER, PAS SUR CHAQUE LIGNE. La deuxième ligne a un
   halo flouté derrière elle ; la faire apparaître séparément ferait arriver le
   halo à contretemps du texte qu'il éclaire.
   ⚠️ `opacity` ET `translate` SEULEMENT — les deux propriétés que le
   compositeur sait animer sans repeindre. Pas de `filter`, pas de `blur`. */
.appel__titre {
  opacity: 0;
  /* ⚠️ 12 px, PAS 22. La section monte DÉJÀ avec le défilement de la page :
     une montée trop marquée par-dessus fait traîner le titre derrière son
     propre cadre. Ce qu'on veut, c'est le fondu ; le décalage n'est là que pour
     lui donner un sens de lecture. */
  translate: 0 12px;
  transition: opacity 0.8s var(--ease), translate 0.8s var(--ease);
}

.appel.est-revele .appel__titre {
  opacity: 1;
  translate: 0 0;
}

/* ⚠️ LE SOUS-TITRE NE DÉCALE PAS LE CALAGE DE LA SECTION, et c'est le padding
   symétrique qui le garantit : le bloc entier reste centré dans la section,
   donc son centre tombe toujours au centre de l'écran à la fin du rail. Ce qui
   change, c'est seulement que le titre monte de la moitié de la hauteur du
   sous-titre — ce qu'on veut d'un bloc centré. Ne pas « corriger » ça en
   remettant des marges inégales : ça casserait le calage. */
.appel__soustitre {
  position: relative;
  margin-top: clamp(1.1rem, 2.6vh, 2rem);
  margin-inline: auto;
  max-width: 34rem;
  color: var(--texte-doux);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem);
  line-height: 1.55;
  text-wrap: pretty;
  opacity: 0;
  translate: 0 12px;
  /* Le retard de 0,16 s fait lire le titre d'abord, la phrase ensuite. C'est
     assez pour se sentir, trop court pour qu'on attende. */
  transition: opacity 0.8s var(--ease) 0.16s, translate 0.8s var(--ease) 0.16s;
}

.appel.est-revele .appel__soustitre {
  opacity: 1;
  translate: 0 0;
}

@media (prefers-reduced-motion: reduce) {
  /* Sans mouvement, le titre est simplement là : pas de fondu retardé qui
     laisserait un écran vide à quelqu'un qui a demandé moins d'animation. */
  .appel__titre,
  .appel__soustitre { opacity: 1; translate: none; transition: none; }
}

/* Le halo. ⚠️ IL EST DERRIÈRE, PAS EN `text-shadow` : un texte dont le
   remplissage est transparent (`background-clip: text`) ne projette plus
   d'ombre de texte — elle se dessinerait sous des lettres vides et on ne
   verrait qu'une tache floue. C'est donc une copie du mot, floutée, posée
   dessous. Exactement `.services__mot::before`, valeurs comprises.
   Le flou n'est JAMAIS animé : il est rastérisé une fois. */
.appel__mauve::before {
  content: attr(data-mot);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: #6A45E8;
  -webkit-text-fill-color: #6A45E8;
  filter: blur(18px);
  opacity: 0.75;
}

@media (prefers-reduced-motion: reduce) {
  .appel__mauve { animation: none; }
}


/* --- Repli EMPILÉ du téléphone (≤ 700 px) ---------------------------------

   Sur tactile, la scène pilotée par le défilement perdait le monde (constat
   Clarity, 2026-08-25) : sous 700 px, main.js N'ÉPINGLE PAS — pas de classe
   `est-epingle`, pas de rail, pas de script — et les quatre volets se lisent
   comme des sections ordinaires. CE BLOC EST LE SEUL ENDROIT qui met en forme
   ce repli ; tout le reste du fichier suppose l'épinglage. Desktop intouché.

   ⚠️ TESTER AVEC LE VRAI CHEMIN : recharger la page sous 700 px (le script
   décide au chargement), pas juste rétrécir la fenêtre. */
/* ⚠️ ROTATION AUTONOME DES TROIS MOTS-CLÉS, TÉLÉPHONE UNIQUEMENT.
   Sur ordinateur, ce sont `js/main.js` et le défilement qui posent l'opacité
   et l'angle de chaque mot ; le script sort à ≤700 px, la place est libre.

   ⚠️ LE CYCLE COMMENCE SUR « VISIBLE » (0 % = opacité 1), et c'est ce qui rend
   le repli sûr : si l'horloge d'animation est arrêtée — onglet en
   arrière-plan, `prefers-reduced-motion`, navigateur ancien — le premier mot
   reste affiché. Il n'existe aucun instant où les trois sont invisibles.

   ⚠️ LES RETARDS SONT NÉGATIFS, PAS POSITIFS : un retard positif laisserait
   les deux autres mots absents pendant les premières secondes. En négatif, ils
   démarrent déjà avancés dans leur cycle, donc déjà à leur place.
   9 s / 3 mots = 3 s chacun, et 9 s est aussi la durée de `mot-respire` : les
   deux horloges restent en phase.

   La bascule reprend la charnière du desktop (`transform-origin: center top`,
   déjà posée) : le mot arrive replié vers le lecteur et se pose à plat. */
@keyframes projet-relief {
  0%, 100% { transform: scale(0.82); filter: brightness(0.45) saturate(0.8); }
  46%, 54% { transform: scale(1);    filter: brightness(1)    saturate(1); }
}

@keyframes mot-bascule {
  0%,  28% { opacity: 1; transform: rotateX(0deg); }
  33%      { opacity: 0; transform: rotateX(-62deg); }
  94%      { opacity: 0; transform: rotateX(62deg); }
  100%     { opacity: 1; transform: rotateX(0deg); }
}

@media (max-width: 700px) and (prefers-reduced-motion: no-preference) {
  /* ⚠️ `mot-respire` EST RECONDUIT ICI. C'est l'animation de la couleur dans
     les lettres, déclarée dans la règle de base : redéclarer `animation` sans
     elle la supprimerait et les mots deviendraient d'un violet plat. */
  .services__mot {
    animation: mot-respire 9s ease-in-out infinite alternate,
               mot-bascule 9s ease-in-out infinite;
  }
  .services__mots .services__mot:nth-child(2) { animation-delay: 0s, -6s; }
  .services__mots .services__mot:nth-child(3) { animation-delay: 0s, -3s; }
}

@media (max-width: 700px) {
  /* Le retrait de -28vh servait à mordre sur le vide de la scène épinglée ;
     sans scène, il fait chevaucher le titre sur l'équipe. */
  .services { margin-top: 0; }

  .services__volet { padding-block: 2.5rem; }
  .services__volet--intro { padding-block: 3rem 0; text-align: center; }
  .services__volet--intro .services__texte { justify-items: center; }

  /* ⚠️ REVIREMENT DU 2026-08-26 : LES TROIS MOTS TOURNENT AUSSI SUR TÉLÉPHONE.
     Jusqu'ici on n'en gardait qu'un, figé, parce que la rotation appartenait à
     la scène épinglée — qui n'existe pas ici (`js/main.js` sort à ≤700 px avant
     de poser le moindre style en ligne). Demande de Parker : « je voudrais que
     les mots changent automatiquement ». La rotation est donc pilotée en CSS,
     par une horloge autonome, et non par le défilement.

     ⚠️ REPLI SÛR OBLIGATOIRE, ET IL EST DANS LE JEU DE CLÉS. L'opacité de base
     est 0 pour la pile, MAIS le premier mot est à 1 — et l'animation commence
     précisément sur lui (0 % = visible). Conséquence : à tout instant où
     l'horloge est arrêtée — onglet en arrière-plan, mouvement réduit,
     navigateur qui ignore l'animation — il y a TOUJOURS exactement un mot
     visible. C'est la règle du site : jamais d'`opacity: 0` en comptant sur
     une animation pour révéler le contenu. */
  .services__mot { display: block; opacity: 0; }
  .services__mots .services__mot:first-child { opacity: 1; }

  /* Les projets du tapis : de VRAIES CARTES, bordées et fermées — sans elles,
     les noms de projets flottaient à la même taille que les titres de section
     et tout se confondait (Parker : « je me perds dans le site »). La vignette
     d'abord, le texte dessous, dans la carte. */
  .services__projet {
    display: flex;
    flex-direction: column;
    margin-top: 1.4rem;
    padding: 0.9rem 0.9rem 1.2rem;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--trait);
    border-radius: 18px;
  }
  .services__projet .services__vignette { order: -1; margin-bottom: 1rem; }
  .services__projet-texte { min-height: 0; }
  .services__projet-nom { font-size: 1.45rem; letter-spacing: 0.02em; }
  .services__projet-desc { margin-top: 0.4rem; font-size: 1rem; max-width: none; }
  .services__projet-accent { margin-top: 0.5rem; font-size: 0.95rem; }

  /* ⚠️ LE TITRE DU VOLET DES SITES EST DE RETOUR (2026-08-26). Il avait été
     retiré le 2026-08-25 — Parker : « enlève tout court le titre, ça fait
     vraiment laid » — parce qu'il tombait alors sur un mot-clé figé et sans
     vie. Avec les trois mots qui tournent, Parker le redemande, capture à
     l'appui. NE PAS LE RE-SUPPRIMER en se fiant au commentaire de la veille. */
  .services__volet[data-projets="glisse"] .services__texte {
    display: grid;
    justify-items: center;
    text-align: center;
  }
  .services__volet[data-projets="glisse"] .services__projet:first-of-type { margin-top: 1.6rem; }

  /* Hiérarchie (recalée le 2026-08-26, retour Parker) : les titres de volet
     — « De la création aux résultats. », « Création de contenu », l'accroche —
     sont du MÊME niveau que « Notre équipe » : ils reprennent donc EXACTEMENT
     son échelle (le jeton `--e` de la section équipe × 3.7). Et les titres
     d'étapes (01 On crée…, 02 On gère…, 03 Vous avancez.) descendent d'un
     étage franc : ils étaient à 2.2 × s, quasi la taille d'un titre de volet,
     et les deux niveaux se confondaient. */
  .services__volet .services__nom { font-size: calc(clamp(0.8rem, 3.4vw, 1.15rem) * 3.7); }
  .services__mot { font-size: calc(clamp(0.8rem, 3.4vw, 1.15rem) * 3.7); justify-self: center; }
  .pub__titre { font-size: 1.5rem; }

  /* Les halos mauves, calmés : à pleine puissance sur un petit écran OLED,
     le mot « crame » (Parker, 2026-08-25). La peinture dans les lettres reste,
     la lueur derrière descend. */
  .services__mot::before,
  .appel__mauve::before { opacity: 0.3; filter: blur(12px); }

  /* Un filet entre les volets : le repère « on change de sujet » que la scène
     épinglée donnait par sa bascule. Fin, centré, fondu aux extrémités. */
  .services__volet + .services__volet {
    position: relative;
    margin-top: 1.6rem;
    padding-top: 3.2rem;
  }
  .services__volet + .services__volet::before {
    content: '';
    position: absolute;
    top: 0;
    left: 18%;
    right: 18%;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--trait) 30%, var(--trait) 70%, transparent);
  }

  /* Le mur du catalogue redevient un bloc du flux — son `position: absolute`
     centre la scène épinglée et ÉCRASE le volet en mode empilé. */
  .mur {
    position: static;
    top: auto;
    transform: none;
  }
  .mur__entete { text-align: center; }

  /* ⚠️ SECOND REVIREMENT SUR CE BLOC — LIRE AVANT DE TOUCHER.
     · 2026-08-26 (matin) : les deux rangées ont été fusionnées en UN carrousel
       9/16 qu'on faisait glisser au doigt, sur le retour de Parker « on en
       voit vraiment pas gros et sont coupés ».
     · 2026-08-26 (après-midi) : Parker redemande les DEUX RANGÉES du desktop,
       capture d'écran de la version ordinateur à l'appui — « c'est beaucoup
       plus beau ».

     ⚠️ ET LA VRAIE CAUSE DU PREMIER PROBLÈME ÉTAIT AILLEURS. Les rangées ne
     bougeaient pas sur téléphone : leur animation est allumée par
     `.est-epingle .services__volet.est-eveille .bande`, et `est-epingle` est
     posée par le script — qui sort à ≤700 px depuis le 2026-08-25. On voyait
     donc deux rangées de carrés FIGÉS et rognés, ce qui n'a effectivement
     aucun intérêt. Ce sont deux rangées QUI DÉFILENT qu'il fallait, pas un
     carrousel manuel. C'est ce que fait le bloc ci-dessous.

     ⚠️ NE PAS RÉTABLIR `display: contents` SUR `.bande` : il dissout les deux
     rangées en une seule et casse la boucle. Et ne pas masquer les copies avec
     `:nth-child(n + 14)` — chaque rangée contient son contenu EN DOUBLE, et
     c'est exactement ce qui permet à la translation de −50 % de reboucler sans
     raccord visible. Les copies ne sont pas du gras, elles sont le mécanisme.

     La largeur des vignettes, elle, n'a rien à faire ici : la règle
     `@media (max-width: 780px) { .video.bande__vue { width: 42vw } }` plus
     haut dans le fichier s'en charge déjà. */
  .bandes {
    /* Pleine largeur d'écran, et on coupe net aux bords — comme sur
       ordinateur, où c'est le cadre de la scène qui rogne. */
    overflow: hidden;
    margin-inline: calc(var(--gouttiere) * -1);
    padding-block: 0.3rem 0.5rem;
  }

  /* ⚠️ C'EST CETTE LIGNE QUI FAIT TOUT PARTIR. Sans elle, les deux rangées
     restent sur l'`animation-play-state: paused` de leur règle de base et ne
     bougent jamais sur téléphone.
     Le repli « mouvement réduit » reste assuré : la règle
     `@media (prefers-reduced-motion: reduce)` plus haut porte un `!important`
     et l'emporte donc sur celle-ci. Les vignettes restent alors visibles,
     simplement immobiles. */
  .bande { animation-play-state: running; }

  /* Le bouton vers la page du service : il n'apparaît QUE dans ce repli.
     Aligné à gauche comme le reste du volet, avec de l'air au-dessus pour ne
     pas se coller à la dernière carte. */
  .services__vers-page {
    display: inline-flex;
    margin-top: 1.8rem;
  }
  /* Le volet des sites est centré (titre, mot-clé, cartes, bouton). */
  .services__volet[data-projets="glisse"] { text-align: center; }

  /* ⚠️ LE CARROUSEL DES PROJETS. Demande de Parker : « je veux que les sites
     soient en mode qu'on peut glisser de gauche à droite, dans le même style
     que les photos d'équipe ». La bande redevient un bloc — c'est le
     CONTENEUR DES WAGONS qui devient le défilé, pas la bande : le titre du
     service est le premier enfant de la bande, et il n'a rien à faire dans le
     défilement horizontal.
     ⚠️ AUCUN JAVASCRIPT. Défilement natif, avec l'inertie et le magnétisme du
     système : ça ne peut pas se figer dans un onglet d'arrière-plan, et ça se
     comporte comme le reste du téléphone. Même recette que le catalogue vidéo
     juste au-dessus. */
  /* ⚠️ `will-change: transform` EST ANNULÉ ICI. Il est posé dans la règle de
     base pour le tapis roulant du desktop — mais sur téléphone le script ne
     tourne pas, il n'y a donc aucune translation à optimiser, et il créerait
     un contexte de composition autour d'un conteneur qui défile. C'est une
     source connue de défilement capricieux sur iOS. */
  .services__bande { display: block; will-change: auto; }

  .services__wagons {
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x mandatory;
    gap: 0.9rem;
    /* Pleine largeur d'écran : les cartes glissent bord à bord et la suivante
       dépasse juste assez pour qu'on devine qu'il y en a d'autres. */
    margin-inline: calc(var(--gouttiere) * -1);
    padding: 0.4rem var(--gouttiere) 0.6rem;
    scroll-padding-inline: var(--gouttiere);
    margin-top: 1.6rem;
    /* La barre de défilement n'apporte rien ici : le débord des cartes dit
       déjà qu'on peut glisser. */
    scrollbar-width: none;
  }
  .services__wagons::-webkit-scrollbar { display: none; }

  .services__inclus {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.4rem;
    margin-top: 1rem;
    list-style: none;
    padding: 0;
  }
  .services__inclus li {
    padding: 0.32rem 0.7rem;
    border: 1px solid var(--trait);
    border-radius: 999px;
    color: var(--texte-doux);
    font-size: 0.78rem;
    line-height: 1.2;
  }

  .services__volet[data-projets="glisse"] .services__projet,
  .services__volet[data-projets="glisse"] .services__projet:first-of-type {
    flex: 0 0 76vw;
    max-width: 320px;
    margin-top: 0;
    scroll-snap-align: center;
    text-align: left;
  }

  /* ⚠️ LE RELIEF DES CARTES — l'effet du carrousel d'équipe, obtenu sans une
     ligne de JavaScript. Demande de Parker : « je peux glisser les projets,
     ils sont superposés, un au premier plan, les deux sur le côté un peu plus
     sombres ».

     ⚠️ POURQUOI PAS LE MÊME CODE QUE L'ÉQUIPE : ce carrousel-là mesure où se
     trouve le VISAGE dans chaque photo (fonction `silhouette`, main.js) pour
     aligner les têtes entre elles. C'est spécifique aux portraits et
     inutilisable pour des cartes. Ici, l'échelle et l'assombrissement suffisent
     à donner la même lecture — un au premier plan, deux en retrait.

     ⚠️ `view(inline)` LIT LA POSITION DE LA CARTE DANS SON DÉFILÉ HORIZONTAL,
     pas le défilement de la page. Le milieu de la course (50 %) est l'instant
     où la carte est centrée : c'est là qu'elle est pleine taille et pleine
     lumière. Aux deux bouts, elle recule.

     ⚠️ TOUT EST DANS UN `@supports`, ET LE REPLI EST LE COMPORTEMENT NORMAL :
     sans prise en charge, les cartes restent à taille pleine et le carrousel
     glisse comme avant. Rien n'est jamais masqué par cette animation — elle
     ne fait que moduler l'échelle et la luminosité de contenu déjà visible. */
  @supports (animation-timeline: view()) {
    .services__volet[data-projets="glisse"] .services__projet {
      animation: projet-relief linear both;
      animation-timeline: view(inline);
    }
  }

  /* ⚠️ LES TROIS TRAITS DE PROGRESSION N'ONT PLUS DE SENS ICI (retrait demandé
     par Parker le 2026-08-26). Ils disent « à quelle étape on est » dans la
     scène épinglée ; empilées, les trois étapes sont toutes visibles en même
     temps et il ne reste que trois tirets gris sous la dernière carte. */
  .pub__progres { display: none; }

  /* Publicité : l'en-tête puis les trois étapes, empilées avec du souffle. */
  .pub__etape { margin-top: 2rem; }

  /* Le même filet avant « Prêt à réaliser votre projet ? » : la sortie de la
     scène marquait cette frontière ; empilé, il ne restait rien. */
  .appel {
    position: relative;
    margin-top: 1.6rem;
    padding-top: 3.2rem;
  }
  .appel::before {
    content: '';
    position: absolute;
    top: 0;
    left: 18%;
    right: 18%;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--trait) 30%, var(--trait) 70%, transparent);
  }
}


/* --- Prise de rendez-vous ------------------------------------------------ */
/* ⚠️ APRÈS LE TITRE, LE DÉFILEMENT REDEVIENT ORDINAIRE. Charles, le
   2026-08-23 : « on retourne au site standard comme au début, plus de
   transition, je veux un scroll standard ». Aucune de ces deux sections n'est
   épinglée, aucune n'est pilotée par la position de la page : ce sont des
   blocs qui défilent, point. Ne rien y accrocher. */

.rdv {
  max-width: var(--largeur-max);
  margin-inline: auto;
  padding-inline: var(--gouttiere);
  /* L'écart avec le titre, et c'est le SEUL. Il n'y a plus de vide à rattraper
     au-dessus : la section du titre fait exactement la hauteur de son contenu. */
  padding-block: clamp(2rem, 4.5vh, 3.5rem) clamp(4rem, 10vh, 8rem);
  /* L'en-tête flotte au-dessus de la page : sans ça, un lien vers `#rdv`
     dépose le haut du cadre SOUS la pastille de navigation. */
  scroll-margin-top: calc(var(--nav-h) + 1rem);
}

/* Le titre existe pour les lecteurs d'écran et pour le plan du document — à
   l'écran, c'est « Prêt à réaliser votre projet ? » juste au-dessus qui
   l'annonce, et le répéter ferait deux titres pour une seule idée. */
.rdv__titre {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* La carte SOMBRE. Le grand carré blanc a déplu (Parker, 2026-08-24 : « ça
   fait un gros carré blanc ») : la page embarquée reçoit désormais NOTRE teinte
   par `?fond=0A0C13` dans `data-rdv` (paramètre ajouté au CRM, V.1.977) — le
   blanc ne subsiste que sur la carte du calendrier lui-même, qui flotte au
   centre d'un écrin assorti au ciel.
   ⚠️ LA TEINTE VIT À DEUX ENDROITS, TOUJOURS IDENTIQUES : ici en `background`,
   et dans le `?fond=` de `data-rdv` (index.html). Si elles divergent, on voit
   le rectangle de l'iframe se découper dans la carte.
   #0A0C13 : un cran au-dessus de `--fond` (#05060A) — la carte se détache du
   ciel au lieu d'y percer un trou sans étoiles.
   ⚠️ `overflow: hidden` + `isolation` : sans quoi les coins du calendrier
   débordent des angles arrondis, en carré dans le rayon. */
.rdv__cadre {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: 24px;
  background: #0A0C13;
  border: 1px solid var(--trait);
  /* Liseré plus clair sur l'arête haute : le même réflexe « verre » que
     l'en-tête, la lumière vient d'en haut. */
  border-top-color: rgba(255, 255, 255, 0.18);
  /* Une ombre portée FIXE (jamais mise à l'échelle) : elle décolle la carte du
     fond sans rien coûter par image — plus un halo mauve très doux, l'écho du
     titre juste au-dessus. */
  box-shadow:
    0 30px 90px rgba(0, 0, 0, 0.55),
    0 0 110px rgba(106, 69, 232, 0.18);
  /* ⚠️ SERRÉ AU CALENDRIER, ET C'EST VOULU (Parker, 2026-08-23 : « la boîte
     blanche autour est beaucoup trop grosse »). La page embarquée est une
     CARTE DE 768 px centrée dans une pleine page claire : en pleine largeur
     (1190 px), le cadre devenait une immense boîte blanche autour d'un petit
     calendrier. LE BOUTON D'AJUSTEMENT, C'EST CES DEUX VARIABLES — largeur et
     hauteur — rien d'autre à recalculer.
     Hauteur : assez pour la carte et sa marge de respiration ; trop court, le
     calendrier fabrique sa propre barre de défilement interne, désagréable au
     milieu d'une page qui défile déjà. */
  --rdv-largeur: 55rem;                     /* 880 px : la carte de 768 + l'air */
  --rdv-hauteur: clamp(580px, 75vh, 700px);
  max-width: var(--rdv-largeur);
  margin-inline: auto;
  height: var(--rdv-hauteur);
}

/* Le cadre témoin, tant qu'aucune adresse n'est branchée. Volontairement PAS
   blanc : il doit se lire comme un emplacement réservé, jamais comme une carte
   finie qu'on aurait oublié de remplir. Il garde en revanche la hauteur et la
   position exactes du vrai calendrier — c'est tout son intérêt. */
.rdv.est-a-brancher .rdv__cadre {
  background: rgba(255, 255, 255, 0.02);
  border: 1px dashed rgba(255, 255, 255, 0.18);
  box-shadow: none;
  display: grid;
  place-items: center;
  padding: var(--gouttiere);
}

.rdv__temoin {
  max-width: 30rem;
  color: var(--texte-doux);
  font-size: 1rem;
  line-height: 1.6;
  text-align: center;
}

.rdv__temoin b {
  display: block;
  margin-bottom: 0.5rem;
  color: var(--texte);
  font-size: 1.15rem;
}

.rdv__temoin code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  color: var(--mauve-vif);
}

/* La sortie de secours, sous la carte. Discrète à dessein : elle ne doit se
   remarquer que par qui en a besoin — c'est-à-dire par qui voit un cadre vide. */
.rdv__secours {
  margin-top: 1.1rem;
  text-align: center;
  /* ⚠️ REMONTÉ DE 0,42 À 0,55 (2026-08-26). C'est la SEULE porte de sortie
     d'un visiteur dont le navigateur refuse le cadre : à 0,42 sur fond noir
     elle se lisait mal, et la personne qui en a besoin est précisément celle
     qui ne voit rien d'autre. Contraste mesuré : 6,4:1, largement au-dessus
     du seuil AA. */
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.875rem;
}

.rdv__secours a {
  color: var(--texte-doux);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color var(--t);
}

@media (hover: hover) {
  .rdv__secours a:hover { color: var(--texte); }
}

.rdv__cadre iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

@media (max-width: 700px) {
  /* Hauteur de DÉPART seulement — le vrai dimensionnement est fait par le
     message `ublo-rdv-hauteur` (voir main.js) qui fait GRANDIR le cadre au fil
     du contenu. Mesuré en émulation iPhone (430 px, 2026-08-24) : contenu au
     repos 590 px, plages horaires dépliées 841 px. 620 = le repos + une marge;
     partir plus grand est irrécupérable (le message ne sait pas rapetisser),
     partir trop petit se corrige tout seul au premier message. Le premier
     essai à 860 px fixes laissait ~270 px de vide sous le calendrier sur
     l'iPhone de Parker : les hauteurs varient trop d'un moteur à l'autre pour
     une valeur unique. */
  .rdv__cadre { height: 520px; border-radius: 20px; }

  /* ⚠️ LE LIEN NE DOIT PAS SE COUPER EN DEUX. « Ouvrir la page de réservation »
     tombait sur deux lignes, coupé après « page de » (Parker, 2026-08-26).
     `nowrap` + `inline-block` le gardent d'un seul tenant : la question passe
     sur sa ligne, le lien sur la sienne, entier.
     ⚠️ NE PAS RÉDUIRE LA TAILLE DU TEXTE POUR LE FAIRE TENIR : c'est déjà la
     sortie de secours d'un visiteur qui ne voit PAS le calendrier. Elle doit
     rester lisible. */
  .rdv__secours a {
    white-space: nowrap;
    display: inline-block;
  }
}


/* --- Bannière de témoins ------------------------------------------------- */
/* Une carte discrète en bas à gauche, dans le langage du panneau déroulant :
   aplat opaque (ni flou ni backdrop-filter, règle du projet), trait fin,
   coins ronds. Elle n'occupe jamais toute la largeur : ce n'est pas un mur. */

.temoins {
  position: fixed;
  z-index: 60;                       /* au-dessus de la nav (50) */
  /* À DROITE — demande de Parker (2026-08-23) : « j'aime pas ça à gauche ». */
  right: clamp(12px, 2.5vw, 28px);
  bottom: clamp(12px, 2.5vw, 28px);
  max-width: 400px;
  padding: 18px 20px 16px;
  border: 1px solid var(--trait);
  border-radius: 18px;
  background: rgba(12, 12, 18, 0.97);
  box-shadow: 0 28px 70px rgba(0, 0, 0, 0.65);

  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}
.temoins.est-visible { opacity: 1; transform: none; }
.temoins.est-fermee  { opacity: 0; transform: translateY(14px); pointer-events: none; }

.temoins__texte {
  margin: 0 0 14px;
  color: var(--texte-doux);
  font-size: 0.9rem;
  line-height: 1.55;
}
.temoins__texte strong { color: var(--texte); }
.temoins__texte a { color: var(--texte); text-underline-offset: 3px; }

.temoins__actions { display: flex; gap: 8px; justify-content: flex-end; }

.temoins__accepter,
.temoins__refuser {
  border: 0;
  border-radius: 10px;
  padding: 10px 16px;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--t), color var(--t);
}
.temoins__accepter { background: var(--mauve); color: #fff; }
.temoins__refuser  { background: transparent; color: var(--texte-doux); }
@media (hover: hover) {
  .temoins__accepter:hover { background: var(--mauve-survol); }
  .temoins__refuser:hover  { color: var(--texte); background: rgba(255, 255, 255, 0.08); }
}

@media (max-width: 480px) {
  .temoins { left: 10px; right: 10px; max-width: none; }
}


/* --- Page confidentialité ------------------------------------------------ */
/* Page de référence, volontairement sans ciel ni pastille : fond plat, une
   colonne de lecture, un chemin de sortie. Elle réutilise les variables du
   site et rien d'autre — ne pas lui inventer une direction visuelle. */

.page-legale { background: var(--fond); }

.legale__entete {
  max-width: 46rem;
  margin-inline: auto;
  padding: clamp(1.5rem, 4vh, 2.5rem) var(--gouttiere) 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.legale__logo img { height: 34px; width: auto; display: block; }

.legale__retour {
  color: var(--texte-doux);
  text-decoration: none;
  font-size: 0.95rem;
  transition: color var(--t);
}
@media (hover: hover) { .legale__retour:hover { color: var(--texte); } }

.legale {
  max-width: 46rem;
  margin-inline: auto;
  padding: clamp(2rem, 5vh, 3.5rem) var(--gouttiere) clamp(3rem, 7vh, 5rem);
}

.legale h1 {
  font-family: var(--police-titre);
  font-size: clamp(2.4rem, 5vw, 3.4rem);
  line-height: 1.02;
  margin: 0 0 0.4rem;
}

.legale__maj { color: var(--texte-doux); font-size: 0.9rem; margin: 0 0 2rem; }

.legale h2 {
  font-size: 1.15rem;
  font-weight: 700;
  margin: 2.1rem 0 0.6rem;
}

.legale p, .legale li {
  color: var(--texte-doux);
  font-size: 1rem;
  line-height: 1.65;
}

.legale p { margin: 0 0 0.9rem; }
.legale ul { margin: 0 0 0.9rem; padding-left: 1.3rem; }
.legale li { margin: 0.35rem 0; }
.legale strong { color: var(--texte); }

.legale a { color: var(--texte); text-underline-offset: 3px; }

.legale__pied {
  max-width: 46rem;
  margin-inline: auto;
  padding: 0 var(--gouttiere) clamp(2rem, 5vh, 3rem);
  border-top: 1px solid var(--trait);
  padding-top: 1.4rem;
  color: rgba(255, 255, 255, 0.42);
  font-size: 0.875rem;
}
.legale__pied a { color: var(--texte-doux); text-decoration: none; }


/* --- Page statistiques (/analytics) -------------------------------------- */
/* Tableau de bord MAISON (js/tableau-stats.js) : mêmes cartes sombres, trait
   fin et mauve que le reste du site. Page interne, noindex, non liée. */

.stats {
  max-width: 1180px;
  margin-inline: auto;
  padding: clamp(1.5rem, 4vh, 2.5rem) var(--gouttiere) clamp(2rem, 5vh, 3rem);
}

.stats__entete { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.4rem; }

.stats__titre {
  font-family: var(--police-titre);
  font-size: clamp(2.2rem, 4.5vw, 3rem);
  line-height: 1.02;
  margin: 0;
}

.stats__controles { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }

.tb__enligne { display: inline-flex; align-items: center; gap: 7px; font-size: 0.85rem; color: var(--texte-doux); }
.tb__enligne i { width: 8px; height: 8px; border-radius: 50%; background: #3ecf72; box-shadow: 0 0 10px rgba(62, 207, 114, 0.8); }

.tb__periodes { display: inline-flex; gap: 4px; padding: 4px; border: 1px solid var(--trait); border-radius: 12px; background: rgba(255, 255, 255, 0.02); }
.tb__periodes button {
  border: 0; background: transparent; color: var(--texte-doux); font: inherit; font-size: 0.85rem; font-weight: 600;
  padding: 7px 13px; border-radius: 8px; cursor: pointer; transition: background-color var(--t), color var(--t);
}
.tb__periodes button.est-active { background: var(--mauve); color: #fff; }
@media (hover: hover) { .tb__periodes button:not(.est-active):hover { color: var(--texte); background: rgba(255, 255, 255, 0.07); } }

.tb { transition: opacity 0.25s var(--ease); }
.tb.est-en-chargement { opacity: 0.45; pointer-events: none; }

.tb__cartes { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
@media (max-width: 900px) { .tb__cartes { grid-template-columns: repeat(2, 1fr); } }
.tb__carte {
  border: 1px solid var(--trait); border-radius: 16px; padding: 16px 18px 14px;
  background: rgba(255, 255, 255, 0.02); display: flex; flex-direction: column; gap: 2px;
}
.tb__carte-nom { font-size: 0.8rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--texte-doux); }
.tb__carte-valeur { font-family: var(--police-titre); font-size: 2.1rem; line-height: 1.1; }
.tb__carte-delta { font-size: 0.8rem; color: rgba(255, 255, 255, 0.35); }
.tb__carte-delta.est-bon { color: #3ecf72; }
.tb__carte-delta.est-mauvais { color: #ff6b6b; }

.tb__graphe {
  margin-top: 12px; border: 1px solid var(--trait); border-radius: 16px; padding: 14px 12px 8px;
  background: rgba(255, 255, 255, 0.02);
}
.tb__graphe svg { display: block; width: 100%; height: clamp(180px, 30vh, 280px); }
.tb__grille line { stroke: rgba(255, 255, 255, 0.06); stroke-width: 1; }
.tb__axe { fill: rgba(255, 255, 255, 0.4); font-size: 13px; font-family: var(--police-texte); }

.tb__blocs { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
@media (max-width: 760px) { .tb__blocs { grid-template-columns: 1fr; } }
.tb__bloc { border: 1px solid var(--trait); border-radius: 16px; padding: 16px 18px; background: rgba(255, 255, 255, 0.02); }
.tb__bloc h2 { font-size: 0.8rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--texte-doux); margin: 0 0 12px; font-family: var(--police-texte); }

.tb__ligne { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(60px, 1fr) auto; align-items: center; gap: 10px; padding: 5px 0; font-size: 0.92rem; }
.tb__ligne-nom { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--texte); }
.tb__ligne-barre { height: 8px; border-radius: 999px; background: rgba(255, 255, 255, 0.06); overflow: hidden; }
.tb__ligne-barre i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--mauve), var(--mauve-vif)); }
.tb__ligne-n { color: var(--texte-doux); font-variant-numeric: tabular-nums; }

.tb__vide { color: var(--texte-doux); font-size: 0.95rem; padding: 1.2rem 0.2rem; margin: 0; }
.tb__vide a { color: var(--texte); }

.stats__note { margin: 1rem 0 0; color: rgba(255, 255, 255, 0.42); font-size: 0.875rem; }
.stats__note a { color: var(--texte-doux); }
.tb__exclure { cursor: pointer; }
.tb__exclure input { accent-color: var(--mauve); margin-right: 4px; vertical-align: -1px; cursor: pointer; }

/* --- Page confidentialité ------------------------------------------------ */
/* Page de référence, volontairement sans ciel ni pastille : fond plat, une
   colonne de lecture, un chemin de sortie. Elle réutilise les variables du
   site et rien d'autre — ne pas lui inventer une direction visuelle. */

.page-legale { background: var(--fond); }

.legale__entete {
  max-width: 46rem;
  margin-inline: auto;
  padding: clamp(1.5rem, 4vh, 2.5rem) var(--gouttiere) 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.legale__logo img { height: 34px; width: auto; display: block; }

.legale__retour {
  color: var(--texte-doux);
  text-decoration: none;
  font-size: 0.95rem;
  transition: color var(--t);
}
@media (hover: hover) { .legale__retour:hover { color: var(--texte); } }

.legale {
  max-width: 46rem;
  margin-inline: auto;
  padding: clamp(2rem, 5vh, 3.5rem) var(--gouttiere) clamp(3rem, 7vh, 5rem);
}

.legale h1 {
  font-family: var(--police-titre);
  font-size: clamp(2.4rem, 5vw, 3.4rem);
  line-height: 1.02;
  margin: 0 0 0.4rem;
}

.legale__maj { color: var(--texte-doux); font-size: 0.9rem; margin: 0 0 2rem; }

.legale h2 {
  font-size: 1.15rem;
  font-weight: 700;
  margin: 2.1rem 0 0.6rem;
}

.legale p, .legale li {
  color: var(--texte-doux);
  font-size: 1rem;
  line-height: 1.65;
}

.legale p { margin: 0 0 0.9rem; }
.legale ul { margin: 0 0 0.9rem; padding-left: 1.3rem; }
.legale li { margin: 0.35rem 0; }
.legale strong { color: var(--texte); }

.legale a { color: var(--texte); text-underline-offset: 3px; }

.legale__pied {
  max-width: 46rem;
  margin-inline: auto;
  padding: 0 var(--gouttiere) clamp(2rem, 5vh, 3rem);
  border-top: 1px solid var(--trait);
  padding-top: 1.4rem;
  color: rgba(255, 255, 255, 0.42);
  font-size: 0.875rem;
}
.legale__pied a { color: var(--texte-doux); text-decoration: none; }


/* --- Page statistiques (/analytics) -------------------------------------- */
/* Page interne (noindex, non liée) : l'en-tête et le pied de la page légale,
   un grand cadre qui porte le tableau Umami. */

.stats {
  max-width: 1180px;
  margin-inline: auto;
  padding: clamp(1.5rem, 4vh, 2.5rem) var(--gouttiere) clamp(2rem, 5vh, 3rem);
}

.stats__titre {
  font-family: var(--police-titre);
  font-size: clamp(2.2rem, 4.5vw, 3rem);
  line-height: 1.02;
  margin: 0 0 1.2rem;
}

.stats__cadre {
  height: min(78vh, 900px);
  border: 1px solid var(--trait);
  border-radius: 18px;
  overflow: hidden;
  isolation: isolate;
  background: #0c0c12;
}

.stats__cadre iframe { display: block; width: 100%; height: 100%; border: 0; }

.stats__cadre.est-a-brancher {
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.02);
  border-style: dashed;
  border-color: rgba(255, 255, 255, 0.18);
}

.stats__temoin {
  max-width: 30rem;
  color: var(--texte-doux);
  font-size: 1rem;
  line-height: 1.6;
  text-align: center;
  padding: var(--gouttiere);
}
.stats__temoin b { display: block; margin-bottom: 0.5rem; color: var(--texte); font-size: 1.15rem; }
.stats__temoin code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; color: var(--mauve-vif); }

.stats__note { margin: 0.9rem 0 0; color: rgba(255, 255, 255, 0.42); font-size: 0.875rem; }

.stats__bouton {
  display: inline-block;
  margin-top: 4px;
  padding: 12px 22px;
  border-radius: 12px;
  background: var(--mauve);
  color: #fff !important;
  font-weight: 600;
  text-decoration: none;
  transition: background-color var(--t);
}
@media (hover: hover) { .stats__bouton:hover { background: var(--mauve-survol); } }


/* --- Bas de page --------------------------------------------------------- */
/* ⚠️ TOUTES LES TAILLES D'ICI ONT ÉTÉ MONTÉES D'UN CRAN LE 2026-08-23. Première
   version calibrée à l'œil sur un écran de portable : sur le grand moniteur de
   Charles, le bas de page paraissait « décalé et petit ». Le conteneur n'était
   pas en cause (c'est `--largeur-max`, comme tout le reste du site) — c'était la
   typo, trop menue pour la largeur, et les colonnes de liens réparties sur
   toute la largeur restante au lieu d'être groupées. */

.pied {
  /* ⚠️ HORS DE `main`, DONC HORS DE SON CONTEXTE D'EMPILEMENT. Le ciel étoilé
     est un canvas positionné en z-index 0 ; un bas de page sans `position` se
     peindrait DESSOUS. La règle qui protège les sections (`main { z-index: 1 }`)
     ne couvre pas cet élément, il porte donc la sienne. */
  position: relative;
  z-index: 1;
  /* Pas de filet en haut du bas de page : le seul trait est celui qui sépare
     les colonnes de la mention légale. Un second, pleine largeur, coupait la
     page en deux juste sous le titre d'appel. */
  padding-inline: var(--gouttiere);
}

.pied__corps {
  max-width: var(--largeur-max);
  margin-inline: auto;
  padding-block: clamp(3.5rem, 9vh, 6.5rem);
  display: grid;
  /* La marque tient la moitié gauche, le groupe de colonnes la moitié droite.
     ⚠️ C'EST UN BLOC CONTRE UN BLOC, PAS TROIS COLONNES DE MÊME RANG : les
     colonnes de liens doivent se serrer entre elles, pas s'étaler. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(2.5rem, 6vw, 6rem);
  align-items: start;
}

.pied__cols {
  display: grid;
  /* `auto-fit` : deux colonnes aujourd'hui, trois le jour où les réseaux
     sociaux arrivent, sans rien retoucher ici. */
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: clamp(1.5rem, 4vw, 3.5rem);
}

.pied__logo img {
  display: block;
  width: auto;
  height: 46px;
}

.pied__phrase {
  margin-top: 1.75rem;
  max-width: 24rem;
  color: var(--texte-doux);
  font-size: 1.0625rem;
  line-height: 1.55;
}

.pied__phrase strong {
  color: var(--texte);
  font-weight: 700;
}

/* La ligne d'accent du bas de page. C'est le seul rappel de couleur ici. */
.pied__lieu {
  margin-top: 1.15rem;
  color: var(--mauve-vif);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.pied__col {
  display: grid;
  /* ⚠️ `align-content: start`, SANS QUOI LES COLONNES COURTES S'ÉTIRENT. Une
     grille dont la hauteur est imposée (ici par la colonne voisine, plus
     longue) répartit ses rangées sur toute cette hauteur : « Contact », qui n'a
     que deux liens, les espaçait de 76 px là où « Pages » en met 46. Deux
     colonnes côte à côte au même interligne — c'est tout ce que ça veut dire. */
  align-content: start;
  /* `justify-items: start` et non `stretch` : sans ça, chaque lien occupe toute
     la largeur de la colonne et sa zone cliquable s'étend loin à droite du mot,
     ce qui se sent au survol. */
  justify-items: start;
  gap: 1.15rem;
}

.pied__titre {
  font-family: var(--police-texte);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--texte);
  margin-bottom: 0.75rem;
}

.pied__col a {
  color: var(--texte-doux);
  text-decoration: none;
  font-size: 1.0625rem;
  transition: color var(--t);
}

@media (hover: hover) {
  .pied__col a:hover { color: var(--texte); }
}

.pied__bas {
  max-width: var(--largeur-max);
  margin-inline: auto;
  border-top: 1px solid var(--trait);
  padding-block: 1.9rem clamp(1.9rem, 4.5vh, 3rem);
  color: rgba(255, 255, 255, 0.42);
  font-size: 0.875rem;
}

@media (max-width: 900px) {
  .pied__corps {
    /* La marque passe au-dessus, les colonnes gardent leur rangée : à une seule
       colonne, le bas de page devient une longue liste qu'on parcourt au pouce. */
    grid-template-columns: minmax(0, 1fr);
    gap: 2.75rem;
  }
}

@media (max-width: 480px) {
  .pied__logo img { height: 38px; }
  .pied__phrase, .pied__col a { font-size: 1rem; }
}


/* --- Bouton « voir la page du service », dans les volets de l'accueil ---

   ⚠️ MASQUÉ SUR ORDINATEUR, ET C'EST DÉLIBÉRÉ. Sur grand écran, ces volets
   forment une scène épinglée dont la géométrie est calée au pixel (positions
   des wagons, course du tapis, bascule des étapes). Y insérer un élément
   décalerait tout le rail. En repli téléphone, la scène n'existe plus : les
   volets sont des blocs empilés, et le bouton y a toute sa place.
   ⚠️ LE LIEN RESTE DANS LE DOM DANS LES DEUX CAS — Google le suit, quelle que
   soit la largeur de la fenêtre. Ce n'est pas du contenu caché au sens des
   consignes : c'est un élément d'interface propre à une taille d'écran, et le
   pied de page reprend les mêmes liens partout. */
.services__vers-page { display: none; }

/* 20. Pages de service ==================================================== */
/*
   Gabarit commun à /creation-site-web, /publicite-numerique,
   /video-entreprise et aux pages de service qui suivront.

   ⚠️ CES PAGES N'ONT NI SCÈNE ÉPINGLÉE NI ANIMATION PILOTÉE PAR LE DÉFILEMENT.
   C'est délibéré : l'accueil est la vitrine, ces pages-là sont faites pour être
   LUES — par un client qui compare des agences, et par Google. Tout le contenu
   est dans le flux, visible sans JavaScript. Ne pas y réintroduire de rail.

   ⚠️ AUCUN `opacity: 0` EN ATTENTE D'ANIMATION. Même règle que partout sur ce
   site : dans un onglet d'arrière-plan l'horloge d'animation est suspendue et
   le contenu resterait invisible pour de bon.
*/

/* ⚠️ `.svc` EST UN CONTENEUR QU'ON OUVRE ET REFERME, PAS LE `<main>`.
   Les sections portées de l'accueil — `.defile`, `.vedette`, `.equipe`,
   `.appel`, `.rdv` — posent DÉJÀ leur propre largeur maximale et leurs
   gouttières sur un wrapper interne (`.equipe__grille`, `.vedette__bande`,
   `.defile__cadre`). Les enfermer dans un `.svc` doublerait les marges et
   elles ne tomberaient plus à la même largeur que sur l'accueil.
   Sur l'accueil, `<main>` ne porte que `position` et `z-index` : on reproduit
   exactement ça, et on referme `.svc` avant chaque bloc pleine largeur. */
.svc {
  max-width: var(--largeur-max);
  margin-inline: auto;
  padding-inline: var(--gouttiere);
  position: relative;
  z-index: 1;
}

/* La barre de navigation est fixe : sans ce retrait, le fil d'Ariane passe
   dessous. Uniquement sur le premier conteneur de la page. */
.svc--tete { padding-top: calc(var(--nav-h) + clamp(1.5rem, 4vh, 3rem)); }

/* ⚠️ LA SECTION `.rdv` PORTE DÉJÀ JUSQU'À 8 REM DE MARGE BASSE (elle terminait
   la page). Un `.svc__section` qui la suit ajoute sa propre marge haute : les
   deux se cumulent et la FAQ de l'accueil se retrouvait à plus de 200 px sous
   le calendrier, avec un trou d'étoiles au milieu. On annule la marge du
   premier bloc qui suit le calendrier. */
.rdv + .svc > .svc__section:first-child { margin-top: 0; }
.svc--fin  { padding-bottom: clamp(3rem, 8vh, 6rem); }

/* ⚠️ `.appel` N'A PAS DE MARGE VERTICALE EN PROPRE : sur l'accueil, sa
   remontée est calculée par `js/main.js` pour chevaucher la fin du rail
   épinglé. Ici il n'y a pas de rail — `caler()` remet donc la marge à zéro et
   la section se retrouverait collée à ses voisines. On lui rend son air. */
.page-service .appel { padding-block: clamp(3rem, 9vh, 6rem) 0; }

/* --- Fil d'Ariane --- */

.svc__fil {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: clamp(2rem, 5vh, 3.5rem);
  color: var(--texte-doux);
  font-size: 0.875rem;
}
.svc__fil a { text-decoration: underline; text-underline-offset: 3px; }
.svc__fil span[aria-hidden] { opacity: 0.4; }
@media (hover: hover) { .svc__fil a:hover { color: var(--texte); } }

/* --- Hero de la page --- */

.svc__hero { max-width: 46rem; }

.svc__surtitre {
  color: var(--mauve-vif);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  margin-bottom: clamp(0.9rem, 2vh, 1.4rem);
}

/* ⚠️ LE PLAFOND EN `rem` EST TRÈS AU-DESSUS DE LA VALEUR EN `vw`, et il le
   faut : avec le même nombre des deux côtés — le piège classique de ce
   projet — le plafond entre en action vers 1600 px et bride tout le titre. */
.svc__titre {
  font-size: clamp(2.6rem, 6.4vw, 5.6rem);
  line-height: 0.94;
  letter-spacing: 0.005em;
}

.svc__soustitre {
  margin-top: clamp(1.1rem, 2.6vh, 1.7rem);
  max-width: 34rem;
  color: var(--texte-doux);
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  line-height: 1.55;
}

.svc__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin-top: clamp(1.8rem, 4vh, 2.6rem);
}

/* --- Sections --- */

.svc__section { margin-top: clamp(3.5rem, 9vh, 7rem); }

.svc__h2 {
  font-size: clamp(2rem, 4.2vw, 3.4rem);
  line-height: 1;
  letter-spacing: 0.01em;
  margin-bottom: clamp(1rem, 2.4vh, 1.6rem);
}

.svc__h3 {
  font-size: clamp(1.25rem, 2vw, 1.65rem);
  line-height: 1.12;
  letter-spacing: 0.012em;
  margin-bottom: 0.55rem;
}

.svc__intro {
  max-width: 42rem;
  color: var(--texte-doux);
  font-size: clamp(1rem, 1.4vw, 1.12rem);
}

.svc__section > p {
  max-width: 42rem;
  margin-top: 1rem;
  color: var(--texte-doux);
}

.svc__lien {
  color: var(--texte);
  text-decoration: underline;
  text-decoration-color: var(--mauve-vif);
  text-underline-offset: 4px;
}
@media (hover: hover) { .svc__lien:hover { text-decoration-color: var(--texte); } }

.svc__liste {
  max-width: 42rem;
  margin-top: 1.1rem;
  padding-left: 1.1rem;
  color: var(--texte-doux);
}
.svc__liste li { margin-top: 0.7rem; }
.svc__liste strong { color: var(--texte); font-weight: 600; }

/* --- Les trois chiffres de preuve --- */

.svc__preuves {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
}

.svc__preuve {
  padding: clamp(1.4rem, 3vw, 2rem);
  background: var(--verre-fond);
  border: 1px solid var(--verre-bord);
  border-radius: 18px;
  box-shadow: var(--verre-reflet);
}

.svc__preuve-chiffre {
  font-family: var(--police-titre);
  font-weight: 400;
  font-size: clamp(2.4rem, 4.6vw, 3.6rem);
  line-height: 1;
  color: var(--mauve-vif);
}

.svc__preuve-texte {
  margin-top: 0.6rem;
  font-size: 1rem;
  line-height: 1.45;
}

.svc__preuve-qui {
  margin-top: 0.9rem;
  color: var(--texte-doux);
  font-size: 0.85rem;
}

/* --- Blocs « ce qu'on livre » --- */

.svc__blocs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: 1rem;
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
}

.svc__bloc {
  padding: clamp(1.4rem, 3vw, 2rem);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--trait);
  border-radius: 18px;
}
.svc__bloc p { color: var(--texte-doux); }

/* --- Grilles de cartes courtes ---
   ⚠️ UNE CARTE = UN TITRE + UNE LIGNE. C'est la règle, pas une suggestion.
   Ces sections étaient des paragraphes suivis d'une liste à puces ; retour de
   Parker le 2026-08-26 : « il y a fucking trop de texte, c'est pas un article
   non plus ». Quelqu'un qui magasine une agence BALAIE la page — et le premier
   argument de vente d'une agence web, c'est la page elle-même.
   Tout ce qui demande plus d'une ligne d'explication descend dans la FAQ :
   repliée, elle reste lue par Google sans encombrer l'écran. */

.svc__cartes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
}

.svc__carte {
  padding: clamp(1.25rem, 2.6vw, 1.7rem);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--trait);
  border-radius: 18px;
}

.svc__carte-titre {
  font-size: clamp(1.15rem, 1.7vw, 1.4rem);
  line-height: 1.08;
  letter-spacing: 0.015em;
}

.svc__carte-ligne {
  margin-top: 0.5rem;
  color: var(--texte-doux);
  font-size: 0.95rem;
  line-height: 1.45;
}

/* Les défauts : un tiret mauve en tête de carte, pour que la grille se lise
   comme un constat et non comme une offre. */
.svc__cartes--defauts .svc__carte { position: relative; padding-top: clamp(1.6rem, 3vw, 2.1rem); }
.svc__cartes--defauts .svc__carte::before {
  content: '';
  position: absolute;
  top: clamp(1.25rem, 2.6vw, 1.7rem);
  left: clamp(1.25rem, 2.6vw, 1.7rem);
  width: 22px;
  height: 2px;
  background: var(--mauve-vif);
  opacity: 0.85;
}

/* La dernière carte de la grille des défauts est une sortie, pas un défaut :
   fond mauve discret, pas de tiret. */
.svc__carte--sortie {
  background: rgba(106, 69, 232, 0.10);
  border-color: rgba(177, 92, 255, 0.28);
}
.svc__cartes--defauts .svc__carte--sortie::before { display: none; }
.svc__carte--sortie { padding-top: clamp(1.25rem, 2.6vw, 1.7rem); }

.svc__apres-grille {
  margin-top: 1.1rem;
  color: var(--texte-doux);
  font-size: 0.95rem;
}

/* --- Les étapes --- */

/* ⚠️ `padding: 0` OBLIGATOIRE. Le reset du haut de ce fichier ne vise que
   `ol[role="list"]` : sans cette ligne, le <ol> garde son retrait natif de
   ~40 px. Avec l'ancien procédé (fond sur le conteneur + `gap: 1px` pour
   dessiner les filets), ce retrait laissait voir une BANDE GRISE sur toute la
   hauteur du bloc — c'est le « on dirait que c'est bugé » de Parker.
   ⚠️ ET PLUS DE FOND OPAQUE SUR LES ÉTAPES. `background: var(--fond)` bouchait
   le ciel étoilé, alors que toutes les autres cartes du site le laissent
   passer. Les filets se font maintenant par `border`, ce qui ne masque rien. */
.svc__etapes {
  list-style: none;
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
  padding: 0;
  border-top: 1px solid var(--trait);
}

.svc__etape {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: 0 clamp(1rem, 3vw, 2.5rem);
  padding-block: clamp(1.4rem, 3.4vh, 2.2rem);
  border-bottom: 1px solid var(--trait);
}
.svc__etape .svc__h3 { grid-column: 2; }
.svc__etape p:not(.svc__etape-num) { grid-column: 2; color: var(--texte-doux); max-width: 42rem; font-size: 0.98rem; }

.svc__etape-num {
  grid-row: 1 / span 2;
  font-family: var(--police-titre);
  font-weight: 400;
  font-size: clamp(1.8rem, 3vw, 2.6rem);
  line-height: 1;
  color: var(--mauve-vif);
  opacity: 0.75;
}

/* --- Projets --- */

.svc__projets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
}

.svc__projet {
  display: block;
  padding: 0.9rem 0.9rem 1.3rem;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--trait);
  border-radius: 18px;
  transition: transform 0.25s var(--ease), border-color 0.25s var(--ease);
}
.svc__projet img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 12px;
  margin-bottom: 1rem;
}
.svc__projet-nom { font-size: 1.5rem; line-height: 1; letter-spacing: 0.02em; }
.svc__projet-desc { margin-top: 0.45rem; color: var(--texte-doux); font-size: 0.95rem; }

@media (hover: hover) {
  .svc__projet:hover { transform: translateY(-3px); border-color: var(--verre-trait); }
}

/* --- FAQ ---
   ⚠️ `<details>` NATIF, PAS DE JAVASCRIPT. Le contenu replié reste dans le DOM
   et Google le lit : c'est ce qui permet de baliser la FAQ en `FAQPage` sans
   mentir sur ce qui est visible. Et sans script, ça marche partout. */

.svc__faq {
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
  border-top: 1px solid var(--trait);
  max-width: 52rem;
}

/* ⚠️ LA FAQ DE L'ACCUEIL EST CENTRÉE — sur cette page, tout ce qui l'entoure
   (titres de sections, appel, calendrier) l'est aussi, et la colonne collée à
   gauche se lisait comme un bogue (Parker, 2026-08-26). Les pages de service,
   elles, GARDENT leur colonne à gauche : le ciblage passe par l'`id` du titre
   de l'accueil, pas par une classe partagée. Le texte des questions reste
   aligné à gauche DANS la boîte centrée : une question se lit, elle ne se
   contemple pas. */
#faqAccueilTitre { text-align: center; }
section[aria-labelledby="faqAccueilTitre"] .svc__faq { margin-inline: auto; }

.svc__q { border-bottom: 1px solid var(--trait); }

.svc__q summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding-block: clamp(1rem, 2.4vh, 1.4rem);
  cursor: pointer;
  /* Le triangle par défaut ne suit pas la mise en page ; on met le nôtre. */
  list-style: none;
}
.svc__q summary::-webkit-details-marker { display: none; }

.svc__q summary .svc__h3 { margin-bottom: 0; }

.svc__q summary::after {
  content: '+';
  flex: 0 0 auto;
  font-family: var(--police-titre);
  font-size: 1.6rem;
  line-height: 1;
  color: var(--mauve-vif);
  transition: transform 0.25s var(--ease);
}
.svc__q[open] summary::after { transform: rotate(45deg); }

.svc__q p {
  padding-bottom: clamp(1rem, 2.4vh, 1.4rem);
  max-width: 46rem;
  color: var(--texte-doux);
}

@media (hover: hover) {
  .svc__q summary:hover .svc__h3 { color: var(--mauve-vif); }
}

/* --- Appel final --- */

.svc__final {
  margin-top: clamp(3.5rem, 9vh, 7rem);
  padding: clamp(2rem, 5vw, 3.5rem);
  background: var(--verre-fond);
  border: 1px solid var(--verre-bord);
  border-radius: 24px;
  box-shadow: var(--verre-reflet);
}

/* --- Maillage vers les autres services --- */

.svc__autres {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
}

.svc__autre {
  display: grid;
  gap: 0.35rem;
  padding: clamp(1.2rem, 2.6vw, 1.6rem);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--trait);
  border-radius: 18px;
  transition: border-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.svc__autre-num {
  font-family: var(--police-titre);
  font-size: 1.1rem;
  color: var(--mauve-vif);
  opacity: 0.8;
}
.svc__autre-nom { font-family: var(--police-titre); font-size: 1.4rem; line-height: 1.05; }
.svc__autre-note { color: var(--texte-doux); font-size: 0.9rem; }

@media (hover: hover) {
  .svc__autre:hover { border-color: var(--verre-trait); transform: translateY(-3px); }
}

/* --- Bouton « Gérer les témoins » ---
   C'était un `<a href="#">` : un lien qui ne mène nulle part, que Google suit
   et qui s'annonce « lien » au clavier alors que c'est un bouton. Il est
   maintenant un vrai `<button>` ; cette règle lui rend son apparence de lien. */
.pied__temoins-lien {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* --- Téléphone --- */

@media (max-width: 700px) {
  /* ⚠️ DEUX COLONNES, PAS UNE. En une seule colonne, les deux grilles de
     cartes faisaient DEUX ÉCRANS DE DÉFILEMENT à elles seules (Parker,
     2026-08-26, captures à l'appui : « c'est 2 scroll, c'est n'importe
     quoi »). Les titres sont en Bebas Neue — une capitale condensée qui tient
     très bien dans une colonne étroite — et chaque ligne fait moins de 40
     caractères : le format tuile fonctionne, la liste verticale non. */
  .svc__cartes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6rem;
  }

  .svc__carte { padding: 0.85rem 0.85rem 1rem; border-radius: 14px; }
  .svc__carte-titre { font-size: 1rem; line-height: 1.05; }
  .svc__carte-ligne { margin-top: 0.35rem; font-size: 0.82rem; line-height: 1.35; }

  .svc__cartes--defauts .svc__carte { padding-top: 1.5rem; }
  .svc__cartes--defauts .svc__carte::before { top: 0.85rem; left: 0.85rem; width: 16px; }

  /* Les blocs plus bavards (projets, autres services) restent sur une colonne :
     ils portent une image ou trois lignes, la tuile ne leur va pas. */
  .svc__projets, .svc__autres { grid-template-columns: minmax(0, 1fr); }

  .svc__etape {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  /* Le numéro reprend sa place dans le flux : en colonne à 4,5 rem, il
     mangeait un quart de la largeur d'un écran de téléphone. */
  .svc__etape-num { grid-row: auto; margin-bottom: 0.35rem; }
  .svc__etape .svc__h3,
  .svc__etape p:not(.svc__etape-num) { grid-column: auto; }

  .svc__q summary { gap: 1rem; }
}
