/*
 * css/style.css — mise en page + identité visuelle du rapport.
 * Story 1.1/1.2 : structure/lisibilité minimale et navigation, en palette
 * neutre. Story 1.3 : la palette/typographie de marque est appliquée ici,
 * mais SEULEMENT via les custom properties de css/tokens.css (AD-3) — plus
 * aucune valeur hex/police codée en dur hors tokens.css.
 */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font-body-md-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-body-md-size);
  font-weight: var(--font-body-md-weight);
  line-height: var(--font-body-md-line-height);
  color: var(--color-on-surface);
  /* Neutre chaud, jamais de blanc pur pour un grand aplat de fond (cf.
     DESIGN.md Do's and Don'ts). */
  background: var(--color-surface-container-low);
  /* Compense la hauteur de .site-nav (position:fixed, cf. plus bas) pour
     que le contenu ne démarre pas caché dessous. */
  padding-top: var(--nav-height);
  /* Story 1.4 (FR11), correctif du point de vigilance Story 1.2 : un mot
     long non sécable (ex. "L'accompagnement", "certifications") dans un
     titre de section peut dépasser la largeur de sa boîte à 48px
     (--font-display-lg-size, jamais réduit ici — la typographie de la
     Story 1.3 n'est pas modifiée) et forcer le navigateur à élargir tout
     le viewport de mise en page plutôt que de scroller. `overflow-wrap`
     (hérité par tous les descendants) autorise la coupure d'un mot trop
     long uniquement quand c'est le seul moyen d'éviter ce débordement —
     confirmé nécessaire et suffisant en vérification navigateur à 360px
     (cf. Design Notes de cette story). */
  overflow-wrap: break-word;
  /* Retour utilisateur (revue Sally, party mode) : `hyphens: auto`
     (correctif revue #5, story 1.4) insérait des traits d'union visibles
     dans ~60% des lignes de corps de texte sur mobile (ex.
     "ex-ploiter", "do-cumenter", "géné-rative") -- gênant en lecture
     courante, alors que la raison d'origine de cette règle (empêcher un
     mot long de titre, ex. "L'accompagnement"/"certifications", de faire
     déborder le viewport) est maintenant couverte séparément : h1-h4,
     `.site-nav__link` et `.chiffres-cles-liste` ont chacun leur propre
     `hyphens: none` explicite (ajoutés depuis), donc `body` ne pilote
     plus que le corps de texte courant, où la césure automatique n'a
     jamais été demandée. `manual` : les traits d'union insérés à la main
     dans le contenu source (rares) restent honorés, `auto` seul est
     retiré. `overflow-wrap: break-word` ci-dessus reste le filet de
     sécurité contre un débordement, inchangé -- seul le trait d'union
     automatique disparaît. */
  hyphens: manual;
}

/* Correctif revue #3 (story 1.4) : défense en profondeur en plus
   d'overflow-wrap/hyphens ci-dessus — si un futur élément résiste malgré
   tout au retour à la ligne (ex. un tableau ou une image à largeur fixe,
   aucun des deux n'existe dans le contenu actuel), il reste caché plutôt
   que de provoquer un scroll horizontal ou d'élargir le viewport de mise
   en page. Sur les deux éléments racine car certains navigateurs
   appliquent le scroll sur html, d'autres sur body. */
html,
body {
  overflow-x: hidden;
}

/* Retour utilisateur ("je ne vois pas trop la différence") : essai
   précédent -- "trame.png" (le picto du logo) répété 7 fois à des
   positions dispersées sur tout le fond de `body`, coordonnée verticale
   en PX FIGÉ (calculée une fois contre la hauteur totale de la page).
   Bug constaté en vérification navigateur (cause probable de "je ne vois
   pas la différence") : ces PX avaient été calculés contre la hauteur de
   page en LARGEUR MOBILE (~24 150px, le texte s'y enroule sur beaucoup
   plus de lignes) -- en largeur desktop (~1280px), la même page ne mesure
   plus que ~12 400px, quasiment la MOITIÉ. Résultat : les positions les
   plus basses (18 658px, 22 693px) tombaient hors de la page en desktop,
   et même les autres ne correspondaient plus au même endroit visuel --
   4 des 7 exemplaires ne s'affichaient jamais sur la largeur d'écran la
   plus consultée. Un simple recalcul des PX pour desktop aurait
   re-cassé au prochain ajout de contenu (même fragilité).
   Remplacé par UN exemplaire par GRANDE SECTION (`.section::before`,
   juste plus bas) plutôt que dispersé sur toute la page : chaque section
   a sa propre boîte, le filigrane s'y ancre en haut à droite avec un
   simple `top`/`right` FIXE EN PX DEPUIS LE COIN DE LA SECTION -- jamais
   une fraction de sa hauteur totale (qui varie énormément, ex. "Les
   études de branches" contient ~16 volets) donc jamais concerné par le
   bug ci-dessus ni par celui, plus ancien, de réaction à l'ouverture des
   volets (même famille de cause : une position en % ou calculée contre
   une hauteur qui bouge). Garanti visible sur CHAQUE section, à n'importe
   quelle largeur de viewport. Taille augmentée (420px, contre 220-380px
   avant) : plus présent, comme demandé -- toujours aucune opacité
   ajoutée, le filigrane reste pâle dans le fichier source lui-même.
   `position: relative`/`z-index: 0` : posés directement sur la règle
   `.section` établie plus bas dans ce fichier (padding/bordure/scroll-
   margin), pas ici en doublon -- cf. son propre commentaire pour le détail
   du contexte d'empilement que `z-index: 0` établit pour ce filigrane. */
.section::before {
  content: "";
  position: absolute;
  /* Retour utilisateur (2e passe, "je ne vois pas trop la différence") :
     agrandi une 2e fois (420px -> 640px) et redescendu (6.5rem -> 11rem) --
     à 420px/6.5rem, le filigrane tombait souvent sous des éléments déjà
     opaques en haut de section (le bloc "chiffres clés" d'avant-propos,
     notamment, le couvrait entièrement -- constaté en navigateur). Plus
     bas et plus grand : déborde largement de la zone d'intro dense de
     chaque section, plus de surface reste dégagée pour qu'il se voie
     malgré sa pâleur volontaire (toujours aucune opacité ajoutée). */
  top: 11rem;
  right: 1.5rem;
  z-index: -1;
  width: 640px;
  height: 640px;
  background-image: url("../images/trame.png");
  background-repeat: no-repeat;
  background-size: contain;
  pointer-events: none;
}

/* Retour utilisateur ("dans le footer je constate qu'une image d'arrière
   plan passe par dessus") : le filigrane ci-dessus (640px, ancré à
   top: 11rem) est pensé pour les GRANDES sections du rapport (cf.
   commentaire plus haut) -- la conclusion (`conclusion()`,
   _includes/section.njk), volontairement courte (2 paragraphes, aucun
   volet/chiffresCles), ne fait que ~590px de haut sur desktop. Le
   filigrane y déborde donc largement sous le bas de la section (176px +
   640px de haut > sa propre hauteur), visible par-dessus le footer juste
   en dessous (constaté en navigateur) -- `.section` n'a pas
   `overflow: hidden` (le filigrane des AUTRES sections compte justement
   sur ce débordement pour rester visible même sous un bloc "chiffres
   clés" haut, cf. plus haut). Désactivé spécifiquement ici plutôt qu'un
   `overflow: hidden` global sur `.section` (casserait ce débordement
   voulu ailleurs) : la conclusion n'a de toute façon jamais eu besoin de
   ce filigrane, jamais demandé pour ce bloc de clôture. */
#conclusion::before {
  display: none;
}

/* Story 1.4 (FR11) : tout élément qui pourrait un jour dépasser la largeur
   de son conteneur (aucune image de contenu à ce stade, cf. Story 1.3 —
   seul le logo de nav, déjà borné par .site-nav__logo) reste contraint à
   la largeur disponible. Règle défensive, sans effet sur le logo (règle
   plus spécifique .site-nav__logo, cf. css/style.css plus bas). */
img {
  max-width: 100%;
  height: auto;
}

main {
  max-width: 1200px;
  margin: 0 auto;
  /* Story 1.4 (FR11) : remplace le padding fixe 1.5rem par les 3 paliers
     de marge de contenu de DESIGN.md (spacing.margin-mobile/-desktop,
     répliqués dans _data/theme.js et css/tokens.css, AD-3). Seule la marge
     horizontale (padding-inline) suit les paliers ; le padding vertical
     reste la valeur existante de la Story 1.1/1.2, hors scope de cette
     story. Mobile-first : <768px = spacing.margin-mobile (20px) par
     défaut ci-dessous, resserré/élargi par les media queries suivantes. */
  padding-block: 1.5rem;
  /* Correctif revue #4 (story 1.4) : valeur de repli alignée sur la valeur
     actuelle de css/tokens.css (--spacing-margin-mobile: 20px), au cas où
     la custom property ne serait pas définie (ex. tokens.css absent/pas
     encore chargé). */
  padding-inline: var(--spacing-margin-mobile, 20px);
}

/* Tablette (768–1023px) : marge resserrée intermédiaire entre mobile
   (20px) et desktop (80px). Aucun token dédié dans _data/theme.js pour ce
   palier (seuls margin-mobile/margin-desktop y sont définis, cf. Code Map
   de cette story) — valeur choisie directement ici plutôt que d'inventer
   un nouveau token/décision d'architecture non demandée. Même valeur
   appliquée à .site-nav__inner (cf. plus bas) pour garder les bords de la
   nav alignés avec ceux du contenu à ce palier (correctif revue #1). */
@media (min-width: 768px) {
  main {
    padding-inline: 2.5rem;
  }
}

/* Desktop (≥1024px) : marge de contenu généreuse, spacing.margin-desktop
   (80px, DESIGN.md/_data/theme.js/css/tokens.css). */
@media (min-width: 1024px) {
  main {
    /* Correctif revue #4 : repli aligné sur css/tokens.css
       (--spacing-margin-desktop: 80px). */
    padding-inline: var(--spacing-margin-desktop, 80px);
  }
}

/* ---------------------------------------------------------------------
 * Hero (spec largeur-1200px-bandeau-hero) : bloc en flux normal juste
 * après la nav fixe, PAS fixe lui-même -- défile normalement au scroll.
 * `background-color` posé ici (sur .hero, pas .hero__inner) : repli si
 * `heroFond` est absent (défaut, aucun fond photo fourni à ce stade) ET
 * repli si l'image fournie ne charge pas -- cf. `.hero--fond` juste plus
 * bas pour le fond photo lui-même (distinct de la vignette encadrée
 * `.hero__photo-frame`, plus bas dans ce fichier, pilotée par `heroImage`).
 *
 * Retour utilisateur (2e passe) : `--color-primary` (identique à
 * `.site-nav`, plus bas dans ce fichier) faisait fondre le hero dans la
 * nav fixe juste au-dessus -- aucune limite visible entre les deux bandes
 * au chargement de la page. Éclairci en `--color-primary-fixed-dim`
 * (tokens.css, #297CA3 -- variante "fixed" de la famille M3 du bleu de
 * marque, pensée précisément pour porter du texte blanc sur un fond plus
 * clair que `--color-primary`) plutôt qu'une valeur hex inventée ici
 * (AD-2) : contraste blanc/fond vérifié à 4,67:1, largement au-dessus des
 * 3:1 (grand texte, le titre) et 4,5:1 (texte courant, WCAG AA) requis --
 * `--color-on-primary` reste donc utilisable tel quel sur ce nouveau fond,
 * aucun changement de couleur de texte nécessaire.
 * --------------------------------------------------------------------- */
.hero {
  background-color: var(--color-primary-fixed-dim);
  min-height: var(--hero-band-min-height);
  display: flex;
  /* Retour utilisateur (3e passe) : `align-items: center` (ancienne
     valeur) ne centrait qu'un `.hero__inner` de hauteur "naturelle"
     (celle du texte, ~200px) dans la bande -- le repli photo, à
     l'intérieur, ne pouvait donc jamais dépasser cette même hauteur
     réduite, laissant un vide au-dessus/en dessous de lui alors que la
     bande elle-même est bien plus haute (`--hero-band-min-height`).
     `stretch` étire `.hero__inner` sur TOUTE la hauteur de la bande --
     son repli photo (`align-items: stretch`, comportement flex par
     défaut, cf. `.hero__inner .image-placeholder` plus bas) en hérite
     directement. Le texte, qui n'a plus ce centrage automatique, se
     recentre lui-même à l'intérieur de sa propre boîte désormais étirée
     (cf. `.hero__text` plus bas). */
  align-items: stretch;
  /* Correctif : ancre de positionnement du numéral fantôme -- doit être
     .hero (toute la hauteur de la bande) et non .hero__inner (hauteur du
     seul bloc de texte), sinon le numéral colle au sous-titre au lieu de
     s'étendre en filigrane sur toute la bande (visible surtout sur mobile,
     où .hero__inner est plus bas que .hero). */
  position: relative;
  /* Retour utilisateur (halos, vérification navigateur) : `z-index: 0`
     explicite -- `.hero` n'établissait jusqu'ici AUCUN contexte
     d'empilement propre (position:relative seul, sans z-index, n'en crée
     pas). `.hero__halos` (position:absolute, cf. plus bas) et
     `.hero__text` (position:relative, cf. plus bas) sont TOUS DEUX des
     descendants positionnés SANS lien de parenté directe entre eux -- sans
     un contexte d'empilement commun explicite ici, leur ordre de peinture
     relatif suit des règles d'empilement CSS qui, en pratique (vérifié en
     navigateur), plaçaient `.hero__text` AU-DESSUS de `.hero__halos` même
     quand celui-ci est plus tôt dans le DOM -- le halo se retrouvait cachÉ
     derrière le bloc de texte (transparent en dehors des glyphes, mais le
     texte lui-même bloquait le halo pile où il aurait dû le border). Ce
     `z-index: 0` fait de `.hero` LA référence d'empilement pour tous ses
     descendants positionnés -- permet à `.hero__halos` de prendre un
     z-index NÉGATIF (plus bas, scopé ICI, jamais en dehors de `.hero`)
     pour repasser proprement sous tout le reste (texte, numéral, photo)
     sans dépendre de subtilités d'ordre du DOM. */
  z-index: 0;
  /* Clippe le numéral fantôme (position absolute, très grand) pour qu'il
     ne déborde jamais sur les petits viewports. */
  overflow: hidden;
}

/* Retour utilisateur : fond photo de toute la bande (`heroFond`,
   _data/rapport.js/layout.njk) avec un voile bleu par-dessus -- distinct
   de la vignette encadrée `.hero__photo-frame` (plus bas), qui affiche
   une 2e image séparée (`heroImage`) à côté du texte. Voile en couche
   UNIQUE (même teinte aux deux extrémités du gradient, pas un dégradé
   haut/bas comme un 1er essai précédent, retiré). Intensité ajustée par
   petites touches successives à la demande de l'utilisateur : 30%
   ("léger", à l'origine) -> 37,5% (+25% relatifs) -> 50% -> 70% (valeurs
   exactes demandées directement).
   `background-image` n'est PAS posée ici (retour utilisateur, bug
   sous-dossier) : layout.njk pose désormais le dégradé ET la photo
   directement en style inline sur `<header>` (formule dupliquée depuis
   ici, cf. son commentaire) -- une custom property `--hero-fond`
   consommée par une règle ici résolvait les URL relatives contre CE
   fichier CSS plutôt que contre la page. `color-mix` de `--color-primary-
   fixed-dim` (AD-2) reste la seule source de vérité pour la couleur. */
.hero--fond {
  background-size: cover;
  background-position: center;
}

/* ---------------------------------------------------------------------
 * Halos de couleur du hero (retour utilisateur, "donner de la vie" puis
 * "beaucoup plus gros... mouvement aléatoire perpétuel") -- cf.
 * commentaire de `.hero__halos` dans layout.njk pour le contexte. Dérive
 * perpétuelle demandée explicitement par l'utilisateur : EXCEPTION
 * consciente à l'interdit "jamais de boucle" d'EXPERIENCE.md > Interaction
 * Primitives > "Banni" (contre-métrique PRD SM-C1, effet gadget) --
 * décision documentée, pas un oubli. Scopée à ce seul fond purement
 * décoratif (`aria-hidden`, cf. `@keyframes hero-halo-drift-1/2/3` plus
 * bas pour le détail des trajectoires).
 * --------------------------------------------------------------------- */
.hero__halos {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* Correctif (vérification navigateur) : `z-index: -1`, scopé au contexte
     d'empilement de `.hero` (`z-index: 0` explicite, cf. plus haut) --
     repasse le halo sous TOUT le reste du hero (texte, numéral fantôme,
     photo du personnage), quel que soit l'ordre du DOM entre eux. Ancien
     raisonnement ("aucun z-index nécessaire, l'ordre du DOM suffit")
     dépassé -- `.hero__text` (position:relative) passait quand même
     par-dessus en pratique (bug constaté : le halo disparaissait
     entièrement derrière le bloc de texte). */
  z-index: -1;
  /* Retour utilisateur ("prendre l'intégralité du header, un océan qui
     existe même en dehors de ses frontières") : dégradé de fond STATIQUE
     -- toujours présent, sur TOUTE la surface de `.hero__halos` (donc de
     `.hero`), indépendamment de la position des 3 halos mobiles
     ci-dessous. C'est LUI qui garantit qu'il n'y a jamais de vide, plutôt
     que de compter sur des halos assez gros et assez rapprochés pour se
     recouvrir en permanence -- ce qui les faisait justement "fusionner"
     (retour utilisateur : "cela les rend opaques", cf. commentaire de
     layout.njk). Les halos, ci-dessous, deviennent des touches de
     couleur un peu plus présentes QUI SE DÉPLACENT par-dessus cet océan
     déjà là, jamais la seule source de couverture. Radiaux très étendus
     (150% de large/haut) centrés hors-cadre (coins) : le dégradé déborde
     largement des bords de `.hero__halos` -- l'"océan" continue
     visuellement au-delà du cadre visible, seule sa fenêtre change. Même
     trio de teintes "container" que les halos, à très faible opacité. */
  background:
    radial-gradient(150% 150% at 10% 15%, color-mix(in srgb, var(--color-primary-container) 22%, transparent), transparent 65%),
    radial-gradient(150% 150% at 92% 85%, color-mix(in srgb, var(--color-secondary-container) 18%, transparent), transparent 65%),
    radial-gradient(140% 140% at 85% 8%, color-mix(in srgb, var(--color-tertiary-container) 15%, transparent), transparent 60%);
}

.hero__halo {
  position: absolute;
  /* Retour utilisateur (référence technique "liquid halo" partagée) :
     forme organique (4 rayons différents par axe) plutôt qu'un cercle
     parfait (`border-radius: 50%`, essai précédent) -- technique CSS pure
     couramment utilisée pour des "blobs" liquides, sans avoir besoin de
     tracés SVG. Un cercle parfait n'a AUCUN rendu différent selon son
     angle de rotation (par définition) -- cette forme asymétrique est ce
     qui permet à la légère rotation ajoutée aux trajectoires plus bas de
     produire un effet réellement visible ("liquide"), pas un no-op. */
  border-radius: 42% 58% 61% 39% / 45% 48% 52% 55%;
  /* Flou important (retour utilisateur, "flou gaussien maximum pour
     diluer") : dilue la couleur en une touche douce plutôt qu'un disque
     nettement contouré. */
  filter: blur(60px);
  /* Retour utilisateur ("le mode screen ne doit pas fonctionner car j'ai
     juste du blanc... overlay, mode incrustation ?") : `overlay` --
     `screen` avec un halo BLANC PUR n'a mathématiquement qu'un seul
     résultat possible (blanc plein, quel que soit le fond en dessous :
     `1 - (1-1)×(1-fond) = 1`) -- ça ne "s'accorde" jamais avec la photo,
     ça la recouvre platement, d'où le "j'ai juste du blanc" constaté.
     `overlay` combine Multiply/Screen SELON la luminosité du fond : il
     éclaircit les zones déjà claires ET assombrit légèrement les zones
     déjà sombres -- la texture/le contraste de la photo restent visibles
     À TRAVERS le halo blanc plutôt que d'être effacés, exactement le
     "on voit l'image de fond" demandé depuis plusieurs passes. */
  mix-blend-mode: overlay;
}

/* Trio dérivé du bleu de marque (section "globale", cf. DESIGN.md > Colors
   > "dégradés... toujours dérivés de la couleur de section dominante") --
   les 3 teintes "container" de _data/theme.js (déjà validées, AD-1/AD-2),
   jamais une couleur inventée. Essai intermédiaire en blanc uni (retour
   utilisateur, "ça fera juste des zones plus claires... peut-être mieux ?")
   comparé en navigateur puis ABANDONNÉ au profit de ce trio -- retour
   utilisateur explicite ("remets les couleurs précédentes"). `mix-blend-
   mode: overlay` (plus haut) fonctionne avec ces 3 couleurs comme avec le
   blanc : il éclaircit/assombrit selon la luminosité du fond, la couleur
   du halo reste visible en teinte plutôt que lavée en simple luminosité.
   Ne peuvent structurellement jamais couvrir le personnage, cf. commentaire
   de `.hero__halos` dans layout.njk (z-index négatif, toujours sous
   `.hero__photo-frame`).
   Retour utilisateur ("ils doivent se repousser comme des aimants ou de
   l'huile dans l'eau" -- jamais fusionner, jamais se chevaucher au point
   d'opacifier) : chaque halo occupe un TERRITOIRE propre -- halo 1 à
   gauche, halo 2 en bas à droite, halo 3 en haut à droite -- avec une
   orbite assez resserrée (cf. `@keyframes hero-halo-drift-*` plus bas)
   pour ne JAMAIS empiéter sur le territoire des 2 autres. C'est cette
   séparation qui donne l'effet "répulsion", pas une fusion de leurs bords.
   Tailles réduites par rapport à une passe précédente (elles n'ont plus
   besoin, seules, de couvrir toute la bande -- l'océan de fond ci-dessus
   s'en charge) -- toujours largement plus grandes que leur territoire pour
   y "flotter" sans jamais laisser voir un bord net.
   `--hero-halo-opacity` : source UNIQUE de l'opacité cible de chaque halo
   (repos ET dérive, cf. `@keyframes` plus bas qui ne touchent que
   `transform`). */
.hero__halo--1 {
  top: 15%;
  left: -15%;
  width: 60vw;
  height: 60vw;
  max-width: 680px;
  max-height: 680px;
  background: var(--color-primary-container);
  --hero-halo-opacity: 0.65;
  opacity: var(--hero-halo-opacity);
}

.hero__halo--2 {
  bottom: -25%;
  right: -15%;
  width: 50vw;
  height: 50vw;
  max-width: 560px;
  max-height: 560px;
  background: var(--color-secondary-container);
  --hero-halo-opacity: 0.55;
  opacity: var(--hero-halo-opacity);
  /* Forme propre, distincte de `.hero__halo` (règle commune) -- 3 blobs
     identiques auraient l'air d'un seul motif dupliqué 3 fois. */
  border-radius: 55% 45% 38% 62% / 62% 40% 60% 38%;
}

.hero__halo--3 {
  top: -20%;
  right: -10%;
  width: 42vw;
  height: 42vw;
  max-width: 480px;
  max-height: 480px;
  background: var(--color-tertiary-container);
  --hero-halo-opacity: 0.48;
  opacity: var(--hero-halo-opacity);
  border-radius: 63% 37% 51% 49% / 39% 58% 42% 61%;
}

/* Retour utilisateur ("les trois halos doivent être animés tout le temps,
   traverser, se repousser" -> "mouvement plus lent" -> "ils doivent se
   repousser comme des aimants ou de l'huile dans l'eau") : dérive
   continue -- SEULE exception du site à "jamais de boucle" (EXPERIENCE.md
   > Interaction Primitives > "Banni"), demandée explicitement et à
   plusieurs reprises par l'utilisateur pour ce fond purement décoratif
   (aucun contenu, aucune fonction, `aria-hidden`).
   3 orbites LOCALES et DISTINCTES, une par territoire (cf. commentaire de
   `.hero__halo--1` plus haut) : chaque halo tourne/dérive autour de SA
   PROPRE position de repos sans jamais s'approcher des 2 autres --
   contrairement aux passes précédentes (trajectoires longues qui
   traversaient toute la bande, ou halos si gros que leur chevauchement
   permanent les fusionnait), l'amplitude reste volontairement modeste
   pour ne jamais quitter le territoire assigné.
   `rotate()` ajouté (retour utilisateur, référence technique "liquid
   halo") : sans forme asymétrique, une rotation n'aurait aucun effet
   visible sur un cercle -- c'est justement pour ça que `.hero__halo` est
   passé à un `border-radius` à 8 valeurs (organique, cf. son commentaire)
   juste avant. `prefers-reduced-motion: reduce` neutralise entièrement
   cette exception : halos parfaitement statiques (repli `opacity`/position
   de base ci-dessus), comme tout le reste du site. */
/* Retour utilisateur ("augmente la vitesse x2") : durées divisées par 2
   (34/42/38s -> 17/21/19s), délais divisés par 2 à l'identique pour
   garder le même DÉCALAGE RELATIF entre halos (donc les mêmes croisements
   de trajectoires, juste deux fois plus vite). */
@media (prefers-reduced-motion: no-preference) {
  .hero__halo--1 {
    animation: hero-halo-drift-1 17s ease-in-out infinite;
  }

  .hero__halo--2 {
    animation: hero-halo-drift-2 21s ease-in-out infinite;
    animation-delay: -7s;
  }

  .hero__halo--3 {
    animation: hero-halo-drift-3 19s ease-in-out infinite;
    animation-delay: -4s;
  }

  @keyframes hero-halo-drift-1 {
    0% {
      transform: translate(0, 0) rotate(0deg) scale(1);
    }
    25% {
      transform: translate(20%, -22%) rotate(35deg) scale(1.2);
    }
    50% {
      transform: translate(-14%, 14%) rotate(-20deg) scale(0.85);
    }
    75% {
      transform: translate(12%, 22%) rotate(15deg) scale(1.1);
    }
    100% {
      transform: translate(0, 0) rotate(360deg) scale(1);
    }
  }

  @keyframes hero-halo-drift-2 {
    0% {
      transform: translate(0, 0) rotate(0deg) scale(1);
    }
    30% {
      transform: translate(-20%, -16%) rotate(-30deg) scale(0.88);
    }
    60% {
      transform: translate(16%, 18%) rotate(20deg) scale(1.2);
    }
    85% {
      transform: translate(-10%, 12%) rotate(-10deg) scale(0.92);
    }
    100% {
      transform: translate(0, 0) rotate(-360deg) scale(1);
    }
  }

  @keyframes hero-halo-drift-3 {
    0% {
      transform: translate(0, 0) rotate(0deg) scale(1);
    }
    35% {
      transform: translate(-16%, 18%) rotate(25deg) scale(1.15);
    }
    65% {
      transform: translate(14%, -14%) rotate(-25deg) scale(0.85);
    }
    100% {
      transform: translate(0, 0) rotate(360deg) scale(1);
    }
  }
}

.hero__inner {
  /* Mêmes paliers de marge horizontale que `main`/.site-nav__inner
     ci-dessus, pour que le contenu du hero reste aligné avec le reste de
     la page à chaque palier. Pas de `position: relative` ici (correctif) :
     le numéral fantôme doit s'ancrer à `.hero` (toute la bande), pas à ce
     bloc -- le laisser positionné en ferait la référence la plus proche
     pour `.hero__numeral`, ce qu'on veut justement éviter. */
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding-block: 3rem;
  padding-inline: var(--spacing-margin-mobile, 20px);
  /* Retour utilisateur (7e passe -- coller au bas, tablette) : `.hero`
     s'étire à `min-height: 60vh` (plus haut) même quand le texte + la
     photo empilés n'en ont pas besoin -- sur tablette (768-1023px, sous
     le palier desktop qui passe en rangée plus bas), l'ancien flux bloc
     laissait tout cet excédent s'accumuler APRÈS le dernier enfant (le
     cadre photo), pas collé au bas comme demandé (bug constaté en
     vérification indépendante : 164px de vide sous le cadre à 768×1024,
     bien plus que les 48px de padding attendus). `display: flex; flex-
     direction: column` transforme texte + cadre en enfants flex
     empilés -- `margin-top: auto` sur le cadre (plus bas) absorbe alors
     tout l'excédent au-dessus de lui plutôt qu'en dessous, le collant
     au bas quelle que soit la hauteur réelle de la bande. Repassé en
     `flex-direction: row` à partir de 1024px (media query plus bas). */
  display: flex;
  flex-direction: column;
}

@media (min-width: 768px) {
  .hero__inner {
    padding-inline: 2.5rem;
  }
}

@media (min-width: 1024px) {
  .hero__inner {
    padding-inline: var(--spacing-margin-desktop, 80px);
    /* Repasse en rangée : la base (plus haut) empile texte + photo en
       colonne pour le palier mobile/tablette (`margin-top: auto` sur le
       cadre photo, plus bas) -- inutile/sans effet ici, la rangée + le
       `flex: 0 0 ...` du cadre gèrent déjà côte-à-côte et pleine hauteur. */
    flex-direction: row;
  }
}

/* Retour utilisateur : zone photo à côté du texte du hero (`imagePlaceholder()`,
   layout.njk -- même repli "photo à venir" que les autres sections tant
   qu'aucune image n'est fournie). Empilé par défaut (mobile/tablette,
   ordre DOM texte puis image = déjà correct sans règle supplémentaire) ;
   côte à côte à partir de 1024px, palier "desktop" du projet, largeur
   fixe partagée avec les autres repli photo du site (`--secondary-
   column-width`, tokens.css). `align-items: stretch` (comportement flex
   par défaut, non repris ici) : `.hero__inner` étant lui-même étiré sur
   toute la hauteur de `.hero` (cf. `align-items: stretch` sur `.hero`
   plus haut), le repli photo en hérite directement -- il occupe
   maintenant toute la hauteur RÉELLE de la bande, pas seulement celle du
   texte voisin. */
@media (min-width: 1024px) {
  .hero__inner {
    display: flex;
    gap: calc(var(--spacing-gutter) * 2);
  }

  /* `display: flex; flex-direction: column; justify-content: center` :
     `.hero__text` est lui-même étiré (comportement stretch hérité de
     `.hero__inner`) sur toute la hauteur de la bande -- sans ce
     recentrage, le kicker+titre se retrouveraient collés en haut de cette
     boîte désormais haute plutôt que verticalement centrés comme avant
     (quand `.hero` centrait directement `.hero__inner`, cf. plus haut). */
  .hero__text {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* Retour utilisateur (4e passe) : `align-items: stretch` (plus haut)
     étire le repli photo/le cadre de la photo réelle sur la hauteur de
     `.hero__inner`, mais celle-ci reste réduite du `padding-block: 3rem`
     du conteneur (plus haut) -- le texte en a besoin pour respirer,
     l'image non. `margin-block: -3rem` (l'exact opposé, littéral pour
     rester lisible plutôt qu'une variable pour une valeur utilisée à cet
     unique endroit) fait déborder l'image de cette même distance en haut
     ET en bas, l'amenant à toucher les bords réels de `.hero` -- vérifié
     en direct : hauteur désormais identique à celle de la bande, à
     l'arrondi près. Règle commune au repli `.image-placeholder` ET au
     cadre de la vraie photo (`.hero__photo-frame`, cf. plus bas) : même
     boîte, même comportement, que l'image soit fournie ou non.

     Retour utilisateur (5e passe) : `.hero__photo` (l'`<img>` lui-même)
     était initialement l'enfant flex directement dimensionné ici --
     `<img>` est un élément remplacé avec un ratio intrinsèque propre
     (1122×1402) : une fois étiré (`align-items: stretch`), le navigateur
     recalculait sa largeur à partir de CE ratio plutôt que de respecter
     `flex-basis: 420px` (bug constaté en vérification indépendante :
     1040×1300px rendus, exactement le ratio source à l'échelle -- `flex-
     basis` mesurait pourtant bien 420px dans les styles calculés, mais
     sans effet sur le rendu final). `.hero__photo-frame` (div neutre,
     cf. layout.njk) est maintenant le SEUL enfant flex dimensionné ici --
     un simple `<div>` n'a aucun ratio intrinsèque à faire valoir, il
     obéit normalement à `flex-basis`/stretch. `.hero__photo` (plus bas)
     se contente de remplir ce cadre déjà correctement dimensionné. */
  .hero__inner .image-placeholder,
  .hero__inner .hero__photo-frame {
    flex: 0 0 var(--secondary-column-width);
    margin-block: -3rem;
  }

  /* Retour utilisateur (6e/8e passes -- tablette/mobile) : en dessous de
     ce palier, `.hero__photo-frame` est contraint par `aspect-ratio`/
     `max-height` (cf. règle plus bas) pour plafonner l'`<img>` à ratio
     intrinsèque qu'elle contient -- les trois neutralisées ici
     (`height`/`aspect-ratio`/`max-height` tous `auto`/`none`) pour
     laisser `align-items: stretch` reprendre ENTIÈREMENT la main sur
     desktop, exactement comme `.hero__photo` avait déjà eu besoin du
     même correctif en 4e passe (plus haut). */
  .hero__inner .hero__photo-frame {
    height: auto;
    aspect-ratio: auto;
    max-height: none;
  }
}

/* Retour utilisateur (7e passe -- coller au bas, tablette/mobile) :
   `margin-top: auto` sur l'image/le repli, dans la colonne flex posée par
   `.hero__inner` (base, plus haut) pour ce palier -- absorbe tout
   l'excédent de hauteur entre le texte et le bas de la bande (`.hero`
   s'étire à `min-height: 60vh`, souvent plus haut que le contenu réel)
   au-dessus de l'image plutôt qu'en dessous, la collant au bord bas de
   `.hero`. Portée à `max-width: 1023px` explicitement (plutôt qu'une
   règle non bornée + un correctif `margin-top` dans le palier desktop)
   pour éviter toute ambiguïté de cascade avec `margin-block: -3rem` du
   palier desktop (plus haut) sur la même propriété. `.hero__inner .image-
   placeholder` (pas `.image-placeholder` seul, classe générique utilisée
   par d'autres sections du site) : la portée reste strictement le hero. */
@media (max-width: 1023px) {
  .hero__inner .image-placeholder,
  .hero__photo-frame {
    margin-top: auto;
  }
}

/* Cadre neutre de la photo réelle du hero (`heroImage.src`, layout.njk) --
   remplace le repli `.image-placeholder` dans la MÊME boîte plutôt que de
   peindre un fond pleine largeur derrière tout le hero (mécanisme
   `.hero--photo` retiré). `overflow: hidden` : nécessaire pour que
   `border-radius` rogne aussi l'`<img>` à l'intérieur (un `border-radius`
   posé sur l'image seule serait masqué par ses propres bords nets sans
   ce clip côté parent).

   Retour utilisateur (6e passe -- tablette/mobile) : `min-height: 200px`
   (essai précédent, calqué sur `.image-placeholder`) ne plafonnait RIEN
   pour un `<img>` réel -- `.image-placeholder` est un simple `<div>` sans
   contenu propre, `min-height` EST alors sa hauteur ; `.hero__photo`
   (l'`<img>`, cf. plus bas) a un ratio intrinsèque (1122×1402) qui pousse
   ce cadre bien AU-DELÀ du plancher dès que rien ne l'en empêche -- bug
   constaté en vérification indépendante : bande hero mesurée à 1079px de
   haut sur tablette (768×1024, l'écran ENTIER ne fait que 1024px), 699px
   sur mobile.

   Retour utilisateur (8e passe -- "tête qui vole") : une hauteur FIXE
   (`height: 260px`, essai précédent) plafonnait bien la hauteur, mais la
   LARGEUR du cadre grandit avec le viewport sur toute la plage empilée
   (375-1023px, cf. `flex-direction: column` sur `.hero__inner`, plus
   haut) -- vers ~900px de large, le cadre devenait très large et très
   plat (805×260 mesuré, ratio ~3:1) alors que la photo est un portrait
   (1122×1402, ratio ~0,8:1) : `object-fit: cover` doit alors zoomer
   ÉNORMÉMENT pour couvrir cette largeur, ne laissant plus voir qu'une
   fine tranche du haut de la photo -- "juste une tête qui vole" (retour
   utilisateur), la largeur du cadre remplie presque entièrement de fond
   autour d'une tête minuscule. `aspect-ratio: 4 / 3` (au lieu d'une
   hauteur fixe) fait grandir la hauteur EN PROPORTION de la largeur --
   le cadre reste un rectangle raisonnable à toute largeur intermédiaire,
   jamais démesurément plat. `max-height: 420px` : plafond de sécurité
   pour ne pas non plus grandir sans limite juste avant le palier desktop
   (1023px de large -> ~682px de haut sans lui). Neutralisés (`aspect-
   ratio: auto`, `max-height: none`) à partir de 1024px (media query plus
   haut) pour laisser `align-items: stretch` reprendre entièrement la
   main sur desktop.

   Retour utilisateur (7e passe -- coller au bas) : `margin-bottom: -3rem`
   fait déborder le cadre du padding-bottom de `.hero__inner` (3rem, cf.
   plus haut) pour toucher le bord réel du bas de `.hero` -- même
   technique que le débord `margin-block: -3rem` du palier desktop (plus
   haut dans ce fichier), mais SEULEMENT en bas ici : contrairement au
   desktop (où la photo occupe toute la hauteur de la bande), le texte
   précède la photo dans l'empilement mobile/tablette et a besoin de son
   propre espacement au-dessus -- seul le bas, qui ne touche plus aucun
   autre contenu avant la fin de la bande, doit déborder. */
.hero__photo-frame {
  aspect-ratio: 4 / 3;
  max-height: 420px;
  margin-bottom: -3rem;
  border-radius: var(--rounded);
  overflow: hidden;
}

/* `object-fit: cover` : l'image fournie (portrait, 1122×1402) remplit ce
   cadre plus large que haut sans se déformer, quitte à en rogner les
   bords -- même logique que `background-size: cover` utilisé partout
   ailleurs sur le site pour une image de remplissage. `width`/`height`
   100% : remplit le cadre déjà dimensionné par `.hero__photo-frame`
   ci-dessus, sans jamais imposer son propre ratio intrinsèque (cf.
   commentaire de la 5e passe plus haut -- exactement le problème que ce
   cadre neutre permet d'éviter). */
/* Retour utilisateur : `object-position: top` -- quand le cadre plus
   court que l'image (portrait, 1122×1402) force `object-fit: cover` à
   rogner, le haut de la photo (visage) doit toujours rester visible,
   jamais le centre (défaut du navigateur, `50% 50%`, qui rognait autant
   en haut qu'en bas). Le rognage porte donc entièrement sur le bas de
   l'image. */
.hero__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
}

/* Numéral fantôme "2025" -- simple effet typographique (texte, pas une
   image), inspiré du grand "2024" en transparence du site 2024. Opacité
   réduite pour ne jamais entraver la lecture du titre/accroche au-dessus
   (position:relative sur .hero__title/.hero__subtitle, cf. plus bas). */
.hero__numeral {
  position: absolute;
  /* Correctif revue : ancré à `.hero` (toute la bande) plutôt qu'à
     `.hero__inner`, `left: 0` collerait le numéral au bord réel du
     viewport sur les très grands écrans (≥1360px), où `.hero__inner`
     (max-width 1200px + 80px de marge desktop) se retrouve centré avec un
     espace vide de chaque côté -- le numéral se déconnecterait alors
     visuellement du titre/accroche. Ce calcul reproduit le bord gauche
     réel du contenu de `.hero__inner` (centrage 1200px + inset 80px). */
  left: max(0px, calc(50% - 600px + var(--spacing-margin-desktop, 80px)));
  /* Retour utilisateur ("le 2025 du header se cache") : remonté de
     `--avant-propos-overlap` (tokens.css, même token que la marge négative
     de `#avant-propos .section-band` plus bas dans ce fichier) -- sans ce
     décalage, le tiers inférieur des glyphes se retrouvait sous la bande
     "Avant-propos" qui chevauche désormais le bas du hero, rendant "25"
     illisible (bug constaté en vérification navigateur, largeur réduite).
     `calc()` plutôt qu'une valeur fixe : reste correct si la police/la
     hauteur de bande changent un jour, un seul token à ajuster (AD-2). */
  bottom: calc(-0.1em + var(--avant-propos-overlap));
  font-family: var(--font-display-lg-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: clamp(6rem, 20vw, 16rem);
  font-weight: var(--font-display-lg-weight);
  line-height: 1;
  color: var(--color-on-primary);
  opacity: 0.18;
  pointer-events: none;
  user-select: none;
}

/* Retour utilisateur (2e passe -- hiérarchie inversée) : enveloppe le
   kicker + le titre (cf. layout.njk) pour porter `position: relative`
   (nécessaire pour que les deux passent AU-DESSUS du numéral fantôme,
   positionné en absolute sur `.hero`, cf. `.hero__numeral` plus haut --
   auparavant chacun des deux le portait individuellement) et servir de
   colonne "texte" dans la mise en page à 2 colonnes de `.hero__inner`
   (repli photo à côté, cf. plus haut) sans affecter la largeur du
   numéral, resté ancré à `.hero` en dehors de cette colonne. */
.hero__text {
  position: relative;
}

/* Kicker : petit repère ("Rapport d'activité 2025", un simple repère
   d'année, pas le titre éditorial de ce rapport) au-dessus du VRAI titre
   -- traité en majuscules + espacement des lettres pour se lire comme une
   étiquette de catégorie plutôt qu'un second titre, à la manière d'une
   Une de magazine. Taille reprise de l'ancien `.hero__subtitle` (corps de
   texte large, --font-body-lg) : suffisamment lisible sans jamais rivaliser
   avec `.hero__title` juste en dessous. */
.hero__kicker {
  margin: 0 0 0.5rem;
  font-family: var(--font-body-lg-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-body-lg-size);
  font-weight: 600;
  line-height: var(--font-body-lg-line-height);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-on-primary);
}

.hero__title {
  margin: 0;
  font-family: var(--font-display-lg-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-display-lg-size);
  font-weight: var(--font-display-lg-weight);
  line-height: var(--font-display-lg-line-height);
  letter-spacing: var(--font-display-lg-letter-spacing);
  color: var(--color-on-primary);
}

/* Mobile (FR11, même principe que h2 plus bas) : le palier mobile du titre
   affiché dans le hero, et un plancher de hauteur réduit par rapport aux
   60vh desktop -- sur un petit écran, 60vh laisse un bloc disproportionné
   par rapport au contenu (titre + accroche courts), cf. Design Notes/I-O
   matrix de la spec ("minHeight peut être réduit à un palier mobile"). */
@media (max-width: 767px) {
  .hero {
    min-height: min(var(--hero-band-min-height), 24rem);
  }

  .hero__title {
    font-size: var(--font-display-lg-mobile-size);
    line-height: var(--font-display-lg-mobile-line-height);
    letter-spacing: var(--font-display-lg-mobile-letter-spacing);
  }
}

/* Retour utilisateur (revue Sally, party mode) : `--spacing-section-gap`
   (96px, tokens.css) était défini par DESIGN.md > Layout & Spacing --
   "un espace généreux qui signale un vrai changement de sujet, cohérent
   avec la structure en chapitres du rapport" -- mais jamais consommé
   nulle part (vérifié par grep) : les sections n'étaient séparées que par
   2.5rem (40px) + un filet 1px, cinq chapitres se lisant comme un flux
   continu plutôt que des chapitres distincts. */
.section {
  padding-block: 0 var(--spacing-section-gap);
  border-bottom: 1px solid var(--color-outline-variant);
  /* Décale la cible de scroll (ancre de nav/deep-link) sous .site-nav
     (position:fixed), même mécanisme que .volet__toggle plus bas. */
  scroll-margin-top: var(--nav-height);
  /* Retour utilisateur ("filigrane sur les grandes sections") : contexte
     d'empilement propre -- permet à `.section::before` (le filigrane
     `trame.png`, cf. son commentaire tout en haut de ce fichier) de
     repasser sous TOUT le contenu réel de la section via un z-index
     négatif SCOPÉ ICI, même technique déjà éprouvée sur `.hero` plus haut
     (bug analogue rencontré et corrigé là-bas : un enfant positionné sans
     z-index explicite peut sinon passer au-dessus d'un texte pourtant
     plus tôt dans le DOM). */
  position: relative;
  z-index: 0;
}

.section:last-child {
  border-bottom: none;
}

/* Retour utilisateur (revue Sally, party mode) : les paragraphes de prose
   narrative mesuraient jusqu'à 140-142 caractères par ligne à 1440px
   (intro "portail", intro "travaux interbranches") -- très au-delà de la
   plage lisible (~60-75 car., déjà noté non tranché dans deferred-work.md
   faute de valeur précise). La valeur manquante était déjà dans le site :
   le corps d'un volet ouvert lit à 62 caractères (contraint par la largeur
   de sa carte). `68ch` reprend ce même ordre de grandeur pour toute la
   prose. Volontairement large (`.section p`, pas un sélecteur plus
   restrictif) pour couvrir tous les contextes où `renderCorps()`/
   `renderBloc()` produit des `<p>` -- corps de section empilé, colonnes
   (avant-propos), encadré, rubrique, textes du portail, veille
   prospective -- sans exception à maintenir à la main. Les grilles de
   volets ne sont PAS concernées en pratique : leur carte est déjà plus
   étroite que 68ch, la règle ne peut donc jamais y élargir quoi que ce
   soit, seulement rétrécir là où c'est encore trop large. */
.section p {
  max-width: 68ch;
}

/* Retour utilisateur (2e passe) : le texte d'intro d'une section sans
   colonne voisine (`.section-intro`, _includes/section.njk -- portail,
   travaux-interbranches, etudes-de-branches, certifications) restait
   aligné à gauche une fois plafonné à 68ch ci-dessus -- le vide à droite
   sur grand écran se lisait comme un oubli plutôt qu'une marge voulue. Un
   1er essai l'a centré (`margin-inline: auto`) : jugé "bizarre et
   perturbant" -- ça cassait l'alignement à gauche partagé avec le titre
   de section et le reste de la page. Remplacé par une vraie 2e colonne
   (`.section-intro-row` ci-dessous) : le texte reste aligné à gauche,
   `max-width` seule suffit maintenant (la colonne elle-même est déjà plus
   étroite que 68ch une fois la grille posée -- répétée malgré tout pour
   le cas où `.section-intro-row` n'existe pas encore dans le DOM). */
.section-intro {
  max-width: 68ch;
}

.section-intro > :first-child {
  margin-top: 0;
}

/* Retour utilisateur ("met le texte sur 2 colonnes équilibré") : les 2
   paragraphes du bloc de conclusion (`conclusion()`, _includes/section.njk)
   -- une grille à 2 colonnes STRICTEMENT égales (1fr 1fr, contrairement à
   `.section-columns` plus bas dont le ratio 600px/320px est volontairement
   inégal texte+chiffresCles) plutôt qu'un flux `column-count: 2` : chaque
   paragraphe reste entier dans SA colonne, jamais coupé au milieu entre les
   deux. Empilé par défaut (mobile/tablette) -- même palier "desktop" que
   `.section-columns`/`.section-intro-row` pour rester cohérent avec le
   reste du site. */
@media (min-width: 1024px) {
  .conclusion-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: calc(var(--spacing-gutter) * 2);
  }

  .conclusion-columns > :first-child {
    margin-top: 0;
  }
}

/* Retour utilisateur : 2e colonne à côté du texte d'intro, occupée par un
   repli "photo à venir" (`imagePlaceholder()`, _includes/section.njk)
   plutôt que laissée vide -- largeur FIXE (`--secondary-column-width`,
   css/tokens.css), pas un ratio de grille (`2fr 1fr` etc.) : demandé pour
   que ce bloc, l'encadré "objectifs" du portail et le bloc chiffres clés
   de `.section-columns` (plus bas) partagent tous la même largeur, plutôt
   que chacun dérive une largeur différente de son propre ratio selon la
   largeur du texte voisin. Même palier ("desktop" du projet, 1024px).
   `align-items: stretch` (pas `start` comme `.section-columns`) : le
   repli photo n'a pas de hauteur de contenu propre à respecter, autant
   qu'il occupe toute la hauteur du texte voisin plutôt que de laisser du
   vide EN DESSOUS de lui. Empilé par défaut (mobile/tablette, ordre DOM
   texte puis image = déjà correct sans règle supplémentaire). */
@media (min-width: 1024px) {
  .section-intro-row {
    display: grid;
    grid-template-columns: 1fr var(--secondary-column-width);
    gap: calc(var(--spacing-gutter) * 2);
    align-items: stretch;
  }

  /* Correctif retour utilisateur : `.encadre` (repli `data.introEncadre`,
     _includes/section.njk) porte `margin: 1rem 0` par défaut (règle de
     base, plus bas dans ce fichier) -- une fois placé comme 2e colonne de
     cette grille, ce margin-top décalait son contour vers le bas par
     rapport au premier paragraphe du texte voisin (qui, lui, n'a aucune
     marge propre à ce niveau). Neutralisé UNIQUEMENT dans ce contexte
     (`.section-intro-row > .encadre`, pas `.encadre` globalement -- les
     encadrés isolés dans un flux de texte classique gardent leur
     respiration verticale habituelle). Le margin-bottom reste : sans
     effet ici (dernier/seul enfant de sa cellule de grille), mais s'il
     devient un jour visible en empilement mobile (ordre DOM texte puis
     encadré), il fournit l'espacement voulu entre les deux. */
  .section-intro-row > .encadre {
    margin-top: 0;
  }
}

/* Repli "photo à venir" générique (`imagePlaceholder()`, _includes/
   section.njk) : même langage visuel que la zone décorative des volets
   (`.volet--photo-zone::after` plus bas -- fond pastel + pictogramme
   "image"), pour rester cohérent avec l'unique autre repli photo du site.
   `min-height` : hauteur de secours pour l'empilement mobile/tablette (où
   `align-items: stretch` ci-dessus ne s'applique pas, rien d'autre ne fixe
   sa hauteur) -- ignorée sur desktop dès que `.section-intro-row` l'étire
   à la hauteur du texte voisin. */
.image-placeholder {
  min-height: 200px;
  border-radius: var(--rounded);
  background-color: color-mix(in srgb, var(--section-accent, var(--color-primary)) 20%, white);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(8,67,111,0.4)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='M21 15l-5-5L5 21'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 48px 48px;
}

.section--secondary .image-placeholder {
  background-color: color-mix(in srgb, var(--section-accent, var(--color-secondary)) 20%, white);
}

/* Retour utilisateur ("toutes les zones image prêtes") : variante du repli
   ci-dessus qui affiche une vraie photo -- pictogramme/teinte pastel
   neutralisés, même logique que `.volet--photo-zone--image` plus bas
   (mécanisme jumeau pour les zones décoratives de volet). Règle dédiée
   pour le cas `.section--secondary` (spécificité égale à `.section
   --secondary .image-placeholder` sinon -- même précaution que
   `.volets.volets--etudes`/`.encadre--image` ailleurs).
   `background-image` n'est PAS posée ici (retour utilisateur, bug
   sous-dossier) : `imageOrPlaceholder()` (_includes/section.njk) la pose
   directement en style inline sur l'élément -- une règle ici via
   `var(--image-placeholder-photo)` résolvait les URL relatives contre
   CE fichier CSS plutôt que contre la page, cf. son commentaire. */
.image-placeholder.image-placeholder--photo {
  background-color: transparent;
  background-size: cover;
  background-position: center;
}

.section--secondary .image-placeholder.image-placeholder--photo {
  background-color: transparent;
}

/* Retour utilisateur ("travaux-interbranches") : zone image AU-DESSUS du
   premier paragraphe du texte d'intro (`data.introImageAvant`, cf.
   `.section-intro` dans _includes/section.njk) -- CONFINÉE à la colonne de
   texte (1er essai en pleine largeur de la rangée, rejeté : "pas sur deux
   colonnes"). Ratio large de type bannière plutôt que le `min-height` de
   secours de `.image-placeholder` (pensé pour une 2e colonne étroite, pas
   ce contexte pleine largeur de colonne). */
.section-image-avant {
  margin-block-end: 1.75rem;
}

.section-image-avant .image-placeholder {
  aspect-ratio: 21 / 9;
  min-height: 0;
}

/* Bandeau de couleur de rôle (FR10) : substitut du hero-band photo (aucune
   photo disponible pour cette story, cf. Design Notes de la spec 1.3). La
   couleur de fond réelle (var(--color-primary)/var(--color-secondary)) est
   posée en style inline par _includes/section.njk selon data.couleur ; ce
   bloc ne fixe que la mise en forme commune (padding, coins, typo, couleur
   de texte). */
.section-band {
  position: relative;
  overflow: hidden;
  padding: 1.5rem 1.75rem;
  border-radius: var(--rounded-md);
  margin-block-end: 1.75rem;
}

/* Retour utilisateur ("améliorer les bandeaux" puis "pareil pour les
   petits headers, plusieurs couleurs ou camaïeu... le header vert on voit
   rien pour le coup") : 2 halos décoratifs en pseudo-éléments -- pas de
   markup supplémentaire à porter (purement visuel), même esprit que
   `.hero__halo` plus haut dans ce fichier.
   Camaïeu plutôt que teintes fixes (1er essai : sheen blanc + vert de
   marque fixe) : un halo VERT sur le bandeau "portail", déjà vert
   (`bandeauVertLogo`), s'y fondait entièrement -- invisible, constaté en
   navigateur. `color-mix(in srgb, white/black N%, var(--section-band-
   color))` (posée par `section()`, _includes/section.njk) dérive une
   teinte plus claire et une plus sombre de la couleur RÉELLE de CE
   bandeau -- garantit un contraste visible sur N'IMPORTE laquelle des 4
   couleurs de bandeau du site (bleu, turquoise, violet, vert du logo),
   jamais une couleur fixe qui pourrait s'y fondre.
   Dérive perpétuelle (`@keyframes section-band-halo-drift` plus bas,
   propre à ce bloc -- amplitude bien plus modeste que les 3 trajectoires
   du hero, cf. leur commentaire : ces bandeaux sont petits, un mouvement
   aussi ample que celui du hero les ferait sortir de cadre en permanence)
   : même exception documentée qu'au hero (cf. son commentaire), même
   exigence de discrétion -- cycles longs, amplitude faible,
   `prefers-reduced-motion: reduce` neutralise entièrement (repli
   `opacity`/position statique ci-dessous). */
/* Retour utilisateur ("fais les mêmes animations sur les bandeaux de
   section" -> "plus flagrant, je vois à peine un halo bouger et pas les
   deux autres, on peut en mettre trois sur toute la largeur") : même
   recette que `.hero__halo` plus haut dans ce fichier -- forme organique
   (`border-radius` à 8 valeurs, pas un cercle), `mix-blend-mode: overlay`,
   rotation + dérive. 3 halos désormais (`::before`/`::after` + un vrai
   3e élément, `.section-band__halo` -- CSS ne fournit que 2
   pseudo-éléments, cf. section.njk), répartis explicitement GAUCHE /
   CENTRE / DROITE plutôt que tous les 2 blottis dans le même angle
   (essai précédent : les 2 halos étaient visuellement proches, un seul
   se remarquait vraiment). Tailles et opacité augmentées (`plus
   flagrant`) : jusqu'à 62%/460px (contre 60%/340px avant) et opacité
   remontée (0,7/0,6/0,5). Flou (24px, inchangé) : ces halos restent bien
   plus petits que ceux du hero, un flou aussi fort que le sien les
   diluerait entièrement. Couleurs INCHANGÉES (camaïeu clair/sombre dérivé
   de `--section-band-color`, cf. commentaire plus haut) -- c'est ce qui
   garantit la visibilité sur les 4 couleurs de bandeau, contrairement au
   trio fixe du hero. */
.section-band::before,
.section-band::after,
.section-band__halo {
  content: "";
  position: absolute;
  pointer-events: none;
  filter: blur(24px);
  mix-blend-mode: overlay;
}

.section-band::before {
  top: -80%;
  left: -8%;
  width: 62%;
  max-width: 460px;
  aspect-ratio: 1;
  background: color-mix(in srgb, white 55%, var(--section-band-color, var(--color-primary)));
  opacity: 0.7;
  border-radius: 42% 58% 61% 39% / 45% 48% 52% 55%;
}

.section-band__halo {
  bottom: -85%;
  left: 38%;
  width: 52%;
  max-width: 380px;
  aspect-ratio: 1;
  background: color-mix(in srgb, white 40%, var(--section-band-color, var(--color-primary)));
  opacity: 0.6;
  border-radius: 63% 37% 51% 49% / 39% 58% 42% 61%;
}

.section-band::after {
  top: -75%;
  right: -10%;
  width: 55%;
  max-width: 400px;
  aspect-ratio: 1;
  background: color-mix(in srgb, black 30%, var(--section-band-color, var(--color-primary)));
  opacity: 0.5;
  border-radius: 55% 45% 38% 62% / 62% 40% 60% 38%;
}

@media (prefers-reduced-motion: no-preference) {
  .section-band::before,
  .section-band::after,
  .section-band__halo {
    animation-name: section-band-halo-drift;
    animation-timing-function: ease-in-out;
    animation-iteration-count: infinite;
  }

  /* Retour utilisateur ("augmente la vitesse d'animation x4 dans le
     bandeau") : durées ET délais divisés par 4 (16/18/20s -> 4/4.5/5s ;
     -5/-11s -> -1.25/-2.75s) -- diviser aussi les délais, pas seulement
     les durées, garde le même DÉCALAGE RELATIF entre les 3 halos (donc
     les mêmes croisements de trajectoires), juste 4x plus vite plutôt que
     désynchronisés. */
  .section-band::before {
    animation-duration: 4s;
  }

  .section-band__halo {
    animation-duration: 4.5s;
    animation-delay: -1.25s;
  }

  .section-band::after {
    animation-duration: 5s;
    animation-delay: -2.75s;
  }

  @keyframes section-band-halo-drift {
    0% {
      transform: translate(0, 0) rotate(0deg) scale(1);
    }
    22% {
      transform: translate(8%, -6%) rotate(25deg) scale(1.15);
    }
    47% {
      transform: translate(-6%, 5%) rotate(-18deg) scale(0.85);
    }
    68% {
      transform: translate(5%, 8%) rotate(14deg) scale(1.1);
    }
    100% {
      transform: translate(0, 0) rotate(360deg) scale(1);
    }
  }
}

.section--primary .section-band {
  color: var(--color-on-primary);
}

.section--secondary .section-band {
  color: var(--color-on-secondary);
}

/* Retour utilisateur : bandeau "portail" dans le vert du logo
   (--color-tertiary, posé par bandeauVertLogo -- cf. _includes/section.njk
   et le commentaire sur VALID_COULEURS dans _data/rapport.js). Règle
   séparée plutôt qu'un 3e cas dans .section--primary/.section--secondary
   ci-dessus : `couleur` (donc la classe `section--primary` ici) reste
   inchangé, seule cette classe additionnelle existe pour corriger le texte.
   Placée APRÈS .section--primary pour gagner le conflit de spécificité
   égale (même nombre de classes) via l'ordre de cascade -- `--color-on-
   tertiary` (sombre) plutôt que le blanc habituel : ce vert clair n'offre
   pas un contraste suffisant avec du texte blanc. */
.section--vert-logo .section-band {
  color: var(--color-on-tertiary);
}

.section-band h2 {
  margin: 0;
  color: inherit;
  /* `position: relative; z-index: 1` : garantit que le titre reste
     au-dessus des halos décoratifs (`.section-band::before`/`::after`
     plus haut dans ce fichier) quel que soit l'ordre de peinture des
     pseudo-éléments par rapport au contenu réel. */
  position: relative;
  z-index: 1;
}

/* Retour utilisateur ("donner un peu de dynamisme") : la bande "Avant-
   propos" chevauche le bas du hero -- environ la moitié de sa propre
   hauteur passe par-dessus le hero, comme une carte qui flotte sur la
   photo plutôt que de s'enchaîner platement dessous. Scopé au SEUL
   #avant-propos (1ère section, juste après .hero dans le flux -- aucune
   autre section n'est adjacente au hero) via id de section, même
   précédent que `#etudes-de-branches .chiffres-cles-liste` plus bas dans
   ce fichier.
   Valeurs empiriques (vérifiées en navigateur) : hauteur réelle de la
   bande mesurée à 83px sous 768px (police h2 mobile) et 91px à partir de
   768px (police h2 desktop, cf. media query juste plus bas dans ce
   fichier) -- la marge négative vise la moitié de CHAQUE valeur, pas une
   fraction de la hauteur du hero (`--hero-band-min-height`, sans rapport
   avec la hauteur de cette bande). `position: relative` + `z-index: 1` :
   garantit l'empilement au-dessus du hero explicitement plutôt que de
   dépendre du seul ordre du DOM (fond flex du hero, cf. `.hero` plus haut
   dans ce fichier). Ombre reprise telle quelle de `.portail-banner__encadre
   .encadre` (même fichier) -- même traitement "carte qui flotte sur une
   photo" déjà éprouvé ailleurs sur ce site, pas une nouvelle valeur
   inventée.
   Correctif (vérification navigateur) : `main` porte son propre padding-
   top (`--spacing-margin-mobile`/24px à tout palier -- cf. règle `main`
   plus haut dans ce fichier, seul `padding-inline` varie par palier) --
   sans en tenir compte, la marge négative absorbe d'abord CE padding
   avant d'entamer le hero, ne laissant qu'un chevauchement de ~24px
   (23,7% de la bande, mesuré) au lieu de la moitié voulue. `--avant-
   propos-overlap` (tokens.css, 2.6rem mobile / 2.85rem desktop ci-dessous)
   porte la moitié de bande voulue (41,5px / 45,6px, mesuré) ; ce token
   commun ajoute encore le padding de `main` (1.5rem) pour obtenir la marge
   négative totale -- PARTAGÉ avec `.hero__numeral` plus bas dans ce
   fichier, qui doit remonter du même montant pour rester lisible malgré ce
   chevauchement (bug constaté : "2025" partiellement caché sous la
   bande sans cette synchronisation). */
#avant-propos .section-band {
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * (var(--avant-propos-overlap) + 1.5rem));
  box-shadow: 0 8px 24px rgba(8, 67, 111, 0.15);
}

/* `:root` (pas `#avant-propos .section-band`) : `--avant-propos-overlap`
   est consommé par 2 éléments SANS lien de parenté DOM (cette bande ET
   `.hero__numeral`, tous deux enfants de `body` mais ni l'un ni l'autre
   ancêtre du second) -- une redéfinition scopée à la seule bande ne
   changerait la valeur QUE pour elle, laissant le numéral désynchronisé à
   ce palier (`.hero__numeral` continuerait d'hériter 2.6rem de `:root`). */
@media (min-width: 768px) {
  :root {
    --avant-propos-overlap: 2.85rem;
  }
}

/* Retour utilisateur : éviter les césures (coupure syllabique avec un
   trait d'union visible) dans les titres -- `hyphens: auto` est posé sur
   `body` (Story 1.4) et hérité par tous les descendants, y compris les
   titres. Désactivée ici pour les niveaux de titre uniquement :
   `overflow-wrap: break-word` (également hérité depuis `body`) reste actif
   comme filet de sécurité anti-débordement pour un mot long non sécable
   (ex. "L'accompagnement", "certifications") -- il coupe le mot si
   nécessaire pour éviter un débordement horizontal, mais sans y insérer de
   trait d'union, contrairement à `hyphens: auto`. Le correctif Story 1.4
   (mot long ne devant jamais élargir le viewport) reste donc garanti,
   seule la préférence esthétique (pas de "-" visible dans un titre)
   change. */
h1,
h2,
h3,
h4 {
  hyphens: none;
}

/* Chaîne de repli (correctif revue #4) : chaque titre garde son PROPRE
   token de famille (--font-display-lg-family / -headline-md- / -headline-sm-
   / -counter-figure-, AD-3 — jamais un token substitué par un autre), mais
   la chaîne de repli après ce token est désormais alignée sur celle de
   `body` (system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif)
   plutôt qu'un `sans-serif` nu, pour une dégradation cohérente si Google
   Fonts ne charge pas. */
/* Retour utilisateur (revue Sally, party mode) : h2 partageait le token
   `display-lg` avec h1 (hero), rendant les deux à 48px -- le titre du
   rapport n'avait alors aucune prééminence sur un titre de chapitre.
   Passe à `headline-lg` (36px, nouveau palier intermédiaire, cf.
   tokens.css) -- h1/hero reste inchangé sur `display-lg`. */
h2 {
  font-family: var(--font-headline-lg-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-headline-lg-size);
  font-weight: var(--font-headline-lg-weight);
  line-height: var(--font-headline-lg-line-height);
  letter-spacing: var(--font-headline-lg-letter-spacing);
  margin-top: 0;
}

/* Story 1.4 (FR11) : palier mobile pour h2, nécessaire pour que les
   titres de section les plus longs ("L'accompagnement sur les
   certifications et les compétences") ne se réduisent pas à des
   coupures de mot en cascade dans .section-band (confirmé en
   vérification navigateur à 360px, cf. Design Notes). overflow-wrap sur
   body reste la garantie anti-débordement même avec ce token mobile.
   Retour utilisateur (revue Sally, party mode) : réutilise
   `--font-headline-md` (28px, déjà existant) plutôt que
   `display-lg-mobile` (32px) -- h2 est passé sur `headline-lg` en
   desktop (cf. règle h2 ci-dessus), garder le même token mobile que
   display-lg (partagé avec h1) aurait juste déplacé la confusion sur
   mobile plutôt que de la résoudre. 28px reste plus petit que les 32px
   d'avant, donc au moins aussi sûr vis-à-vis du risque de débordement
   d'origine. */
@media (max-width: 767px) {
  h2 {
    font-size: var(--font-headline-md-size);
    line-height: var(--font-headline-md-line-height);
    letter-spacing: var(--font-headline-md-letter-spacing);
  }
}

/* Retour utilisateur (capture d'écran de référence) : trait vertical épais
   dans la couleur d'accent verte (--color-tertiary) devant les titres de
   groupe -- appliqué au niveau h3 uniquement (jamais h4/.volet__heading,
   qui reste le titre affiché DANS une carte de volet à côté de son propre
   picto -- h3 n'est de toute façon jamais utilisé à l'intérieur d'un volet
   dans ce gabarit, donc pas de garde supplémentaire nécessaire pour
   respecter "sauf sur les cartes qui ont déjà leur picto"). Couvre les 5
   usages de h3 du site (titre de chiffresCles(), etudesTitre,
   veilleProspective.titre, etudesThematiquesTitre, encadre.titre) de la
   même façon, sans logique par section. Vert réservé habituellement à la
   nav/au focus clavier (DESIGN.md : "garder le vert rare et intentionnel")
   -- ici volontairement étendu par l'utilisateur à un niveau de titre
   précis et cohérent, pas une utilisation généralisée. */
h3 {
  position: relative;
  padding-left: 1.25rem;
  font-family: var(--font-headline-md-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-headline-md-size);
  font-weight: var(--font-headline-md-weight);
  line-height: var(--font-headline-md-line-height);
}

h3::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.125em;
  bottom: 0.125em;
  width: 6px;
  border-radius: var(--rounded-full);
  background-color: var(--color-tertiary);
}

h4 {
  font-family: var(--font-headline-sm-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-headline-sm-size);
  font-weight: var(--font-headline-sm-weight);
  line-height: var(--font-headline-sm-line-height);
  margin-bottom: 0.25rem;
}

/* Chapô de section (Story 1.3, correctif revue #1) : token body-lg —
   défini depuis la création de theme.js/tokens.css mais jusqu'ici jamais
   référencé par une règle CSS. */
.sous-titre {
  font-family: var(--font-body-lg-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-body-lg-size);
  font-weight: var(--font-body-lg-weight);
  line-height: var(--font-body-lg-line-height);
}

p,
ul,
ol {
  margin: 0 0 1rem;
}

ul,
ol {
  padding-left: 1.25rem;
}

/* Retour utilisateur : puce personnalisée pour les listes à puces du
   contenu -- asset fourni directement par l'utilisateur (docs/source/
   puceobs.png, copié tel quel dans images/puce.png, 43×43px carré) plutôt
   que le disque par défaut du navigateur. Correctif : la 1re version de
   cette règle utilisait une forme recadrée depuis le logo -- l'utilisateur
   a précisé que ça n'avait aucun rapport avec la puce demandée et a fourni
   le fichier exact à utiliser. Scopé par exclusion des deux seules autres
   listes du site, qui ont chacune déjà leur propre traitement `list-style:
   none` pour une raison différente (grille de chiffres clés, menu de
   navigation horizontal) -- une liste à puces "nue" (`<ul>` sans classe,
   seule sortie possible de `bloc.liste` dans renderBloc()) reste donc le
   cas par défaut qui reçoit la puce. */
ul:not(.chiffres-cles-liste):not(.site-nav__list) {
  list-style: none;
  padding-left: 1.5rem;
}

/* Retour utilisateur : espace entre deux puces pour aérer une liste
   dense -- aucune marge n'existait entre les `li` jusqu'ici (le seul
   espacement venait du line-height du texte lui-même). Neutralisée sur le
   dernier élément (même idiome que `.encadre > :last-child` plus bas)
   pour ne pas ajouter d'espace en trop avant ce qui suit la liste. */
ul:not(.chiffres-cles-liste):not(.site-nav__list) li {
  position: relative;
  margin-bottom: 0.5rem;
}

ul:not(.chiffres-cles-liste):not(.site-nav__list) li:last-child {
  margin-bottom: 0;
}

ul:not(.chiffres-cles-liste):not(.site-nav__list) li::before {
  content: "";
  position: absolute;
  left: -1.5rem;
  top: 0.3em;
  width: 1rem;
  height: 1rem;
  background-image: url("../images/puce.png");
  background-size: contain;
  background-repeat: no-repeat;
}

/* Retour utilisateur : hiérarchisation des listes -- une sous-liste
   (`item.sousListe`, _includes/section.njk) est un `<ul>` imbriqué DANS un
   `<li>` du dessus, sans classe propre -- hérite donc déjà de tout ce qui
   précède (puce, padding-left, espacement entre `li`), ce qui l'indente
   mécaniquement une 2e fois par rapport au niveau parent (l'effet
   recherché : le lecteur voit tout de suite qu'un sous-point appartient au
   point juste au-dessus, pas à la liste entière). Seul ajout : un peu
   d'air entre le texte du point parent et le début de sa sous-liste. */
ul:not(.chiffres-cles-liste):not(.site-nav__list) li > ul {
  margin-top: 0.5rem;
}

/* ---------------------------------------------------------------------
 * Volets (Story 2.1) : études interbranches, veilles prospectives et
 * branches (études de branches / certifications) partagent le même
 * gabarit de carte -- picto + titre + résumé toujours visibles dans un
 * vrai <button>, contenu complet dans un panneau qui s'agrandit/se réduit
 * (`grid-template-rows` animé, `inert` quand fermé -- cf. règles
 * `.volet__panel` plus bas). Structure statique posée par la Story 2.1 ;
 * ombre au survol et anneau de focus (`--volet-hover-shadow`/`--volet-focus-ring`
 * de css/tokens.css) ainsi que le comportement de clic/clavier sont actifs
 * depuis la Story 2.2.
 * --------------------------------------------------------------------- */

/* Correctif revue (story 2.1) : `.branches` déclarait `display: block`
   APRÈS `.volets` ci-dessous, alors que le conteneur de branches porte les
   deux classes (`class="branches volets"`, même spécificité) -- l'ordre de
   déclaration faisait gagner `.branches`, donc les grilles de volets de
   branches (etudes-de-branches, certifications) n'obtenaient jamais le
   layout flex prévu, contrairement aux grilles d'études. `.branches` ne
   porte plus aucune déclaration `display` propre : `.volets` (ci-dessous)
   est désormais la seule source de vérité de layout pour toutes les
   grilles de volets, branches comme études/veilles. */
.volets {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-gutter);
  margin-block: var(--spacing-gutter) 0;
}

@media (min-width: 768px) {
  .volets {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Rythme alterné (spec "Grille 2 colonnes alternée + remplissage
   décoratif") : un item sur trois s'étend sur les deux colonnes pour
   casser la régularité de la grille. Piloté uniquement en CSS via
   `:nth-child` sur les enfants directs de `.volets` -- aucun markup
   supplémentaire dans _includes/section.njk. Sans effet visible sous
   768px (une grille à une colonne est déjà pleine largeur), donc pas
   besoin de le neutraliser dans une media query mobile séparée. */
.volets > *:nth-child(3n) {
  grid-column: 1 / -1;
}

/* Retour utilisateur : "Les études interbranches" (4 volets, `.volets--
   etudes`, _includes/section.njk) doit rester une grille 2x2 régulière
   (2 par colonne) plutôt que suivre le rythme alterné ci-dessus -- avec
   exactement 4 items, le rythme alterné ferait pointer le 3e item en
   pleine largeur, cassant la répartition 2/2 voulue. Sélecteur composé
   (`.volets.volets--etudes`, pas juste `.volets--etudes`) pour garantir
   la priorité sur la règle générique ci-dessus quel que soit l'ordre des
   règles dans ce fichier -- même précaution que `.encadre--image` /
   `.section-columns__chiffres .chiffres-cles-liste` ailleurs (bug de
   cascade déjà rencontré 2 fois sur ce projet avec un simple sélecteur à
   une classe). */
.volets.volets--etudes > *:nth-child(3n) {
  grid-column: auto;
}

/* Correctif retour utilisateur (3e itération) : même en isolant la zone
   décorative avec sa propre marge/son propre rayon, elle restait DANS le
   même contour que `.volet` (une seule bordure autour des deux), donc
   toujours perçue comme faisant partie du bloc "carte" plutôt que comme un
   espace distinct en dessous. `.volet` devient une simple coquille de mise
   en page (flex column, aucune bordure/fond propre) ; le bloc visuellement
   "carte" (bordure, rayon, fond, contenu cliquable) est maintenant
   `.volet__card`, un vrai enfant DOM séparé (voir _includes/section.njk)
   qui n'entoure que le bouton + le panneau -- jamais la zone décorative. */
/* Correctif retour utilisateur (Firefox, 4e tentative) : trois essais
   CSS-only ont échoué (marge sur le `::after` flex-grow ; marge déplacée
   sur `.volet__card` ; marge remplacée par un `border-top` transparent) --
   l'utilisateur confirme le décalage toujours présent après chacun.
   Abandon complet du calcul de hauteur par `flex-grow` : plutôt que de
   continuer à deviner quelle combinaison de propriétés flexbox les
   moteurs interprètent de façon cohérente, la hauteur de la zone
   décorative est maintenant calculée en JS (`syncPhotoZoneHeight()`,
   js/volets.js) via `getBoundingClientRect()` -- une API de mesure, pas un
   algorithme de mise en page, donc sans ambiguïté inter-navigateurs -- et
   posee comme valeur fixe (`--photo-zone-height`). `.volet` redevient un
   simple bloc (plus de `display:flex`) : plus besoin de conteneur flex du
   tout puisque la hauteur n'est plus distribuée automatiquement, ses deux
   enfants (`.volet__card`, `::after`) s'empilent en flux normal.

   Retour utilisateur (révision de l'effet d'apparition) : pas de
   transition CSS propre sur la zone photo (essayée puis rejetée) --
   l'effet voulu est que la zone soit "toujours là, en fixe", révélée
   seulement par la place qui devient disponible à l'ouverture, jamais par
   une animation qui lui est propre. `overflow: hidden` ici accomplit
   exactement ça sans rien animer explicitement : `.volet` est étiré par
   la grille (`.volets`, align-items: stretch) à la hauteur de sa rangée,
   qui grandit déjà en douceur (le navigateur recalcule la mise en page de
   la grille à chaque frame pendant que `.volet__panel` transitionne
   `grid-template-rows` dans le volet ouvert voisin) -- tant que ce clip
   masque le surplus, la zone photo (hauteur posée directement en px final
   par `syncPhotoZoneHeight()`, sans étape intermédiaire) se révèle au même
   rythme que la rangée grandit, comme si elle avait toujours été là. */
.volet {
  overflow: hidden;
}

/* Retour utilisateur : fond blanc plutôt que le neutre chaud
   (--color-surface-container-low) utilisé par défaut ailleurs sur la
   page. */
.volet__card {
  border: var(--volet-border);
  border-radius: var(--volet-radius);
  background: white;
  overflow: hidden;
}

/* Retour utilisateur ("micro-interactions... au hover des cartes" puis
   "l'interaction ne se voit pas") : ombre au survol -- DÉJÀ prévue par
   DESIGN.md > Components > "Volet (accordéon)... Survol (desktop) : ombre
   douce" et son token `--volet-hover-shadow` (tokens.css), les deux posés
   dès le début du projet mais jamais câblés jusqu'ici.
   Cause du "ne se voit pas" (bug trouvé, pas juste une histoire
   d'intensité) : `.volet` -- l'article qui englobe CETTE carte -- porte
   son propre `overflow: hidden`, posé pour un besoin totalement différent
   (révéler la zone photo d'un voisin de rangée sans l'animer explicitement,
   cf. commentaire de `.volet` plus haut dans ce fichier). N'importe quelle
   ombre EXTÉRIEURE posée sur `.volet__card` se faisait entièrement
   rogner par ce clip, quelle que soit son intensité -- constaté en
   vérification indépendante après plusieurs essais de réglage d'opacité
   qui n'auraient jamais pu marcher. `--volet-hover-shadow` est donc passé
   en `inset` (cf. son commentaire, tokens.css) : rendue À L'INTÉRIEUR du
   cadre de la carte, elle n'a jamais besoin de déborder -- immunisée
   contre ce clip par construction. Même raison pour ne PAS ajouter de
   soulèvement (`transform: translateY`) en complément, piste explorée puis
   abandonnée : un déplacement, comme une ombre extérieure, se fait
   rogner par le même `overflow: hidden` dès qu'il sort du cadre de
   `.volet`. Posée sur `.volet__card` (la carte entière, cf. DESIGN.md)
   plutôt que sur `.volet__toggle` (le bouton d'en-tête seul) -- l'anneau
   doit entourer toute la carte, pas seulement sa zone cliquable du haut.
   Pas d'équivalent tactile (EXPERIENCE.md > State Patterns, "Volet —
   survol (desktop)... Pas d'équivalent tactile") : `:hover` seul, jamais
   simulé au tap. */
@media (prefers-reduced-motion: no-preference) {
  .volet__card {
    transition: box-shadow 0.2s ease;
  }
}

/* Retour utilisateur ("la bordure de couleur au hover devrait être de la
   couleur de la partie") : couleur posée ICI, pas dans le token
   `--volet-hover-shadow-inset` (tokens.css -- ne porte que la FORME,
   jamais la couleur, cf. son commentaire pour le pourquoi : une custom
   property définie sur `:root` fige son `var()` interne au moment de SA
   PROPRE résolution, elle ne peut pas "s'adapter" à la section qui
   l'utilise plus bas). En le posant directement ici, `--section-chiffre-
   color` se résout correctement AU NIVEAU DE `.volet__card` -- qui hérite
   bien la couleur de sa section ancêtre (même custom property que
   `.chiffre-cle__valeur`, cf. son commentaire) -- vérifié en navigateur
   (avec `void el.offsetHeight` pour forcer un recalcul de style avant
   lecture : `getComputedStyle` peut renvoyer une valeur non rafraîchie
   juste après l'insertion d'une règle, ce qui avait d'abord fait
   suspecter à tort un vrai bug de résolution `var()`). */
.volet__card:hover {
  box-shadow: var(--volet-hover-shadow-inset) var(--section-chiffre-color, var(--color-primary));
}

/* Zone décorative : bloc à part entière APRÈS `.volet__card` (jamais à
   l'intérieur), séparé par un espace visible (`margin-top`, fiable
   maintenant que la hauteur elle-même n'est plus calculée par flex-grow)
   -- se lit sans ambiguïté comme un emplacement photo distinct de la
   carte, pas comme un fond qui en ferait partie. `height: var(--photo-
   zone-height, 0px)` : 0 par défaut (invisible, rien à combler) ; posée en
   pixels exacts par `syncPhotoZoneHeight()` (js/volets.js) quand un voisin
   de rangée est ouvert -- jamais par flex-grow.

   Correctif retour utilisateur (important) : CSS seul ne peut pas
   distinguer "la rangée est plus haute parce qu'un voisin est ouvert" de
   "la rangée est plus haute à cause d'un simple écart de contenu entre
   deux volets fermés" (ex. un titre de branche sur 2 lignes plutôt qu'une)
   -- le stretch de grille remplit l'espace dans les deux cas, sans savoir
   pourquoi. Un essai précédent (réduire `margin-top`) rendait ces petits
   écarts NATURELS visibles, alors que l'utilisateur a clarifié l'inverse :
   deux volets fermés ne doivent JAMAIS afficher de zone photo, même si
   l'un est plus petit que l'autre. D'où la séparation en deux règles : ce
   `::after` de base reste un bloc neutre (aucun fond, aucune icône) qui
   participe seulement à la mise en page (occupe l'espace, invisible) ;
   seule la classe `.volet--photo-zone` (posée par `updatePhotoZones()`
   dans js/volets.js, uniquement quand un volet de la même rangée est
   réellement ouvert) active le fond pastel + l'icône ci-dessous. Sans JS,
   aucun volet ne s'ouvre (comportement déjà dégradé, Story 2.2) donc cette
   classe n'est jamais posée -- dégradation cohérente, pas de zone visible
   à tort. */
.volet::after {
  content: "";
  display: block;
  height: var(--photo-zone-height, 0px);
  pointer-events: none;
}

/* Retour utilisateur (2026-08-17, transition "bizarre" à la fermeture d'un
   volet long) : la zone décorative du voisin fermé était volontairement
   SANS transition propre (cf. commentaire plus haut, "toujours là, en
   fixe") -- révélée uniquement par le stretch de grille (`.volets`,
   align-items: stretch) qui grandit/rétrécit en même temps que la ligne de
   grille du volet ouvert voisin transitionne `grid-template-rows`.
   Mesuré en direct (traçage image par image) : ce stretch ne suit PAS le
   volet ouvert en continu -- la hauteur de LIGNE de grille (dimensionnement
   intrinsèque "auto") reste figée à la valeur haute pendant presque toute
   la transition, puis "saute" d'un coup à la toute fin (quand `--photo-
   zone-height` est enfin recalculée après la fermeture, cf. js/volets.js).
   Résultat : un grand vide blanc s'ouvre sous le volet qui se referme
   pendant ~200-450ms, comblé seulement au dernier instant -- exactement le
   "saut" signalé. Cause racine : SEUL le panneau ouvert a sa propre
   transition CSS ; la zone décorative du voisin, elle, change de valeur
   d'un bloc (immédiatement à l'ouverture pour la prédire à l'avance,
   différée à la fermeture jusqu'à `transitionend`) -- jamais en continu.
   Corrigé en donnant à `--photo-zone-height` sa PROPRE transition ici,
   avec une durée synchronisée sur celle du panneau voisin (`--photo-zone-
   transition-duration`, posée par `js/volets.js` au moment du bascule) --
   les deux hauteurs (panneau + zone décorative) glissent maintenant en
   parallèle sur la même durée, qu'elles soient dans le même conteneur grid
   ou non, sans dépendre du recalcul de ligne de grille intrinsèque. */
@media (prefers-reduced-motion: no-preference) {
  .volet::after {
    transition: height var(--photo-zone-transition-duration, 0.2s) ease;
  }
}

/* Teinte pastel (`color-mix`, 20% de la couleur d'accent/rôle) +
   pictogramme "image" centré (SVG, pas une vraie photo -- Never de la
   spec) pour signaler sans ambiguïté "photo à venir ici" -- seulement
   quand `.volet--photo-zone` est posée (voir commentaire ci-dessus).

   Correctif retour utilisateur : un `max-height` avait été ajouté pour
   plafonner les cas où l'écart de hauteur devient très grand, puis retiré
   à la demande explicite de l'utilisateur -- pas de limite pour l'instant,
   le calibrage définitif se fera une fois de vraies illustrations
   disponibles. */
.volet--photo-zone::after {
  margin-top: calc(var(--spacing-unit) * 2);
  border-radius: var(--rounded);
  background-color: color-mix(in srgb, var(--section-accent, var(--color-primary)) 20%, white);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(8,67,111,0.4)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='M21 15l-5-5L5 21'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 32px 32px;
}

.section--secondary .volet--photo-zone::after {
  background-color: color-mix(in srgb, var(--section-accent, var(--color-secondary)) 20%, white);
}

/* Retour utilisateur : pour "Les seniors"/"Managers de demain", la zone
   décorative du voisin fermé affiche la vraie photo (`senior.png`/
   `manager.png`, `--photo-zone-image` posée par `syncPhotoZoneImage()`,
   js/volets.js) plutôt que l'icône générique ci-dessus -- `background-color`
   neutralisée (teinte pastel réservée au repli SANS photo) : une vraie
   image occupe toute la zone, garder la couleur dessous ne sert à rien et
   déborderait visiblement si l'image met un instant à charger.
   Sélecteur composé avec `.volet` en plus de `.volet--photo-zone.volet--
   photo-zone--image` (3 classes, pas 2) pour garantir la priorité sur LES
   DEUX règles ci-dessus (`.volet--photo-zone::after` ET `.section--
   secondary .volet--photo-zone::after`, cette dernière à spécificité égale
   à 2 classes) quel que soit l'ordre des règles ou la couleur de section --
   même précaution que `.encadre--image`/`.volets.volets--etudes` ailleurs
   (bug de cascade déjà rencontré sur ce projet avec un sélecteur à
   spécificité insuffisante). */
.volet.volet--photo-zone.volet--photo-zone--image::after {
  background-color: transparent;
  background-image: var(--photo-zone-image);
  /* `contain` + plafond de hauteur (`--photo-zone-max-height`, posée par
     js/volets.js) : image vue en entier à pleine largeur, jamais rognée
     (retour utilisateur, images "plus au bon ratio"). */
  background-size: contain;
  background-position: top center;
  max-height: var(--photo-zone-max-height, none);
  /* Retour utilisateur ("agrandis un peu l'image de la photographie") :
     `--photo-zone-height` (js/volets.js) reflète l'écart entre le voisin
     ouvert et la carte fermée -- quand ce voisin ouvert est court (peu de
     texte), l'écart l'est aussi, et `contain` rétrécit alors l'image bien
     en dessous de sa largeur disponible (constaté : ~254px de large sur
     une zone de 508px). Plancher qui laisse l'image respirer un peu plus
     sans pour autant réintroduire un vide déconnecté du contenu voisin
     (cf. commentaire de `.volet::after` plus haut) -- pas d'effet sur les
     zones déjà plus hautes (seniors/managers, ~900px). */
  min-height: 230px;
}

/* Retour utilisateur : demande de voir les parties d'image cachées hors du
   bloc (débordant sous les volets voisins/hors des colonnes) SANS survol
   ni redimensionnement -- essai en ce sens abandonné, revenu à l'état
   ci-dessus (`cover` fixe, recadré). Non implémenté car en conflit direct
   avec le mécanisme d'apparition en douceur de cette même zone : `.volet`
   porte `overflow: hidden` (plus haut, voir son commentaire) précisément
   pour clipper `::after` -- sa hauteur finale est posée directement en px
   par `syncPhotoZoneHeight()` (js/volets.js) et c'est CE clip qui la
   révèle progressivement, au même rythme que la ligne de grille grandit
   pendant la transition du panneau voisin. Retirer/contourner cet
   `overflow: hidden` pour laisser l'image déborder casserait cette
   apparition (déjà réglée en plusieurs passes, cf. commentaires "Firefox,
   4e tentative" ci-dessus) pour TOUTES les zones décoratives, pas
   seulement celles avec une vraie photo -- régression jugée disproportionnée
   par rapport à l'effet demandé. */

.volet__heading {
  /* Neutralise la marge/typo par défaut de h4 : le titre visuel réel est
     porté par .volet__titre à l'intérieur du bouton -- ce h4 n'existe que
     pour garder le volet repérable dans la hiérarchie de titres du
     document (navigation par titres des lecteurs d'écran), cf. pattern
     ARIA disclosure recommandé (heading > button). Niveau h4 (correctif
     revue) : h2 section > h3 groupe ("Les études interbranches" etc.,
     déjà en h3 plus haut dans ce fichier) > h4 volet -- un h3 ici en
     ferait un frère du titre de groupe plutôt qu'un enfant. */
  margin: 0;
  font: inherit;
}

.volet__toggle {
  display: flex;
  /* Correctif retour utilisateur : le picto s'alignait sur le centre de
     tout le bouton (titre + résumé), ce qui le faisait paraître décalé
     par rapport au nom de la branche/étude. Aligné en haut, contre la
     première ligne du titre. */
  align-items: flex-start;
  gap: 1rem;
  width: 100%;
  min-height: 44px;
  padding: var(--volet-padding);
  border: 0;
  border-radius: var(--volet-radius);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  /* Retour utilisateur (revue Sally, party mode) : un <button> ne reçoit
     pas `cursor: pointer` par défaut (contrairement à un <a>) -- jamais
     posé jusqu'ici, alors qu'un commentaire plus bas prétendait déjà s'y
     appuyer comme signal d'interactivité au survol (erreur constatée,
     corrigée dans ce commentaire aussi). */
  cursor: pointer;
  /* Cible tactile ≥44×44px (EXPERIENCE.md > Accessibility Floor). */
  /* Deep-link de volet (Story 2.3) : évite que le scroll (natif ou
     programmatique) vers un bouton de volet ne le cache sous la nav fixe,
     même règle que `.section` ci-dessous. */
  scroll-margin-top: var(--nav-height);
}

/* Correctif retour utilisateur : l'ombre au survol (Story 2.2) créait un
   "cadre" trop marqué, jugé superflu -- retirée. Le picto/chevron et
   l'anneau de focus clavier ci-dessous restent des signaux
   d'interactivité suffisants sans ombre portée. (Correctif ultérieur,
   revue Sally, party mode : ce commentaire citait aussi "le curseur
   pointeur" comme signal déjà en place -- il ne l'était pas, `cursor:
   pointer` n'existait nulle part sur `.volet__toggle` avant d'être ajouté
   juste au-dessus.) */

/* Retour utilisateur (WCAG 1.4.11) : `--volet-focus-ring` (vert) échoue le
   contraste 3:1 requis sur cette carte blanche (1,86:1 mesuré) --
   `--focus-ring-on-light` (bleu primary, 10,28:1) porte le contraste ;
   le halo vert (`box-shadow`) garde la signature de couleur sans la
   perdre. Cf. tokens.css pour la mesure complète et pourquoi les autres
   usages de `--volet-focus-ring` (nav, back-to-top -- fond bleu foncé)
   restent inchangés. */
.volet__toggle:focus-visible {
  outline: var(--focus-ring-on-light);
  outline-offset: -2px;
  box-shadow: var(--focus-ring-on-light-glow);
}

/* Accent de badge picto (spec "Couleurs d'accent — gamme secondaire de la
   charte") : `--section-accent` est posée en style inline sur le conteneur
   de section par _includes/section.njk uniquement quand `data.accent` est
   défini (indirection propre vers une custom property globale --accent-*,
   jamais un hex codé en dur dans le HTML). `var(--section-accent, ...)`
   retombe sur la couleur de rôle habituelle quand la variable n'existe pas
   -- comportement strictement inchangé pour toute section sans accent.
   Seul `.volet__picto` est concerné : le bandeau de section, la bordure de
   carte et la couleur de nav active restent exclusivement pilotés par
   primary/secondary (FR-10), jamais par cette variable. */
/* Correctif retour utilisateur : agrandi de moitié (40px -> 60px) pour
   mieux porter le picto de branche/étude, désormais plus lisible dans la
   grille à 2 colonnes. */
/* Retour utilisateur : format carré (léger arrondi) au lieu du cercle --
   un cercle rognait les pictos rectangulaires/carrés fournis (coins de
   l'image perdus sous le masque circulaire, cf. `object-fit: cover` sur
   .volet__picto-img). */
.volet__picto {
  flex-shrink: 0;
  width: 60px;
  height: 60px;
  border-radius: var(--rounded-sm);
  /* Aucun fond (retour utilisateur) : un picto absent/en cours de chargement
     laissait un carré coloré sans raison d'être. */
  overflow: hidden;
}

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

/* Retour utilisateur ("animation sur les pictos de branche, genre une
   rotation comme une pièce sur sa tranche") : rotation 3D (rotateY) du
   picto ENTIER (`.volet__picto`, fond + image ensemble -- pas seulement
   l'image, sinon seule l'image tournerait dans un cadre fixe, moins
   lisible comme "pièce qui tourne"), déclenchée au survol/focus clavier
   du volet.
   PAS une boucle infinie (EXPERIENCE.md > Interaction Primitives >
   "Banni") : un seul aller au survol/focus (`transition`, pas
   `animation` -- rien ne rejoue tant que la souris ne ressort pas puis ne
   revient pas), jamais de répétition automatique. Parité clavier
   (`:focus-visible`, même volet déjà focusable) : cohérent avec le reste
   du site, qui ne réserve jamais une micro-interaction au seul hover
   souris (cf. `.volet__toggle:focus-visible` déjà existant plus haut).

   Correctif retour utilisateur ("certains pictos s'étirent lors de leur
   rotation, principalement les cartes pleine largeur") : `perspective`
   posée sur `.volet__toggle` (essai précédent) -- ce bouton fait TOUTE la
   largeur de la carte, y compris pour les volets "pleine largeur" (1 sur 3,
   cf. `branche()`/`pleineLargeur` dans _includes/section.njk), bien plus
   large qu'une carte de grille normale. `perspective-origin` par défaut
   (50% 50%, centre de l'élément qui la porte) se retrouvait donc TRÈS loin
   du picto (toujours à l'extrême gauche du bouton) sur ces cartes larges --
   la rotation se voyait alors depuis un angle très oblique, écrasant/
   étirant le picto au lieu de le faire tourner proprement (bug constaté en
   vérification navigateur, "Négoce de l'ameublement" notamment). La
   fonction `perspective()` DANS le `transform` du picto lui-même (plutôt
   que la propriété `perspective` sur un ancêtre) ancre le point de fuite
   au picto (60×60px) -- toujours centré sur lui-même, quelle que soit la
   largeur du bouton parent. */
@media (prefers-reduced-motion: no-preference) {
  .volet__picto {
    transform-style: preserve-3d;
  }

  /* Retour utilisateur ("l'animation ne doit se faire qu'au hover, pas
     quand je quitte la carte") : `transition` posée UNIQUEMENT sur l'état
     survolé/focus, plus sur `.volet__picto` de base -- à la sortie du
     survol, l'élément retombe sur la règle de base qui n'a plus de
     `transition` du tout : le retour à 0deg est instantané (aucune
     propriété transition héritée à ce moment-là), au lieu de rejouer la
     rotation en sens inverse sur 0.6s. */
  .volet__toggle:hover .volet__picto,
  .volet__toggle:focus-visible .volet__picto {
    transition: transform 0.6s ease;
    transform: perspective(400px) rotateY(360deg);
  }
}

.volet__intro {
  flex: 1;
  min-width: 0;
}

.volet__titre {
  display: block;
  /* Retour utilisateur (2e révision de cette règle) : --font-headline-md
     (28px, même taille que h3) faisait paraître le titre de volet (h4, cf.
     .volet__heading dans section.njk) au même niveau visuel que les
     titres h3 qui les regroupent (ex. "La veille prospective") alors qu'il
     leur est structurellement subordonné -- l'écart de taille doit suivre
     l'écart de niveau de titre (h3 > h4), pas l'inverse. Retour à
     --font-headline-sm (22px, la taille "normale" d'un h4 sur ce site).
     Ceci recrée le problème que la révision précédente de cette règle
     visait à éviter (collision avec le <h4> `sousTitre` à l'intérieur
     d'un panneau ouvert, aussi 22px) -- réglé cette fois du côté du
     sousTitre interne plutôt qu'en regonflant ce titre-ci (cf.
     `.volet__panel-inner h4` plus bas) : le titre du volet garde la
     priorité visuelle, le sousTitre interne est maintenant plus petit que
     lui, jamais égal. */
  font-family: var(--font-headline-sm-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-headline-sm-size);
  font-weight: var(--font-headline-sm-weight);
  line-height: var(--font-headline-sm-line-height);
  color: var(--color-on-surface);
}

.volet__resume {
  display: -webkit-box;
  margin-top: 0.25rem;
  font-family: var(--font-body-md-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-body-md-size);
  line-height: var(--font-body-md-line-height);
  color: var(--color-on-surface);
  /* Troncature purement visuelle (Design Notes de la story) : le texte
     complet reste dans .volet__resume lui-même ET dans .volet__panel --
     rien n'est raccourci côté données. */
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Correctif retour utilisateur : le résumé (premier paragraphe) et le
   panneau ouvert affichent le même texte -- masquer le résumé une fois le
   volet ouvert évite la répétition visuelle. Purement visuel (`hidden`
   n'existe qu'en CSS, jamais posé dans le DOM) : le texte du résumé reste
   présent et lisible sans JS/reduced-motion, seul son affichage change
   avec l'état déjà porté par `aria-expanded` (AD-5, aucune nouvelle
   logique JS). */
.volet__toggle[aria-expanded="true"] .volet__resume {
  display: none;
}

/* Correctif retour utilisateur : le chevron (1rem, --color-tertiary) était
   trop petit et trop peu contrasté pour se lire comme une affordance
   "dépliable" -- agrandi et recoloré dans la couleur de rôle/accent de la
   section (même mécanisme que .volet__picto). 2e correctif : encore un peu
   plus gros (1.75rem -> 2rem) et remis en `align-self: flex-start` (plutôt
   que `center`) pour s'aligner sur le haut du bloc, comme le picto et la
   première ligne du titre (`.volet__toggle` est déjà en `align-items:
   flex-start`) -- un chevron centré verticalement dérivait vers le bas sur
   les volets à résumé multi-lignes. */
.volet__chevron {
  flex-shrink: 0;
  align-self: flex-start;
  font-size: 2rem;
  line-height: 1;
  color: var(--section-accent, var(--color-primary));
}

.section--secondary .volet__chevron {
  color: var(--section-accent, var(--color-secondary));
}

/* Rotation du chevron branchée sur l'attribut DOM `aria-expanded` du bouton
   (AD-5) -- jamais une classe `.open` séparée : l'attribut reste la seule
   source de vérité d'état (Story 2.2). */
.volet__toggle[aria-expanded="true"] .volet__chevron {
  transform: rotate(180deg);
}

/* Transition courte (≤600ms, AD-6) appliquée uniquement quand le visiteur
   n'a pas demandé de mouvement réduit -- même motif que `html { scroll-behavior:
   smooth }` plus bas dans ce fichier : sans préférence exprimée, on anime ;
   avec `prefers-reduced-motion: reduce`, la bascule reste instantanée. Le
   survol (`box-shadow`) partage la même transition courte que le chevron,
   pour une finition cohérente entre les deux micro-interactions du volet --
   et la même neutralisation sous reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
  .volet__chevron {
    transition: transform 0.2s ease;
  }

  .volet__toggle {
    transition: box-shadow 0.2s ease;
  }
}

/* Story bugfix "le panneau de volet s'agrandit au lieu d'apparaître d'un
   coup" : le panneau est un conteneur grid à une ligne dont la hauteur
   ("auto") est animée en transitionnant `grid-template-rows` de `0fr` à
   `1fr` (le navigateur interpole la fraction) -- fermé par défaut (pas de
   classe `.is-open`), sans connaître la hauteur finale à l'avance
   (contrairement à `max-height`). L'enfant direct `.volet__panel-inner`
   porte `overflow: hidden` pour clipper le contenu pendant la transition
   -- le clipping doit porter sur l'enfant, jamais sur le conteneur grid
   lui-même (une ligne de grille à `overflow: hidden` empêcherait
   l'animation). Padding déplacé ici depuis `.volet__panel` pour ne pas
   fausser le calcul de hauteur du conteneur grid. */
.volet__panel {
  display: grid;
  grid-template-rows: 0fr;
}

.volet__panel.is-open {
  grid-template-rows: 1fr;
}

@media (prefers-reduced-motion: no-preference) {
  .volet__panel {
    transition: grid-template-rows 0.2s ease;
  }
}

/* Retour utilisateur ("dans les cartes veille prospective, aligne les
   blocs de texte avec le titre, pas avec le picto") : le contenu du
   panneau ouvert héritait du même padding-inline que `.volet__toggle`
   (`--volet-padding`, 24px) -- correct pour LE BOUTON, dont le picto (60px,
   cf. `.volet__picto`) occupe justement ce bord gauche ; mais le TITRE
   (`.volet__titre`), lui, ne commence qu'après le picto + le `gap` du
   bouton (1rem, cf. `.volet__toggle`) -- soit 76px plus loin. Le texte du
   panneau, aligné sur le padding nu, démarrait donc 76px plus à GAUCHE que
   le titre juste au-dessus de lui (constaté en navigateur, surtout visible
   sur les cartes "veille prospective", empilées en 1 seule colonne où
   l'écart saute aux yeux). Padding-left compense cet écart (24px + 60px +
   1rem) -- le padding-right, lui, reste inchangé (rien à aligner de ce
   côté). */
.volet__panel-inner {
  overflow: hidden;
  padding: 0 var(--volet-padding) 0 calc(var(--volet-padding) + 60px + 1rem);
}

/* Retour utilisateur : le `sousTitre` (bloc `{sousTitre}`, rendu `<h4>` par
   renderBloc()) qui apparaît DANS un panneau ouvert (ex. "Ingénierie des
   compétences et des certifications") héritait de la taille h4 générique
   du site (--font-headline-sm, 22px) -- égale à `.volet__titre` (revenu à
   22px lui aussi, cf. plus haut) une fois celui-ci ramené à sa taille h4
   normale. Rétréci ici pour rester visuellement subordonné au titre du
   volet qui le contient : --font-body-lg (18px) plutôt qu'un nouveau
   token, la famille/le poids/l'interlignage restent ceux du h4 générique
   (seule la taille change). */
.volet__panel-inner h4 {
  font-size: var(--font-body-lg-size);
}

.volet__panel-inner > :first-child {
  margin-top: 0;
}

/* Espacement bas en `margin` (sur le dernier enfant) plutôt qu'en `padding`
   sur `.volet__panel-inner` elle-même : un `padding` porté par l'élément
   `overflow: hidden` est un plancher de hauteur incompressible (rendu
   inconditionnellement, quelle que soit la taille de piste de grille
   allouée) -- fermé, la piste ne collapse jamais complètement à 0 et laisse
   dépasser une ligne de contenu sous la carte. Un `margin` porté par un
   enfant fait partie du contenu que `overflow: hidden` clippe et que la
   "automatic minimum size" (min-height:auto -> 0) ramène bien à 0 fermé. */
.volet__panel-inner > :last-child {
  margin-bottom: var(--volet-padding);
}

/* Retour utilisateur : "j'ai mis l'image [...] mais je ne la vois pas" --
   cf. commentaire de `branche()`, _includes/section.njk. Empilé par défaut
   (texte puis image, image en pleine largeur -- cohérent avec le reste du
   site où une image commence toujours pleine largeur avant de rejoindre
   une 2e colonne à partir du palier tablette). */
.volet__panel-pleine-largeur__photo {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--rounded);
  margin-top: 1rem;
}

/* Tablette (< 1024px) : photo SOUS le texte, pleine largeur (retour
   utilisateur : côte à côte, la colonne de texte devenait trop étroite). */
@media (min-width: 1024px) {
  .volet__panel-pleine-largeur {
    display: flex;
    align-items: flex-start;
    gap: calc(var(--spacing-gutter) * 2);
  }

  .volet__panel-pleine-largeur__texte {
    flex: 1;
    min-width: 0;
  }

  .volet__panel-pleine-largeur__photo {
    /* Retour utilisateur ("agrandis la zone image du volet chaussure pour
       équilibrer le bloc") : un texte court (ex. "Commerce succursaliste
       de la chaussure", un seul paragraphe) laisse un vide sous le texte
       ET sous une image trop petite pour la remplir -- agrandie à
       `--secondary-column-width` (420px, tokens.css), la largeur déjà
       utilisée pour TOUTES les autres colonnes photo du site (repli
       "photo à venir", zones de voisin de rangée) plutôt qu'une valeur
       propre à ce bloc -- plus grande, donc plus proche visuellement de
       la hauteur du texte, et cohérente avec le reste du site. */
    flex: 0 0 var(--secondary-column-width);
    width: var(--secondary-column-width);
    height: auto;
    margin-top: 0;
  }
}

/* Retour utilisateur : padding uniforme sur les 4 côtés (était 1rem haut/
   bas contre 1.25rem gauche/droite) -- une seule règle pour tous les
   encadrés du site (aucune surcharge de padding ailleurs, vérifié), donc
   ce changement s'applique de façon identique partout où un encadré
   apparaît. */
.encadre {
  border: 1px solid var(--color-outline-variant);
  border-radius: var(--rounded);
  padding: 1.25rem;
  margin: 1rem 0;
  background: var(--color-surface-container);
}

/* Retour utilisateur : pour "Les travaux interbranches" (`data.
   introImageEncadre`, _includes/section.njk / _data/rapport.js), le repli
   "photo à venir" (`.image-placeholder`, cf. plus haut) est lui-même
   encadré comme un `.encadre` à part entière plutôt que posé nu --
   variation demandée pour cette section précise. Placée APRÈS `.encadre`
   ci-dessus (pas juste à côté de `.image-placeholder`, plus haut dans ce
   fichier) : à spécificité égale (une seule classe de chaque côté), la
   cascade CSS retient la règle la plus TARDIVE dans la feuille de style --
   posée avant `.encadre`, cette règle se faisait silencieusement écraser
   par son padding/margin (bug constaté en vérification indépendante :
   611px de haut mesurés au lieu des 643px attendus, l'écart correspondant
   exactement à `margin: 1rem 0`). `padding`/`margin` à 0 : contrairement à
   un encadré de texte, le repli photo doit remplir tout l'espace
   disponible dans sa cellule de grille (`.section-intro-row`, plus haut),
   pas laisser une marge autour de lui. */
.encadre--image {
  padding: 0;
  margin: 0;
  overflow: hidden;
}

.encadre--image .image-placeholder {
  min-height: 100%;
  border-radius: 0;
}

/* Retour utilisateur : la marge haute par défaut du navigateur sur <h3>
   (jamais neutralisée pour ce contexte -- même classe de bug que
   `.section-columns__chiffres h3`, 10e passe) s'ajoutait au padding de
   l'encadré et créait un espace visiblement plus grand au-dessus du titre
   qu'ailleurs. Neutralisée ici uniquement (pas de `margin-bottom`, qui
   reste l'espacement voulu entre le titre et le contenu). */
.encadre h3 {
  margin-top: 0;
}

/* Retour utilisateur : même défaut côté bas -- `p, ul, ol` portent tous
   une `margin-bottom: 1rem` (règle globale plus haut dans ce fichier),
   qui s'ajoutait au padding de l'encadré quand le dernier élément du
   contenu était une liste (ou un paragraphe) et créait un espace plus
   grand en bas qu'ailleurs. Neutralisée sur le DERNIER enfant de
   l'encadré, quel que soit son type (ul rencontré ici, mais un paragraphe
   final aurait le même souci) plutôt que juste `ul`, pour éviter de
   retomber sur le même bug avec un autre type de bloc. Même idiome que
   `.volet__panel-inner > :last-child` déjà utilisé ailleurs. */
.encadre > :last-child {
  margin-bottom: 0;
}

/* Retour utilisateur : repli "photo à venir" à côté du texte d'un encadré
   précis (`bloc.encadre.image`, cf. _includes/section.njk -- "D'où
   viennent les données de Perspectives commerce ?" dans `donneesEncadre`,
   portail). `.encadre > :last-child` ci-dessus ne s'applique plus ici
   (`.encadre__row`, pas un `<p>`/`<ul>`, devient le dernier enfant direct
   de `.encadre`) -- répétée sur `.encadre__text` pour garder le même
   résultat visuel (pas de marge basse superflue sous le dernier élément
   du texte). Empilé par défaut (mobile/tablette, ordre DOM texte puis
   image = déjà correct) ; côte à côte à partir de 1024px, largeur fixe
   partagée avec les autres repli photo du site (`--secondary-column-
   width`, tokens.css) et `align-items: stretch` (comportement flex par
   défaut) pour que le repli photo occupe toute la hauteur du texte
   voisin, même patron que `.hero__inner`/`.section-intro-row` plus haut. */
.encadre__text > :last-child {
  margin-bottom: 0;
}

@media (min-width: 1024px) {
  .encadre__row {
    display: flex;
    gap: calc(var(--spacing-gutter) * 2);
  }

  .encadre__text {
    flex: 1;
  }

  .encadre__row .image-placeholder {
    flex: 0 0 var(--secondary-column-width);
    /* Retour utilisateur : image à gauche, texte à droite -- inversion
       purement visuelle (`order`, pas de changement d'ordre DOM) : le
       texte reste le premier enfant lu/atteint au clavier, l'image
       (`aria-hidden="true"`, imageOrPlaceholder()) est de toute façon
       invisible pour un lecteur d'écran. Scopé à ce palier desktop
       uniquement -- empilé sur mobile/tablette, l'ordre DOM (texte puis
       image) reste l'ordre voulu, cf. commentaire plus haut. */
    order: -1;
  }
}

/* Retour utilisateur : phrase d'annonce mise en avant (gras) avant une
   liste de volets -- même famille/taille que le corps de texte environnant
   (aucun `font-size`/`font-family` posé ici), seul le poids change. 600
   (semibold, déjà chargé par la police Source Sans 3, cf. layout.njk)
   plutôt que 700 (réservé aux titres, --font-display-lg-weight) pour rester
   un gras "dans le paragraphe", pas un titre déguisé. */
.bloc-accroche {
  font-weight: 600;
}

.rubrique {
  margin: 1rem 0;
}

/* Retour utilisateur : les titres de rubrique (« Observatoire de
   l'alternance », « Localiser l'offre… », « Des fiches métiers
   enrichies ») n'étaient pas assez mis en valeur comme sous-titres de
   paragraphe -- même traitement que les h3 du site (taille headline-md,
   trait vert devant), tout en restant des h4 dans la hiérarchie. */
.rubrique > h4 {
  position: relative;
  padding-left: 1.25rem;
  margin-top: 2rem;
  margin-bottom: 0.75rem;
  font-family: var(--font-headline-md-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-headline-md-size);
  font-weight: var(--font-headline-md-weight);
  line-height: var(--font-headline-md-line-height);
}

.rubrique > h4::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.125em;
  bottom: 0.125em;
  width: 6px;
  border-radius: var(--rounded-full);
  background-color: var(--color-tertiary);
}

/* Retour utilisateur : mise en page bespoke à 4 rangées pour la section
   "portail" (introduction/image, alternance/formations, encadré pleine
   largeur, texte de clôture/exemples+CTA) -- cf. portailSection() dans
   _includes/section.njk. Empilé par défaut (mobile/tablette, ordre DOM
   déjà correct) ; grille 2 colonnes à partir de 1024px, même palier
   "desktop" que `.section-columns` ci-dessus. `align-items: start` pour
   qu'une colonne plus courte (ex. le texte à côté de l'image+encadré) ne
   s'étire pas inutilement. */
.portail-row {
  margin-block: var(--spacing-gutter);
}

.portail-row__col > :first-child {
  margin-top: 0;
}

@media (min-width: 1024px) {
  .portail-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: calc(var(--spacing-gutter) * 2);
    align-items: start;
  }
}

/* Bandeau image du portail (2e révision, retour utilisateur) : l'encadré
   "objectifs" se superpose désormais à l'image plutôt que de vivre dans
   une colonne à côté du texte -- `background-image` (posée en style
   inline par portailBanner(), _includes/section.njk, uniquement quand
   `imagePortail.src` existe) plutôt qu'un <img>, pour permettre cette
   superposition. Sans `src` : repli pastel (même mécanisme que les autres
   zones photo du site, ex. .volet::after) -- l'encadré reste lisible
   dessus grâce à son propre fond opaque (.encadre), donc rien de cassé en
   attendant une vraie image. Empilement centré par défaut (mobile/
   tablette, bandeau juste assez haut pour contenir l'encadré) ; à partir
   de 1024px, bandeau plus haut et encadré poussé à droite, verticalement
   centré. */
.portail-banner {
  position: relative;
  border-radius: var(--rounded);
  overflow: hidden;
  margin-bottom: var(--spacing-gutter);
  background-color: color-mix(in srgb, var(--color-primary) 12%, white);
  background-size: cover;
  background-position: center;
  padding: 1.5rem;
  display: flex;
  justify-content: center;
}

.portail-banner__encadre {
  width: 100%;
  max-width: var(--secondary-column-width);
}

.portail-banner__encadre .encadre {
  margin: 0;
  box-shadow: 0 8px 24px rgba(8, 67, 111, 0.15);
}

@media (min-width: 1024px) {
  .portail-banner {
    min-height: 420px;
    align-items: center;
    justify-content: flex-end;
    padding: 2.5rem;
  }
}

.portail-cta {
  margin-top: 1.5rem;
}

/* Retour utilisateur : bouton d'appel à l'action plutôt qu'un lien texte
   nu pour "Accédez au portail..." -- vert d'accent (--color-tertiary) +
   texte --color-on-tertiary, la paire de tokens déjà prévue par la charte
   pour un usage plein fond sur ce vert (cf. _data/theme.js). Même logique
   "rare et intentionnel" que le trait devant les h3 (DESIGN.md) : un seul
   bouton de ce type sur toute la page. */
.btn--cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1.5rem;
  border-radius: var(--rounded-full);
  background: var(--color-tertiary);
  color: var(--color-on-tertiary);
  font-weight: 600;
  text-decoration: none;
  /* Retour utilisateur ("il faut un cursor main au hover des boutons") :
     déjà le comportement par défaut d'un `<a href>`, posé ici en explicite
     pour cohérence avec `.lien-bouton` (css/style.css) plutôt que de
     compter sur le repli navigateur. */
  cursor: pointer;
}

.btn--cta__icone {
  flex-shrink: 0;
}

.btn--cta:hover {
  background: color-mix(in srgb, var(--color-tertiary) 80%, black);
}

/* Retour utilisateur (WCAG 1.4.11) : ce bouton a lui-même un fond vert
   (--color-tertiary) -- un anneau vert dessus serait quasi invisible.
   Même correctif que .volet__toggle ci-dessus (cf. tokens.css). */
.btn--cta:focus-visible {
  outline: var(--focus-ring-on-light);
  outline-offset: 2px;
  box-shadow: var(--focus-ring-on-light-glow);
}

/* Retour utilisateur : sur desktop, la section avant-propos (texte long,
   plusieurs paragraphes) sépare le texte des chiffres clés en 2 colonnes
   plutôt que de tout empiler, pour raccourcir la longueur de lecture
   perçue. Piloté par `chiffresClesColonne` (optionnel, cf. _data/rapport.js
   et _includes/section.njk) -- sections qui ne l'activent pas (ex.
   etudes-de-branches) gardent le rendu empilé existant. Empilé par défaut
   (mobile/tablette, ordre DOM texte puis chiffres = comportement déjà
   correct sans règle supplémentaire) ; grille 2 colonnes à partir de
   1024px seulement, palier "desktop" du projet (Story 1.4), pour ne
   jamais compresser le bloc de chiffres sur tablette. Largeur FIXE
   (`--secondary-column-width`, css/tokens.css) plutôt qu'un ratio de
   grille (`2fr 1fr`, valeur d'origine) : demandé pour que ce bloc partage
   la même largeur que l'encadré "objectifs" du portail et les repli photo
   de `.section-intro-row` (plus haut), plutôt que chacun dérive une
   largeur différente de son propre ratio selon la largeur du texte
   voisin. */
@media (min-width: 1024px) {
  /* Retour utilisateur : essai précédent avec la colonne de texte en `1fr`
     (remplit tout l'espace restant, jusqu'à 672px) + chaque paragraphe
     recentré individuellement dans ce vide -- fonctionnait mais laissait
     un vide "scattered" par paragraphe plutôt qu'un ensemble cohérent.
     Remplacé par : colonne de texte bornée à `minmax(0, 600px)` (assez
     large pour les paragraphes à 68ch/541px + un peu d'air, jamais plus
     large que nécessaire -- rétrécit sous 600px si le conteneur est plus
     étroit, ne déborde jamais) + `justify-content: center` sur la grille
     -- centre les DEUX colonnes ENSEMBLE comme un seul bloc dans la
     section dès que leur largeur combinée (600 + gap + 320) est plus
     étroite que l'espace disponible, plutôt que de pousser tout le vide
     dans la colonne de texte. Décale mécaniquement la colonne de chiffres
     vers la gauche (elle n'est plus collée au bord droit du conteneur). */
  .section-columns {
    display: grid;
    grid-template-columns: minmax(0, 600px) 320px;
    justify-content: center;
    gap: calc(var(--spacing-gutter) * 2);
    /* Retour utilisateur : `align-items: stretch` (était `start`) -- la
       colonne de chiffres étire maintenant sa hauteur pour égaler celle,
       plus grande, de la colonne de texte voisine, plutôt que de garder sa
       hauteur naturelle (plus courte) alignée en haut. Nécessaire pour que
       la répartition des chiffres sur la hauteur (ci-dessous) ait un
       espace réel à occuper. */
    align-items: stretch;
  }

  /* La liste de chiffres clés passe d'une grille à 3 colonnes (stat block
     pleine largeur, cf. règle .chiffres-cles-liste plus bas) à une seule
     colonne empilée : 3 colonnes dans une colonne de page qui ne fait plus
     que 1/3 de la largeur serait beaucoup trop dense. Séparateur horizontal
     (`border-top`) à la place du séparateur vertical entre colonnes. */
  .section-columns__chiffres .chiffres-cles-liste {
    grid-template-columns: 1fr;
  }

  /* Retour utilisateur : un espace apparaissait au-dessus de la colonne de
     chiffres, la décalant vers le bas par rapport au premier paragraphe de
     la colonne de texte -- pas une règle à nous (aucune margin-top sur h3
     ailleurs dans ce fichier), mais la marge par défaut du navigateur pour
     un <h3> (jamais neutralisée jusqu'ici, sans effet visible tant que le
     h3 suivait un paragraphe dans le même flux empilé). Neutralisée
     seulement dans ce contexte de colonne, où le h3 est le tout premier
     élément et doit s'aligner avec le haut du texte voisin (`align-items:
     start` sur `.section-columns` ci-dessus). */
  /* Correctif retour utilisateur (suite) : `margin-top: 0` seul laissait la
     marge basse par défaut du navigateur (28px, jamais neutralisée non
     plus) créer un écart différent de celui, symétrique, entre deux
     chiffres (padding haut/bas égaux sur chaque `li`, cf. plus bas) --
     remplacée par une valeur maîtrisée alignée sur ce même rythme. */
  .section-columns__chiffres h3 {
    margin-top: 0;
    margin-bottom: 1rem;
  }

  /* Même logique : `<ul>` porte une marge basse par défaut du navigateur
     (16px) jamais neutralisée -- s'ajoutait au padding-bottom de l'encadré
     et rendait l'espace après le dernier chiffre plus grand que l'espace
     avant le premier (padding-top de l'encadré seul). Neutralisée
     (`margin-bottom: 0` ci-dessous) pour que les deux espaces (avant le
     premier chiffre, après le dernier) ne dépendent que du padding
     symétrique de `.section-columns__chiffres`.

     Retour utilisateur (suite) : les 6 chiffres restaient packés en haut du bloc,
     laissant un vide en dessous une fois `.section-columns` étiré
     (`align-items: stretch` plus haut) à la hauteur de la colonne de texte
     -- demandé de les répartir sur toute la hauteur disponible. Un 1er
     essai (`display: flex` + `justify-content: space-between`) espaçait
     bien les chiffres mais pas les séparateurs : le `border-top` de
     chaque `li` reste collé à SON PROPRE bord haut, donc tout l'espace
     ajouté par `space-between` tombait entre la fin d'un chiffre et le
     trait du suivant -- le trait se retrouvait collé au chiffre d'après
     plutôt que centré entre les deux. Remplacé par `display: grid;
     grid-auto-rows: 1fr` (au lieu du `display: grid` à hauteur "auto" de
     la règle de base `.chiffres-cles-liste`, plus bas dans ce fichier --
     écrasé ici, portée limitée à ce contexte de colonne) : chaque `li`
     devient une RANGÉE DE HAUTEUR ÉGALE (1/6e de la hauteur disponible
     chacune, quel que soit le nombre de lignes de son libellé) plutôt
     qu'un bloc de hauteur "naturelle" empilé -- le contenu de chaque `li`
     est ensuite centré verticalement DANS sa rangée (cf. règle `li`
     ci-dessous), ce qui place mécaniquement le `border-top` (au bord haut
     de la rangée, donc à mi-chemin entre deux rangées de même hauteur) à
     équidistance du chiffre précédent et du chiffre suivant.

     Correctif retour utilisateur (suite) : `flex: 1` (posé ici pour
     occuper l'espace laissé par le `h3`, `.section-columns__chiffres` en
     `display: flex; flex-direction: column`) mesurait bien `flex-grow: 1`
     une fois calculé, mais NE grandissait PAS -- vérifié en direct (force
     `height: 100%`/`flex-basis: 0px` en style inline : aucun des deux ne
     remplissait non plus l'espace disponible). Cas limite constaté :
     `flex-grow` ne se propage pas de façon fiable à travers un enfant
     dont la hauteur vient elle-même d'un étirement de grille CSS (`align-
     items: stretch` sur `.section-columns`, plus haut) -- contrairement à
     l'étirement de grille lui-même, qui fonctionne (`.section-columns__
     chiffres` mesure bien 1145px, identique à la colonne de texte).

     2e correctif (suite) : un 1er remplacement par `display: grid; grid-
     template-rows: auto 1fr` posé directement sur `.section-columns__
     chiffres` restait SANS EFFET (vide identique, 251px) -- cause réelle
     trouvée en inspectant le DOM : `chiffresCles()` (_includes/section.njk)
     enveloppe `h3` ET `.chiffres-cles-liste` dans un `<div class="chiffres
     -cles">` intermédiaire, jamais visible dans ce fichier CSS jusqu'ici.
     `.section-columns__chiffres` n'a donc qu'UN SEUL enfant direct (ce
     `.chiffres-cles`) -- un `grid-template-rows: auto 1fr` dessus créait
     une 2e rangée vide plutôt que de séparer h3/liste (qui vivent tous
     deux DANS le 1er et seul enfant). Corrigé en ciblant le bon niveau :
     `.chiffres-cles` (pas `.section-columns__chiffres`) reçoit `height:
     100%` (résout contre la hauteur définie de son parent, cf. ci-dessus)
     puis `display: grid; grid-template-rows: auto 1fr` -- CETTE fois h3 et
     `.chiffres-cles-liste` sont bien ses deux enfants directs. */
  .section-columns__chiffres .chiffres-cles {
    height: 100%;
    display: grid;
    grid-template-rows: auto 1fr;
  }

  /* Correctif retour utilisateur : `grid-auto-rows: minmax(0, 1fr)`
     (essai précédent) forçait des RANGÉES de hauteur égale, mais les
     libellés varient énormément en longueur (1 à 6 lignes selon le
     chiffre) -- centrer un contenu de hauteur variable dans une rangée de
     hauteur FIXE donne mécaniquement des marges chiffre<->séparateur
     différentes d'un item à l'autre (contenu court = grandes marges,
     contenu long = petites marges, jamais égales entre elles). Remplacé
     par un flux normal (`display: block`, plus de rangées forcées) : la
     hauteur de chaque `<li>` redevient sa hauteur naturelle
     (contenu + padding), et c'est le PADDING (calculé et posé en JS,
     `js/chiffres-cles.js`, uniforme pour tous les `<li>`) qui absorbe
     l'espace disponible -- garantit des marges identiques partout, quelle
     que soit la longueur du libellé, tout en sommant exactement à la
     hauteur de la colonne de texte voisine. */
  .section-columns__chiffres .chiffres-cles-liste {
    display: block;
    margin-bottom: 0;
  }

  /* Retour utilisateur : traiter la colonne de chiffres comme un encadré
     (même traitement que `.encadre` ailleurs sur le site : contour, rayon,
     fond, padding) plutôt qu'un bloc de texte nu -- lui donne une présence
     visuelle propre plutôt que de compter uniquement sur l'espacement pour
     s'équilibrer face à la colonne de texte. */
  .section-columns__chiffres {
    border: 1px solid var(--color-outline-variant);
    border-radius: var(--rounded);
    padding: 1.5rem 1.75rem;
    background: var(--color-surface-container);
  }

  /* Retour utilisateur : la colonne de chiffres restait visuellement plus
     courte que la colonne de texte -- agrandie pour mieux équilibrer les
     deux : plus d'espace vertical par chiffre (padding), chiffre plus gros
     (taille propre à cette colonne, n'affecte pas le stat block pleine
     largeur ailleurs -- ex. etudes-de-branches -- qui garde --counter-
     figure-size), intitulé en corps de texte un cran plus grand
     (--font-body-lg au lieu de --font-body-md hérité).

     Retour utilisateur (2e passe -- séparateurs) : chaque `li` est
     maintenant une rangée de grille de hauteur ÉGALE (`grid-auto-rows:
     1fr` sur `.chiffres-cles-liste` ci-dessus), pas un bloc empilé à
     hauteur "naturelle" -- `display: flex; flex-direction: column;
     justify-content: center` centre ici le contenu (chiffre + libellé)
     VERTICALEMENT dans cette rangée, quelle que soit sa propre hauteur de
     contenu (1 ou 2 lignes de libellé). Le `border-top`, posé au bord
     haut de la rangée (pas du contenu), se retrouve ainsi mécaniquement à
     mi-chemin entre le bas du contenu précédent et le haut du contenu
     suivant -- centré entre deux chiffres plutôt que collé à l'un des
     deux.

     Correctif retour utilisateur (marges inégales, notamment visible sur
     "18") : le centrage par rangée de hauteur égale ci-dessus est
     abandonné (cf. commentaire de `.chiffres-cles-liste` plus haut) --
     `padding-block: 0.5rem` ici n'est plus qu'un REPLI statique (si JS est
     désactivé/échoue, amélioration progressive habituelle de ce projet) :
     `js/chiffres-cles.js` mesure la hauteur naturelle de chaque `<li>`
     (padding remis à 0), calcule le padding UNIFORME qui fait sommer le
     total exactement à la hauteur de la colonne de texte voisine, et le
     pose en style inline sur chaque `<li>` -- gauche/droite ci-dessous
     (`display: flex; flex-direction: column`) n'a plus besoin de
     `justify-content: center` : le padding (identique en haut/bas grâce à
     `padding-block`) centre déjà le contenu par construction. */
  .section-columns__chiffres .chiffres-cles-liste li {
    display: flex;
    flex-direction: column;
    border-inline-start: none;
    border-top: 1px solid var(--color-outline-variant);
    padding-block: 0.5rem;
  }

  .section-columns__chiffres .chiffres-cles-liste li:first-child {
    border-top: none;
  }

  /* Retour utilisateur ("chiffres 2x plus gros") : 2.75rem -> 5.5rem, même
     doublement que `--counter-figure-size` (tokens.css/css/style.css) --
     cette colonne a sa PROPRE taille (commentaire plus haut) donc n'hérite
     pas du token, mais suit le même principe. Les chiffres de cette
     colonne (avant-propos : 9, 5, 18, 39, 2, 18) restent au maximum sur 2
     chiffres, jamais de risque de recasse même à cette taille (vérifié en
     navigateur) -- contrairement à "70000" ailleurs sur le site, qui a
     dicté le plafond mobile plus prudent du token partagé. */
  .section-columns__chiffres .chiffre-cle__valeur {
    font-size: 5.5rem;
  }

  .section-columns__chiffres .chiffre-cle__label {
    font-size: var(--font-body-lg-size);
    line-height: var(--font-body-lg-line-height);
    /* Retour utilisateur ("je ne vois pas de différence") : cette règle,
       plus spécifique que `.chiffre-cle__label` (plus bas dans ce fichier),
       gardait son propre `margin-top: 0.75rem` -- appliquée à l'avant-propos
       (colonne "chiffresClesColonne", ex. "18 développements informatiques
       ..."), c'est justement le bloc cité en exemple par l'utilisateur :
       la réduction faite sur la règle de base ne s'y appliquait donc pas
       du tout. Même valeur ici, pour rester cohérent partout. */
    margin-top: 0.25rem;
  }
}

.chiffres-cles-liste {
  list-style: none;
  padding-left: 0;
  display: grid;
  /* Retour utilisateur ("chiffres 2x plus gros") : minimum de colonne
     élargi de 9rem à 10.5rem -- le plus grand chiffre du site ("70000", 5
     chiffres tabulaires) a besoin de cette marge en plus à 56px (nouvelle
     taille mobile, cf. --font-counter-figure-size dans tokens.css) pour ne
     pas recasser en 2 lignes au milieu du nombre (vérifié en navigateur). */
  grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
  gap: 0;
}

/* Correctif retour utilisateur : à partir de la tablette, les blocs de 5-6
   chiffres tenaient tous sur une seule rangée (auto-fit), jugée trop
   dense. Plafonné à 3 colonnes pour forcer un retour à la ligne (2 rangées
   pour les blocs actuels de 5 et 6 chiffres) et donner plus de respiration
   -- mobile inchangé (règle auto-fit ci-dessus, ~2 colonnes à 10.5rem).
   Même palier : `--font-counter-figure-size` passe à sa valeur pleine
   (80px, x2 net par rapport à l'ancienne taille -- retour utilisateur)
   dès que la grille passe à 3 colonnes fixes, où chaque colonne dispose de
   nettement plus de largeur que le plancher mobile (cf. tokens.css pour la
   valeur mobile intermédiaire et pourquoi). */
@media (min-width: 768px) {
  .chiffres-cles-liste {
    grid-template-columns: repeat(3, 1fr);
  }

  :root {
    --font-counter-figure-size: 80px;
  }
}

/* Séparateur = bordure CSS entre colonnes (jamais un élément DOM ajouté par
   bloc, cf. spec chiffres-cles-stat-block > Always). :first-child couvre la
   première rangée ; en cas de retour à la ligne sur grille responsive, le
   premier élément d'une rangée suivante garde sa bordure gauche -- limitation
   connue des grilles CSS pures, acceptée (cf. Design Notes de la spec). */
.chiffres-cles-liste li {
  text-align: center;
  /* Retour utilisateur ("les blocs sont encore très collés, ça ne
     respire pas") : padding vertical réélargi (1.5rem -> 2.25rem) -- le
     rapprochement chiffre/libellé fait juste avant (0.75rem -> 0.25rem)
     réduisait la hauteur de CHAQUE bloc, ce qui rendait l'espace ENTRE
     deux blocs voisins (lui, gouverné par CE padding, pas par le margin
     chiffre/libellé) proportionnellement plus mince à l'œil -- ce padding
     est le seul responsable de la respiration entre deux chiffres clés
     voisins, jamais le lien chiffre/libellé À L'INTÉRIEUR d'un même bloc. */
  padding: 2.25rem 1.5rem;
  border-inline-start: 1px solid var(--color-outline-variant);
}

.chiffres-cles-liste li:first-child {
  border-inline-start: none;
}

/* Retour utilisateur : les 5 chiffres clés de "Les travaux de branches en
   chiffres" (section etudes-de-branches) ne se répartissaient pas
   correctement sur les 2 lignes attendues (3 + 2) -- la grille à 3
   colonnes ci-dessus laisse la 2e ligne incomplète collée à gauche (2
   éléments sur 3 colonnes, un vide visible à droite) et hérite du bug de
   bordure documenté juste au-dessus (seul le tout premier élément de la
   liste perd sa bordure gauche, pas le premier de CHAQUE ligne -- le 4e
   élément, qui débute la 2e ligne, gardait donc une bordure parasite).
   Scopé à `#etudes-de-branches` (id de section posé par `section()` dans
   _includes/section.njk) plutôt que de modifier la grille de base
   partagée avec "Études et travaux interbranches 2025" (6 éléments, 2
   lignes de 3 pleines, jamais concerné par ce défaut). Flexbox plutôt que
   grid ici : `justify-content: center` centre une ligne INCOMPLÈTE sous
   la précédente -- CSS Grid ne sait pas centrer une rangée en particulier,
   seulement des colonnes entières sur toutes les rangées. */
@media (min-width: 1024px) {
  #etudes-de-branches .chiffres-cles-liste {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
  }

  #etudes-de-branches .chiffres-cles-liste li {
    flex: 0 0 calc(100% / 3);
  }

  #etudes-de-branches .chiffres-cles-liste li:nth-child(3n + 1) {
    border-inline-start: none;
  }
}

/* Retour utilisateur ("les deux chiffres de 'Les certifications en
   chiffres' répartis sur la largeur") : la grille de base (3 colonnes
   fixes, plus haut) ne pose que 2 éléments pour cette section précise (14
   branches / 11 licences) -- ils se retrouvaient donc confinés aux 2/3
   gauches de la largeur, la 3e colonne restant vide à droite plutôt que
   les 2 chiffres se partageant TOUTE la largeur. Scopé à `#certifications`
   (même précédent que `#etudes-de-branches` juste au-dessus) plutôt que de
   changer la grille de base : les autres blocs à 3+ chiffres du site
   restent inchangés. */
@media (min-width: 1024px) {
  #certifications .chiffres-cles-liste {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Retour utilisateur : pas de césure (trait d'union de coupure syllabique)
   dans les chiffres clés ni leurs intitulés -- même logique que h1-h4 plus
   haut (`hyphens: auto` hérité de `body`, jamais souhaité ici). Pas de mot
   assez long dans ces libellés courts pour que `overflow-wrap` (toujours
   hérité, inchangé) ait besoin d'intervenir. */
/* Retour utilisateur ("les chiffres de la couleur de leur section
   respective") : `--section-chiffre-color`, posée sur `<section>` par la
   macro `section()` (_includes/section.njk) -- même couleur de rôle que
   le bandeau de CETTE section (bleu, vert du logo, turquoise ou violet
   pour "certifications"), jamais une couleur inventée ici. Repli sur
   `--color-on-surface` (couleur de texte habituelle du site) si la
   variable venait à manquer -- ne s'applique en pratique jamais, chaque
   section en pose une. */
.chiffre-cle__valeur {
  display: block;
  font-family: var(--counter-figure-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--counter-figure-size);
  font-weight: var(--counter-figure-weight);
  line-height: var(--counter-figure-line-height);
  letter-spacing: var(--counter-figure-letter-spacing);
  font-variant-numeric: tabular-nums;
  hyphens: none;
  /* Retour utilisateur ("met les espaces quand il manque, genre 70 000") :
     l'espace insécable ajoutée par `formatNombre` (eleventy.config.js/
     js/compteurs.js) empêche déjà une coupure DEVANT le séparateur de
     milliers, mais `overflow-wrap: break-word` (hérité de `body`) coupait
     quand même le nombre n'importe où pour tenir dans la colonne (ex.
     "70 00" / "0") -- un chiffre clé reste toujours court, jamais de
     raison de le couper : `nowrap` l'emporte ici sur l'héritage.
     Vérifié en navigateur (colonne "Le portail en chiffres", 70 000). */
  white-space: nowrap;
  color: var(--section-chiffre-color, var(--color-on-surface));
}

.chiffre-cle__label {
  display: block;
  /* Retour utilisateur ("rapproche les phrases des chiffres... ça
     permettra une meilleure aération entre les blocs") : 0.75rem -> 0.25rem
     -- le chiffre et son propre libellé doivent se lire comme UN bloc ;
     l'espace qui les séparait empiétait sur la respiration entre deux
     chiffres clés VOISINS (le padding de `.chiffres-cles-liste li`
     ci-dessus reste, lui, inchangé -- c'est LUI qui doit porter cet
     espacement, pas le lien chiffre/libellé). */
  margin-top: 0.25rem;
  hyphens: none;
}

/* Retour utilisateur : "harmonise les liens vidéo pour qu'ils soient comme
   les boutons étude, même si pas encore de lien" -- `.video-label`
   (traitement texte italique + icône séparé) est retiré : `item.video`
   (veilles prospectives) passe désormais par `lienBouton(null, item.video,
   "motion")` (_includes/section.njk), rendu `<span class="lien-bouton
   lien-bouton--motion">` -- même habillage que les boutons étude, prêt à
   devenir un vrai `<a>` sans revoir le gabarit le jour où une URL est
   fournie. Contour plutôt que fond plein (`.btn--cta`, plus haut) : ce
   dernier est explicitement réservé à l'UNIQUE CTA principal du site
   ("Accéder au portail...") -- le réutiliser ici en diluerait la
   signification. `cursor: pointer` explicite (retour utilisateur) : un
   `<span>` (cas `item.video`, sans URL) n'a pas le curseur pointeur par
   défaut d'un `<a>` -- posé ici pour les DEUX variantes, cohérence visuelle
   même si le `<span>` n'est pas cliquable. */
.lien-bouton {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1.1rem;
  border: 1px solid var(--color-outline-variant);
  border-radius: var(--rounded-full);
  background: var(--color-surface-container-low);
  color: var(--color-primary);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.lien-bouton:hover {
  background: var(--color-surface-container);
  border-color: var(--color-primary);
}

.lien-bouton:focus-visible {
  outline: var(--focus-ring-on-light);
  outline-offset: 2px;
  box-shadow: var(--focus-ring-on-light-glow);
}

.lien-bouton__icone {
  flex-shrink: 0;
}

/* `.section--secondary` (études de branches/certifications, turquoise) :
   même logique de rôle que le reste du site (FR-10) -- le bouton suit la
   couleur secondaire plutôt que de garder le bleu primary partout. */
.section--secondary .lien-bouton {
  color: var(--color-secondary);
}

.section--secondary .lien-bouton:hover {
  border-color: var(--color-secondary);
}

/* Retour utilisateur (revue Sally, party mode) : bouton discret en fin de
   panneau de volet, pour replier sans remonter chercher l'en-tête sur un
   contenu long (certaines fiches de branche dépassent deux écrans).
   `background:none`/`border:0` neutralisent le style natif de bouton ;
   le reste (soulignement, couleur --color-primary) le fait lire comme un
   lien d'action, cohérent avec le placement en fin de contenu plutôt
   qu'une action de carte comme .volet__toggle. */
/* `inline-block` (et non inline-flex + gap) : l'espace entre la flèche et
   « Replier » reste du texte, donc souligné -- un `gap` flex n'est jamais
   souligné (retour utilisateur). */
.volet__collapse {
  display: inline-block;
  margin-top: 1.5rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-primary);
  font: inherit;
  font-size: var(--font-caption-size);
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.volet__collapse:hover {
  color: color-mix(in srgb, var(--color-primary) 80%, black);
}

/* Même correctif WCAG 1.4.11 que .volet__toggle/.btn--cta (cf. tokens.css)
   -- ce bouton vit aussi sur le fond blanc de la carte. */
.volet__collapse:focus-visible {
  outline: var(--focus-ring-on-light);
  outline-offset: 2px;
  box-shadow: var(--focus-ring-on-light-glow);
}

/* Regroupement des veilles prospectives (Story 1.3, correctif revue #1) :
   jusqu'ici sans règle CSS propre, cette classe héritait du style de texte
   par défaut sans passer par les tokens de spacing/couleur. */
.veille-prospective {
  margin-block-start: var(--spacing-gutter);
  padding-block-start: var(--spacing-gutter);
  border-top: 1px solid var(--color-outline-variant);
}

.veille-prospective__texte > :first-child,
.veille-prospective__volets > :first-child {
  margin-top: 0;
}

/* Retour utilisateur ("le texte a tout décalé, rééquilibre la largeur des
   colonnes... pour que ce soit équilibré en hauteur") : ratio 1fr/2fr
   (1/3 texte, 2/3 volets) pensé quand l'intro tenait en 2 courts
   paragraphes -- une fois le contenu du docx validé/appliqué (intro sur
   l'ObSoCo + les 4 formats de veille + leur liste à puces), le texte est
   devenu bien plus long que les 5 cartes empilées à côté, laissant un
   grand vide sous ces dernières (constaté en navigateur). Élargi à 2fr/3fr
   (2/5 texte, 3/5 volets) : assez de largeur en plus pour que le texte
   tienne sur moins de lignes (donc moins haut), sans pour autant inverser
   la hiérarchie visuelle voulue à l'origine (les volets restent la colonne
   la plus large). Même palier ("desktop" du projet, 1024px). Empilé par
   défaut (mobile/tablette, ordre DOM texte puis volets = comportement
   déjà correct sans règle supplémentaire). */
@media (min-width: 1024px) {
  .veille-prospective {
    display: grid;
    grid-template-columns: 6fr 7fr;
    gap: calc(var(--spacing-gutter) * 2);
    align-items: start;
  }

  /* La grille de volets repasse à 1 seule colonne dans ce contexte plutôt
     que d'hériter des 2 colonnes de la règle générale (`.volets` plus
     haut, pensée pour la pleine largeur de la section) : même une fois la
     colonne élargie à 2/3 (ratio ci-dessus), 2 cartes côte à côte y
     seraient nettement plus étroites que le gabarit standard des volets
     ailleurs sur la page -- une seule colonne, plus large, garde une
     lecture cohérente avec le reste du site. L'alternance pleine-largeur
     `:nth-child(3n)` (règle générale plus haut) devient un no-op sans
     effet ici (une grille à 1 colonne est déjà pleine largeur), donc pas
     besoin de la neutraliser séparément. */
  .veille-prospective__volets .volets {
    grid-template-columns: 1fr;
  }
}

/* Légende sous une section (Story 1.3, correctif revue #1 ; renommée --
   retour utilisateur -- de `.branches-concernees`, la liste des branches
   concernées par une étude interbranche : retirée entre-temps, cette
   classe sert maintenant à la légende de l'astérisque "20 branches
   professionnelles du commerce*" de l'avant-propos, `data.legende`,
   _includes/section.njk). Typo caption + --color-on-surface (pas
   on-surface-variant : ce dernier token ne passe pas le seuil AA 4.5:1
   pour du texte de lecture sur cette palette, cf. .lien-bouton plus haut
   pour le même constat). */
.legende {
  font-family: var(--font-caption-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-caption-size);
  font-weight: var(--font-caption-weight);
  line-height: var(--font-caption-line-height);
  color: var(--color-on-surface);
  /* Retour utilisateur ("un peu d'espace... avec un filet très fin
     au-dessus") : marge agrandie (0.5rem -> 2rem) pour détacher visuellement
     la légende du texte qui la précède, + `border-top` fin comme séparateur
     -- même token que le filet entre chiffres clés voisins
     (`--color-outline-variant`, cf. `.chiffres-cles-liste li` plus bas). */
  margin-top: 2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--color-outline-variant);
}

/* Retour utilisateur ("la liste de branches passe dessous les chiffres
   clés, alors qu'elle ne doit pas dépasser la largeur de la colonne 1
   sous l'édito") : un essai précédent en pleine largeur (marge négative
   pour déborder jusqu'au bord de la colonne de chiffres clés) faisait
   déborder la légende PAR-DESSUS/EN TRAVERS de la colonne de chiffres
   clés voisine (les deux colonnes partagent la même rangée, `align-items:
   stretch`, cf. `.section-columns` plus haut) -- retiré. La légende reste
   un simple élément de `.section-columns__texte` (colle à "Bonne
   lecture."), contenue par cette colonne comme le reste du texte.

   Retour utilisateur (suite, "la colonne fait 540 mais la légende
   seulement 439") : `.section p { max-width: 68ch }` plus haut dans ce
   fichier s'appliquait aussi à la légende (un `<p>`) -- une liste de 20
   noms de branches n'est pas un texte de lecture au long cours (pas la
   raison d'être du plafond 68ch, pensé pour les paragraphes de l'édito),
   elle doit occuper toute la largeur de sa colonne. Sélecteur à 2 classes
   (`.section-columns__texte .legende`) : nécessaire pour gagner sur
   `.section p` (1 classe + 1 élément, spécificité légèrement supérieure à
   1 classe seule) sans `!important`. */
.section-columns__texte .legende {
  max-width: none;
}

/* ---------------------------------------------------------------------
 * Navigation ancrée, scrollspy, hamburger, retour en haut (Story 1.2 pour
 * le comportement/structure, palette et typographie de marque appliquées
 * en Story 1.3 via les custom properties de css/tokens.css : fond
 * `--color-primary` bleu foncé, liens `label-caps` blancs, lien actif
 * souligné en `--color-tertiary` vert — cf. DESIGN.md Components >
 * "Navigation ancrée").
 * --------------------------------------------------------------------- */

:root {
  --nav-height: 3.5rem;
  /* Correctif retour utilisateur (revue Sally, party mode) : `.site-nav`
     utilisait `min-height: var(--nav-height)` -- au palier mobile,
     `--nav-height` bascule à 16rem (repli sans-JS, cf. plus bas) et
     `initNavHeightSync()` (js/nav.js, ResizeObserver) mesure ensuite la
     nav rendue et RÉÉCRIT `--nav-height` avec cette même valeur mesurée --
     dépendance circulaire qui s'auto-confirme à 256px même une fois le
     menu réduit en dropdown par JS (`display:none` sur la liste fermée),
     alors que la nav ne devrait plus faire que ~56px de haut à ce
     moment-là. `--nav-bar-min-height` casse ce cycle : une constante
     jamais réécrite par JS ni changée en media query, qui ne sert QUE de
     plancher à `.site-nav`/`.site-nav__inner` elles-mêmes -- `--nav-height`
     reste réservé aux CONSOMMATEURS de l'offset (padding-top du body,
     scroll-margin-top des sections), jamais à la nav elle-même. */
  --nav-bar-min-height: 3.5rem;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

/* Retour utilisateur ("compteur de progression de lecture discret, liseré
   en haut de page qui se remplit au scroll") : cf. commentaire complet dans
   layout.njk pour le contexte (position, z-index, couleur). `pointer-
   events: none` sur les deux : purement visuel, ne doit jamais intercepter
   un clic destiné à `.site-nav` juste en dessous (même z-index élevé,
   même bande du haut de l'écran). */
.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  z-index: 101;
  pointer-events: none;
}

.reading-progress__fill {
  height: 100%;
  width: 0%;
  background: var(--color-tertiary);
  pointer-events: none;
}

/* Transition courte (0.1s) : lisse les à-coups d'un `requestAnimationFrame`
   qui recalcule la largeur à chaque frame de scroll (js/reading-progress.js)
   -- pas une "animation" au sens d'EXPERIENCE.md > Interaction Primitives >
   "Banni" (le liseré ne bouge jamais de lui-même, seulement en réaction
   directe et immédiate au scroll de l'utilisateur). Sous `prefers-reduced-
   motion: reduce`, la largeur saute instantanément à sa valeur cible, sans
   lissage -- même logique que `scroll-behavior: smooth` juste au-dessus. */
@media (prefers-reduced-motion: no-preference) {
  .reading-progress__fill {
    transition: width 0.1s linear;
  }
}

.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  min-height: var(--nav-bar-min-height);
  background: var(--color-primary);
  color: var(--color-on-primary);
}

.site-nav__inner {
  max-width: 1200px;
  min-height: var(--nav-bar-min-height);
  margin: 0 auto;
  /* Correctif revue #1 (story 1.4) : suit désormais les 3 mêmes paliers de
     marge que `main` (ci-dessus) plutôt qu'un padding-inline fixe de
     1.5rem — sinon les bords de la nav cessaient de s'aligner avec ceux
     du contenu dès la tablette. Mêmes valeurs, mêmes seuils, même repli
     (correctif revue #4). */
  padding-inline: var(--spacing-margin-mobile, 20px);
  /* Retour utilisateur ("ajoute du padding haut et bas... pour que le menu
     soit plus aéré") : la nav n'avait jusqu'ici QUE `min-height` (3.5rem,
     `--nav-bar-min-height`) pour sa hauteur -- le logo (2.25rem) s'y
     retrouvait centré avec très peu de respiration de part et d'autre.
     Ce padding-block s'AJOUTE à cette hauteur (jamais une hauteur fixée en
     dur qui l'écraserait) -- la nav grandit simplement au-delà de son
     plancher pour l'absorber. `--nav-height` reste correct automatiquement :
     `initNavHeightSync` (js/nav.js) mesure la hauteur RENDUE de la nav via
     `ResizeObserver`, peu importe d'où vient cette hauteur (contenu,
     min-height ou ce padding) -- aucun autre réglage à toucher. */
  padding-block: 0.75rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  justify-content: space-between;
}

@media (min-width: 768px) {
  .site-nav__inner {
    padding-inline: 2.5rem;
  }
}

@media (min-width: 1024px) {
  .site-nav__inner {
    padding-inline: var(--spacing-margin-desktop, 80px);
  }
}

/* Retour utilisateur : lien enrobant le logo (le réflexe "retour en
   haut" sur un one-page). `.site-nav__logo` garde son propre
   `flex-shrink:0` -- ce lien, lui, n'a besoin que d'aligner son contenu
   (l'image) verticalement au centre, comme le faisait déjà nativement
   l'image seule dans le flex de `.site-nav__inner`. Anneau de focus
   même traitement que `.site-nav__toggle`/`.back-to-top` juste plus
   bas (vert sur fond bleu foncé, déjà au-dessus de 3:1, cf. WCAG 1.4.11
   -- pas besoin du correctif `--focus-ring-on-light`). */
/* Correctif retour utilisateur ("le logo n'est pas proportionnel [...] il
   est déformé") : `.site-nav__logo` porte son propre `flex-shrink: 0`,
   mais c'est CE lien (son parent direct) qui est le véritable enfant flex
   de `.site-nav__inner` -- sans son propre `flex-shrink: 0`, il se
   faisait comprimer par le flex-shrink par défaut du navigateur (la nav
   est déjà à l'étroit à ce palier, cf. commentaire sur `.site-nav__list`
   plus bas), écrasant l'image logée à l'intérieur via `max-width: 100%`
   malgré son ratio HTML correct -- mesuré en direct : rendu à 3,4:1 au
   lieu du vrai 4,475:1 du fichier avant ce correctif. */
.site-nav__logo-link {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.site-nav__logo-link:focus-visible {
  outline: var(--volet-focus-ring);
  outline-offset: 2px;
}

.site-nav__logo {
  display: block;
  /* Retour utilisateur : agrandi (1.75rem -> 2.25rem, +28%). `width: auto`
     conserve le ratio de l'asset -- rien d'autre à ajuster, `.site-nav`/
     `.site-nav__inner` (min-height, jamais une hauteur fixe) et `--nav-
     height` (recalculé dynamiquement par js/nav.js via ResizeObserver, cf.
     commentaire sur cette custom property) absorbent la nav légèrement
     plus haute automatiquement. */
  height: 2.25rem;
  width: auto;
  flex-shrink: 0;
}

/* Retour utilisateur : la nav passait sur 2 rangées à cause du libellé le
   plus long ("L'accompagnement sur les certifications et les
   compétences") -- `flex-wrap` empêchait ce seul item de retourner à la
   ligne DANS son propre bouton, en renvoyant plutôt toute la barre sur une
   2e rangée. `nowrap` ici force la barre elle-même sur une seule ligne :
   l'espace total manque pour que les 5 libellés tiennent sur une seule
   ligne chacun à cette largeur de viewport (mesuré : ~1250px de largeur
   naturelle cumulée pour ~910px disponibles), donc le rétrécissement
   proportionnel par défaut de flexbox (`flex-shrink`, min-content comme
   plancher naturel -- le mot le plus long d'un libellé, pas le libellé
   entier) fait retourner plusieurs boutons à la ligne, pas seulement le
   plus long -- accepté tel quel plutôt que de forcer artificiellement les
   autres à rester sur une ligne (ce qui les ferait déborder du
   conteneur). `--nav-height` reste correct dans tous les cas : `js/nav.js`
   (`initNavHeightSync`, déjà présent) mesure la hauteur RENDUE de la nav
   via `ResizeObserver`, peu importe le nombre de lignes par bouton.

   Retour utilisateur (révision) : "Le portail Perspectives commerce"
   passait à 3 lignes -- mesuré à 169px de large pour un besoin de 175px
   (2 lignes) seulement. Un 1er ajustement (column-gap 1.25rem -> 0.875rem)
   corrigeait le cas mesuré mais ne laissait quasiment AUCUNE marge (174-
   175px, soit <1px de battement) -- l'utilisateur a ensuite signalé qu'au
   survol, l'item repassait à 3 lignes et y restait. Cause probable :
   largeur au bord du seuil, sensible au moindre écart (ex. barre de
   défilement verticale qui prend ~15px de largeur sur cet environnement,
   `window.innerWidth` 1280 vs `document.documentElement.clientWidth` 1265
   mesuré -- rien à voir avec `:hover` en tant que tel, qui ne change ici
   QUE `border-bottom-color`, jamais une propriété de mise en page).
   `column-gap` réduit une 2e fois (0.875rem -> 0.5rem) pour une vraie
   marge de sécurité plutôt que le strict minimum mesuré.

   Retour utilisateur (révision) : plus de padding-inline du tout sur
   `.site-nav__link` (cf. plus bas) -- le trait vert (survol/actif) doit
   coller exactement à la largeur du texte, pas à une boîte élargie par du
   padding. `column-gap` compense l'espace retiré des liens -- ATTENTION,
   contre-intuitif : augmenter le gap réduit aussi la largeur dispo pour le
   TEXTE (le gap et les items se partagent le même espace total), donc ce
   n'est pas un simple "+1.25rem" gratuit. Valeur choisie par test direct
   (balayage de `column-gap` sous contrainte simulée, largeur réduite
   jusqu'à -40px pour stress-tester) : 0.875rem tient sur 2 lignes max pour
   les 5 libellés même sous cette contrainte, 1.25rem cassait déjà "Le
   portail Perspectives commerce" à -20px. Ne pas remonter cette valeur
   sans re-tester de la même façon. */
/* Retour utilisateur ("aère le menu, plus d'espace entre les boutons") :
   `column-gap` remonté (0.875rem -> 1.75rem, x2) -- valeur d'origine
   choisie par stress-test serré (cf. historique de ce commentaire dans le
   diff) à une époque où chaque libellé dépendait de `flex-wrap` pour
   trouver SEUL son point de coupure ; les 5 libellés ont désormais un
   `<br>` fixe (retour utilisateur précédent, nav.njk) qui fige la ligne la
   plus large de chaque bouton indépendamment de l'espace restant -- reste
   à vérifier en navigateur que cette ligne la plus large ("et les
   compétences" du plus long libellé) tient encore à ce palier plus
   généreux, pas à re-tester tout le balayage historique. */
.site-nav__list {
  list-style: none;
  display: flex;
  flex-wrap: nowrap;
  gap: 0.25rem 1.75rem;
  margin: 0;
  padding: 0;
}

.site-nav__link {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Retour utilisateur : marge haut/bas plutôt que padding -- `.site-
     nav__item` (li) est étiré par `.site-nav__list` (align-items:stretch
     par défaut) à la hauteur de l'item le plus grand de la rangée (celui
     sur 2 lignes) ; sans cette marge, un lien sur 1 ligne (ex.
     "Avant-propos") remplissait toute cette hauteur -- `display:flex` +
     `align-items:center` centre déjà son texte dedans, mais le trait de
     survol/actif (`border-bottom`, sur ce lien) touchait alors directement
     le bord bas de la nav (aucune marge entre les deux). `margin-block`
     crée cet espace ET centre mieux chaque lien, quel que soit son nombre
     de lignes, sur la hauteur du logo à côté. */
  margin-block: 0.5rem;
  /* Retour utilisateur : aucun padding-inline (retiré) -- le trait vert
     (border-bottom, survol/actif) doit épouser exactement la largeur du
     texte, pas une boîte élargie par du padding. L'espacement entre
     boutons vient maintenant entièrement du `column-gap` de
     `.site-nav__list` ci-dessus, remonté en conséquence. */
  color: var(--color-on-primary);
  text-decoration: none;
  /* Retour utilisateur ("les textes du menu doivent être en bas de casse") :
     `text-transform: uppercase` retiré -- DESIGN.md > Typography prévoyait
     `label-caps` ("tout capitales, tracking ouvert") pour les libellés de
     nav, mais demande explicite de l'utilisateur de revenir à la casse
     naturelle des titres de section (`data.titre`, jamais modifiée --
     ce retrait est purement visuel, AD-1 reste respecté : le contenu
     affiché est déjà exactement `data.titre`, seule sa PRÉSENTATION
     changeait). `letter-spacing`/`font-size` (`label-caps`, plus bas)
     restent inchangés -- seule la casse est concernée par cette demande. */
  /* Retour utilisateur : centré plutôt qu'aligné à gauche (repli par
     défaut) -- un bouton qui retourne à la ligne (cf. .site-nav__list
     ci-dessus) a un rendu plus propre en 2 lignes centrées qu'en 2 lignes
     alignées à gauche dans une boîte plus large que son texte. */
  text-align: center;
  /* Correctif revue #2 (story 1.4) : instance manquée du correctif revue
     #4 de la Story 1.3 (chaîne de repli complète alignée sur body, plutôt
     qu'un "sans-serif" nu) — appliqué partout ailleurs (h2/h3/h4,
     .sous-titre, counter-figure, .legende) mais oublié ici. */
  font-family: var(--font-label-caps-family), system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: var(--font-label-caps-size);
  font-weight: var(--font-label-caps-weight);
  letter-spacing: var(--font-label-caps-letter-spacing);
  /* Retour utilisateur : pas de césure dans les libellés de nav (ex.
     "PERSPEC-TIVES") -- `hyphens: auto` est hérité de `body`, jamais
     neutralisé ici jusqu'à présent (contrairement à h1-h4 et
     .chiffres-cles-liste, qui ont déjà ce même correctif ailleurs dans ce
     fichier). `overflow-wrap: break-word` (aussi hérité) reste le filet de
     sécurité si un mot ne tient vraiment pas dans la largeur disponible --
     seul le trait d'union de coupure syllabique disparaît. */
  hyphens: none;
  /* Retour utilisateur (révision) : `border-bottom` remplacé par
     `text-decoration` -- un border-bottom appartient à la BOÎTE du lien
     (sa largeur assignée par flexbox), jamais au texte lui-même. Une fois
     le padding retiré (ci-dessus) et le texte replié sur 2 lignes de
     largeurs différentes (ex. "Avant-" / "propos"), la boîte reste plus
     large que chaque ligne individuelle (le flex-basis vient de la largeur
     NATURELLE d'une seule ligne, rétrécie mais pas resserrée sur le
     texte replié) -- le trait border-bottom continuait donc à déborder du
     texte des deux côtés, malgré le padding à 0. `text-decoration-line:
     underline` peint un trait sous CHAQUE ligne de texte individuellement,
     à sa propre largeur -- c'est la seule façon d'obtenir un trait qui
     épouse vraiment le texte replié. Transparent par défaut (même logique
     que l'ancien border-bottom transparent) ; `text-underline-offset`
     ajoute un peu d'espace entre texte et trait (un `text-decoration`
     colle plus près de la ligne de base qu'un border-bottom ne le
     faisait). */
  text-decoration-line: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.3em;
}

/* Retour utilisateur : trait vert (--color-tertiary) au survol, comme
   celui déjà utilisé pour le lien actif juste plus bas (`.is-active`) --
   même couleur pour les deux états plutôt que blanc au survol / vert
   seulement une fois arrivé, pour que le survol annonce visuellement le
   même signal que l'état actif vers lequel il mène. */
.site-nav__link:hover {
  text-decoration-color: var(--color-tertiary);
}

/* Correctif revue #3 : les liens de nav (les plus utilisés de la page)
   n'avaient qu'un changement de trait au focus, un indicateur plus
   faible que l'outline déjà utilisé sur .site-nav__toggle et .back-to-top.
   Même traitement outline ici, en plus du soulignement.
   Note (revue Sally, party mode, WCAG 1.4.11) : `--volet-focus-ring`
   (vert) reste volontairement INCHANGÉ ici -- contrairement à
   .volet__toggle/.btn--cta (fonds clairs, cf. leurs règles), ce lien vit
   sur le fond bleu foncé de la nav (--color-primary) où le vert mesure
   5,53:1, largement au-dessus du minimum 3:1. Idem pour
   .site-nav__toggle et .back-to-top juste plus bas, tous deux sur ce même
   fond. */
.site-nav__link:focus-visible {
  text-decoration-color: var(--color-on-primary);
  outline: var(--volet-focus-ring);
  outline-offset: 2px;
}

.site-nav__toggle:focus-visible {
  outline: var(--volet-focus-ring);
  outline-offset: 2px;
}

/* Lien actif : souligné en tertiary (vert), cf. DESIGN.md Components >
   "Navigation ancrée" — un des rares usages intentionnels du vert hors logo. */
.site-nav__link.is-active {
  text-decoration-color: var(--color-tertiary);
  font-weight: 700;
}

.site-nav__toggle {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--color-on-primary);
}

.site-nav__toggle-bars,
.site-nav__toggle-bars::before,
.site-nav__toggle-bars::after {
  display: block;
  width: 22px;
  height: 2px;
  background: currentColor;
  position: relative;
  transition: transform 0.15s ease, opacity 0.15s ease, top 0.15s ease;
}

.site-nav__toggle-bars::before,
.site-nav__toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.site-nav__toggle-bars::before {
  top: -6px;
}

.site-nav__toggle-bars::after {
  top: 6px;
}

.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-bars {
  background: transparent;
}

.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-bars::before {
  top: 0;
  transform: rotate(45deg);
}

.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-bars::after {
  top: 0;
  transform: rotate(-45deg);
}

/* Retour utilisateur (revue Sally, party mode) : ce seuil était 767px,
   partagé avec le point de bascule "tablette" générique du reste du site
   (grilles, marges) -- mais mesuré en direct entre 768px et ~1300px, la
   nav horizontale devient un pavé de 99px sur 3-4 lignes (le libellé le
   plus long ne tient nulle part avant ~1440px). EXPERIENCE.md > Responsive
   promet "nav horizontale si elle tient sinon hamburger" -- elle ne tient
   pas dans cette plage, donc remonté à 1099px (spécifique à LA NAV
   uniquement, cf. MOBILE_BREAKPOINT dans js/nav.js -- les autres seuils
   768px du fichier, grilles/marges générales, restent inchangés,
   volontairement pas de lien entre les deux). */
@media (max-width: 1099px) {
  :root {
    /* Repli statique pour l'état sans JS (ou avant l'exécution de
       nav.js) : sans JS, .site-nav__toggle reste masqué et la liste des
       5 sections s'affiche empilée en flux normal (cf. règles
       html:not(.no-js) ci-dessous), donc la barre de nav est nettement
       plus haute qu'un simple bandeau. nav.js recalcule --nav-height
       dynamiquement dès qu'il s'exécute (ResizeObserver). */
    --nav-height: 16rem;
  }

  /* Sans JS (ou avant l'exécution de nav.js) : la liste reste toujours
     visible en flux normal, empilée sous le nom de la barre de nav —
     un visiteur mobile doit pouvoir naviguer même si le script échoue à
     charger, cohérent avec le principe déjà appliqué ailleurs dans le
     projet (contenu accessible sans JS). Le hamburger n'apparaît que
     lorsque JS a retiré la classe "no-js" du <html>. */
  .site-nav__list {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin: 0;
    padding: 0.5rem 0 0.75rem;
  }

  html:not(.no-js) .site-nav__toggle {
    display: inline-flex;
  }

  html:not(.no-js) .site-nav__list {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--color-primary);
    padding: 0.5rem 1.5rem 1rem;
    max-height: calc(100vh - var(--nav-height));
    overflow-y: auto;
  }

  html:not(.no-js) .site-nav__list.is-open {
    display: flex;
  }

  .site-nav__link {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0.5rem 0.25rem;
  }
}

.back-to-top {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--rounded-full);
  background: var(--color-primary);
  color: var(--color-on-primary);
  text-decoration: none;
  font-size: 1.25rem;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.back-to-top:focus-visible {
  outline: var(--volet-focus-ring);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------
 * Animation d'entrée au scroll (Story 3.2) : encadrés et cartes de volet
 * marqués `.reveal` (cf. _includes/section.njk). L'état masqué n'existe
 * QUE sous `html.js-reveal-ready` -- une classe posée par js/reveal.js
 * uniquement quand `prefers-reduced-motion` n'est pas actif (AD-6) : sans
 * JS, ou sous reduced-motion, `.reveal` n'a aucune règle et reste visible
 * par défaut, jamais masqué. Le stagger entre volets d'une même grille
 * (`transitionDelay`) est posé en style inline par js/reveal.js.
 * --------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  html.js-reveal-ready .reveal {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 280ms ease-out, transform 280ms ease-out;
    /* Correctif revue : tant qu'un bloc n'est pas révélé, son contenu
       interactif (ex. `.volet__toggle`) reste invisible mais ne doit pas
       rester cliquable/activable au clic ou au tap. */
    pointer-events: none;
  }

  html.js-reveal-ready .reveal.is-revealed {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }

  /* Grille de volets en 2 colonnes (>= 768px) : la carte de gauche arrive de
     la gauche, celle de droite de la droite (décalage court, 40px), la
     pleine largeur (1 sur 3) garde l'arrivée par le bas. Rythme général :
     3n+1 gauche, 3n+2 droite ; grille 2x2 des études : impair/pair. */
  @media (min-width: 768px) {
    html.js-reveal-ready .volets > .reveal:not(.is-revealed):nth-child(3n + 1) {
      transform: translateX(-40px);
    }
    html.js-reveal-ready .volets > .reveal:not(.is-revealed):nth-child(3n + 2) {
      transform: translateX(40px);
    }
    html.js-reveal-ready .volets.volets--etudes > .reveal:not(.is-revealed):nth-child(odd) {
      transform: translateX(-40px);
    }
    html.js-reveal-ready .volets.volets--etudes > .reveal:not(.is-revealed):nth-child(even) {
      transform: translateX(40px);
    }
  }

  /* Le décalage latéral ne doit jamais créer de défilement horizontal. */
  body {
    overflow-x: clip;
  }
}

/* Correctif revue : un bloc `.reveal` jamais scrollé dans le viewport avant
   une impression directe (Ctrl+P) resterait masqué sur la page imprimée --
   l'impression n'a pas de notion de scroll/viewport progressif. */
@media print {
  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ---------------------------------------------------------------------
 * Pied de page (retour utilisateur, "fais le pied de page en te basant sur
 * la charte graphique") : logo + coordonnées + réseaux sociaux + logo
 * partenaire, cf. _includes/footer.njk pour le détail des choix de fond
 * clair (logos sans variante claire disponible) et de contenu (repris du
 * site RA 2024, jamais inventé).
 * --------------------------------------------------------------------- */
/* Retour utilisateur : fond blanc (pas le neutre chaud utilisé ailleurs
   pour les grands aplats, DESIGN.md > Do's and Don'ts) + coins arrondis en
   haut -- même logique que `.volet__card` (fond blanc plutôt que le neutre
   chaud par défaut, cf. son propre commentaire plus haut) : le pied de
   page doit se détacher visiblement du fond de page (`--color-surface-
   container-low`, body) pour que l'arrondi se voie, exactement comme les
   cartes de volet se détachent du même fond. Vert exclu (charte/DESIGN.md
   > Do's and Don'ts : "garder le vert rare et intentionnel -- pas une
   couleur de fond"). `--rounded-lg` (1rem, tokens.css) plutôt que
   `--rounded-md` (0.75rem, cartes de volet) : élément pleine largeur, un
   rayon plus grand reste proportionné à cette échelle. */
/* Retour utilisateur : largeur = celle du "conteneur central" -- même
   boîte que `main`/`.site-nav__inner` (max-width 1200px centré + mêmes 3
   paliers de marge horizontale), pas une marge fixe. Portée ici sur
   `.site-footer` lui-même (pas juste `.site-footer__inner`) : la carte
   blanche/arrondie doit correspondre exactement à cette largeur, pas
   seulement son contenu interne -- `.site-footer__inner` n'a donc plus
   besoin de son propre max-width/padding (ci-dessous), seulement du
   flex. */
.site-footer {
  background-color: var(--color-surface);
  border: var(--volet-border);
  border-radius: var(--rounded-lg) var(--rounded-lg) 0 0;
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: var(--spacing-margin-mobile, 20px);
}

@media (min-width: 768px) {
  .site-footer {
    padding-inline: 2.5rem;
  }
}

@media (min-width: 1024px) {
  .site-footer {
    padding-inline: var(--spacing-margin-desktop, 80px);
  }
}

.site-footer__inner {
  padding-block: calc(var(--spacing-unit) * 4);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--spacing-unit) * 3);
}

.site-footer__logo {
  height: 40px;
  width: auto;
}

.site-footer__contact {
  color: var(--color-on-surface);
  font-size: var(--font-body-md-size);
  line-height: var(--font-body-md-line-height);
}

.site-footer__contact p {
  margin: 0;
}

.site-footer__organisme {
  font-weight: 600;
}

.site-footer__contact a {
  color: inherit;
}

/* Retour utilisateur (cible tactile ≥44×44px, même exigence que les
   boutons de volet -- EXPERIENCE.md > Accessibility Floor) : le padding
   agrandit la zone cliquable au-delà de la seule icône 20×20. */
.site-footer__social {
  display: flex;
  gap: calc(var(--spacing-unit) * 1);
}

.site-footer__social-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--rounded);
  color: var(--color-primary);
  background-color: var(--color-surface-container-high);
}

.site-footer__social-link:hover {
  color: var(--color-on-primary);
  background-color: var(--color-primary);
}

.site-footer__social-link:focus-visible {
  outline: var(--volet-focus-ring, 2px solid var(--color-primary));
  outline-offset: 2px;
}

.site-footer__logo-partenaire {
  height: 30px;
  width: auto;
}

/* Empilé centré sous 768px : la rangée à 4 blocs (logo/contact/social/
   logo partenaire) n'a pas la place de rester côte à côte sur mobile. */
@media (max-width: 767px) {
  .site-footer__inner {
    flex-direction: column;
    text-align: center;
  }

  .site-footer__contact {
    order: 1;
  }

  .site-footer__social {
    order: 2;
  }

  .site-footer__logo-partenaire {
    order: 3;
  }
}

/* ---------------------------------------------------------------------
 * "Effet wahou" (retour utilisateur) : éclat de fin de décompte, survol
 * des chiffres clés et des boutons. Tout sous no-preference (reduced-motion
 * = rien ne bouge). `.is-eclat` est posée par js/compteurs.js à la fin du
 * décompte, avant de lancer le chiffre suivant du groupe.
 * --------------------------------------------------------------------- */
@keyframes chiffre-eclat {
  0%   { transform: scale(1);    text-shadow: 0 0 0 transparent; }
  35%  { transform: scale(1.14); text-shadow: 0 0 28px color-mix(in srgb, var(--section-chiffre-color, var(--color-primary)) 60%, transparent); }
  100% { transform: scale(1);    text-shadow: 0 0 0 transparent; }
}

@media (prefers-reduced-motion: no-preference) {
  .chiffre-cle__valeur.is-eclat {
    animation: chiffre-eclat 500ms ease-out;
  }

  .chiffres-cles-liste li .chiffre-cle__valeur {
    transition: transform 0.25s ease;
  }
  .chiffres-cles-liste li:hover .chiffre-cle__valeur {
    transform: scale(1.06);
  }

  .lien-bouton {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
  a.lien-bouton:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.14);
  }
}

/* ---------------------------------------------------------------------
 * Images entières en mobile/tablette (retour utilisateur, "image coupée").
 * Empilées (< 1024px), les zones photo gardaient une hauteur fixe alors que
 * leur largeur grandit : `cover` rognait jusqu'à 75 % de l'image.
 * - `.image-placeholder--photo` : js/images-entieres.js pose `--img-ratio`
 *   (ratio réel de l'image) + `.has-img-ratio` -> la boîte épouse l'image.
 * - `.portail-banner` : l'image passe au-dessus, l'encadré en dessous (au
 *   lieu d'être superposé sur une image que sa hauteur rognait).
 * --------------------------------------------------------------------- */
/* Mobile (< 768px) : image entière, la boîte épouse son ratio. */
@media (max-width: 767px) {
  .image-placeholder.image-placeholder--photo.has-img-ratio {
    aspect-ratio: var(--img-ratio);
    min-height: 0;
    background-size: 100% 100%;
  }

  /* Les cartes n'ont aucune illustration en mobile (retour utilisateur) :
     photo interne des volets « pleine largeur » masquée. */
  .volet__panel-pleine-largeur__photo {
    display: none;
  }
}

/* Tablette (768-1023px) : zoom/recadrage accepté (retour utilisateur) --
   boîte de ratio fixe 16/7 (4/3 pour une image portrait), `cover`, plutôt
   que pleine largeur naturelle qui rendait certaines images minuscules. */
@media (min-width: 768px) and (max-width: 1023px) {
  .image-placeholder.image-placeholder--photo.has-img-ratio {
    aspect-ratio: 16 / 7;
    min-height: 0;
  }
  .image-placeholder.image-placeholder--photo.has-img-ratio.is-portrait {
    aspect-ratio: 4 / 3;
  }
}

@media (max-width: 1023px) {
  .portail-banner[style*="background-image"] {
    display: block;
    padding: 0;
    background-size: 100% auto;
    background-repeat: no-repeat;
    background-position: top center;
  }
  .portail-banner[style*="background-image"]::before {
    content: "";
    display: block;
    aspect-ratio: 1600 / 662;
  }
  .portail-banner[style*="background-image"] .portail-banner__encadre {
    max-width: none;
    padding: 1rem;
  }
}

/* ---------------------------------------------------------------------
 * Chiffres clés en mobile/tablette (< 1024px) : grille + séparateurs
 * recalculés pour CHAQUE nombre de colonnes (retour utilisateur : séparateurs
 * inutiles sur les côtés, horizontaux manquants, chiffre seul à recentrer).
 * - < 520px : 1 colonne (séparateurs horizontaux seulement).
 * - 520-767px : 2 colonnes ; 768-1023px : 3 colonnes (grille de 6 pistes,
 *   chaque bloc en occupe 2).
 * - Dernière rangée incomplète : le bloc seul occupe toute la largeur
 *   (donc centré) ; deux blocs restants se partagent la largeur en deux.
 * - Séparateur vertical = bordure gauche de tout bloc qui n'est PAS en début
 *   de rangée ; horizontal = bordure haute de tout bloc hors première rangée.
 * Le desktop (>= 1024px) garde ses règles propres plus haut.
 * --------------------------------------------------------------------- */
@media (max-width: 1023px) {
  .chiffres-cles-liste {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
  }

  .chiffres-cles-liste li,
  .chiffres-cles-liste li:first-child {
    border-inline-start: none;
    border-top: none;
  }

  .chiffres-cles-liste li:nth-child(n + 2) {
    border-top: 1px solid var(--color-outline-variant);
  }
}

@media (min-width: 520px) and (max-width: 767px) {
  .chiffres-cles-liste {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .chiffres-cles-liste li:last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }

  .chiffres-cles-liste li:nth-child(2) {
    border-top: none;
  }

  .chiffres-cles-liste li:nth-child(even) {
    border-inline-start: 1px solid var(--color-outline-variant);
  }
}

@media (min-width: 768px) and (max-width: 1023px) {
  .chiffres-cles-liste {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .chiffres-cles-liste li {
    grid-column: span 2;
  }

  .chiffres-cles-liste li:nth-child(-n + 3) {
    border-top: none;
  }

  .chiffres-cles-liste li:nth-child(3n + 2),
  .chiffres-cles-liste li:nth-child(3n) {
    border-inline-start: 1px solid var(--color-outline-variant);
  }

  /* dernière rangée : 1 bloc seul = pleine largeur ; 2 blocs = moitié chacun */
  .chiffres-cles-liste li:last-child:nth-child(3n + 1) {
    grid-column: 1 / -1;
  }

  .chiffres-cles-liste li:nth-last-child(2):nth-child(3n + 1),
  .chiffres-cles-liste li:last-child:nth-child(3n + 2) {
    grid-column: span 3;
  }
}

/* Mobile/tablette (< 1024px) : le texte d'un volet ouvert n'est plus décalé
   sous le titre (24px + picto 60px + 1rem) -- ce retrait de ~100px réduisait
   inutilement la colonne de texte (retour utilisateur). Il reprend le
   simple padding de la carte ; le desktop garde l'alignement sur le titre. */
@media (max-width: 1023px) {
  .volet__panel-inner {
    padding-left: var(--volet-padding);
  }
}
