/*
Theme Name: McLeod
*/

/* Polices auto-hébergées, aucune requête externe (Google Fonts ou autre) */
@font-face {
	font-family: "Plus Jakarta Sans";
	src: url("assets/fonts/plus-jakarta-sans-variable.woff2") format("woff2-variations");
	font-weight: 200 800;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Glamour Palace";
	src: url("assets/fonts/glamour-palace-regular.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

/* Emphase forte : token préparé en session dédiée, jamais consommé jusqu'ici */
strong,
b {
	color: var(--wp--preset--color--contrast);
	font-weight: var(--wp--custom--font-weight--strong);
}
::selection {
  background: var(--wp--preset--color--accent);
  color: black;
  text-shadow: none;
}

::-moz-selection {
  background: var(--wp--preset--color--accent);
  color: black;
  text-shadow: none;
}
/* Fragment manuscrit dans un titre (ex. hero, accroche centrée). Couleur propre
   (contrast) : sans elle, un fragment .script hérite simplement du texte courant,
   la classe ne définissait jusqu'ici que la police et la taille. */
.script {
	font-family: var(--wp--preset--font-family--script);
	font-size: 1.2em;
	color: var(--wp--preset--color--contrast);
	padding-left: .15em;
  	padding-right: .15em;
}

/* Panneau indigo : texture à pointillés en arrière-plan, jamais au-dessus du contenu */
.is-style-panel-indigo {
	position: relative;
}

.is-style-panel-indigo::before {
	content: "";
	position: absolute;
	inset: var(--wp--preset--spacing--sm);
	background-image: radial-gradient(currentColor 1px, transparent 1px);
	background-size: 14px 14px;
	color: var(--wp--preset--color--foreground);
	opacity: 0.240;
	pointer-events: none;
	z-index: 0;
}

.is-style-panel-indigo > * {
	position: relative;
	z-index: 1;
}

/* Panneau clair (variation "is-style-panel-clair" sur core/group) : règles de
   contexte, comme .is-style-panel-indigo pour l'eyebrow. Titres et mention en
   accent-2 (contraste 4,98:1 sur surface, vérifié) ; strong neutralisé en couleur
   héritée, sinon il reste blanc (règle globale, pensée pour un fond sombre) —
   illisible sur ce fond clair. */
.is-style-panel-clair :is(h1, h2, h3, h4, h5, h6),
.is-style-panel-clair .mention {
	color: var(--wp--preset--color--accent-2);
}

.is-style-panel-clair strong,
.is-style-panel-clair b {
	color: inherit;
}

/* Boutons dans un panneau (clair ou indigo) ou dans le pied de page : pas d'ombre portée — le bouton
   global (styles.elements.button, theme.json) en porte une par défaut, pensée
   pour un bouton posé directement sur le fond de page, pas dans un panneau déjà
   surélevé visuellement. Les boutons hors panneau gardent leur ombre. */
.is-style-panel-clair .wp-block-button__link,
.is-style-panel-indigo .wp-block-button__link,
.pied-de-page .wp-block-button__link {
	box-shadow: none;
}

/* Sur-titre discret avant un titre de section (ex. "À propos", "Texte et liste").
   Couleur dépendante du CONTEXTE, jamais d'un réglage d'instance (textColor) :
   corail par défaut, ambré dans un panneau indigo (lisible sur ce fond, cf.
   CLAUDE.md). */
.eyebrow {
	font-size: var(--wp--preset--font-size--eyebrow);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--accent);
}

.is-style-panel-indigo .eyebrow {
	color: var(--wp--preset--color--accent-3);
}

/* Panneau de texte du pattern "À propos" (patterns/about.php) : padding RETIRÉ de
   l'attribut du bloc (style inline), posé ici par une classe dédiée à la place —
   lg sur desktop, md sous le breakpoint mobile (≤781px). Pourquoi une classe et
   pas l'attribut : un style inline poserait padding:lg partout, et le ramener à md
   sur mobile par-dessus aurait exigé !important (un style inline a une spécificité
   que seul !important peut battre) — explicitement écarté par Yvan. Une classe CSS
   ordinaire n'a pas ce problème : les deux valeurs (desktop/mobile) sont posées au
   même niveau de spécificité, la media query choisit simplement laquelle s'applique
   selon la largeur, sans jamais avoir besoin de forcer la cascade. Mêmes tokens
   qu'avant (lg/md), juste déplacés du bloc vers une règle de contexte — le patron
   déjà suivi ailleurs dans ce thème pour .cartes-ressources__carte, .cta-panneau,
   etc. (voir CLAUDE.md, "Panneau clair"/"CTA panneau"). */
.about-panel {
	padding: var(--wp--preset--spacing--lg);
}

@media (max-width: 781px) {
	.about-panel {
		padding: var(--wp--preset--spacing--md);
	}
}

/* Déclencheur de contact (pattern "À propos") : taille de boîte fixe (dimension "picto"), jamais
   modifiée par le survol — seul le SVG à l'intérieur se met à l'échelle (transform), voir plus bas. */
.about-contact-trigger {
	display: block;
	position: relative;
	width: var(--wp--preset--dimension--picto);
	height: var(--wp--preset--dimension--picto);
	padding: 0;
	border: 0;
	background: none;
	cursor: pointer;
}

.about-contact-trigger svg {
	display: block;
	position: relative;
	width: var(--wp--preset--dimension--picto);
	height: var(--wp--preset--dimension--picto);
	transition: transform 0.3s ease-out;
}

/* Composition d'images du pattern "À propos" : colonne 1 fixe (picto), ligne 1 à la hauteur du
   portrait, ligne 2 (photo laptop) prend le reste. Le portrait garde son ratio 4/5 à toutes les
   largeurs (aspect-ratio inline posé par l'attribut du bloc Image, plus de height:100% qui l'écrasait) :
   il n'est jamais l'image réduite. La pastille contact reste carrée à sa taille, calée en haut. */
.about-media {
	display: grid;
	grid-template-columns: var(--wp--preset--dimension--picto) 1fr;
	grid-template-rows: auto 1fr;
	gap: var(--wp--preset--spacing--md);
	height: 100%;
}

/* Neutralise la marge par défaut du layout "flow" de WordPress : l'espacement vient du gap de la grille */
.about-media > * {
	margin-block-start: 0;
}

.about-media > :nth-child(1) {
	grid-column: 1;
	grid-row: 1;
	align-self: start;
}

.about-media > :nth-child(2) {
	grid-column: 2;
	grid-row: 1;
}

.about-media > :nth-child(3) {
	grid-column: 1 / -1;
	grid-row: 2;
	height: 100%;
}

.about-media .wp-block-image {
	height: 100%;
	overflow: hidden;
	border-radius: var(--wp--preset--border-radius--md);
}

.about-media .wp-block-image img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	border-radius: var(--wp--preset--border-radius--md);
}

/* Portrait : hauteur dérivée de sa largeur par son ratio 4/5, jamais imposée par la rangée. Deux
   classes + pseudo-classe : bat .about-media .wp-block-image(/img) ci-dessus. */
.about-media > .wp-block-image:nth-child(2),
.about-media > .wp-block-image:nth-child(2) img {
	height: auto;
}

/* Une colonne au même seuil que la section Méthode (container query sur la largeur de contenu de la
   section, pas sur la fenêtre) : 978/979px, voir l'en-tête de blocks/methode/style.css pour l'origine
   de la valeur — à changer aux deux endroits ensemble. Le container est la section du pattern, repérée
   par sa structure (:has), aucune classe à ajouter dans le pattern ni dans la page 2.
   Les colonnes passent en grille d'une seule colonne : core/columns ne s'empile nativement qu'à
   781px (flex-basis inline + flex-wrap:nowrap!important du cœur au-delà) ; en grille, ni l'un ni
   l'autre ne s'applique, aucun !important nécessaire. Panneau puis mosaïque (rangée ou empilement,
   voir plus bas). Le gap reste celui du bloc (blockGap md). */
.wp-block-group:has(> .wp-block-columns > .wp-block-column > .about-media) {
	container: a-propos / inline-size;
}

/* Photo laptop retirée du flux à partir de 600px de section (deux colonnes ET rangée, ci-dessous) :
   elle remplit sa cellule, recadrée (object-fit:cover), sans jamais peser sur la hauteur. En deux
   colonnes, son ratio 16/10 entrait sinon dans la hauteur de la rangée de colonnes et allongeait le
   panneau (mesuré : 499 -> 592px à 1366px) ; en rangée, la hauteur est celle du portrait seul. Sous
   600px (empilement), elle garde son ratio naturel. */
@container a-propos (min-width: 600px) {
	.about-media > .wp-block-image:nth-child(3) {
		position: relative;
	}

	.about-media > .wp-block-image:nth-child(3) img {
		position: absolute;
		inset: 0;
	}
}

/* Deux colonnes (section ≥ 979px) : comportement d'avant f7828f1 rétabli — ligne 1 à la hauteur du
   picto, portrait recadré (object-fit:cover) à cette hauteur, aligné sur la pastille contact. Le
   ratio 4/5 du portrait ne vaut qu'en rangée et en empilement (sous 979px). width + height à 100%
   sur l'img : l'aspect-ratio inline du bloc Image est alors ignoré. Même spécificité que la règle
   "height:auto" du portrait plus haut, placée après : elle l'emporte. */
@container a-propos (min-width: 979px) {
	.about-media {
		grid-template-rows: var(--wp--preset--dimension--picto) 1fr;
	}

	.about-media > .wp-block-image:nth-child(2),
	.about-media > .wp-block-image:nth-child(2) img {
		height: 100%;
	}
}

@container a-propos (max-width: 978px) {
	.wp-block-columns:has(> .wp-block-column > .about-media) {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
	}
}

/* Une colonne, section de 600 à 978px : mosaïque en RANGÉE, portrait 4/5 à gauche, photo laptop à
   droite, même hauteur (celle du portrait). Colonnes 1fr / 2fr : la photo laptop fait alors 2 × la
   largeur du portrait pour 1,25 × cette largeur de haut, soit 1,6 = son ratio 16/10 exact, sans
   recadrage. Pastille contact masquée dans cette plage (décision d'Yvan). Sous 600px, empilement de
   la version mobile (pastille + portrait, puis photo laptop) : en rangée, le portrait y passerait
   sous ~190 x 237px (600px de section : (600 − md) / 3 de large). 600 : seuil proposé, valeur en
   dur faute de token dans une condition de container query. */
@container a-propos (min-width: 600px) and (max-width: 978px) {
	.about-media {
		grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
		grid-template-rows: auto;
	}

	.about-media > :nth-child(1) {
		display: none;
	}

	.about-media > :nth-child(2) {
		grid-column: 1;
		grid-row: 1;
	}

	.about-media > :nth-child(3) {
		grid-column: 2;
		grid-row: 1;
	}
}

/* Hero : centrage vertical du contenu (agit grâce à la min-height de la variation) */
.is-style-hero {
	display: flex;
	flex-direction: column;
	justify-content: center;
}

/* Hero : centrage horizontal du groupe de boutons */
.is-style-hero .wp-block-buttons {
	justify-content: center;
}

/* Hero : largeur maximale du paragraphe d'introduction */
.is-style-hero .hero__lead {
	max-width: /*var(--wp--custom--measure)*/ 27.5ch;
	margin-bottom:1rem;
}

/* Hero : retours à la ligne équilibrés sur le titre */
.is-style-hero h1 {
	text-wrap: balance;
	margin-bottom:var(--wp--preset--spacing--sm);
}

/* Hero, mobile (781px et moins) : fragment manuscrit réduit de 1.2em à 0.94em. Recalculé en session 63 pour l'étoile
   du titre rotatif : « qu'on a envie de montrer » (variante la plus longue) + réserve (voir plus bas) donne à 1em
   -1 % de marge à 375px (déborde), à 0.95em 4,28 % (sous les 5 % visés), à 0.94em 5,33 % — retenu. Sans étoile
   (`.script` hors `.titre-rotatif--actif`, ex. « Accroche centrée »), ce réglage n'a pas d'effet : le sélecteur ne
   cible que le hero. Aucun effet au-dessus de 781px. */
@media (max-width: 781px) {
	.is-style-hero h1 .script {
		font-size: 0.94em;
	}
}

/* Hero, mobile (781px et moins) : fragment manuscrit à 41px (2.5625rem, remplace le 0.94em ci-dessus ;
   le reste du H1 garde la taille xl) et interligne 1.5 (réglages d'Yvan, tokens settings.custom.hero).
   Interligne sans unité (et non 1.5em) : le fragment le recalcule sur sa propre taille. */
@media (max-width: 781px) {
	.is-style-hero h1 {
		line-height: var(--wp--custom--hero--interligne-mobile);
	}
	.is-style-hero h1 .script {
		font-size: var(--wp--custom--hero--script-mobile);
	}
}

/* Titre rotatif (inc/titre-rotatif.php, assets/js/titre-rotatif.js) — refonte session 63 : effacement/apparition
   avec étoile, pilotés par l'API Web Animations (element.animate), pas par des transitions CSS. Réglages déclarés
   une seule fois, lus par le JS via getComputedStyle (comme --diapo-duree, --etude-passage-*) : durées et courbes
   des DEUX phases, délai de pause au creux, largeurs des deux zones de réserve, taille de l'étoile.
   `.titre-rotatif--actif` n'est posée par le serveur que si le fragment a au moins 2 variantes. L'étoile (::after)
   n'est PAS sous `.js` : elle s'affiche statique dès le CSS seul, avec ou sans script (voir plus bas). */
.titre-rotatif {
	--titre-pause: 2.5s;
	--titre-effacement-duree: 500ms;
	--titre-effacement-courbe: cubic-bezier(0.55, 0, 1, 0.45);
	--titre-apparition-duree: 600ms;
	--titre-apparition-courbe: cubic-bezier(0, 0.55, 0.45, 1);
	--titre-creux-delai: 100ms;
	/* Apparition animée du premier suffixe au chargement (voir plus bas, section dédiée) : délai avant que le script
	   ne joue sa propre apparition (compté après police prête + hero visible), et réglages du filet de sécurité CSS
	   seul (délai avant son déclenchement, durée de son fondu). */
	--titre-apparition-delai-initial: 150ms;
	--titre-filet-delai: 2.5s;
	--titre-filet-fondu: 300ms;
	--titre-masque-opaque: 0.3em;
	--titre-masque-degrade: 0.25em;
	/* Réserve verticale/gauche anti-rognage (session 63, ajustement) : un mask-image coupe tout ce qui dépasse de la
	   boîte de l'élément masqué, quel que soit overflow (overflow-y:visible n'y change rien — la clip du masque suit
	   sa propre zone, pas la propriété overflow). Valeurs = dépassements d'encre mesurés (Glamour Palace, les trois
	   variantes réelles de la page), arrondis avec marge : bas jusqu'à 14,22px/57,6px = 0,247em (constant à toutes
	   les tailles, mêmes glyphes) ; haut jusqu'à 5,36px = 0,093em ; gauche jusqu'à 4,7px = 0,082em (le "q" de "qui",
	   commun aux trois variantes, déborde légèrement AVANT le bord gauche de la boîte). */
	--titre-masque-haut: 0.1em;
	--titre-masque-bas: 0.25em;
	--titre-masque-gauche: 0.12em;
	/* Distance mesurée entre le bas de la boîte de texte (bord de son padding-box, avant tout ajustement) et la vraie
	   ligne de base typographique, pour Glamour Palace : ~0,20em (mesuré via les métriques de police, constant entre
	   30px et 57,6px de corps). Sert à corriger l'alignement de l'étoile (voir .script::after) : align-items:flex-end
	   seul alignerait les bas de BOÎTE, pas les lignes de base. Alex Brush : ~0,34em (non retenu, police de repli
	   seulement, écart non corrigé si la police change un jour). */
	--titre-etoile-ligne-base: 0.2em;
	/* Taille choisie par Yvan à l'œil après comparaison de deux captures réelles (75 % et 50 % de l'ancienne valeur
	   0,45em) : 50 % retenu. */
	--titre-etoile-taille: 0.22em;
	/* Écart entre la chasse (fin d'avance) de la dernière lettre et l'étoile, au repos : distance typographique de
	   point final. Posé en marge négative sur l'étoile (voir .script::after) : comme le texte ne bouge jamais dans
	   son propre padding-right (la réserve, ci-dessous, reste constante), l'étoile — plus proche — suit alors le bord
	   du texte DÉVOILÉ pendant le mouvement, pas le bord de la réserve.
	   0,06em d'abord (proposition initiale), puis 0,17em (0,06 + la moitié de --titre-etoile-taille à 0,22em, sur
	   retour d'Yvan : « trop proche », décalée vers la droite) — CHEVAUCHEMENT D'ENCRE PERSISTANT sur « qui vous
	   sert » (le « t » final déborde de 0,282em/16,23px à 1081-1440px, largement plus que cet écart) : voir
	   CLAUDE.md, non corrigé sans accord d'Yvan. */
	/* 0,06em puis 0,17em (retour d'Yvan : « trop proche ») puis 0,24em (retour : l'effet de l'étoile qui passe sous
	   le texte restait visible) : décalée à nouveau vers la droite. Le chevauchement d'encre sur « qui vous sert »
	   (le « t » final déborde de 0,282em/16,23px à 1081-1440px) n'est complètement résolu qu'à partir de ~0,29-0,30em
	   — voir CLAUDE.md pour la mesure exacte, non poussé jusque-là sans confirmation. */
	--titre-etoile-ecart: 0.24em;
	/* Avance/retard temporaire de l'étoile par rapport à sa position naturelle (retour d'Yvan : elle semblait « se
	   faire manger » par le texte à l'ouverture) — une bosse de translateX, nulle aux deux bouts de chaque phase,
	   ajoutée par le script à la rotation. Valeurs proposées, à ajuster à l'œil : légère avance à l'ouverture
	   (compense la sensation), plus léger retard à la fermeture. */
	--titre-etoile-avance-ouverture: 0.15em;
	--titre-etoile-avance-fermeture: 0.08em;
}

/* Fragment = boîte de texte + étoile, dans cet ordre. `display` en deux valeurs (extérieur/intérieur) : bloc sur sa
   propre ligne jusqu'à 1080px (centré par margin-inline:auto, indépendant du reste du texte), en ligne au-delà
   (centré par l'héritage text-align:center du hero, comme le reste du H1) — une seule paire de règles remplace le
   jeu grid/inline-block de la version précédente (empilement de mots), le centrage ne change jamais de mécanisme
   visible en traversant le seuil. Sous `.js` seulement : sans script, `.script` reste un span inline ordinaire (le
   texte suit le flux normal du H1, `text-wrap:balance` s'applique comme avant), seule l'étoile (::after, réglée plus
   bas hors de `.js`) s'ajoute après le texte. */
.js .titre-rotatif--actif .script {
	display: block flex;
	/* flex-end, pas baseline : mesuré (session 63, ajustement), align-items:baseline pour cet item (overflow-x:clip
	   le prive d'une vraie ligne de base, il retombe sur son bord bas de boîte) donne le MÊME résultat que flex-end —
	   autant utiliser la valeur au comportement garanti par la spec (aligne les bords bas de boîte des deux items),
	   corrigé ensuite par une marge sur le texte (voir .titre-rotatif__texte) pour viser la vraie ligne de base. */
	align-items: flex-end;
	/* box-sizing:content-box (le site entier est en border-box, voir "Bandeau à filets") : la largeur posée par le
	   script est la « place réservée » au sens strict (texte dévoilé + réserve + étoile), jamais mêlée au padding
	   propre de la classe `.script` de base (.15em de chaque côté, qui reste inchangé, en plus, à l'extérieur). */
	box-sizing: content-box;
	width: max-content; /* valeur avant le premier rendu du script ; remplacée par une largeur explicite dès l'init. */
	margin-inline: auto;
}

@media (min-width: 1081px) {
	/* Les mots inactifs de l'ancienne version sortaient de la fenêtre (scrollWidth 1092 pour 1081 de large,
	   jusqu'à ~1139px) : la boîte de texte actuelle ne déborde plus (une seule variante, largeur toujours ≤ à sa
	   valeur naturelle), mais `clip` est conservé par précaution (aucun effet mesuré si rien ne déborde, coûte
	   moins cher qu'une vérification). `clip` sur l'axe X seul : l'axe Y reste visible, les paraphes de Glamour
	   Palace ne sont pas rognés (jamais `overflow: hidden`). */
	.is-style-hero {
		overflow-x: clip;
	}

	.js .titre-rotatif--actif {
		white-space: nowrap;
		text-wrap: nowrap;
	}

	.js .titre-rotatif--actif .script {
		display: inline flex;
		width: auto; /* idem : remplacée par une largeur explicite dès l'init du script. */
		margin-inline: 0;
	}
}

/* Étoile : assets/images/etoile.svg (tracé seul, nettoyé), en mask-image comme le chevron/téléchargement — seul le
   canal alpha compte pour le masque, la couleur vient de `background-color` (token accent, celui déjà intégré dans
   le fichier source). `::after` d'un conteneur flex devient lui-même un item flex, placé après le texte dans
   l'ordre des enfants ; SA POSITION PAR DÉFAUT (flush après la boîte de texte, donc après toute la réserve du
   padding-right — zone opaque + dégradé) est ramenée plus près par une marge négative (session 63, ajustement) :
   distance d'un point final après la chasse de la dernière lettre (`--titre-etoile-ecart`), jamais un calcul de
   position — la réserve elle-même (padding-right de `.titre-rotatif__texte`) ne change pas. Comme la réserve reste
   constante alors que le texte, lui, change de largeur, cette marge fait suivre l'étoile au bord du texte DÉVOILÉ
   (pas au bord de la réserve) aussi bien au repos que pendant le mouvement : elle passe alors par-dessus le début de
   la réserve (zone opaque), ce qui masque visuellement l'amorce du bord du masque (effet voulu). PAS sous `.js` :
   visible et immobile même sans script (texte + étoile, statique). */
.titre-rotatif--actif .script::after {
	content: "";
	display: inline-block;
	flex: none;
	/* Bas de l'étoile sur la ligne de base du texte : `vertical-align:baseline` seul suffit ici (sans JS, flux inline
	   normal — l'étoile, sans contenu, synthétise sa propre ligne de base à son bord bas de boîte, qui s'aligne alors
	   sur celle du texte voisin). En contexte flex (avec JS), voir `align-items:flex-end` + la marge de
	   `.titre-rotatif__texte` : ce `vertical-align` n'a alors aucun effet (ignoré pour un item flex), sans conflit. */
	vertical-align: baseline;
	width: var(--titre-etoile-taille);
	aspect-ratio: 39.6 / 41.2;
	/* Ramène l'étoile de sa position par défaut (après opaque + dégradé) à `--titre-etoile-ecart` après la chasse du
	   texte : marge négative = réserve totale du texte moins l'écart voulu. */
	margin-left: calc((var(--titre-masque-opaque) + var(--titre-masque-degrade) - var(--titre-etoile-ecart)) * -1);
	/* Angle courant : posé par le script comme valeur DE REPOS (pas seulement pendant l'animation), 0deg par défaut
	   (sans JS, ou avant le premier départ). Le script anime toujours VERS cette valeur déjà posée (fill par défaut,
	   jamais forwards) : une fois l'animation terminée, la disparition de son effet ne change rien, la propriété
	   personnalisée porte déjà la bonne valeur — aucun commit ni cancel nécessaire pour fixer l'arrivée. */
	transform: rotate(var(--titre-etoile-angle, 0deg));
	background-color: var(--wp--preset--color--accent);
	-webkit-mask-image: url("assets/images/etoile.svg");
	mask-image: url("assets/images/etoile.svg");
	-webkit-mask-size: contain;
	mask-size: contain;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
}

/* Boîte de texte, créée par le script (`.titre-rotatif__texte`) : sans JS elle n'existe pas, `.script` garde son
   texte brut, sans overflow ni masque (aucune conséquence : le point ci-dessus vaut aussi ici). Largeur DÉVOILÉE
   animée par l'API Web Animations (jamais de transition CSS) — distincte de la largeur RÉSERVÉE, portée par `.script`
   lui-même (voir plus haut et assets/js/titre-rotatif.js).
   **Piège corrigé en session 63 (ajustement)** : un `mask-image` coupe tout ce qui dépasse de la boîte de l'élément
   masqué, quel que soit `overflow` — `overflow-y: visible` (ou `overflow-x: clip` côté X) ne change rien à cette
   règle, propre au masque (sa zone de peinture, `mask-clip`, est bornée à la boîte, indépendamment de la propriété
   `overflow`). Rognage constaté (bas du « q » et du « g », capture d'Yvan) : `overflow-x: clip` reste nécessaire
   pour l'axe X (couper le texte qui sort de la largeur animée), mais ne protège plus les dépassements d'encre
   verticaux ni le débord gauche — d'où la réserve en padding sur les TROIS autres côtés ci-dessous, compensée par
   une marge négative égale pour ne rien changer à la mise en page (la boîte peint plus large, mais n'occupe pas plus
   de place). */
.titre-rotatif__texte {
	display: inline-block;
	/* Le site entier est en box-sizing:border-box (voir "Bandeau à filets") : override explicite en content-box,
	   pour que la largeur posée par le script (JS) reste la largeur du TEXTE seul, jamais celle du texte + réserve. */
	box-sizing: content-box;
	overflow-x: clip;
	overflow-y: visible;
	white-space: nowrap;
	/* Réserve en padding sur les quatre côtés, jamais dans le masque seul (voir le piège ci-dessus et, pour le bord
	   droit, le commentaire plus bas sur les deux zones opaque/dégradé) :
	   - haut/bas (--titre-masque-haut/-bas) : couvre le dépassement d'encre vertical (ascendantes/descendantes des
	     paraphes de Glamour Palace), compensé par une marge négative ÉGALE sur ces deux côtés (aucun effet sur la
	     hauteur de ligne ni la position du texte) ;
	   - gauche (--titre-masque-gauche) : couvre le débord du premier jambage (mesuré : le « q » de « qui », commun
	     aux trois variantes, déborde très légèrement AVANT le bord gauche de la boîte), compensé de la même façon ;
	   - droite : AUCUNE compensation (la réserve droite est un espace fonctionnel voulu, pour le dégradé et
	     l'étoile, pas un correctif invisible) — voir le commentaire dédié.
	   La marge du bas cumule aussi la correction de ligne de base de l'étoile (--titre-etoile-ligne-base) : voir
	   `align-items:flex-end` de `.script`, plus haut. */
	padding: var(--titre-masque-haut) calc(var(--titre-masque-opaque) + var(--titre-masque-degrade)) var(--titre-masque-bas) var(--titre-masque-gauche);
	margin: calc(var(--titre-masque-haut) * -1) 0 calc((var(--titre-masque-bas) + var(--titre-etoile-ligne-base)) * -1) calc(var(--titre-masque-gauche) * -1);
	/* Réserve permanente à DROITE du texte, en DEUX zones dans le padding : d'abord OPAQUE (--titre-masque-opaque,
	   couvre le dépassement d'encre à droite mesuré jusqu'à 16,23px à 1081/1440px, soit 0,282em du corps du script,
	   constant à toutes les tailles — jamais pâlie, sinon la dernière lettre semblerait s'effacer au repos), puis en
	   DÉGRADÉ (--titre-masque-degrade, fondu vers transparent, juste avant l'étoile). C'est cette réserve permanente
	   qui évite qu'une lettre ne pâlisse d'un coup au démarrage : la première portion effacée par le rétrécissement
	   de la boîte est toujours cette zone vide, jamais une lettre. */
	-webkit-mask-image: linear-gradient(to right, black calc(100% - var(--titre-masque-degrade)), transparent 100%);
	mask-image: linear-gradient(to right, black calc(100% - var(--titre-masque-degrade)), transparent 100%);
}

/* Apparition animée du premier suffixe au chargement — DEUXIÈME exception à la règle de mouvement du thème (voir
   CLAUDE.md, « Couches de fond » : la première est la rotation elle-même, sans bouton pause). Plutôt que d'afficher
   le premier texte d'un coup, le script attend la police prête + le hero visible + un court délai
   (--titre-apparition-delai-initial), puis joue la même apparition (masque + étoile) que pour un changement de
   variante normal. Le texte brut (avant que le script n'ait construit `.titre-rotatif__texte`) est donc masqué
   pendant une fenêtre brève — jamais indéfiniment : un FILET DE SÉCURITÉ en CSS seul, indépendant du script, révèle
   texte et étoile par un simple fondu si `titre-rotatif--pilote` n'a pas encore été posé. Ce marqueur est ajouté par
   le script UNIQUEMENT au moment où il est prêt à jouer sa propre apparition (jamais à son démarrage, voir
   assets/js/titre-rotatif.js) : jusque-là, le filet tourne pour son propre compte, sans savoir si le script existe.
   Deux façons dont le filet peut arriver le premier, sans script cassé : police plus lente que 2,5s (JS termine par
   jouer SON apparition normalement, un peu plus tard) ; script bloqué ou en échec (le filet ne rencontre jamais
   `--pilote`, le texte reste révélé et STATIQUE — comme les autres composants du thème dont le script est en échec,
   voir « Onglets »). Réserve à droite IDENTIQUE à celle de `.titre-rotatif__texte` (opaque + dégradé) posée sur le
   span brut lui-même : aucun changement de largeur/centrage au moment où le script prend la main (mesures dans
   PASSATION.md). Pas de réserve gauche/verticale à répliquer : elles sont déjà annulées par une marge négative égale
   côté `.titre-rotatif__texte` (voir plus haut), sans effet sur son propre encombrement. */
@media (prefers-reduced-motion: no-preference) {
	.js .titre-rotatif--actif:not(.titre-rotatif--pilote) .script {
		padding-right: calc(var(--titre-masque-opaque) + var(--titre-masque-degrade));
		opacity: 0;
	}

	.js .titre-rotatif--actif:not(.titre-rotatif--pilote) .script,
	.js .titre-rotatif--actif:not(.titre-rotatif--pilote) .script::after {
		animation: titre-filet-reveal var(--titre-filet-fondu) var(--titre-filet-delai) forwards;
	}

	.js .titre-rotatif--actif:not(.titre-rotatif--pilote) .script::after {
		opacity: 0;
	}
}

@keyframes titre-filet-reveal {
	to {
		opacity: 1;
	}
}

@media (prefers-reduced-motion: no-preference) {
	/* Horloge : animation d'attente sur un pseudo-élément VIDE de 1px (transparent, hors flux) du TITRE (le H1,
	   `.titre-rotatif`, distinct de l'étoile qui est le ::after de `.script`), en opacité : animation composée,
	   aucun travail sur le fil principal pendant l'attente (mesuré : 0 image sur 5 s, contre ~60 par seconde avec
	   une propriété personnalisée). Sa fin (`animationend`) déclenche l'effacement. Pause = même animation, en
	   pause. Inchangée depuis la session 62 : ne pilote plus que la phase d'attente (2,5s), l'effacement et
	   l'apparition sont désormais des Animation (API Web Animations) créées par le script, pas des animations CSS. */
	.titre-rotatif.is-rotation::after {
		content: "";
		position: absolute;
		width: 1px;
		height: 1px;
		opacity: 0;
		pointer-events: none;
		animation: titre-attente var(--titre-pause) linear forwards;
	}

	.titre-rotatif.is-rotation.is-en-pause::after {
		animation-play-state: paused;
	}

	/* Même mécanisme (::after du H1, animationend), classe et keyframe DISTINCTES : délai avant la première
	   apparition (voir plus haut), jamais confondu avec l'attente entre deux variantes (`is-rotation`). Les deux
	   classes ne sont jamais posées en même temps (le script retire l'une avant de poser l'autre). Non mis en pause
	   par `appliquerPause()` (fenêtre de 150ms au maximum, hero déjà confirmé visible pour l'atteindre) : limite
	   acceptée, voir CLAUDE.md. */
	.titre-rotatif.is-attente-initiale::after {
		content: "";
		position: absolute;
		width: 1px;
		height: 1px;
		opacity: 0;
		pointer-events: none;
		animation: titre-delai-initial var(--titre-apparition-delai-initial) linear forwards;
	}
}

@keyframes titre-attente {
	to {
		opacity: .01;
	}
}

@keyframes titre-delai-initial {
	to {
		opacity: .01;
	}
}

/* Boutons : transition au survol (global, comme styles.elements.button) */
.wp-element-button,
.wp-block-button__link {
	transition:
		background-color var(--wp--custom--transition--duration) var(--wp--custom--transition--easing),
		color var(--wp--custom--transition--duration) var(--wp--custom--transition--easing);
}

@media (prefers-reduced-motion: reduce) {
	.wp-element-button,
	.wp-block-button__link {
		transition: none;
	}
}

/* Bordure corail autour des boutons à fond accent (par défaut + variations Télécharger/Formulaire,
   qui ne redéfinissent pas leur couleur) — jamais sur les boutons "en contour" (fond déjà
   transparent, bordure différente déjà en place), LinkedIn (fond accent-2) ni le formulaire de
   contact (fond blanc) : tous hors du périmètre "fond corail". box-sizing:border-box (site entier)
   : la bordure mord sur le padding existant, ne change pas la taille du bouton. */
.wp-block-button:not(.is-style-contour-accent, .is-style-outline, .is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link,
.wp-element-button:not(.wp-block-mcleod-contact .wp-element-button) {
	/*border: 1px solid var(--wp--preset--color--accent);*/
}

/* Même piège que le calque de remplissage (voir plus bas) : Contour accent et LinkedIn sont exclus
   de la première branche (via le wrapper .wp-block-button) mais rattrapés par la seconde
   (.wp-element-button, que TOUT lien de core/button porte aussi) — neutralisé ici, à spécificité
   égale (3 classes) et après dans le fichier. Outline n'a pas besoin de ce correctif : sa propre
   règle de contexte (pied de page, plus bas) est déjà à 3 classes et déjà après celle-ci. */
.wp-block-button.is-style-contour-accent > .wp-block-button__link {
	border-width: 2px;
}

.wp-block-button.is-style-bouton-linkedin > .wp-block-button__link,
.wp-block-button.is-style-bouton-substack > .wp-block-button__link {
	border-width: 0;
}

/* Changement de couleur au survol par translation verticale (bas → haut), pour tous
   les boutons PLEINS (y compris les .wp-element-button natifs du formulaire de
   contact) — remplace le saut plat ci-dessus (simple changement de background-color)
   par une couche qui se révèle depuis le bas. Couleur de la couche portée par
   --bouton-survol, réglée une fois ici (défaut : color-mix accent 82%+noir,
   identique à la couleur de survol déjà en place pour le bouton par défaut et les
   variations Télécharger/Formulaire, qui ne redéfinissent pas leur couleur) puis
   recontextualisée ci-dessous où le fond du bouton diffère — même logique que
   --fill/--base du scrollspy du menu (voir plus bas), une règle de contexte plutôt
   qu'un réglage d'instance.
   Exclus : les boutons "en contour" (fond transparent au repos) — "Contour accent"
   (styles/bouton-contour-accent.json, sa propre inversion de couleur est déjà en
   place plus bas dans ce fichier) et "Outline" natif du cœur (filet du pied de
   page) — remplir un bouton pensé comme creux est une décision visuelle à part,
   non prise ici.
   background-position n'est jamais transitionné, seul background-size l'est (même
   mécanique que le scrollspy) : c'est ce bord net, sans interpolation de position,
   qui donne son sens au remplissage plutôt qu'à un fondu. */
.wp-block-button:not(.is-style-contour-accent, .is-style-outline) > .wp-block-button__link,
.wp-element-button {
	--bouton-survol: color-mix(in oklab, var(--wp--preset--color--accent-2) 80%, black);

	background-image: linear-gradient(var(--bouton-survol), var(--bouton-survol));
	background-repeat: no-repeat;
	background-position: left bottom;
	background-size: 100% 0%;
	transition:
		background-size var(--wp--custom--transition--duration) var(--wp--custom--transition--easing),
		background-color var(--wp--custom--transition--duration) var(--wp--custom--transition--easing),
		color var(--wp--custom--transition--duration) var(--wp--custom--transition--easing);
}

/* Chaque lien de core/button porte AUSSI .wp-element-button (vérifié dans le HTML
   servi, les deux classes coexistent toujours) : la branche générique de la règle
   ci-dessus le rattrape donc même sous un wrapper exclu par le :not() (qui ne
   protège que la branche .wp-block-button__link). Neutralisé ici, explicitement,
   pour les deux variations "en contour". */
.is-style-contour-accent > .wp-block-button__link,
.pied-de-page .is-style-outline > .wp-block-button__link {
	background-image: none;
}

/* État piloté par le script (assets/js/boutons-survol.js) : le fondu plat ci-dessus
   cède la main (background-image repassé à none, en style inline posé par le script
   — un inline bat toute règle de classe, pas de bataille de spécificité à gérer) à
   une ellipse (.bouton-filler, un vrai <span> inséré par le script, pas un
   pseudo-élément : ::before/::after sont déjà pris par les pictos de Télécharger/
   Formulaire/LinkedIn) qui monte depuis le bas à l'entrée et CONTINUE vers le haut à
   la sortie — jamais un retour en arrière, ce qu'une transition CSS ne sait pas
   faire seule (elle revient toujours par le même chemin).
   overflow:hidden rogne l'ellipse au gabarit du bouton ; un box-shadow serait rogné
   de la même façon (il peint hors de la boîte) — l'ombre passe donc en
   filter:drop-shadow (même recette exacte que le token, survit au rognage),
   neutralisée dans les mêmes contextes que le box-shadow plus haut (panneaux, pied
   de page). z-index:0 sur le bouton + z-index:-1 sur l'ellipse : la gardent sous le
   TEXTE (contenu réel du bouton) tout en restant au-dessus du fond du bouton. */
.bouton-survol--pilote {
	position: relative;
	z-index: 0;
	overflow: hidden;
	box-shadow: none;
	filter: drop-shadow(var(--wp--preset--shadow--sm));
}

.is-style-panel-clair .bouton-survol--pilote,
.is-style-panel-indigo .bouton-survol--pilote,
.pied-de-page .bouton-survol--pilote {
	filter: none;
}

.bouton-filler {
	position: absolute;
	z-index: -1;
	top: -50%;
	left: -25%;
	width: 150%;
	height: 200%;
	border-radius: 50%;
	background-color: var(--bouton-survol);
	transform: translateY(75%);
	pointer-events: none;
}

/* LinkedIn (fond accent-2) : jusqu'ici "aucun état de survol" (son propre fond,
   plus spécifique que le survol générique, bloquait le changement de couleur) —
   reçoit maintenant la même mécanique que les autres, avec SA couleur. */
.wp-block-button.is-style-bouton-linkedin > .wp-block-button__link,
.wp-block-button.is-style-bouton-substack > .wp-block-button__link {
	--bouton-survol: color-mix(in oklab, var(--wp--preset--color--accent-2) 82%, black);
}

/* Formulaire de contact (boutons blancs, blocks/contact/style.css) : même mécanique, survol en
   base (sombre) — même couleur que le calque JS (blocks/contact/style.css), pour le repli sans JS. */
.wp-block-mcleod-contact .wp-element-button {
	--bouton-survol: var(--wp--preset--color--base);
}

.wp-block-button:not(.is-style-contour-accent, .is-style-outline) > .wp-block-button__link:is(:hover, :focus-visible),
.wp-element-button:is(:hover, :focus-visible) {
	background-size: 100% 100%;
}

@media (prefers-reduced-motion: reduce) {
	.wp-block-button:not(.is-style-contour-accent, .is-style-outline) > .wp-block-button__link,
	.wp-element-button {
		transition: none;
	}
}

/* Boutons "Télécharger"/"Formulaire" (variations is-style-bouton-telecharger/
   is-style-bouton-formulaire sur core/button) : picto décoratif à droite du texte,
   qui seul porte le sens. Taille en em (relative au texte du bouton, comme les
   pictos de Pastille/Élément à picto) — non exprimable en token theme.json.
   is-style-X est posé par WP sur le wrapper .wp-block-button, jamais sur le lien
   lui-même : le picto est donc un ::after sur .wp-block-button__link, coloré en
   currentColor via mask-image (background-color visible, mask = juste la forme). */
.is-style-bouton-telecharger .wp-block-button__link,
.is-style-bouton-formulaire .wp-block-button__link,
.is-style-bouton-linkedin .wp-block-button__link,
.is-style-bouton-substack .wp-block-button__link {
	display: inline-flex;
	align-items: center;
	gap: 0.5em;
}

.is-style-bouton-telecharger .wp-block-button__link::after,
.is-style-bouton-formulaire .wp-block-button__link::after,
.is-style-bouton-linkedin .wp-block-button__link::before,
.is-style-bouton-substack .wp-block-button__link::before {
	content: "";
	display: block;
	width: 1.25em;
	height: 1.25em;
	background-color: currentColor;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
}

.is-style-bouton-telecharger .wp-block-button__link::after {
	-webkit-mask-image: url("assets/icons/telecharger.svg");
	mask-image: url("assets/icons/telecharger.svg");
}

.is-style-bouton-formulaire .wp-block-button__link::after {
	-webkit-mask-image: url("assets/icons/enveloppe.svg");
	mask-image: url("assets/icons/enveloppe.svg");
}

/* "LinkedIn" : même mécanique, picto à GAUCHE du texte (::before). Tracé issu de
   core/social-link, normalisé dans assets/icons/linkedin.svg. Couleurs dans
   styles/bouton-linkedin.json. */
.is-style-bouton-linkedin .wp-block-button__link::before {
	-webkit-mask-image: url("assets/icons/linkedin.svg");
	mask-image: url("assets/icons/linkedin.svg");
}

/* "Substack" : jumeau de LinkedIn (mêmes couleurs, styles/bouton-substack.json, picto à gauche). Tracé issu de
   Simple Icons (CC0), une seule forme pleine, normalisé dans assets/icons/substack.svg. */
.is-style-bouton-substack .wp-block-button__link::before {
	-webkit-mask-image: url("assets/icons/substack.svg");
	mask-image: url("assets/icons/substack.svg");
}

/* LinkedIn/Substack : bouton rond (picto seul), qui s'élargit au survol/focus clavier pour révéler le texte.
   Le texte reste TOUJOURS dans le DOM et dans la boîte (nom accessible du lien) : il est seulement rogné par
   overflow:hidden au-delà du cercle. Cercle = cible (44px, WCAG 2.5.8), padding latéral calculé pour centrer le
   picto (1.25em) ; radius pill hérité de theme.json (cercle replié, pilule déployée).
   max-width plutôt que width : width:auto ne se transitionne pas partout. 20em est une borne haute (texte
   actuel ≈ 16,5em déployé) : à la fermeture, le rétrécissement visible démarre avec un léger retard (la
   portion 20em → largeur réelle ne se voit pas). Allonger le texte au-delà de 20em le ferait rogner. */
.wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link {
	--bouton-rs-retrait: calc((var(--bouton-rs-taille, var(--wp--preset--dimension--cible)) - var(--bouton-rs-picto, 1.25em)) / 2);
	--bouton-rs-duree: 0.7s;

	height: var(--bouton-rs-taille, var(--wp--preset--dimension--cible));
	max-width: var(--bouton-rs-taille, var(--wp--preset--dimension--cible));
	padding-block: 0;
	padding-inline: var(--bouton-rs-retrait);
	overflow: hidden;
	white-space: nowrap;
	/* Texte au repos invisible : c'est un nœud texte nu (aucune balise à laquelle poser une opacity), le fondu
	   passe donc par sa couleur, transparente au repos. Le texte reste dans l'arbre d'accessibilité. */
	color: transparent;
	transition:
		max-width var(--bouton-rs-duree) var(--wp--custom--transition--easing),
		padding-inline-end var(--bouton-rs-duree) var(--wp--custom--transition--easing),
		background-size var(--bouton-rs-duree) var(--wp--custom--transition--easing),
		background-color var(--bouton-rs-duree) var(--wp--custom--transition--easing),
		color var(--bouton-rs-duree) var(--wp--custom--transition--easing);
}

/* Le picto ne se tasse jamais quand la boîte est plus étroite que son contenu. Sa couleur ne suit plus
   currentColor (transparent au repos, ci-dessus) : fixée sur celle du texte déployé. */
.wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link::before {
	flex-shrink: 0;
	background-color: var(--wp--preset--color--contrast);
}

.wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link:is(:hover, :focus-visible) {
	max-width: 20em;
	padding-inline-end: var(--wp--preset--spacing--md);
	color: var(--wp--preset--color--contrast);
}

/* Côte à côte en permanence : sans nowrap, le bouton voisin passerait à la ligne quand l'autre s'élargit. */
.pied-de-page__identite .wp-block-buttons:has(.is-style-bouton-linkedin, .is-style-bouton-substack) {
	flex-wrap: nowrap;
	gap: var(--wp--preset--spacing--stack);
}

@media (prefers-reduced-motion: reduce) {
	.wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link {
		transition: none;
	}
}

/* Bandeau à filets : conteneur pleine hauteur avec filet haut/bas, padding vertical
   lg, padding horizontal sm (écart avec le bord du bandeau, contenu décalé vers
   l'intérieur sans changer la largeur du bandeau lui-même — box-sizing:border-box).
   Générique (pas propre aux cartes-picto) — la largeur reste portée par l'attribut
   de bloc align:"wide" du conteneur, jamais dupliquée ici en CSS. .cartes-picto en
   hérite sans changement de rendu (padding vertical déjà identique en attribut de
   bloc, filets identiques). */
.bandeau-filets,
.cartes-picto {
	border-top: 1px solid var(--wp--preset--color--line);
	border-bottom: 1px solid var(--wp--preset--color--line);
	padding-top: var(--wp--preset--spacing--lg);
	padding-bottom: var(--wp--preset--spacing--lg);
	padding-left: var(--wp--preset--spacing--sm);
	padding-right: var(--wp--preset--spacing--sm);
}

/* Mobile : bandeau bord à bord (filets d'un bord à l'autre de l'écran), contenu retiré du root padding
   — aligné sur les sections voisines. Le root padding passe de la section parente (has-global-padding,
   remis à 0) au bandeau lui-même ; le bandeau reste align:"wide" (sa largeur max ne joue pas à cette
   taille), aucune marge négative à écrire contre le margin-inline:auto !important du layout constrained. */
@media (max-width: 781px) {
	.wp-block-group.has-global-padding:has(> .bandeau-filets) {
		padding-left: 0;
		padding-right: 0;
	}

	.bandeau-filets {
		padding-left: var(--wp--style--root--padding-left);
		padding-right: var(--wp--style--root--padding-right);
	}
	.cartes-picto{
		padding-left:0;
		padding-right:0;
	}
}

/* Grille : autant de colonnes égales que de cartes (2, 3 ou 4), gap du token lg.
   Groupe par défaut (pas de layout natif imposé) + CSS Grid sur la classe, comme
   .about-media plus haut : le layout natif de WordPress ("flow"/"constrained")
   ajouterait une marge automatique entre les cartes (24px par défaut) qu'il
   faudrait de toute façon neutraliser ; autant piloter tout l'espacement par le
   gap de la grille, seule source de vérité. */
.cartes-picto__grille {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
	gap: var(--wp--preset--spacing--lg);
}

.cartes-picto__grille > * {
	margin-block-start: 0;
}

/* Carte : picto au-dessus par défaut (desktop, et tablette à 2 ou 3 cartes) */
.cartes-picto__carte {
	display: grid;
	grid-template-columns: 1fr;
}

.cartes-picto__carte > * {
	margin-block-start: 0;
}

.cartes-picto__carte .wp-block-icon svg {
	width: var(--wp--preset--dimension--icon-md);
	height: auto;
}

.cartes-picto__carte .wp-block-icon {
	margin-bottom: var(--wp--preset--spacing--md);
}

.cartes-picto__carte .wp-block-heading {
	margin-bottom: var(--wp--preset--spacing--stack);
}

/* Tablette, uniquement à 4 cartes : 2 colonnes (2+2), picto à gauche du bloc
   titre+texte dans chaque carte. Détection du nombre de cartes en CSS via
   :has() : la grille a un 4e enfant mais pas de 5e. Ordre du DOM inchangé
   (picto, titre, texte) : seul le placement en grille change visuellement. */
@media (min-width: 782px) and (max-width: 1080px) {
	.cartes-picto__grille:has(> .cartes-picto__carte:nth-child(4)):not(:has(> .cartes-picto__carte:nth-child(5))) {
		grid-template-columns: repeat(2, 1fr);
	}

	.cartes-picto__grille:has(> .cartes-picto__carte:nth-child(4)):not(:has(> .cartes-picto__carte:nth-child(5))) .cartes-picto__carte {
		grid-template-columns: auto 1fr;
		column-gap: var(--wp--preset--spacing--md);
		align-items: start;
	}

	.cartes-picto__grille:has(> .cartes-picto__carte:nth-child(4)):not(:has(> .cartes-picto__carte:nth-child(5))) .cartes-picto__carte .wp-block-icon {
		grid-column: 1;
		grid-row: 1 / 3;
		margin-bottom: 0;
	}

	.cartes-picto__grille:has(> .cartes-picto__carte:nth-child(4)):not(:has(> .cartes-picto__carte:nth-child(5))) .cartes-picto__carte .wp-block-heading {
		grid-column: 2;
		grid-row: 1;
	}

	.cartes-picto__grille:has(> .cartes-picto__carte:nth-child(4)):not(:has(> .cartes-picto__carte:nth-child(5))) .cartes-picto__carte p {
		grid-column: 2;
		grid-row: 2;
	}
}

/* Mobile : une colonne, picto à gauche du bloc titre+texte dans chaque carte,
   taille réduite (icon-sm). Même principe de placement en grille qu'en
   tablette à 4 cartes, sans condition de nombre : en dessous de 782px, toutes
   les grilles passent en une colonne quel que soit le nombre de cartes. */
@media (max-width: 781px) {
	.cartes-picto__grille {
		grid-template-columns: 1fr;
	}

	.cartes-picto__carte {
		grid-template-columns: auto 1fr;
		column-gap: var(--wp--preset--spacing--md);
		align-items: start;
	}

	.cartes-picto__carte .wp-block-icon {
		grid-column: 1;
		grid-row: 1 / 3;
		margin-bottom: 0;
	}

	.cartes-picto__carte .wp-block-heading {
		grid-column: 2;
		grid-row: 1;
	}

	.cartes-picto__carte p {
		grid-column: 2;
		grid-row: 2;
	}

	/* La taille choisie dans l'éditeur (style inline sur le <svg>, réglage de
	   largeur du bloc Icône) doit céder la place à icon-sm sur mobile : c'est
	   voulu, un picto de carte ne doit jamais dépasser cette taille sur petit
	   écran, quel que soit le réglage fait ailleurs. D'où le !important, seul
	   moyen de l'emporter sur un style inline. */
	.cartes-picto__carte .wp-block-icon svg {
		width: var(--wp--preset--dimension--icon-sm) !important;
	}

	
}

/* Pastille (variation de style "is-style-pill" sur core/group) : ce que theme.json
   ne sait pas exprimer (tailles relatives à la taille du texte de la pastille, en
   em, pas en token fixe) ou n'a pas de mécanisme de preset pour ça (text-transform,
   couleur par défaut du picto), même logique que .eyebrow plus haut. */
.is-style-pill {
	display: inline-flex;
	align-items: center;
	gap: 0.4em; /* écart picto/texte */
	padding: 0.20em 1.9em 0.35em 0.9em; /* padding pilule, relatif à la taille du texte */
	font-variant-caps: all-small-caps;
	white-space: nowrap; /* le texte ne passe jamais à la ligne dans la pastille */
}

.is-style-pill .wp-block-icon {
	margin: 0;
}

.is-style-pill .wp-block-icon svg {
	width: 1em;
	height: 1em;
	/* Couleur par défaut du picto : accent-3, distincte du texte (contrast) posé par
	   pill.json. currentColor sur les <path> des SVG mcleod hérite de cette valeur. */
	color: var(--wp--preset--color--accent-3);
}

.is-style-pill p {
	margin: 0;
}

/* Pastille "variante accent" (classe additionnelle, pas une variation de style à
   part : même composant Pastille, fond accent/corail au lieu d'accent-2) — statut
   "à venir" sur les cartes Ressources. Charge après le CSS global (ordre des
   feuilles), bat la couleur de fond de .is-style-pill à spécificité égale. Sans
   picto : padding droit ramené à la même valeur que le gauche (0.9em), l'écart
   d'origine (1.9em) ne compensait qu'un picto absent ici. */
.pastille-accent {
	background-color: var(--wp--preset--color--accent);
	padding-right: 0.9em;
}

/* Liste de pastilles (variation "yvan-perso/liste-pastilles" sur core/group en ul) :
   reset des styles de liste par défaut du navigateur, l'espacement vient du gap du
   layout flex (attribut de bloc, token stack). */
.pastilles-liste {
	list-style: none;
	margin: 0;
	padding: 0;
}

/* Liste à pictos (variation "yvan-perso/liste-picto" sur core/group en ul, layout
   "default"). Contrairement à .pastilles-liste (layout flex, gap en CSS gap — jamais
   en conflit avec une marge), ce layout simule l'écart entre enfants par une marge
   haute générée dynamiquement par le cœur (margin-block-start sur le conteneur
   parent). Ne jamais réinitialiser marge haut/bas ici (margin: 0 les écraserait,
   même spécificité qu'une classe, chargée après = toujours gagnante) : seules les
   marges gauche/droite sont à zéro (déjà le cas par défaut navigateur, gardé pour
   documenter l'intention). */
.liste-a-pictos {
	list-style: none;
	margin-inline: 0;
	padding: 0;
}

/* Élément à picto (variation "is-style-picto-item" sur core/group, composant "Liste
   à pictos") : ce que theme.json ne sait pas exprimer (grille picto/texte, picto
   aligné sur la première ligne, tailles en em relatives au texte "chapo" posé par
   picto-item.json ; filet séparateur au-dessus de chaque élément, y compris le
   premier — aucun n'est posé sous le dernier, il n'y en a simplement jamais). */
.is-style-picto-item {
	display: grid;
	grid-template-columns: auto 1fr;
	column-gap: 0.6em;
	align-items: start;
	border-top: 1px solid var(--wp--preset--color--line);
}

/* Hauteur d'une ligne (1lh) : le picto se centre dans cette hauteur, donc sur le
   milieu de la première ligne du texte, pas sur le haut de sa propre boîte.
   line-height réaffecté : core/icon pose line-height:0 sur .wp-block-icon
   (wp-includes/blocks/icon/style.css), ce qui annulerait 1lh (relatif à la
   line-height de l'élément lui-même) sans ce rétablissement. */
.is-style-picto-item .wp-block-icon {
	display: flex;
	align-items: center;
	line-height: inherit;
	height: 1lh;
	margin: 0;
}

.is-style-picto-item .wp-block-icon svg {
	width: 2em;
	height: 2em;
	color: var(--wp--preset--color--accent);
}

.is-style-picto-item p {
	margin: 0;
}

/* Cartes ressources : grille responsive (points de rupture du thème).
   Desktop (> 1080px, par défaut) : 3 colonnes inégales (2fr 2fr 3fr), gap lg. */
.cartes-ressources__grille {
	display: grid;
	grid-template-columns: 2fr 2fr 3fr;
	gap: var(--wp--preset--spacing--sm);
}

/* Carte en colonne flex : le bouton (dernier enfant) est poussé en bas par une
   marge automatique, ce qui aligne les boutons entre cartes d'une même ligne —
   la grille étire déjà les cartes à la même hauteur par défaut (comportement
   natif de CSS Grid, aucune règle supplémentaire nécessaire pour ça).
   .wp-block-buttons : la spécificité de ce sélecteur (deux classes) l'emporte
   sur la marge générée par le blockGap de la carte (une classe) — un seul écart
   final s'applique, jamais deux qui s'additionnent. padding-block-start double
   la garantie : même si "auto" ne produisait aucun espace (carte qui remplit
   exactement sa hauteur), l'écart minimum resterait garanti par le padding. */
.cartes-ressources__carte {
	display: flex;
	flex-direction: column;
	padding: var(--wp--preset--spacing--md);
}

.cartes-ressources__carte .wp-block-buttons {
	margin-block-start: auto;
	margin-block-end: 0;
	padding-block-start: var(--wp--preset--spacing--md);
}

/* Boutons « Me prévenir de la sortie » (is-style-outline) : le remplissage au survol
   (.bouton-filler, assets/js/boutons-survol.js) prend la couleur du filet du bouton
   dans sa carte, le texte s'inverse pour rester lisible sur ce remplissage. Le filet
   est en border-color:currentColor (style généré par le cœur pour is-style-outline) :
   sans le figer ici, il suivrait la nouvelle couleur du texte au survol.
   Carte claire (check-list) : filet base, texte surface au survol. */
.cartes-ressources__carte.is-style-panel-clair .is-style-outline > .wp-block-button__link .bouton-filler {
	background-color: var(--wp--preset--color--base);
}

.cartes-ressources__carte.is-style-panel-clair .is-style-outline > .wp-block-button__link:is(:hover, :focus-visible) {
	border-color: var(--wp--preset--color--base);
	color: var(--wp--preset--color--surface);
}

/* Carte indigo (autres ressources) : filet foreground, texte accent-2 au survol. */
.cartes-ressources__carte.is-style-panel-indigo .is-style-outline > .wp-block-button__link .bouton-filler {
	background-color: var(--wp--preset--color--foreground);
}

.cartes-ressources__carte.is-style-panel-indigo .is-style-outline > .wp-block-button__link:is(:hover, :focus-visible) {
	border-color: var(--wp--preset--color--foreground);
	color: var(--wp--preset--color--accent-2);
}

/* Bouton outline du hero (« Évaluez votre site ») : au repos, filet dans la couleur de texte
   choisie dans l'éditeur (classe de preset has-*-color sur le lien, filet du cœur en
   currentColor), texte dans la couleur du corps (foreground). Le texte passe par
   -webkit-text-fill-color, jamais par color : color doit rester la couleur de l'admin pour que
   le filet la suive. Au survol : remplissage accent-2, filet accent-2, texte contrast. Le
   remplissage est celui de tous les boutons (--bouton-survol : .bouton-filler avec le script,
   background-size en repli sans JS ou sous prefers-reduced-motion), seule sa couleur change ici.
   La transition reprend celle des boutons pleins, plus border-color. */
.is-style-hero .is-style-outline > .wp-block-button__link {
	--bouton-survol: var(--wp--preset--color--accent-2);

	/* Filet 1px au lieu des 2px de la variation Outline du cœur (:root :where(), 0,1,0). */
	border-width: 1px;
	-webkit-text-fill-color: var(--wp--preset--color--foreground);

	transition:
		background-size var(--wp--custom--transition--duration) var(--wp--custom--transition--easing),
		border-color var(--wp--custom--transition--duration) var(--wp--custom--transition--easing),
		-webkit-text-fill-color var(--wp--custom--transition--duration) var(--wp--custom--transition--easing);
}

.is-style-hero .is-style-outline > .wp-block-button__link:is(:hover, :focus-visible) {
	border-color: var(--wp--preset--color--accent-2);
	-webkit-text-fill-color: var(--wp--preset--color--contrast);
}

@media (prefers-reduced-motion: reduce) {
	.is-style-hero .is-style-outline > .wp-block-button__link {
		transition: none;
	}
}

/* Titre de la carte indigo : 2.2rem à toutes les largeurs (valeur littérale demandée par Yvan),
   au lieu du preset lg fluide posé en attribut (pattern et copie de la page 2). !important
   obligatoire : la classe de preset has-lg-font-size l'est déjà (cœur WP). Règle de contexte,
   aucun contenu modifié en base. */
.cartes-ressources__carte.is-style-panel-indigo > .wp-block-heading {
	font-size: 2.2rem !important;
}

/* Tablette : deux colonnes égales, carte 3 (dernière) sur toute la largeur. */
@media (min-width: 782px) and (max-width: 1080px) {
	.cartes-ressources__grille {
		grid-template-columns: repeat(2, 1fr);
	}

	.cartes-ressources__carte:last-child {
		grid-column: 1 / -1;
	}
}

/* Mobile : une colonne. */
@media (max-width: 781px) {
	.cartes-ressources__grille {
		grid-template-columns: 1fr;
	}
}

/* Section « Témoignages » (patterns/temoignages.php, inc/champs-client.php) — session 7a. Rendu statique, aucun
   JS : tous les avis empilés, lisibles sans script. Navigation entre avis (points, animation) hors périmètre
   (session 7b). Colonne de logos animés hors périmètre (session ultérieure) : les colonnes 9-12 de la grille
   restent réservées, sans bloc, sans overflow/isolation/z-index/transform sur la section (règle du thème pour
   tout futur élément qui devra sortir visuellement des sections voisines, voir CLAUDE.md « Couches de fond »).
   Grille 12 colonnes déclarée une seule fois : la zone avis occupe les colonnes 2 à 7 au-dessus de 1080px
   (colonne 1 et 8 vides de part et d'autre, réserve logos en 9-12), pleine largeur en dessous. */
.temoignages {
	--temoignages-colonnes: 12;
	--temoignages-gouttiere: var(--wp--preset--spacing--md);
	/* Navigation entre avis : durée et courbe du fondu, lues par le script via getComputedStyle. */
	--temoignages-fondu-duree: 300ms;
	--temoignages-fondu-courbe: ease;
	/* Swipe : distance horizontale minimale (px, nombre sans unité) pour changer d'avis au relâchement. */
	--temoignages-swipe-seuil: 50;
	display: grid;
	grid-template-columns: repeat(var(--temoignages-colonnes), minmax(0, 1fr));
	gap: var(--temoignages-gouttiere);
}

.temoignages__avis {
	grid-column: 2 / 8;
	padding-block: var(--wp--preset--spacing--xl);
}

@media (max-width: 1080px) {
	/* Une seule colonne : toutes les zones occupent déjà 1 / -1 à cette largeur. Les 12 colonnes ne
	   servent qu'au-delà de 1080px ; gardées ici, leurs 11 gouttières md (352px) dépassaient la largeur
	   disponible à 375px (311px) et la bande de logos, élargie de ±32px, faisait défiler la page
	   horizontalement (417px de large, mesuré). */
	.temoignages {
		grid-template-columns: minmax(0, 1fr);
	}

	.temoignages__avis {
		grid-column: 1 / -1;
		/* Padding bas retiré, SEULEMENT ici (le haut reste xl, rythme avec la section précédente) : en colonne
		   (>1080px), ce padding équilibre l'avis par rapport au panneau de logos voisin ; empilé, il s'ADDITIONNE
		   au gap de grille (md, 32px) avant la bande de logos — même piège que les deux paddings verticaux déjà
		   documentés ailleurs dans ce fichier (« Texte et liste »/« Accroche centrée »). Mesuré avant correctif :
		   jusqu'à 222px d'écart visuel (1080px) entre le dernier avis et la bande ; avec ce correctif, seul le
		   gap de grille (32px) reste. */
		padding-block-end: 0;
	}
}

/* Collage : la section n'a pas de marge propre (attribut margin-top:0, comme CTA panneau), et la section
   SUIVANTE (Cartes ressources) ne doit pas recevoir le blockGap de post-content (24px, :root :where(.is-layout-
   constrained) > *, spécificité 0,1,0 à cause du :root) — règle de contexte, jamais un attribut sur l'instance
   voisine (voir « Méthode de travail »). Deux classes (0,2,0) pour battre à coup sûr cette règle du cœur, sans
   dépendre de l'ordre de chargement (le même choix que le footer, `.wp-site-blocks:has(.cta-panneau) > footer`,
   pour la même raison). */
.entry-content .section-temoignages + * {
	margin-top: 0;
}

/* Empilement sans JS : gap CSS + margin:0 sur les <li> (repli flow 24px neutralisé, piège déjà connu, voir
   « Cartes ressources »), filet 1px line entre deux témoignages (bordure haute, jamais sur le premier). */
.temoignages__avis .wp-block-post-template {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--lg);
	list-style: none;
	margin: 0;
	padding: 0;
}

.temoignages__avis .wp-block-post-template > li {
	margin: 0;
}

.temoignages__avis .wp-block-post-template > li:not(:first-child) {
	border-top: 1px solid var(--wp--preset--color--line);
}

/* figure/figcaption : reset des marges UA (le cœur ne les génère pas, ce sont des balises HTML natives).
   margin-block ET margin-inline séparés (jamais le raccourci margin), même piège que « Liste à pictos » : un
   margin générique écraserait une marge verticale générée par blockGap sans erreur ni avertissement. Ici
   margin-block:0 est sans risque (figure est fille unique d'article, blockGap ne s'applique jamais à un fils
   unique) mais on garde la forme longhand par prudence, comme sur le blockquote juste en dessous. */
.temoignage > figure {
	margin-block: 0;
	margin-inline: 0;
}

/* Écart blockquote -> figcaption à md (32px, était lg/60px avant l'espace ajouté au-dessus de la
   navigation) : remplace le blockGap sm (20px) généré par WordPress
   sur .temoignage__auteur (second enfant de la figure). Règle à 2 classes (ancêtre .temoignage + la
   classe propre du figcaption) pour battre sans ambiguïté la classe générée par instance, même logique
   que .temoignages__avis .temoignages__navigation plus bas. */
.temoignage .temoignage__auteur {
	margin-block-start: var(--wp--preset--spacing--md);
}

/* Citation : taille chapo (18px, règle de contexte — jamais en attribut de bloc), faux italique EN PLACE,
   PROVISOIRE, en l'absence de face italique de Plus Jakarta Sans (aucun fichier dans assets/fonts/, un seul
   @font-face style:normal) — non validé par Yvan, face italique à ajouter en session dédiée.
   margin-inline SEUL (jamais margin) : le blockquote porte par défaut la marge horizontale du navigateur
   (40px de chaque côté, vérifié en valeur calculée) — margin-block n'est PAS touché ici, il calcule déjà à 0px
   dans ce thème (vérifié), aucun rapport avec le blockGap de la figure (qui ne génère jamais de marge sur son
   premier enfant, seulement sur figcaption). position:relative : ancre le guillemet en lettrine (::before),
   positionné en absolu, sans transform/filter/overflow/isolation/z-index (contrainte de la section, voir
   « Couches de fond »). */
.temoignage blockquote {
	font-size: var(--wp--preset--font-size--md);
	/*font-style: italic;*/
	margin-inline: 0;
	position: relative;
	/* Taille et dépassement du guillemet en lettrine, à ajuster à l'œil par Yvan (session 7c). */
	--temoignage-guillemet-taille: 2em;
	--temoignage-guillemet-depassement: 0.5em;
}
.temoignage blockquote p:first-child{
	font-size: var(--wp--preset--font-size--chapo);
}

/* Guillemet en lettrine (session 7c, remplace le caractère « " » en Glamour Palace) : assets/images/guillemet.svg
   en mask-image (seul le canal alpha compte, la couleur du fichier source — accent en dur — n'a aucun effet,
   même mécanisme que l'étoile du titre rotatif et le chevron de la FAQ), couleur accent. Positionné en haut à
   gauche du blockquote, dépassant au-dessus de la première ligne — purement décoratif (`content:""`, jamais de
   texte), rien à exposer aux lecteurs d'écran. */
.temoignage blockquote::before {
	content: "";
	position: absolute;
	top: calc(var(--temoignage-guillemet-depassement) * -1);
	left: 0;
	width: var(--temoignage-guillemet-taille);
	height: var(--temoignage-guillemet-taille);
	background-color: var(--wp--preset--color--accent);
	-webkit-mask-image: url("assets/images/guillemet.svg");
	mask-image: url("assets/images/guillemet.svg");
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-size: contain;
	mask-size: contain;
}

/* Retrait de première ligne : laisse la place au guillemet sur la seule ligne qu'il chevauche, jamais sur les
   lignes suivantes ni sur un éventuel second paragraphe. */
.temoignage blockquote > p:first-child {
	text-indent: calc(var(--temoignage-guillemet-taille) + 0.2em);
}

.temoignage__personne {
	align-items: center;
}

.temoignage__identite {
	color: var(--wp--preset--color--foreground);
}

.temoignage__nom {
	color: var(--wp--preset--color--contrast);
	font-weight: var(--wp--custom--font-weight--strong);
}

/* Poste — entreprise sur une même ligne (session 7c). Les deux <p> sont frères adjacents dans le HTML servi ;
   le rôle disparaît entièrement du DOM si la méta est vide (filtre existant, session 7a), donc le sélecteur
   `+` ci-dessous ne matche — et le séparateur n'apparaît — que lorsque le poste existe réellement.
   `font-size:0` sur le conteneur puis restauré sur chaque enfant : neutralise l'espace blanc du HTML source
   entre les deux <p> (un texte de largeur nulle plutôt qu'un espace collapsé d'1 caractère) — l'espacement
   affiché est entièrement piloté par `content`, jamais par la mise en forme du fichier source. */
.temoignage__identite {
	font-size: 0;
}

.temoignage__nom {
	font-size: 1rem;
}

/* Poste et entreprise en sm (14px, session 7c) — règle de contexte, jamais un attribut de bloc. Le nom de
   l'auteur reste à sa taille propre (1rem, ci-dessus), seule cette ligne change. Les écarts du séparateur
   (nbsp, margin-inline-end, plus bas) sont en em : le ::before hérite du font-size de .temoignage__entreprise,
   ils suivent donc automatiquement ce nouveau sm (16px → 14px), sans rien à toucher côté séparateur. */
.temoignage__role,
.temoignage__entreprise {
	font-size: var(--wp--preset--font-size--sm);
	display: inline;
}

/* Espace insécable avant le tiret : il reste collé au poste si l'entreprise passe seule à la ligne en mobile
   (le tiret ne se retrouve jamais en début de ligne). Repli à texte alternatif : deux déclarations successives
   (la syntaxe "texte" / "alt" n'est pas supportée partout) — un navigateur qui ignore la seconde garde la
   première, un lecteur d'écran qui la supporte lit une virgule au lieu du tiret.
   PIÈGE CSS vérifié en valeur calculée : un espace normal FINAL dans une chaîne `content` est rogné par le
   navigateur (la valeur calculée se résout à `" —"`, sans le troisième caractère pourtant écrit dans la feuille
   de style — mesuré aussi en largeur, 0px attribués à cet espace) — le tiret collait à « Anko » sans espace.
   Corrigé en sortant l'espace du `content` : `margin-inline-end` sur le pseudo-élément, qui rend toujours,
   quelle que soit la chaîne. Conséquence acceptée : la ligne ne peut plus se couper entre le tiret et
   l'entreprise (ce n'est qu'une marge, pas une espace insécable) — elle se coupe dans les mots du poste, qui en
   offre déjà plusieurs ; jamais un tiret seul en fin de ligne. */
.temoignage__role + .temoignage__entreprise::before {
	content: "\00a0\2014";
	content: "\00a0\2014" / ", ";
	margin-inline-end: 0.25em;
}

/* Vignette : carrée au token icon-md, radius md, recadrée en cover (fonctionne aussi bien pour une vraie photo
   que pour le repli assets/images/avatar.svg, dont le viewBox n'est pas exactement carré). */
.champ--temoignage_photo img {
	width: var(--wp--preset--dimension--icon-md);
	height: var(--wp--preset--dimension--icon-md);
	border-radius: var(--wp--preset--border-radius--md);
	object-fit: cover;
}

/* Pastilles de prestations (session 7c) : écarts resserrés propres aux témoignages, en règle de contexte —
   jamais sur .pastilles-liste ou .pastilles-prestations elles-mêmes (classes partagées avec l'étude de cas).
   .pastilles-liste { margin:0 } (règle partagée) est sans risque ici : son layout est flex avec gap, jamais de
   marge sur les enfants (contrairement au piège de « Liste à pictos », layout default). Deux classes (ancêtre +
   pastilles-liste, 0,2,0) pour battre la règle de layout générée par le cœur sur ce bloc (gap et blockGap,
   0,1,0 à cause de sa classe unique par instance) — vérifié en valeur calculée. */
.temoignage__auteur .pastilles-liste {
	gap: calc(var(--wp--preset--spacing--stack) / 2);
	margin-block-start: var(--wp--preset--spacing--stack);
}

/* Navigation entre avis (assets/js/temoignages.js) — session 7b. Un seul avis visible à la fois, aucune rotation
   automatique. Même mécanique que les Onglets de l'étude de cas (session 6c) : état final appliqué dès le CSS
   sous `.js` pour ne jamais décaler le bloc suivant à l'exécution du script, empilement en grid-area 1/1,
   `inert` posé par le script. E = « JS actif ET au moins deux avis » (sans attendre le script) OU « le script a
   fini » (`is-navigation`) : la mise en page est la même aux deux moments. Une seule fiche : `li + li` n'existe
   pas, rien ne change (le script n'est de toute façon pas mis en file, voir inc/champs-client.php). */
:is(.js .section-temoignages:has(.wp-block-post-template > li + li), .section-temoignages.is-navigation) .wp-block-post-template {
	display: grid;
	gap: 0;
}

:is(.js .section-temoignages:has(.wp-block-post-template > li + li), .section-temoignages.is-navigation) .wp-block-post-template > li {
	grid-area: 1 / 1;
	min-width: 0;
	/* Neutralise le filet de la 7a (border-top sur chaque avis sauf le premier) : parasite en mode navigation —
	   quel que soit l'avis actif, il afficherait SA propre bordure haute (absente du premier), un filet visible
	   au-dessus du contenu actif et un décalage vertical d'1px du texte selon l'avis. Jamais touché hors de cet
	   état : la règle de la 7a (empilement sans JS) reste intacte pour le rendu sans script. */
	border-top: 0;
}

/* Avant que le script ait posé `inert` : seule la première fiche est visible (aussi la fiche active à
   l'initialisation). Limite : JS actif (.js) mais script en échec = avis 2 et suivants masqués, sans navigation. */
.js .section-temoignages:has(.wp-block-post-template > li + li):not(.is-navigation) .wp-block-post:nth-child(n+2) {
	visibility: hidden;
}

.section-temoignages.is-navigation .wp-block-post[inert] {
	visibility: hidden;
}

/* Swipe (assets/js/temoignages.js) : le geste vertical reste au navigateur (défilement de la page), seul le
   geste horizontal est confié au script. Pendant un glisser horizontal, aucune sélection de texte. */
.section-temoignages.is-navigation .wp-block-post-template {
	touch-action: pan-y;
}

.section-temoignages.is-navigation .wp-block-post-template.is-glisse {
	user-select: none;
	-webkit-user-select: none;
	cursor: grabbing;
}

/* Réserve la place de la rangée de navigation (points + flèche) AVANT que le script ne la crée (même raison que
   les Onglets : sans ça, son insertion au moment où le script s'exécute décale le bloc suivant). Hauteur =
   exactement ce que la rangée ajoute une fois réelle : écart stack + hauteur de la rangée. La rangée est
   dominée par la flèche (cible, 44px) depuis la session 7c (retrait du filet propre à la navigation, dont la
   réserve d'espace n'a plus lieu d'être — le filet empilé de la 7a, lui, n'a jamais eu besoin de réserve : il
   fait partie du flux normal, sans JS). Un ::after n'est jamais concerné par le blockGap du cœur (sélecteur
   `> *`, ne matche pas les pseudo-éléments) : rien à neutraliser ici. Disparaît dès que `.is-navigation` est
   posé (le vrai élément occupe alors cette place). */
.js .section-temoignages:has(.wp-block-post-template > li + li):not(.is-navigation) .temoignages__avis::after {
	content: "";
	display: block;
	height: calc(var(--wp--preset--spacing--md) + var(--wp--preset--spacing--xs) + var(--wp--preset--dimension--cible));
}

/* Rangée de navigation : flèches et compteur « n / total » seuls (les points ont été retirés, redondants
   avec le compteur), alignés sur le bord gauche de la zone avis. Écart
   md + xs au-dessus (44px, entre md et lg ; réserve d'avant script ci-dessus alignée sur la même valeur), jamais le blockGap lg de .temoignages__avis (deux classes, 0,2,0, bat la règle générée par
   le cœur pour ce blockGap, 0,1,0 à cause de sa classe unique par instance — vérifié en valeur calculée). */
.temoignages__avis .temoignages__navigation {
	display: flex;
	align-items: center;
	justify-content: flex-start;
	margin-block-start: calc(var(--wp--preset--spacing--md) + var(--wp--preset--spacing--xs));
}

/* Bande de logos animée (sessions 8a-8b). Colonnes 9-12 de .temoignages réservées depuis la 7a : deux bandes,
   côte à côte en mode colonne (>1080px), une seule visible en mode bande horizontale (≤1080px, voir plus bas —
   la seconde y créait deux rangées décalées, masquée dans ce mode). Défilement continu, aucun JS.
   Boucle continue en CSS pur : chaque copie (deux <ul class="logos-liste">, une exposée, une aria-hidden) répète
   les logos jusqu'à mcleod_logos_min_par_copie() (inc/champs-client.php) pour ne jamais laisser de trou.
   Seamless, PAR CONSTRUCTION : aucun `gap` sur .logos-piste ni .logos-liste (un `gap` flex n'ajoute jamais
   d'espace après le dernier enfant) — l'écart est une marge portée par CHAQUE logo (margin-block-end en vertical,
   margin-inline-end en horizontal), y compris le dernier de chaque copie. Le pas (logo + écart) est alors
   rigoureusement constant : la hauteur/largeur d'une copie est un multiple exact de ce pas, et les deux copies
   (identiques) ont une hauteur/largeur strictement égale — translateY/translateX de -50% à 0% déplace donc
   exactement de la hauteur/largeur d'UNE copie, sans discontinuité au point de bouclage.
   --logos-vitesse EN PX/S, RÉELLEMENT IDENTIQUE dans les deux modes — pas juste une durée partagée. Une durée
   partagée (--logos-n × une durée fixe, essayé d'abord) donne une VITESSE différente par mode dès que le pas
   (logo + écart) diffère : mesuré, 48,2px/s en bande horizontale contre 73,9px/s en colonne avec le même
   --logos-n (30) et la même durée — le pas plus large de la colonne (184,6px) parcourait plus de distance que
   celui de la bande horizontale (120,6px) dans le même temps. Ici, la durée se déduit de la vitesse ET du pas
   RÉEL de chaque mode (`--logos-pitch`, mesuré Playwright, différent par mode — déclaré une fois ci-dessous,
   réécrit dans le media query ≤1080px) : `durée = (n × pitch) / vitesse`.
   **`--logos-pitch` et `--logos-vitesse` sont des NOMBRES SANS UNITÉ (px implicites), jamais des longueurs.**
   Bug trouvé (signalé par Yvan, reproduit dans Firefox 141 avant correctif) : une première version déclarait
   `--logos-pitch` EN px puis retirait l'unité dans le calc() via `pitch / 1px` (diviser une LONGUEUR par une
   LONGUEUR, arithmétique typée CSS) — syntaxe valide en soi, mais dont le support diffère selon les moteurs :
   dans Firefox, la déclaration entière devenait invalide (aucune erreur visible), `animation-duration` retombait
   à sa valeur initiale `0s`, et `transform` à `none` — confirmé (`getComputedStyle`) : `animationDuration:"0s"`,
   aucune `Animation` sur l'élément, déplacement mesuré nul sur 2s. En gardant `--logos-pitch`/`--logos-vitesse`
   SANS unité dès leur déclaration, `n × pitch / vitesse` ne manipule que des NOMBRES du début à la fin (aucune
   division de longueur nulle part) : le `* 1s` final convertit ce nombre en `<time>` valide, une seule fois, à
   la toute fin — la même règle vaut dans l'autre sens : `calc(var(--logos-pitch) * 1px)` si une vraie longueur
   en px était nécessaire quelque part (pas le cas ici, --logos-pitch ne sert qu'à ce calcul de durée).
   **Règle générale (voir aussi CLAUDE.md, « Méthode de travail ») : jamais de division par une valeur À UNITÉ
   dans un `calc()` CSS — diviser par un NOMBRE pur uniquement.** Valeur proposée (25, soit 25px/s) nettement plus
   lente que les 48-74px/s mesurés avant ce réglage — à ajuster à l'œil par Yvan, un seul endroit à changer pour
   les deux modes. Prouvé dans Chromium, Firefox ET WebKit (animation-duration calculée, déplacement mesuré à
   deux instants, 375 et 1440px) après ce correctif. */
.temoignages {
	--logos-ecart: var(--wp--preset--spacing--lg);
	--logos-vitesse: 25;
	/* Glisser (temoignages-logos.js) : retour de l'inertie vers la croisière (décroissance par seconde) et
	   plafond d'un lancer (multiple de la vitesse de croisière). Nombres sans unité, lus par le script. */
	--logos-inertie: 2.5;
	--logos-lancer-max: 60;
}

.temoignages__logos {
	--logos-pitch: 184.6375;
}

/* Bug trouvé et mesuré (Playwright) : une rangée de grille en taille `auto` grandit pour accueillir le CONTENU
   RÉEL de chacun de ses items, quel que soit `min-height`/`overflow` posé SUR l'item — ces propriétés bornent la
   compression de l'item quand l'espace manque, elles n'empêchent pas une rangée `auto`, sans contrainte
   concurrente, de grandir POUR s'adapter à un contenu qui n'a nulle part ailleurs où aller. Avec la piste (28 × 2
   logos, plusieurs milliers de px) en flux normal, `.temoignages` passait de ~935-1042px (mesuré avant la bande,
   diagnostic de session) à 5599px à 1440px : toute la rangée — donc `.temoignages__avis` avec elle — grandissait
   pour loger la bande, au lieu que la bande s'étire dans la hauteur déjà fixée par les avis. `min-height:0` seul
   (essayé, insuffisant, mesuré identique) ne change rien tant que le contenu reste en flux.
   **Corrigé en sortant `.logos-piste` du flux** (`position:absolute`, comme `.champ-image__cadre` pour la même
   raison exacte : « les captures n'imposent pas leur hauteur au cadre », session 6d) : `.temoignages__logos`
   devient le repère (`position:relative`) et n'a alors plus RIEN en flux normal, sa hauteur ne dépend donc plus
   du tout de son contenu — seul le `overflow:hidden` de la section « CSS » ci-dessous continue de border la
   fenêtre visible. Elle peut alors s'étirer normalement (stretch, comportement par défaut d'un item de grille)
   dans la hauteur de la rangée fixée par les avis, aux deux niveaux de grille imbriqués (`.temoignages` puis la
   grille interne à 2 colonnes de `.temoignages__logos-zone`). */
.temoignages__logos-zone {
	grid-column: 9 / -1;
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--wp--preset--spacing--md);
}

.temoignages__logos {
	position: relative;
	overflow: hidden;
}

.logos-piste {
	position: absolute;
	inset-inline: 0;
	top: 0;
	display: flex;
	flex-direction: column;
}

.logos-liste {
	display: flex;
	flex-direction: column;
	list-style: none;
	margin: 0;
	padding: 0;
}

.logo-client {
	display: flex;
	align-items: center;
	justify-content: center;
	margin-block-end: var(--logos-ecart);
}

/* Blanc à 60% d'opacité : sur l'<img> (feuille), jamais sur un ancêtre — cohérent avec la règle générale du
   thème (aucun filter/transform/backdrop-filter sur un ancêtre d'élément animé, voir « Animation et éléments
   sticky »). max-height/max-width à 100% (Yvan, après la session 8a — remplace le token logo-carte fixe qui
   bornait l'image directement) : la borne RÉELLE change de rôle selon le mode. En colonne (>1080px), c'est
   max-width qui agit (la largeur de .logo-client, flex-item de la colonne, est définie) — l'image garde son
   ratio propre (object-fit) sans jamais dépasser la largeur de la bande. En bande horizontale (≤1080px), c'est
   max-height qui doit agir, mais seulement si .logo-client a lui-même une hauteur définie : voir le correctif
   `.logo-client { height:100% }` dans le media query ≤1080px plus bas — sans lui, max-height:100% ne se résout
   contre rien (parent à hauteur `auto`) et l'image rend à sa taille native, débordant hors du cadre. */
.logo-client__image {
	max-height: 100%;
	max-width: 100%;
	width: auto;
	height: auto;
	object-fit: contain;
	filter: brightness(0) invert(1);
	opacity: .6;
}

@media (prefers-reduced-motion: no-preference) {
	.logos-piste {
		animation: logos-defilement-vertical calc(var(--logos-n) * var(--logos-pitch) / var(--logos-vitesse) * 1s) linear infinite;
	}

	/* Pause au survol de la ZONE, SANS JS SEULEMENT : un seul survol arrête les deux bandes (session 8b). Quand
	   le script pilote (.is-pilote), la pause au survol est retirée : appuyer sur une bande les arrête toutes les
	   deux, à la souris, au doigt et au stylet (décision d'Yvan). Exception WCAG 2.2.2 toujours assumée
	   (mouvement autonome > 5s, aucune pause au clavier) — comme le titre rotatif (CLAUDE.md, « Exception à la
	   règle de mouvement »), troisième exception du thème. */
	.temoignages__logos-zone:not(.is-pilote):hover .logos-piste {
		animation-play-state: paused;
	}

	/* Bande 2 (session 8b) : mêmes keyframes que la bande 1, sens inversé par animation-direction seule — aucun
	   second jeu de @keyframes. translateY(-50%→0%) inversé fait décroître Y (mouvement vers le haut, "bas →
	   haut") ; translateX(-50%→0%) inversé fait décroître X ("droite → gauche"), quel que soit le point de
	   rupture actif (la propriété s'applique à l'animation en cours, peu importe laquelle des deux @keyframes
	   `animation-name` désigne à cet instant). */
	.temoignages__logos--inverse .logos-piste {
		animation-direction: reverse;
	}

	/* Glisser (temoignages-logos.js). Bandes verticales : `touch-action` par défaut, le doigt défile la page
	   (gestes tactiles ignorés par le script) ; bandes horizontales : `pan-y` plus bas. */
	.temoignages__logos-zone.is-pilote .temoignages__logos {
		cursor: grab;
	}

	.temoignages__logos-zone.is-glisse,
	.temoignages__logos-zone.is-glisse .temoignages__logos {
		cursor: grabbing;
		user-select: none;
		-webkit-user-select: none;
	}
}

@keyframes logos-defilement-vertical {
	from { transform: translateY(-50%); }
	to { transform: translateY(0%); }
}

@keyframes logos-defilement-horizontal {
	from { transform: translateX(-50%); }
	to { transform: translateX(0%); }
}

@media (max-width: 1080px) {
	.temoignages__logos-zone {
		grid-column: 1 / -1;
		display: flex;
		flex-direction: column;
		gap: var(--wp--preset--spacing--md);
		margin-left: calc(var(--wp--style--root--padding-right) * -1);
		margin-right: calc(var(--wp--style--root--padding-right) * -1);
	}

	.temoignages__logos {
		height: var(--wp--preset--dimension--logo-carte);
		/* Pas réel de ce mode (logo + écart), mesuré à la hauteur de bande actuelle (100px, token logo-carte,
		   remonté depuis 72px — logos jugés trop petits) — remplace la valeur colonne déclarée plus haut, pour
		   que --logos-vitesse (px/s) reste la vitesse RÉELLE ici aussi, pas seulement en colonne. Nombre sans
		   unité, comme la valeur colonne (voir plus haut : jamais de longueur en px pour --logos-pitch, sinon le
		   calc() de durée doit diviser une longueur par une longueur — invalide dans Firefox, cause du défilement
		   figé corrigé après coup). Remesuré (Playwright, 375/782/1080px, bande réelle) : 210, identique aux trois
		   largeurs. */
		--logos-pitch: 210;
	}

	/* Bande 2 affichée aussi en mode bande horizontale (session 107, revient sur la décision de la 8b, qui la
	   masquait comme un doublon) : deux rangées empilées, sens opposés, plus de logos visibles à la fois. Ce sont
	   les mêmes clients dans l'ordre inverse, pas des clients supplémentaires. Sens : voir le media query suivant. */

	/* `.logos-piste` est un enfant de bloc ORDINAIRE de `.temoignages__logos` (pas un flex-item : son parent n'est
	   pas flex) — sa largeur `auto`, sans ce correctif, REMPLIT la largeur disponible (comme tout bloc), au lieu
	   de s'ajuster à ses deux <ul> (chacun bien plus large que la bande visible). `.logos-liste`, elle, est un
	   flex-item de la piste : sa largeur `auto` s'ajuste déjà correctement à son contenu (`flex-basis:auto` d'un
	   item utilise une taille de type max-content), aucun correctif nécessaire à ce niveau. Sans `max-content` sur
	   la piste, le flex-shrink par défaut (marges NON compressibles, contenu écrasé) réduisait tout le contenu
	   (jusqu'à l'image) à 0×0 — bug trouvé et mesuré (Playwright) avant ce correctif : piste à la largeur de la
	   bande (718px à 782px) au lieu de la somme de ses deux copies (3360px), <li> réduits à la largeur de leur
	   seule marge, image à 0×0 malgré un fichier chargé. Absent du mode vertical (>1080px) : `height:auto` d'un
	   bloc s'ajuste déjà à son contenu par défaut (asymétrie largeur/hauteur du modèle de boîte), vérifié. */
	/* Inset inversé par rapport au mode vertical : `inset-inline:0` (base) contraindrait ici la largeur de la
	   piste à celle de la bande (718px…), le même bug que le flex-shrink corrigé plus haut mais par une autre
	   voie — `left:0` seul, sans `right`, laisse la largeur se calculer sur le contenu (shrink-to-fit d'un absolu
	   sans bord opposé posé), confirmé par `width: max-content` en plus, explicite. `top/bottom:0` remplace
	   `inset-inline` pour la hauteur : la piste occupe toute la hauteur de la bande (40px, fixée juste au-dessus),
	   jamais sa propre hauteur de contenu (qui vaudrait ici la hauteur d'une ligne seule, sans conséquence, mais
	   gardé pour la même logique que le mode vertical). */
	.logos-piste {
		inset-inline: auto;
		left: 0;
		top: 0;
		bottom: 0;
		flex-direction: row;
		width: max-content;
	}

	.logos-liste {
		flex-direction: row;
	}

	.logo-client {
		margin-block-end: 0;
		margin-inline-end: var(--logos-ecart);
	}

	/* Borne en HAUTEUR ici (mode bande horizontale), jamais en LARGEUR (rôle inversé du mode colonne, où c'est
	   .logo-client__image { max-width } qui borne — voir plus bas, hors media query). Posé explicitement dans
	   CE media query, pas sans condition : une résolution de `height:100%` dépend d'un parent à hauteur DÉFINIE
	   (`<li>`, étiré à 40px par le flex de .logos-liste ICI SEULEMENT — en mode colonne le <li> a une hauteur
	   `auto`, donc `height:100%` s'y résout en `auto` aussi, mais c'est un mécanisme implicite du modèle de
	   boîte, pas une garantie à documenter comme un choix : la règle n'a sa place que là où elle a un effet réel.
	   Bug trouvé et mesuré (Playwright) : `.logo-client` est un enfant BLOC ORDINAIRE du `<li>` (pas un flex-item
	   du flex de .logos-liste, qui ne concerne que ses enfants DIRECTS) — le <li> s'étire bien à 40px, mais
	   `.logo-client` n'en hérite rien, sa hauteur reste `auto` = celle de son contenu. Avec l'ajustement de taille
	   d'Yvan (`max-height:100%` sur .logo-client__image, session 8a deuxième passe, remplace le token logo-carte
	   fixe), l'image n'a alors PLUS AUCUNE hauteur de référence : elle rend à sa taille native (440×280 pour le
	   repli, mesuré), déborde du `<li>` de 240px, et `overflow:hidden` sur `.temoignages__logos` n'affiche que la
	   tranche du HAUT de cette image surdimensionnée — vide pour le repli (coins transparents, forme centrée) et
	   pour les vrais logos mesurés. Aucun logo visible à aucune des trois largeurs de ce mode, confirmé par
	   capture avant correctif (fond uni, rien d'autre). */
	.logo-client {
		height: 100%;
	}

	/* Bug cross-navigateur trouvé (signalé par Yvan, reproduit dans Firefox 141 et WebKit 26) : avec
	   `max-height:100%` (règle d'Yvan, non conditionnelle, ci-dessus), Chromium calcule correctement la largeur
	   `auto` de `.logo-client` (flex-item, axe principal) à partir de la taille RENDUE de l'image (113px) —
	   Firefox et WebKit la calculent à partir de sa taille NATIVE (440px, avant réduction par max-height),
	   laissant un vide de 327px de chaque logo. Mesuré : largeur de `.logo-client` 113px (Chromium) contre 440px
	   (Firefox, WebKit) pour le même logo, à la même largeur d'écran — un pas 2,3× plus grand, donc une vitesse
	   RÉELLE 2,3× plus rapide qu'en colonne dans ces deux moteurs, malgré `--logos-vitesse` identique. Cause :
	   la résolution d'intrinsèque (contribution « max-content ») d'un conteneur flex enveloppant un élément
	   remplacé contraint par un POURCENTAGE de hauteur n'est pas normalisée entre moteurs — les trois s'accordent
	   dès que la contrainte est une LONGUEUR fixe plutôt qu'un pourcentage (prouvé : `max-height:72px` au lieu de
	   `100%` donne 173,14px/173,15px/173,14px de large pour le même `<li>` dans Chromium/Firefox/WebKit).
	   Corrigé en réintroduisant une longueur fixe SEULEMENT ici (mode bande horizontale, où `.logo-client` doit
	   shrink-to-fit sur son axe principal) — jamais en colonne, où la largeur de `.logo-client` est le pas
	   TRANSVERSAL du flex (stretch, une longueur déjà définie par le parent, aucune ambiguïté mesurée dans les
	   trois moteurs à ce niveau). Le token reste `--wp--preset--dimension--logo-carte` (72px), la même valeur que
	   la hauteur de la bande : pas de nouvelle valeur à documenter. */
	.logo-client__image {
		max-height: var(--wp--preset--dimension--logo-carte);
	}
}

/* Smartphone : les deux bandes collées l'une à l'autre (session 107, réglage d'Yvan), la tablette garde md. */
@media (max-width: 781px) {
	.temoignages__logos-zone {
		gap: 0;
	}
}

@media (max-width: 1080px) and (prefers-reduced-motion: no-preference) {
	.logos-piste {
		animation-name: logos-defilement-horizontal;
		/* Sens voulu : droite vers gauche. Le jeu de clés (-50%→0%) joué normalement fait CROÎTRE translateX
		   (mouvement gauche → droite, même logique que celle déjà documentée pour translateY) — inversé ici pour
		   la bande 1 dans ce mode (la bande 2 est remise dans l'autre sens juste en dessous). */
		animation-direction: reverse;
	}

	/* Bande 2 : sens opposé à la bande 1 (gauche vers droite). Deux classes, bat la règle ci-dessus (une classe)
	   et remplace le `reverse` de la règle générale `.temoignages__logos--inverse .logos-piste` (même spécificité,
	   déclarée plus haut dans le fichier). */
	.temoignages__logos--inverse .logos-piste {
		animation-direction: normal;
	}

	/* Bandes horizontales : un glisser vertical du doigt fait défiler la page (le navigateur annule alors le
	   geste, `pointercancel`), un glisser horizontal attrape la bande. */
	.temoignages__logos-zone.is-pilote .temoignages__logos {
		touch-action: pan-y;
	}
}

/* reduced-motion : aucune animation (héritée, la règle ci-dessus ne s'applique que sous no-preference), logos
   statiques, tous visibles, en ligne qui passe à la ligne — jamais les répétitions ni la seconde copie
   (aria-hidden), qui n'ont de sens que pour la boucle. */
@media (prefers-reduced-motion: reduce) {
	.logos-piste,
	.logos-liste {
		flex-direction: row;
		flex-wrap: wrap;
	}

	.logo-client {
		margin-block-end: var(--logos-ecart);
		margin-inline-end: var(--logos-ecart);
	}

	/* Bande 2 (session 8b) masquée entièrement : statique, elle montrerait les 14 mêmes clients que la bande 1,
	   juste dans l'autre ordre — un doublon, pas une information supplémentaire. */
	.temoignages__logos--inverse {
		display: none;
	}

	.logos-liste[aria-hidden],
	.logos-liste > li[aria-hidden] {
		display: none;
	}
}

/* Mécanisme partagé FAQ / Volets (session 6b). Deux variations de core/accordion, mêmes règles :
   `.is-faq` (titres h3, 21px) et `.is-volets` (titres h4, 16px, section Étude de cas). Les sélecteurs
   partagés sont `:is(.is-faq, .is-volets)` (même spécificité qu'une seule classe) ; ce qui varie passe par
   des custom properties posées sur l'accordéon (--acc-taille, --acc-filet, --acc-picto), jamais par une
   règle dupliquée. Les règles qui FERMENT un volet non ouvert (grille 0fr, opacité 0, translation) sont
   regroupées en un seul endroit plus bas : c'est là que `.js` conditionne les Volets.
   Historique et pièges ci-dessous, écrits pour la FAQ (session J) : ils valent pour les deux.

   FAQ (variation "yvan-perso/faq" sur core/accordion, classe is-faq).
   Le cadre appartient à la rangée de question (accordion-heading) seule, jamais
   à l'accordion-item entier : la réponse n'a pas de cadre, elle s'insère entre
   deux rangées bordées quand elle est visible.
   Cadre permanent, jamais conditionné à .is-open : chaque rangée porte ses
   quatre côtés en 1px en toute circonstance, aucun filet ne se pose ni ne se
   retire pendant l'ouverture/fermeture. Pas de double filet entre deux rangées
   consécutives : chaque item sauf le premier remonte de -1px (marge négative),
   assez pour que son filet haut se superpose exactement au filet bas de l'item
   précédent — un seul trait visible, sans dépendre d'un ordre de peinture
   particulier (même couleur des deux côtés). Neutralise au passage le fallback
   global WordPress (24px, :where(.is-layout-flow) > *) qui introduirait sinon
   un espace entre chaque accordion-item.
   --faq-row-size (2 × sm + une ligne de texte en h3) posée ici, au niveau de
   l'item, et réutilisée pour la ligne 1 de la grille ET pour la cellule carrée
   du chevron plus bas (héritée par le bouton). font-size:h-3 posé ici aussi,
   pour que 1em se résolve identiquement partout où --faq-row-size est utilisée.
   Piège trouvé, cause réelle du filet qui semblait "animé" : grid-template-rows
   en `auto 0fr`/`auto 1fr` fait interpoler Chromium sur LA LIGNE 1 ("auto")
   autant que sur la ligne 2 pendant la transition — vérifié à l'écran, la
   hauteur du H3 (72px au repos) montait jusqu'à ~123px au milieu d'une
   fermeture avant de redescendre, entraînant son cadre avec elle. Une valeur
   "auto" n'a pas de longueur figée à interpoler ; remplacée par --faq-row-size
   (une longueur fixe, identique avant/pendant/après), la ligne 1 ne bouge
   plus jamais, seule la ligne 2 (0fr/1fr) anime.
   Second piège, trouvé après coup (Firefox, animation totalement absente) :
   --faq-row-size utilisait d'abord `1lh` (hauteur de ligne réellement rendue),
   unité non supportée avant Firefox 129 (sortie août 2024) — dans un Firefox
   plus ancien, `1lh` invalide tout le calc(), donc toute la custom property,
   donc grid-template-rows entier retombe à sa valeur initiale ("none") : plus
   de ligne figée, plus de 0fr/1fr, plus rien à animer. Remplacé par
   `1em * 1.625` (1.625 = line-height global, theme.json
   styles.typography.lineHeight, en dur ici faute d'équivalent exposé en
   variable CSS) — valeur strictement identique à 1lh dans ce cas précis (aucun
   line-height propre à l'élément), mais em/calc n'ont aucune contrainte de
   support récent. */
.is-faq {
	--acc-taille: var(--wp--preset--font-size--h-3);
	--acc-filet: var(--wp--preset--color--line);
	--acc-picto: var(--wp--preset--color--accent);
}

.is-volets {
	--acc-taille: var(--wp--preset--font-size--md);
	--acc-filet: var(--wp--preset--color--line);
	--acc-picto: var(--wp--preset--color--accent);
}

/* Sur le fond de l'étude de cas (#483C9E) : filet éclairci (line y est invisible, 1,15:1) et chevron ambré
   (accent y tombe à 2,64:1, accent-3 à 4,62:1) — règle de contexte, comme l'eyebrow. */
.etude .is-volets {
	--acc-filet: var(--etude-filet);
	--acc-picto: var(--wp--preset--color--accent-3);
}

/* Base = ouvert (grille 1fr) : c'est l'état sans JS des Volets. La fermeture est posée plus bas. */
:is(.is-faq, .is-volets) .wp-block-accordion-item {
	--faq-row-size: calc(var(--wp--preset--spacing--sm) * 2 + 1em * 1.625);
	font-size: var(--acc-taille);
	margin-block-start: -1px;
	display: grid;
	grid-template-rows: var(--faq-row-size) 1fr;
}

:is(.is-faq, .is-volets) .wp-block-accordion-item:first-child {
	margin-block-start: 0;
}

/* FERMETURE (un item sans .is-open). Trois déclarations, toutes ici : ligne 2 de la grille à 0fr, panneau
   transparent et abaissé de xs. FAQ ET Volets, seulement sous .js (classe posée sur <html> par
   inc/classe-js.php) : sans JavaScript rien ne peut rouvrir un volet, ils restent tous lisibles. Le préfixe
   .js sur la FAQ (session 6b) n'a rien changé au rendu avec JS : 0 pixel de différence sur 8 captures
   (1440/375, initial, tout fermé, un ouvert, animation figée à 150 ms). */
.js :is(.is-faq, .is-volets) .wp-block-accordion-item:not(.is-open) {
	grid-template-rows: var(--faq-row-size) 0fr;
}

.js :is(.is-faq, .is-volets) .wp-block-accordion-item:not(.is-open) .wp-block-accordion-panel {
	opacity: 0;
	transform: translateY(var(--wp--preset--spacing--xs));
}

:is(.is-faq, .is-volets) .wp-block-accordion-heading {
	/* Un <h3> brut porte par défaut le multiplicateur de taille du navigateur
	   (1.17em) — sans ce reset, il grossit encore la taille déjà posée sur
	   l'item (21px → 24,57px), et --faq-row-size/--faq-chevron-size, réévaluées
	   à ce niveau via 1em, ne correspondent plus à la valeur figée dans la
	   grille : c'est ce qui faisait déborder et décentrait le chevron. */
	font-size: inherit;
	border: 1px solid var(--acc-filet);
}

/* Bouton : le padding natif du cœur (1em haut/bas) est retiré — le padding de la
   rangée (sm haut/bas, md à gauche) est porté par le titre seul.
   --faq-chevron-size = --faq-row-size moins le border haut+bas du h3 (1px
   chacun) : la grille réserve --faq-row-size pour LA LIGNE ENTIÈRE (bordure du
   h3 comprise, à juste titre — le h3 s'y étire correctement, bordure incluse),
   mais le bouton, lui, vit dans la zone de CONTENU du h3, réduite de 2px par
   cette même bordure. Utiliser --faq-row-size tel quel pour la cellule du
   chevron la faisait déborder de 2px sous le filet bas du h3 (constaté à
   l'écran), et décalait d'autant le centrage visuel du picto. */
:is(.is-faq, .is-volets) .wp-block-accordion-heading__toggle {
	--faq-chevron-size: calc(var(--faq-row-size) - 2px);
	padding: 0;
	align-items: stretch;
	background-image: linear-gradient(var(--acc-filet), var(--acc-filet));
	background-repeat: no-repeat;
	background-size: 1px 100%;
	background-position: calc(100% - var(--faq-chevron-size)) 0;
}

:is(.is-faq, .is-volets) .wp-block-accordion-heading__toggle-title {
	display: flex;
	align-items: center;
	padding-block: var(--wp--preset--spacing--sm);
	padding-inline-start: var(--wp--preset--spacing--md);
}

/* Cellule du chevron : carrée (largeur = hauteur de la rangée, via
   --faq-row-size ci-dessus), quelle que soit la taille du texte. showIcon est
   à false sur la variation (icône native "+" désactivée), ce chevron est donc
   un ::after ajouté sur le bouton. mask-size fixe (2em, pas relatif à
   --faq-row-size) pour que le picto reste fin et centré, indépendamment de la
   taille de la cellule qui le contient — 2em choisi après mesure : le tracé
   utile du SVG (chevron.svg, viewBox 24×24) n'occupe que 45,8% de sa largeur
   et 23,3% de sa hauteur (marges internes du fichier), 1em rendait donc un
   chevron visible d'environ 12% de la largeur de la cellule au lieu des ~25%
   de la maquette ; 2em vise ce ratio.
   Piège trouvé (filet vertical invisible sur la page réelle) : mask-image
   s'applique à tout ce qui est peint sur l'élément masqué, y compris son
   propre border — un border-left posé ici disparaissait donc avec le reste
   du carré, seul le tracé du chevron (la zone opaque du masque) restait
   visible. Le filet est maintenant un background-image (dégradé 1px) posé
   sur le bouton lui-même, non masqué, positionné au bord gauche de cette
   cellule via --faq-row-size (voir règle du bouton ci-dessus). */
:is(.is-faq, .is-volets) .wp-block-accordion-heading__toggle::after {
	content: "";
	display: block;
	flex-shrink: 0;
	width: var(--faq-chevron-size);
	height: var(--faq-chevron-size);
	background-color: var(--acc-picto);
	-webkit-mask-image: url("assets/icons/chevron.svg");
	mask-image: url("assets/icons/chevron.svg");
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: 2em;
	mask-size: 2em;
}

@media (prefers-reduced-motion: no-preference) {
	:is(.is-faq, .is-volets) .wp-block-accordion-heading__toggle::after {
		transition: transform 0.2s ease-in-out;
	}
}

:is(.is-faq, .is-volets) .wp-block-accordion-item.is-open .wp-block-accordion-heading__toggle::after {
	transform: rotate(180deg);
}

/* FAQ responsive (mobile, ≤781px — le seul seuil mobile du thème, voir "Points de rupture" ;
   la cote de ~56px demandée pour la colonne chevron est obtenue par composition de deux tokens
   réels, jamais une valeur en dur). Symptôme corrigé : une question sur 2-3 lignes débordait de sa
   rangée (--faq-row-size n'était dimensionné que pour UNE ligne) et chevauchait la réponse/la
   question suivante. --faq-row-size reste une LONGUEUR FIXE (jamais `auto`/`minmax(…,auto)` : c'est
   ce qui permettait l'animation d'ouverture lisse de la ligne 2, voir le commentaire du haut de ce
   bloc) — élargie ici à 3 lignes de budget plutôt qu'une, pour couvrir la question réelle la plus
   longue de la FAQ sans déborder (vérifié à 360/390/430px, la plus longue question du site ne
   dépasse jamais ce budget). Compromis assumé : une question courte garde cet espace réservé même
   si elle n'en a pas besoin (pas de recalcul par question, qui exigerait du JS de mesure — hors
   périmètre ici). Le chevron n'est plus dérivé de --faq-row-size sur ce seuil (il grandirait avec le
   budget élargi) : largeur FIXE et indépendante, et le texte passe en grille (1fr auto) pour que la
   colonne de question profite de tout l'espace libéré, align-items:start pour que le chevron reste
   calé en haut quand la question fait plusieurs lignes plutôt que centré sur une hauteur qui ne
   correspond plus à une seule ligne. */
@media (max-width: 781px) {
	.is-faq .wp-block-accordion-item {
		--faq-row-size: calc(var(--wp--preset--spacing--sm) * 2 + 1em * 1.625 * 3);
	}

	.is-faq {
		--acc-taille: clamp(var(--wp--preset--font-size--md), 4vw, var(--wp--preset--font-size--h-3));
	}

	.is-faq .wp-block-accordion-heading__toggle {
		--faq-chevron-size: calc(var(--wp--preset--dimension--cible) + var(--wp--preset--spacing--xs));
		display: grid;
		grid-template-columns: 1fr auto;
		align-items: start;
	}

	.is-faq .wp-block-accordion-heading__toggle-title {
		display: block;
		/* box-sizing:border-box explicite : ce span reste en content-box (hérité d'un reset du cœur,
		   jamais le border-box global du thème — vérifié en valeur calculée), min-height s'ajouterait
		   sinon au padding-block existant (20px + 20px) au lieu de l'inclure, débordant la rangée de
		   40px (mesuré : 158px au lieu des 118px de --faq-row-size). */
		box-sizing: border-box;
		min-height: var(--faq-row-size);
	}

	.is-faq .wp-block-accordion-heading__toggle::after {
		align-self: center;
		justify-self: center;
	}
}

/* Réponse : alignée sur le texte de la question (même retrait à gauche) ; cadre
   gauche/droite de la FAQ plus bas. Largeur 65ch : mesure de lecture, volontairement hors de l'échelle
   d'espacement du thème (pas un token, documenté comme tel dans CLAUDE.md).
   Espace au-dessus/en dessous : md. margin-block-start:0 annule le même
   fallback global (24px) que ci-dessus, cette fois appliqué au panneau lui-même
   plutôt qu'à un token — l'espacement voulu est porté par le padding, pas par
   ce reliquat de marge. */
:is(.is-faq, .is-volets) .wp-block-accordion-panel {
	margin-block-start: 0;
	padding-inline-start: var(--wp--preset--spacing--md);
	padding-block: var(--wp--preset--spacing--md);
	max-width: 65ch;
	font-size: var(--wp--preset--font-size--md);
	overflow: hidden;
	min-height: 0;
}

/* Animation de fermeture : le cœur pose "hidden=until-found" sur le panneau au
   même instant qu'il retire la classe is-open — content-visibility:hidden
   (déclenché par cet attribut) effondre alors la hauteur du panneau à 0
   instantanément, avant même que la transition grid-template-rows (posée par
   le cœur sur .wp-block-accordion-item) n'ait pu jouer une seule frame : rien
   à animer, la ligne du dessous saute au lieu de glisser. À l'ouverture, pas
   de souci symétrique : "hidden" est retiré immédiatement, le contenu redevient
   mesurable dès la première frame, et grid-template-rows peut l'agrandir
   progressivement.
   content-visibility fait partie des propriétés "discrètes" animables via
   allow-discrete (même mécanisme que display) : la nouvelle valeur s'applique
   immédiatement quand elle rend l'élément visible (donc aucun effet sur
   l'ouverture, déjà correcte), mais seulement à la fin de la transition quand
   elle le masque — le panneau reste pleinement rendu pendant les 0,3s de la
   fermeture, grid-template-rows a enfin quelque chose à réduire
   progressivement, et content-visibility:hidden ne s'applique réellement
   (retrait de l'arbre d'accessibilité, non focusable) qu'une fois l'animation
   terminée. Aucune modification du store du cœur : uniquement une propriété
   CSS de plus sur un sélecteur déjà à nous, la valeur de content-visibility
   continue de venir de la règle native [hidden="until-found"].
   Navigateur non compatible avec allow-discrete (syntaxe non reconnue) :
   déclaration ignorée dans son ensemble, comportement actuel inchangé (saut
   instantané, jamais d'état cassé).
   opacity + translateY (glissement de xs, token d'espacement) ajoutés à la
   même transition : fondu et glissement vers le haut à l'ouverture, vers le
   bas à la fermeture, synchronisés sur les 0,3s de grid-template-rows (même
   durée, même timing — posés par le cœur, pas dupliqués ici). Sans ces deux
   propriétés, le contenu restait fixe pendant que la ligne de grille
   l'agrandissait/réduisait : la clé de lecture visuelle du mouvement venait
   uniquement du rognage (overflow:hidden), pas d'un déplacement du texte
   lui-même.
   Durée de l'opacité asymétrique : à la fermeture, le texte doit disparaître
   bien avant que le glissement/la hauteur n'aient fini (sinon on le voit se
   décaler encore lisible) — 0.1s au lieu de 0.3s. La règle qui s'applique lors
   d'une transition est celle déclarée sur l'état D'ARRIVÉE : la règle de base
   (fermé) porte donc l'opacité rapide, la règle .is-open (ouvert) la remet à
   0.3s pour un fondu d'entrée qui suit le même rythme que le glissement. */
@media (prefers-reduced-motion: no-preference) {
	:is(.is-faq, .is-volets) .wp-block-accordion-panel {
		transition:
			content-visibility 0.3s ease-out allow-discrete,
			opacity 0s ease-out,
			transform 0.3s ease-out;
	}

	:is(.is-faq, .is-volets) .wp-block-accordion-item.is-open .wp-block-accordion-panel {
		transition:
			opacity 0.3s ease-out,
			transform 0.3s ease-out;
	}
}

/* Entre deux paragraphes de la réponse : sm, à la place du fallback global
   (24px, :where(.is-layout-flow) > *, cf. rapport de session — proche de la
   ligne de 26px en md mais pas issu d'un token du thème). */
:is(.is-faq, .is-volets) .wp-block-accordion-panel.is-layout-flow > * {
	margin-block-start: var(--wp--preset--spacing--sm);
}

:is(.is-faq, .is-volets) .wp-block-accordion-panel.is-layout-flow > :first-child {
	margin-block-start: 0;
}

/* FAQ : la réponse est encadrée à gauche et à droite (même filet que les questions, --acc-filet), sur toute la
   largeur de la rangée ; c'est son CONTENU qui garde la mesure de lecture de 65ch, plus le panneau. Un panneau
   fermé est à opacité 0 (puis hidden) : ses filets disparaissent avec lui. Les Volets (.is-volets) ne sont pas
   concernés. */
.is-faq .wp-block-accordion-panel {
	max-width: none;
	padding-inline-end: var(--wp--preset--spacing--md);
	border-inline: 1px solid var(--acc-filet);
}

.is-faq .wp-block-accordion-panel > * {
	max-width: 65ch;
}

/* Dernier volet ouvert : aucune rangée de question ne vient fermer le cadre en dessous, filet bas sur le panneau.
   Sans JS (pas de classe .js), tous les volets sont ouverts sans porter .is-open : le dernier reçoit aussi le filet. */
.is-faq .wp-block-accordion-item:last-child.is-open .wp-block-accordion-panel,
html:not(.js) .is-faq .wp-block-accordion-item:last-child .wp-block-accordion-panel {
	border-block-end: 1px solid var(--acc-filet);
}

/* Composant "CTA panneau" (patterns/cta-panneau.php) : depuis le footer dévoilé, il vit
   dans la zone CTA du footer (parts/footer.html), sur le même fond accent-2 que lui. Les
   règles qui le faisaient passer pour un panneau "posé derrière le footer" (coins bas à 0,
   bande d'ombre ::after au bord bas, footer collé par :has()) sont retirées : sans objet.
   La texture à points (.is-style-panel-indigo::before) est conservée. Padding porté par
   les attributs de bloc, jamais ici. */

/* Mesure du paragraphe : 48ch à 18px coupe après « prendre forme, » (mesuré à
   1440px). Exception documentée à la règle « aucune valeur en dur », comme 65ch
   dans la FAQ. La spécificité l'emporte sur le max-width du layout constrained
   (:where, spécificité nulle) ; le centrage vient de la marge auto du layout. */
.cta-panneau p {
	max-width: 60ch;
}

/* Portrait optionnel au-dessus du titre du CTA panneau (patterns/cta-panneau.php) :
   core/image natif, sans image choisie par défaut (figure sans <img src>) — le
   rendu dynamique du cœur (render_block_core_image()) supprime alors le bloc
   entier, rien à masquer en CSS. Centrage horizontal : hérité de la règle native
   du layout constrained (margin-inline:auto sur tout enfant non aligné), pas
   redéclaré ici. Taille 120px : exception documentée à la règle des tokens
   (portrait décoratif, aucune dimension du thème ne correspond), ajustable via
   la custom property. Le margin-bottom (md, 32px) est posé en attribut de bloc
   (style inline, voir le pattern) et se fond par collapsing de marge avec le
   margin-top (stack, 16px) généré par le blockGap du panneau sur le titre
   suivant — résultat final : 32px, le plus grand des deux. */
.cta-panneau__portrait {
	--cta-panneau-portrait-taille: 120px;
	width: var(--cta-panneau-portrait-taille);
	height: var(--cta-panneau-portrait-taille);
}

.cta-panneau__portrait img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	border-radius: 50%;
}

/* Tampon décoratif du panneau indigo du CTA (patterns/cta-panneau.php, ancre
   #contact) : SVG inline (assets/svg/stamp.svg, deux groupes — "stamp__centre"
   = disque blanc + texte indigo statique, "stamp__anneau" = anneau de mots
   blancs qui tourne), posé en haut à droite, en débord du panneau (comme un
   tampon de poste sur un timbre). Deux classes pour battre sans ambiguïté
   .is-style-panel-indigo > * (position:relative, une classe) déclarée plus
   haut dans ce fichier. Taille et débord : custom properties, ajustables. */
.cta-panneau .cta-panneau__tampon {
	--cta-panneau-tampon-taille: 150px;
	--cta-panneau-tampon-debord: 0.35;
	position: absolute;
	top: calc(var(--cta-panneau-tampon-taille) * var(--cta-panneau-tampon-debord) * -1);
	right: calc(var(--cta-panneau-tampon-taille) * var(--cta-panneau-tampon-debord) * -1);
	width: var(--cta-panneau-tampon-taille);
	height: var(--cta-panneau-tampon-taille);
	margin: 0;
	z-index: 2;
	pointer-events: none;
}

.cta-panneau__tampon svg {
	display: block;
	width: 100%;
	height: 100%;
}

/* Rotation infinie de l'anneau de mots seul (jamais le disque central ni son
   texte) : transform-box:view-box pour que les pourcentages/coordonnées de
   transform-origin se lisent dans le repère du viewBox du SVG, pas de la boîte
   propre du groupe. Origine = centre exact du disque (calculé depuis les points
   du tracé "M125.4,39...", voir CLAUDE.md pour la méthode), pas le centre du
   viewBox (à 1-2px près, négligeable mais vérifié). */
.cta-panneau__tampon .stamp__anneau {
	transform-box: view-box;
	transform-origin: 125.4px 125.5px;
}

@media (prefers-reduced-motion: no-preference) {
	.cta-panneau__tampon .stamp__anneau {
		animation: cta-panneau-tampon-rotation var(--cta-panneau-tampon-duree, 20s) linear infinite;
	}
}

@keyframes cta-panneau-tampon-rotation {
	to {
		transform: rotate(360deg);
	}
}

/* Aucun écart entre la page et le footer, sur toutes les pages : le blockGap racine du
   cœur (:where(.wp-site-blocks) > *, 24px) décollerait le footer de <main> et, en footer
   dévoilé, laisserait une bande de 24px sous la feuille. Spécificité supérieure au :where
   du cœur. */
.wp-site-blocks > footer {
	margin-block-start: 0;
}

/* Fonds décoratifs "lettres" : le SVG garde sa couleur intégrée (#231E26 à
   30 %), exception documentée à la règle des tokens, fichier décoratif de
   marque. URL relative au fichier CSS, comme les masques d'icônes.
   - .fond-lettres : pied de page, recouvre son bloc (cover), sur son propre
     fond opaque (base).
   - .fond-lettres-page : haut de page, div injecté par inc/fond-page.php
     (wp_body_open). Absolu dans le bloc conteneur initial (html et body n'ont
     ni position ni contexte d'empilement), il défile avec la page. Le ratio est
     celui du viewBox du SVG (1512 × 999). z-index négatif : sous le contenu,
     au-dessus du fond base du canevas. Jamais dans le header ni ancêtre d'un
     élément fixed/sticky. */
.fond-lettres {
	background-image: url("assets/images/bg_letters.svg");
	background-repeat: no-repeat;
	background-position: center top;
	background-size: cover;
	/* Même effet que les lettres de page, sans mix-blend-mode sur le footer
	   entier : l'image est multipliée avec le fond base du footer, à l'intérieur
	   de l'élément. */
	background-blend-mode: multiply;
}

.fond-lettres-page {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	aspect-ratio: 1512 / 999;
	z-index: -1;
	pointer-events: none;
	/* #231E26 est plus sombre que les pixels clairs du grain : en opacité simple
	   les lettres ne ressortent pas. multiply les fait lire comme des formes
	   sombres qui absorbent le grain. Porté par le CONTENEUR : il forme un groupe
	   isolé, les lettres filles s'y composent entre elles puis le groupe est
	   multiplié avec le fond ; des filles en multiply se fondraient avec le vide
	   transparent du groupe. */
	mix-blend-mode: multiply;
}

/* Une lettre = un div injecté par inc/fond-page.php, qui porte sa boîte en unités
   du viewBox de bg_letters.svg (--x, --y, --w, --h, 1512 × 999), son rang (--i) et
   un clip-path optionnel (--clip) quand la boîte rectangulaire contient des pixels
   d'une lettre voisine. Même image en fond, cadrée par background-size/position
   en % : à scroll 0 le rendu est proche de l'image entière, pas identique au pixel
   (voir CLAUDE.md, « Couches de fond »). */
.fond-lettres-page > div {
	position: absolute;
	left: calc(var(--x) / 1512 * 100%);
	top: calc(var(--y) / 999 * 100%);
	width: calc(var(--w) / 1512 * 100%);
	height: calc(var(--h) / 999 * 100%);
	background-image: url("assets/images/bg_letters.svg");
	background-repeat: no-repeat;
	background-size: calc(1512 / var(--w) * 100%) calc(999 / var(--h) * 100%);
	background-position: calc(var(--x) / (1512 - var(--w)) * 100%) calc(var(--y) / (999 - var(--h)) * 100%);
	clip-path: var(--clip, none);
}

/* Parallaxe différenciée : pilotée par assets/js/parallaxe-lettres.js (chargé par
   inc/fond-page.php), qui pose un translateY sur chaque lettre et la classe
   is-parallaxe sur le conteneur. Vitesse de la lettre de rang i :
   v = lettres × (1 + ecart × sin(i × 2.4)) (tokens theme.json ; 2.4 rad ≈ angle
   d'or, deux rangs voisins n'ont jamais des vitesses voisines). En position:
   fixed le conteneur n'ajoute rien à la zone défilable (pages courtes) ; sans JS
   ou sous reduce il reste absolu et statique. Une lettre haute plus rapide que la
   lettre basse qu'elle recouvre en colonnes ne peut jamais la toucher : c'est le
   cas des sept paires du fichier avec les rangs actuels (2, 4, 5, 6, 7, 8, 9 ; le
   « i » est un seul div, rang 1). À revérifier si un rang change. */
.fond-lettres-page.is-parallaxe {
	position: fixed;
}

/* will-change: preuve de nécessité mesurée (Chromium 153, 625 pas de molette) :
   sans lui les lettres, déplacées depuis le fil principal, sont re-rastérisées à
   chaque frame (RasterTask 3 952 à 1440, 368 à 375) ; avec lui 333 et 181. Posé sur
   les feuilles (aucun descendant fixe), et seulement quand le script est actif. */
.fond-lettres-page.is-parallaxe > div {
	will-change: transform;
}

/* Smartphone : comme le footer (.fond-lettres, cover), le fond est mis à l'échelle
   pour couvrir un conteneur un peu plus haut que la fenêtre (token
   fond-lettres.hauteur-mobile, en svh) et recadré au centre. --s = échelle (px par
   unité du viewBox) ; à cette largeur c'est la hauteur qui la fixe, l'image déborde
   des deux côtés (overflow: hidden, pas de défilement horizontal). Mêmes
   proportions que le fond entier : chaque lettre est cadrée avec la même échelle. */
@media (max-width: 781px) {
	.fond-lettres-page {
		--s: max(100vw / 1512, var(--wp--custom--fond-lettres--hauteur-mobile) / 999);
		height: var(--wp--custom--fond-lettres--hauteur-mobile);
		aspect-ratio: auto;
		overflow: hidden;
	}

	.fond-lettres-page > div {
		left: calc(50% - 756 * var(--s) + var(--x) * var(--s));
		top: calc(var(--y) * var(--s));
		width: calc(var(--w) * var(--s));
		height: calc(var(--h) * var(--s));
		background-size: calc(1512 * var(--s)) calc(999 * var(--s));
		background-position: calc(-1 * var(--x) * var(--s)) calc(-1 * var(--y) * var(--s));
	}
}

/* Grain de page : div injecté par inc/fond-page.php, fixe sur toute la fenêtre,
   sous les lettres (z-index -2 contre -1) et au-dessus du fond base du canevas.
   Deux tuiles 256×256 en masque (bruit binaire, graines différentes, assets/
   images/grain-a.png et grain-b.png) : ::before = teinte sombre, ::after = teinte
   claire, 1 px d'image = 1 px CSS. Les pseudo-éléments sont des feuilles : le div
   lui-même ne porte ni transform, ni filtre. Statique ici ; animé plus bas, sous
   prefers-reduced-motion: no-preference seulement. */
.fond-grain {
	position: fixed;
	inset: 0;
	z-index: -2;
	pointer-events: none;
}

.fond-grain::before,
.fond-grain::after {
	content: "";
	position: absolute;
	inset: 0;
	opacity: var(--wp--custom--grain--opacite);
	-webkit-mask-repeat: repeat;
	mask-repeat: repeat;
	-webkit-mask-size: auto;
	mask-size: auto;
}

.fond-grain::before {
	background-color: var(--wp--custom--grain--sombre);
	-webkit-mask-image: url("assets/images/grain-a.png");
	mask-image: url("assets/images/grain-a.png");
}

.fond-grain::after {
	background-color: var(--wp--custom--grain--clair);
	-webkit-mask-image: url("assets/images/grain-b.png");
	mask-image: url("assets/images/grain-b.png");
}

/* Animation du grain (session V), CSS seul, sous no-preference uniquement : sous
   reduce, le grain statique ci-dessus reste tel quel (y compris l'inset). Les
   deux pseudo-éléments débordent du viewport d'une tuile (token tuile = taille
   des PNG, à garder identique) et sautent entre des positions entières en px,
   tenues (step-end) : seul transform bouge, jamais mask-position ni une
   propriété qui repeint. Décalages entre -128 et +127 px (une tuile de
   amplitude), écart >= 32 px entre positions consécutives sur chaque axe, boucle
   comprise, y compris modulo la tuile. Deux cycles de longueur différente
   (8 et 7 positions) : la séquence combinée ne se répète que toutes les 5,6 s. */
@media (prefers-reduced-motion: no-preference) {
	.fond-grain::before,
	.fond-grain::after {
		inset: calc(var(--wp--custom--grain--tuile) * -1);
	}

	.fond-grain::before {
		animation: grain-a var(--wp--custom--grain--duree-a) step-end infinite;
	}

	.fond-grain::after {
		animation: grain-b var(--wp--custom--grain--duree-b) step-end infinite;
	}

	@keyframes grain-a {
		0%     { transform: translate(-1px, 38px); }
		12.5%  { transform: translate(53px, -127px); }
		25%    { transform: translate(92px, -2px); }
		37.5%  { transform: translate(-9px, 103px); }
		50%    { transform: translate(75px, -18px); }
		62.5%  { transform: translate(124px, 43px); }
		75%    { transform: translate(-84px, -91px); }
		87.5%  { transform: translate(-41px, -43px); }
	}

	@keyframes grain-b {
		0%       { transform: translate(8px, 30px); }
		14.2857% { transform: translate(114px, 106px); }
		28.5714% { transform: translate(-96px, -3px); }
		42.8571% { transform: translate(69px, 38px); }
		57.1429% { transform: translate(-53px, 93px); }
		71.4286% { transform: translate(64px, -30px); }
		85.7143% { transform: translate(-61px, -120px); }
	}
}

@media print {
	.fond-lettres-page,
	.fond-grain {
		display: none;
	}
}

/* En-tête (parts/header.html) : <header> sticky sous la barre d'admin, au-dessus
   des enfants des panneaux (z-index 1). Exception documentée aux points de rupture
   du thème : 601px = seuil de la barre d'admin du cœur, qui passe en
   position:absolute à 600px et moins (elle défile) — le sticky y colle donc à 0.
   Sans .wp-site-blocks > header + * , le blockGap racine du cœur (24px, :where)
   décollerait le contenu. */
.wp-site-blocks > header {
	position: sticky;
	top: 0;
	z-index: 10;
	padding-top: var(--entete-barre);
}

@media (min-width: 601px) {
	.wp-site-blocks > header,
	.wp-site-blocks > header::before,
	.wp-site-blocks > header::after {
		top: var(--wp-admin--admin-bar--height, 0px);
	}
}

/* Header "en pilule" (≤781px, session V2) : l'en-tête flotte à 0.75rem du haut au
   lieu d'être collé à 0 — la pilule (voir .entete plus bas) ne touche plus jamais
   le bord de l'écran. padding-top retiré ici : il ne sert qu'à réserver la place de
   la barre de progression (toujours fixe à top:0, inchangée) quand le header est
   collé au bord ; une fois le header lui-même décalé de 0.75rem (> --entete-barre,
   8px), la réservation est redondante et doublerait l'espace au-dessus du logo. */
@media (max-width: 781px) {
	.wp-site-blocks > header {
		top: var(--entete-decalage-haut);
		padding-top: 0;
	}
}

.wp-site-blocks > header + * {
	margin-block-start: 0;
}

/* Filet clair sur fond indigo : section Étude de cas (.etudes) et barre basse du footer
   (.pied-de-page__bas, attribut de bloc). Sur :root pour être lisible des deux, jamais
   seulement sur .etudes (une custom property ne s'hérite que par les descendants). */
:root {
	/*--etude-filet: var(--wp--preset--color--surface);*/
	--etude-filet: #6f64b8;
}

/* Hauteur de l'en-tête, dérivée des tokens : 2 × padding vertical (sm) + hauteur
   du logo (token logo × 72/71, ratio du viewBox de yr-monogram.svg, comme la
   règle du logo plus bas). Consommée par scroll-padding-top : un lien d'ancre
   amène la section juste sous l'en-tête. :root bat le `html` de admin-bar.css
   (qui pose sa propre valeur), d'où l'ajout de la hauteur de la barre d'admin,
   sauf à 600px et moins où elle défile. */
:root {
	--entete-barre: var(--wp--preset--dimension--progression);
	--entete-hauteur: calc(var(--entete-barre) + 2 * var(--wp--preset--spacing--sm) + var(--wp--preset--dimension--logo) * 72 / 71);
	scroll-padding-top: calc(var(--entete-hauteur) + var(--wp-admin--admin-bar--height, 0px));
}

@media (max-width: 600px) {
	:root {
		scroll-padding-top: var(--entete-hauteur);
	}
}

/* Barre de progression de lecture (décorative, aucun ARIA : content vide).
   ::before = piste et ::after = remplissage, tous deux en position:fixed (jamais
   absolus dans le header sticky : une animation sur un descendant d'un sticky fait
   repeindre le calque racine à chaque frame ; piste et remplissage fixes ensemble,
   sinon la piste suit le rebond élastique du haut de page et se détache du
   remplissage), avec le même `top` que le header (règle 601px plus haut). Sortis du
   flux, ils ne réservent plus de place : c'est le padding-top du header
   (--entete-barre, plus haut) qui la garde, d'où --entete-barre dans
   --entete-hauteur. Aucun transform/filter/backdrop-filter sur le header lui-même
   (piège position:fixed) : le transform n'est que sur le ::after, feuille sans
   descendant. Le ::after fait 100 % de large et se décale : translateX(-100 % + 5px)
   -> 0, les % d'un translate se rapportent à l'élément, donc 5px exact à toute
   largeur (scaleX en est incapable). Une seule @keyframes : avec support, pilotée
   par scroll(root) ; sans, mise en pause et positionnée par animation-delay négatif
   = --progression (0 à 1, écrite par assets/js/progression.js). Pas de
   prefers-reduced-motion : la barre suit le défilement de l'utilisateur, ce n'est
   pas une animation autonome. */
@keyframes progression-lecture {
	from { transform: translateX(calc(-100% + 0px)); }
	to { transform: translateX(0); }
}

.wp-site-blocks > header::before {
	content: "";
	display: block;
	height: var(--entete-barre);
	background-color: color-mix(in srgb, var(--wp--preset--color--accent-2) 20%, transparent);
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	border-bottom:1px solid rgba(255, 255, 255, .1);
}

.wp-site-blocks > header::after {
	content: "";
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: var(--entete-barre);
	background-color: var(--wp--preset--color--accent-2);
	animation: progression-lecture 1s linear both paused;
	animation-delay: calc(var(--progression, 0) * -1s);
}

@supports (animation-timeline: scroll()) {
	.wp-site-blocks > header::after {
		animation: progression-lecture linear both;
		animation-timeline: scroll(root);
	}
}

/* Défilement fluide vers les ancres (et retour en haut par le logo), respecte
   scroll-padding-top. Désactivé pour prefers-reduced-motion: reduce. */
@media (prefers-reduced-motion: no-preference) {
	html {
		scroll-behavior: smooth;
	}
}

/* Transition entre pages : fondu au noir (fond `base`) en sortie, fondu d'entrée sur la nouvelle page.
   Natif (View Transitions entre documents, même origine), aucun JS ; un navigateur sans support change
   de page normalement. L'intro du monogramme et les reveal jouent sous le fondu d'entrée, inchangés.
   Durées en dur, réglées ici (les pseudo-éléments héritent de :root). Rien sous reduce. */
:root {
	--page-sortie: .3s;
	--page-entree: .4s;
}

@media (prefers-reduced-motion: no-preference) {
	@view-transition {
		navigation: auto;
	}

	::view-transition {
		background: var(--wp--preset--color--base);
	}

	::view-transition-old(root) {
		animation: mcleod-page-sortie var(--page-sortie) ease-in forwards;
	}

	::view-transition-new(root) {
		opacity: 0;
		animation: mcleod-page-entree var(--page-entree) ease-out var(--page-sortie) forwards;
	}
}

@keyframes mcleod-page-sortie {
	to {
		opacity: 0;
	}
}

@keyframes mcleod-page-entree {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

/* Couche motion : apparition des blocs (assets/js/motion.js, inc/motion.php). Classes opt-in posées
   dans l'éditeur (Avancé > Classes CSS additionnelles) : `reveal`, `reveal reveal--fade`,
   `reveal reveal--interligne`, `reveal-stagger` (enfants directs en cascade). Valeurs : tokens settings.custom.motion.
   Exception à la règle de mouvement (CLAUDE.md) : état masqué SOUS `.js` uniquement, levé en
   impression et sous prefers-reduced-motion. Jamais sur le hero, au-dessus de la ligne de
   flottaison, ni sur une section qui contient un élément sticky ou fixed (`translate` en cours
   d'animation fait de l'élément le bloc conteneur de ses descendants fixed).
   Animation (keyframes) et non transition : `transition` écraserait celles que l'élément a déjà
   (survol d'un bouton...). Sans `fill: forwards` : la fin de l'animation rend la main au style
   normal de l'élément, aucun résidu. `fill: backwards` tient l'état de départ pendant le délai
   de cascade. `translate` plutôt que `transform` : n'écrase pas un transform existant. */
.js :is(.reveal, .reveal-stagger > *):not(.is-revealed) {
	opacity: 0;
}

.js :is(.reveal, .reveal-stagger > *).is-revealed {
	animation: mcleod-reveal var(--wp--custom--motion--duration) var(--wp--custom--motion--easing) backwards;
	animation-delay: calc(var(--motion-i, 0) * var(--wp--custom--motion--stagger));
}

.reveal--fade {
	--motion-distance: 0px;
}

@keyframes mcleod-reveal {
	from {
		opacity: 0;
		translate: 0 var(--motion-distance, var(--wp--custom--motion--distance));
	}
}

/* `reveal reveal--interligne` : fondu du bloc (sans montée) + lignes qui se resserrent. motion.js
   enveloppe chaque mot dans un span.reveal-mot et lui donne --ligne-pos (rang de sa ligne, 0 pour la
   première, qui ne bouge pas : les suivantes remontent vers elle) et au bloc --interligne-ecart (interligne × (facteur − 1), px). Chaque mot part
   décalé de --ligne-pos × écart et revient en place par `translate` : composité, sous-pixel, aucun
   recalcul de mise en page (le bloc a sa hauteur finale dès la première image). Méthode
   précédente abandonnée : animer `line-height` recalcule la mise en page à chaque image et cale
   les lignes au pixel entier, donc saccadé, et la suite du bloc bougeait.
   inline-block (nécessaire à `translate`) seulement pendant l'animation (is-interligne-anime,
   retirée par le script à la fin). Même délai de cascade que `reveal`. Sous no-preference : sous
   reduce, le script ne découpe rien et la règle `animation: none` ci-dessous reste seule. */
.reveal--interligne {
	--motion-distance: 0px;
}

@media (prefers-reduced-motion: no-preference) {
	.js .reveal--interligne.is-interligne-anime .reveal-mot {
		display: inline-block;
		animation: mcleod-interligne var(--wp--custom--motion--interligne--duration) var(--wp--custom--motion--interligne--easing) backwards;
		animation-delay: calc(var(--motion-i, 0) * var(--wp--custom--motion--stagger));
	}
}

@keyframes mcleod-interligne {
	from {
		translate: 0 calc(var(--ligne-pos, 0) * var(--interligne-ecart, 0px));
	}
}

@media (prefers-reduced-motion: reduce) {
	.js :is(.reveal, .reveal-stagger > *):not(.is-revealed) {
		opacity: 1;
	}

	.js :is(.reveal, .reveal-stagger > *).is-revealed {
		animation: none;
	}
}

@media print {
	.js :is(.reveal, .reveal-stagger > *):not(.is-revealed) {
		opacity: 1;
	}
}

/* Parallaxe : CSS seul, sur la VRAIE position de scroll (le scroll inertiel interpole le scroll
   natif, il ne le simule pas). Classe `parallax` sur un bloc Image ou Couverture (image de fond,
   pas le mode « fond fixe »). Images photographiques seulement (CLAUDE.md). Timeline nommée posée
   sur le CADRE (figure / couverture, qui ne bouge pas), jamais sur l'image animée elle-même : sa
   progression ne dépend pas de la transformation qu'elle pilote. Échelle 1.25 = 12,5 % de marge par
   bord ; amplitude 10 % < 12,5 % : aucun bord découvert. Sans support (Firefox) : image statique, non
   agrandie. Sous `.js` : rien dans l'éditeur (style.css y est chargé, la classe js n'y est pas). */
@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		.js :is(.wp-block-image, .wp-block-cover).parallax {
			overflow: hidden;
			view-timeline: --mcleod-parallax block;
		}

		.js .wp-block-image.parallax img,
		.js .wp-block-cover.parallax > .wp-block-cover__image-background {
			scale: var(--wp--custom--motion--parallax--echelle);
			animation: mcleod-parallax linear both;
			animation-timeline: --mcleod-parallax;
		}
	}
}

@keyframes mcleod-parallax {
	from {
		translate: 0 calc(var(--wp--custom--motion--parallax--amplitude) * -1);
	}

	to {
		translate: 0 var(--wp--custom--motion--parallax--amplitude);
	}
}

/* Fond "verre" partagé par la pilule, le bouton Contact et le burger : base à
   60 % (dérivé du token, jamais un hex). Le flou (--wp--custom--blur) est posé
   sur chaque élément, jamais sur un ancêtre d'un élément position:fixed. Déclaré sur :root (et non
   plus sur .entete) pour être lisible hors de l'en-tête : réponses du diagnostic (plugin). */
:root {
	--entete-verre: color-mix(in srgb, var(--wp--preset--color--base) 60%, transparent);
}

/* Header "en pilule" au défilement (≤781px, session V2 — remplace .entete__fond,
   supprimé). Fond sombre + flou posés DÉSORMAIS DIRECTEMENT sur .entete. Le menu
   mobile n'est plus un descendant de .entete depuis la reconstruction en <dialog>
   indépendant (voir plus bas, « Dialogue du menu mobile ») : le piège backdrop-
   filter/position:fixed (CLAUDE.md, « backdrop-filter et éléments position:fixed »)
   qui imposait de neutraliser ce backdrop-filter pendant l'ouverture de l'ancien
   overlay natif ne s'applique plus, rien à neutraliser ici. Repos : blur(0) plutôt
   que `none`, pour que la transition interpole (CSS n'anime pas none <-> une
   valeur de filtre). */
@media (max-width: 781px) {
	.entete {
		border-radius: 5rem;
		margin-left: var(--entete-marge-laterale);
		margin-right: var(--entete-marge-laterale);
		background-color: transparent;
		-webkit-backdrop-filter: blur(0);
		backdrop-filter: blur(0);
	}

	.entete.est-descendu {
		background-color: color-mix(in srgb, var(--wp--preset--color--base) calc(var(--wp--custom--entete--fond-opacite) * 100%), transparent);
		-webkit-backdrop-filter: blur(var(--wp--custom--entete--blur-fond));
		backdrop-filter: blur(var(--wp--custom--entete--blur-fond));
	}

	/* Glissement : le header flotte désormais à top:0.75rem (voir plus haut) — pour
	   qu'il disparaisse ENTIÈREMENT (pas un filet de 0.75rem resté collé au bord
	   haut), la translation doit couvrir sa propre hauteur ET ce décalage.
	   Le piège backdrop-filter/position:fixed qui occupait cet endroit (neutraliser
	   .entete pendant que l'ancien overlay natif était ouvert) a disparu avec lui :
	   le menu mobile est désormais un <dialog> indépendant, enfant direct de <body>
	   (inc/menu-mobile-dialogue.php), jamais un descendant de .entete — aucun risque
	   que cet ancêtre devienne son bloc conteneur, quel que soit son propre
	   backdrop-filter/transform. */
	.entete.est-masque {
		transform: translateY(calc(-100% - 0.75rem));
	}

	@media (prefers-reduced-motion: no-preference) {
		.entete {
			transition: transform 250ms ease, background-color 250ms ease, -webkit-backdrop-filter 250ms ease, backdrop-filter 250ms ease;
		}
	}
}

/* Contact, toutes largeurs : même fond et même flou que la pilule (bat le
   transparent de la variation, à spécificité nulle) ; le survol de la variation
   (trois classes) reste plus fort : inversion accent/base inchangée. */
.entete .wp-block-button__link {
	background-color: var(--entete-verre);
	-webkit-backdrop-filter: blur(var(--wp--custom--blur));
	backdrop-filter: blur(var(--wp--custom--blur));
}

/* En-tête, disposition. Le bloc reste en layout flex côté attributs ; le CSS
   surcharge display en grille (.wp-block-group.entete, deux classes, l'emporte sur
   is-layout-flex). Padding horizontal en CSS (pas en attribut : un style inline
   ne serait pas surchargeable par point de rupture).
   - Au-delà de 1080px : 1fr auto 1fr, le menu est centré sur la page quelle que
     soit la largeur du logo et du bouton (flex space-between le décalerait).
   - Jusqu'à 1080px : logo à gauche, Contact puis burger groupés à droite, tous
     deux enfants de .wp-block-buttons (parts/header.html) : le burger n'est plus
     dans le bloc navigation (reconstruction en <dialog> indépendant, voir plus
     bas) — ordre visuel et ordre de focus coïncident désormais (DOM = Contact
     puis burger, dans cet ordre, à cette seule et même case de la grille). */
.wp-block-group.entete {
	display: grid;
	align-items: center;
	padding-inline: var(--wp--preset--spacing--lg);
	grid-template-columns: 1fr auto auto;
	column-gap: var(--wp--preset--spacing--xs);
}

/* grid-row explicite : sans lui, le placement automatique (dense off) renvoie
   Contact (colonne 2) sur une seconde rangée, puisque le menu (colonne 3, avant
   lui dans le DOM) a déjà avancé le curseur. */
.entete > .wp-block-site-logo {
	grid-area: 1 / 1;
}

.entete > .entete__nav {
	grid-area: 1 / 3;
}

.entete > .wp-block-buttons {
	grid-area: 1 / 2;
	justify-self: end;
}

/* Pilule desktop/tablette masquée en dessous de 1081px : depuis overlayMenu:
   "never" (parts/header.html), core/navigation ne rend plus ici qu'un simple <ul>
   — sans mécanisme natif pour le cacher à cette largeur (l'ancien overlay s'en
   chargeait lui-même). Les liens restent accessibles via le menu mobile en
   <dialog> (inc/menu-mobile-dialogue.php), qui lit le même menu WordPress. */
@media (max-width: 1080px) {
	.entete__nav .wp-block-navigation__container {
		display: none;
	}
}

@media (min-width: 1081px) {
	.wp-block-group.entete {
		grid-template-columns: 1fr auto 1fr;
		column-gap: 0;
	}

	.entete > .entete__nav {
		grid-area: 1 / 2;
	}

	.entete > .wp-block-buttons {
		grid-area: 1 / 3;
	}

	/* Menu en pilule : fond "verre" (base à 60 %, dérivé du token) + flou, radius
	   pill. IMPÉRATIF : backdrop-filter reste dans CETTE media query et n'est jamais
	   posé sur un parent du menu mobile (voir CLAUDE.md, piège backdrop-filter /
	   position:fixed — le menu mobile est désormais un <dialog> indépendant, mais
	   la règle générale reste une bonne discipline). Le blockGap du bloc (sm,
	   attribut) sert à l'écart entre entrées ; ici gap:0, l'écart vient du padding
	   des liens. RÉSERVÉ à la barre horizontale (au-delà de 1080px) : depuis
	   overlayMenu:"never" (parts/header.html), ce bloc ne rend plus QUE ce <ul> —
	   masqué en dessous de 1080px (règle juste après), seul le menu mobile en
	   <dialog> (inc/menu-mobile-dialogue.php) y affiche les liens. */
	.entete__nav .wp-block-navigation__container {
		gap: 0;
		background-color: var(--entete-verre);
		-webkit-backdrop-filter: blur(var(--wp--custom--blur));
		backdrop-filter: blur(var(--wp--custom--blur));
		border: 1px solid var(--wp--preset--color--line);
		border-radius: var(--wp--preset--border-radius--pill);
		padding-inline: var(--wp--preset--spacing--xs);
	}

	/* Liens en taille sm (l'overlay garde celle du bloc, chapo). letter-spacing
	   posé ICI et non sur le conteneur : un letter-spacing en em s'hérite en
	   valeur calculée, donc 0.05em de 18px (conteneur) au lieu de 0.05em de 14px. */
	.entete__nav .wp-block-navigation-item__content {
		padding: var(--wp--preset--spacing--xs) var(--wp--preset--spacing--sm);
		font-size: var(--wp--preset--font-size--sm);
		letter-spacing: 0.05em;
	}
}

/* Le cœur pose background-color:inherit sur chaque item (pensé pour les
   sous-menus) : il repeindrait le fond translucide de la pilule par-dessus
   lui-même, avec des coins carrés et un fond doublé. */
.entete__nav .wp-block-navigation-item {
	background-color: transparent;
}
@media (min-width: 1080px) {
	.entete__nav .wp-block-navigation-item.hide-on-desktop{ display:none;}
}
/* Scrollspy (assets/js/scrollspy.js, front page uniquement) : état "current" du lien de menu dont
   la section ancrée est visible à l'écran, et :hover/:focus-visible — jauge qui remplit les
   LETTRES de la couleur d'accent (le texte est peint par son propre fond, background-clip:text).
   Plus de soulignement (retiré, demandé). Réglages propres à cet effet, déclarés une seule fois sur
   .entete__nav, distincts du token bouton --wp--custom--transition--* (durée différente demandée :
   0.35s ici contre 0.2s pour les boutons).
   POSÉ SUR LE SPAN (.wp-block-navigation-item__label), PAS SUR LE <a> — diagnostic : le cœur WP
   (wp-includes/blocks/navigation/style.min.css) pose `.wp-block-navigation .wp-block-navigation-
   item__content.wp-block-navigation-item__content { color: inherit }`, classe DOUBLÉE (technique
   volontaire du cœur pour gonfler sa spécificité à 3 classes), qui battait `color:transparent` posé
   ici sur le <a> (2 classes seulement) : `-webkit-text-fill-color` passait, `color` retombait sur
   blanc hérité, suffisant à casser l'effet. Aucune règle du cœur ne cible `color` sur le span
   (vérifié) : en posant directement dessus, plus de concurrence de spécificité à gérer, jamais
   besoin de doubler une classe ni d'`!important`. `display:inline-block` : un span `inline` ne
   donne pas une base fiable aux pourcentages de `background-size` d'un moteur à l'autre.
   Couleur de remplissage = accent PARTOUT (desktop ET overlay mobile), décision explicite d'Yvan
   après signalement : l'overlay mobile a lui-même un fond accent (overlayBackgroundColor,
   parts/header.html, vérifié rgb(236,96,91)) — un lien actif y devient donc accent sur accent,
   illisible. Écart connu et accepté tel quel, pas corrigé ici. */
.entete__nav {
	--base: var(--wp--preset--color--contrast);
	--fill: var(--wp--preset--color--accent);
	--duree-remplissage: 0.35s;
	--allure-remplissage: cubic-bezier(.65, 0, .35, 1);
}

.entete__nav .wp-block-navigation-item__content .wp-block-navigation-item__label {
	display: inline-block;
	/* En CSS, la PREMIÈRE image listée dans `background-image` est peinte AU-DESSUS, jamais en
	   dessous (vérifié par test isolé après un premier essai dans le mauvais ordre : la couche du
	   bas, opaque et à 100% en permanence, recouvrait alors totalement la couche de remplissage —
	   seuls les bords anti-aliasés des lettres, partiellement transparents, laissaient deviner la
	   couleur dessous, d'où un effet de "contour" au lieu d'un vrai remplissage).
	   Couche du HAUT (--fill, listée en premier) : ancrée à DROITE au repos, taille 0 (invisible,
	   la couche du dessous reste donc seule visible). Couche du BAS (--base) : pleine largeur,
	   fixe, jamais animée. background-position n'est JAMAIS transitionné — seule background-size
	   l'est : c'est ce jump sans transition qui donne son sens au remplissage/vidage, pas une
	   interpolation de position. */
	background-image: linear-gradient(var(--fill), var(--fill)), linear-gradient(var(--base), var(--base));
	background-repeat: no-repeat, no-repeat;
	background-position: 100% 0, 0 0;
	background-size: 0% 100%, 100% 100%;
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	-webkit-text-fill-color: transparent;
	transition: background-size var(--duree-remplissage) var(--allure-remplissage);
}

/* État piloté par le <a> (hover/focus/scrollspy), appliqué au span qui porte l'effet. Un lien
   .is-current survolé puis quitté reste plein : il appartient toujours à ce groupe de sélecteurs,
   qu'importe l'état :hover à côté. */
.entete__nav .wp-block-navigation-item__content:is(:hover, :focus-visible, .is-current) .wp-block-navigation-item__label {
	background-position: 0 0, 0 0;
	background-size: 100% 100%, 100% 100%;
}

/* Remontée (data-direction="haut", posé par scrollspy.js sur .entete__nav juste avant de changer
   l'état actif — comparaison de l'index de l'ancienne et de la nouvelle cible dans l'ordre du
   menu) : mêmes règles, MIROIR — le repos ancre à gauche (au lieu de droite), l'état actif ancre à
   droite (au lieu de gauche). Le remplissage entre donc par la droite et sort par la gauche,
   suivant le sens du parcours plutôt qu'un sens fixe quelle que soit la direction. Seule
   `background-position` change ici (jamais `background-size`, déjà correct par ailleurs) :
   spécificité plus élevée que les deux règles ci-dessus (classe + attribut), les bat sans
   ambiguïté quel que soit l'ordre des feuilles. */
.entete__nav[data-direction="haut"] .wp-block-navigation-item__content .wp-block-navigation-item__label {
	background-position: 0 0, 0 0;
}

.entete__nav[data-direction="haut"] .wp-block-navigation-item__content:is(:hover, :focus-visible, .is-current) .wp-block-navigation-item__label {
	background-position: 100% 0, 0 0;
}

@media (prefers-reduced-motion: reduce) {
	.entete__nav .wp-block-navigation-item__content .wp-block-navigation-item__label {
		transition: none;
	}
}

/* Mode contraste forcé (Windows High Contrast etc.) : l'effet de fond/texte transparent y rendrait
   le texte invisible (le SE ignore les couleurs personnalisées mais pas forcément background-clip)
   — repli en texte plein, couleur système (currentColor). */
@media (forced-colors: active) {
	.entete__nav .wp-block-navigation-item__content .wp-block-navigation-item__label {
		background: none;
		color: currentColor;
		-webkit-text-fill-color: currentColor;
	}
}

/* Burger : nouveau bouton indépendant (.menu-mobile-dialogue__ouvrir,
   parts/header.html, assets/js/menu-mobile-dialogue.js) — remplace le bouton
   natif de core/navigation (voir PASSATION.md pour l'historique). Même gabarit
   visuel que l'ancien (cercle 44px, fond verre, contour 1px line), jamais affiché
   au-delà de 1080px (le menu mobile — le <dialog> — n'existe qu'en dessous).
   Cibles tactiles de 44px minimum (burger et Contact) jusqu'à 1080px. 44px =
   minimum WCAG 2.5.5, valeur fixe non exprimable en token. */
@media (max-width: 1080px) {
	.menu-mobile-dialogue__ouvrir {
		position: relative;
		display: flex;
		align-items: center;
		justify-content: center;
		box-sizing: border-box;
		width: 44px;
		height: 44px;
		border: 1px solid var(--wp--preset--color--line);
		border-radius: var(--wp--preset--border-radius--pill);
		background-color: var(--entete-verre);
		-webkit-backdrop-filter: blur(var(--wp--custom--blur));
		backdrop-filter: blur(var(--wp--custom--blur));
		color: var(--wp--preset--color--contrast);
		cursor: pointer;
	}

	.menu-mobile-dialogue__ouvrir::before {
		content: "";
		display: block;
		width: 1.5rem;
		height: 1.5rem;
		background-color: currentColor;
		-webkit-mask: url("assets/icons/menu.svg") center / contain no-repeat;
		mask: url("assets/icons/menu.svg") center / contain no-repeat;
	}
}

@media (min-width: 1081px) {
	.menu-mobile-dialogue__ouvrir {
		display: none;
	}
}

@media (max-width: 1080px) {
	/* Contact réduit à son picto (enveloppe, mask-image comme "Formulaire"). Le
	   texte reste dans le lien pour le nom accessible : il est sorti du cadre par
	   text-indent + overflow (la classe .screen-reader-text, définie côté front,
	   ne peut pas se poser sur un nœud texte nu de core/button). display:block :
	   un inline-block à overflow:hidden aligne sa ligne de base sur son bord bas, ce
	   qui ajoutait ~7px de hauteur (mesuré) au wrapper .wp-block-button. */
	.entete .wp-block-button__link {
		display: block;
		position: relative;
		box-sizing: border-box;
		width: 44px;
		height: 44px;
		padding: 0;
		overflow: hidden;
		white-space: nowrap;
		text-indent: 100%;
	}

	.entete .wp-block-button__link::before {
		content: "";
		position: absolute;
		inset: 0;
		margin: auto;
		width: 1.75em;
		height: 1.75em;
		background-color: currentColor;
		-webkit-mask: url("assets/icons/enveloppe.svg") center / contain no-repeat;
		mask: url("assets/icons/enveloppe.svg") center / contain no-repeat;
	}
}

@media (max-width: 781px) {
	.wp-block-group.entete {
		padding-inline: var(--entete-espacement);
	}

	/* Trois custom properties partagées (session V2-bis) : positionnement de la
	   pilule elle-même (flottement, marges, padding), consommées aussi par
	   --entete-hauteur juste en dessous. Le burger tombe à top:32px / right:36px
	   par construction (grille + ce padding, aucune propriété CSS directe) —
	   mais le bouton de FERMETURE, dans l'overlay plein écran, est sur un système
	   de coordonnées différent (l'overlay se repère au viewport, pas à .entete) :
	   une formule CSS miroir a été essayée puis abandonnée, mesurée en défaut
	   (voir plus bas, "Fermeture") — sa position consomme la valeur RENDUE du
	   burger, mesurée en JS (assets/js/entete-scroll.js), jamais ces trois
	   variables directement. */
	:root {
		--entete-decalage-haut: 0.75rem; /* flottement du header, voir plus haut */
		--entete-marge-laterale: 1rem; /* marge gauche/droite de la pilule, voir .entete */
		--entete-espacement: var(--wp--preset--spacing--sm); /* padding de la pilule, haut/bas ET gauche/droite */
	}

	:root {
		--entete-barre: var(--wp--preset--dimension--progression-sm);
		/* --entete-decalage-haut (flottement du header, pas --entete-barre) + logo à
		   2.5rem (voir "Logo et pictos" plus bas — plus logo-sm) : le header ne
		   réserve plus de padding-top pour la barre de progression (retiré
		   ci-dessus), c'est le flottement lui-même qui l'en dégage. */
		--entete-hauteur: calc(var(--entete-decalage-haut) + 2 * var(--entete-espacement) + 2.5rem * 72 / 71);
	}

	/* Logo et pictos (session V2) : logo, bouton enveloppe et burger/fermeture
	   uniformément à 2.5rem (40px) — sous le minimum WCAG 2.5.5 (44px), compensé
	   par une cible invisible plus grande (::before/::after ci-dessous), sans
	   changer le rendu visuel. Valeur littérale demandée explicitement par Yvan
	   (exception documentée au même titre que les 44px déjà en place plus haut). */
	.custom-logo-link {
		position: relative;
	}

	/* La taille du logo (2.5rem) est déclarée une seule fois, plus bas dans ce
	   fichier (règle "Logo réduit jusqu'à 781px") — jamais ici en double, pour ne
	   pas dépendre de l'ordre des règles à spécificité égale. */

	.menu-mobile-dialogue__ouvrir,
	.entete .wp-block-button__link {
		width: 2.5rem;
		height: 2.5rem;
	}

	/* Cible tactile à 44px (2.5rem + 2px de marge de chaque côté = 44px), centrée,
	   totalement transparente ("sans changer le rendu"). Sur .custom-logo-link,
	   ::before, libre. Sur le burger et le bouton Contact, ::before porte déjà
	   l'icône (masque) : la cible passe par ::after sur les deux. */
	.custom-logo-link::before,
	.menu-mobile-dialogue__ouvrir::after,
	.entete .wp-block-button__link::after {
		content: "";
		position: absolute;
		inset: -2px;
	}
}

/* Dialogue du menu mobile (inc/menu-mobile-dialogue.php, assets/js/menu-mobile-
   dialogue.js) : <dialog> indépendant, même mécanisme que le dialogue de contact
   (inc/contact-dialogue.php) — remplace l'overlay natif de core/navigation
   (overlayMenu:"mobile"), abandonné après plusieurs bugs non résolus en navigateur
   réel (bloc conteneur backdrop-filter, animation fill:forwards jamais annulée,
   superposition bouton Fermer/burger réel à la fermeture — voir PASSATION.md pour
   l'historique complet). Enfant direct de <body> (injecté par wp_footer, hors de
   .wp-site-blocks, comme contact-dialogue.php) : aucun ancêtre ne peut jamais
   devenir son bloc conteneur par accident (voir CLAUDE.md, « backdrop-filter et
   éléments position:fixed »), à la différence de l'ancien overlay, nécessairement
   un descendant de .entete. */
.menu-mobile-dialogue,
.menu-mobile-dialogue * {
	box-sizing: border-box;
}

.menu-mobile-dialogue {
	position: fixed;
	inset: 0;
	margin: 0;
	padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
		env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
	border: 0;
	width: 100%;
	height: 100%;
	max-width: none;
	max-height: none;
	/* overflow:clip sur les DEUX axes — trouvé par mesure (scrollWidth 416 contre
	   375 à 375px ; puis scrollHeight 854 contre 812, isolé en masquant le
	   filigrane un par un : le débordement vertical tombe à 0 sans lui) : le
	   filigrane (.menu-mobile-dialogue__filigrane) déborde INTENTIONNELLEMENT de
	   15% en bas ET à droite par transform (voir sa règle), un débordement que
	   l'ancien overlay natif recadrait via son propre overflow:hidden — oublié
	   ici à la reconstruction. `clip`, PAS `hidden` : vérifié que `hidden`
	   bloque bien la BARRE de défilement mais laisse encore passer un défilement
	   réel (molette, tactile, scrollTop en JS) — seul `clip` empêche tout
	   défilement par n'importe quel moyen. Jamais de scroll réel nécessaire pour
	   5 liens courts ; à revoir si le menu grandit un jour au point de ne plus
	   tenir verticalement. */
	overflow: clip;
	background-color: var(--wp--preset--color--accent-2);
	color: var(--wp--preset--color--contrast);
}

/* `display` SCOPÉ à [open] — piège trouvé par capture (le dialogue fermé restait
   visuellement affiché) : un `display` d'AUTEUR sans condition bat TOUJOURS le
   `dialog:not([open]){display:none}` de l'agent utilisateur, quelle que soit sa
   spécificité (l'origine "auteur" l'emporte sur "agent utilisateur" dans la
   cascade). contact-dialogue.js évite ce piège en ne posant JAMAIS `display` lui-
   même ; ici le flex (barre + liens empilés) en a besoin, donc scopé à [open]
   plutôt que retiré, pour ne jamais reprendre la main sur l'état fermé. */
.menu-mobile-dialogue[open] {
	display: flex;
	flex-direction: column;
}

/* Jamais affiché au-delà de 1080px : le déclencheur (.menu-mobile-dialogue__ouvrir)
   n'existe lui-même pas à ces largeurs — garde-fou supplémentaire, pas le
   mécanisme principal (rien ne peut de toute façon l'ouvrir sans lui). */
@media (min-width: 1081px) {
	.menu-mobile-dialogue[open] {
		display: none;
	}
}

.menu-mobile-dialogue__barre {
	position: fixed;
	top: var(--wp--preset--spacing--lg);
	right: var(--wp--preset--spacing--lg);
	z-index: 2;
}

/* Bouton Fermer : même gabarit que contact-dialogue__fermer (cercle à contour
   blanc, icône assets/icons/fermer.svg en mask-image) — réutilisé à l'identique,
   deux composants distincts mais la même convention visuelle pour "fermer un
   dialogue plein écran" sur ce site. */
.menu-mobile-dialogue__fermer {
	width: var(--wp--preset--dimension--cible);
	height: var(--wp--preset--dimension--cible);
	display: grid;
	place-items: center;
	background-color: transparent;
	color: var(--wp--preset--color--contrast);
	border: 1px solid var(--wp--preset--color--contrast);
	border-radius: var(--wp--preset--border-radius--pill);
	cursor: pointer;
}

.menu-mobile-dialogue__fermer:focus-visible {
	outline: 2px solid var(--wp--preset--color--base);
	outline-offset: 2px;
}

.menu-mobile-dialogue__fermer::before {
	content: "";
	display: block;
	width: 0.875rem;
	height: 0.875rem;
	background-color: currentColor;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
	-webkit-mask-image: url("assets/icons/fermer.svg");
	mask-image: url("assets/icons/fermer.svg");
}

/* Focus posé sur le dialogue lui-même à l'ouverture au doigt ou à la souris
   (assets/js/menu-mobile-dialogue.js) : simple point de départ de la tabulation,
   jamais un repère visible. */
.menu-mobile-dialogue:focus {
	outline: none;
}

@media (max-width: 640px) {
	.menu-mobile-dialogue__barre {
		top: var(--wp--preset--spacing--sm);
		right: var(--wp--preset--spacing--sm);
	}
}

/* Scroll de la page bloqué tant que le dialogue est ouvert — même règle, même
   principe que .contact-dialogue (scopée à CE dialogue, jamais à "dialog[open]"
   en général). */
body:has(.menu-mobile-dialogue[open]) {
	overflow: hidden;
}

/* Liens : grands, empilés, centrés verticalement dans l'espace disponible sous la
   barre. Tailles/espacement repris tels quels de l'ancien overlay natif (déjà
   choisis et validés par Yvan), portés ici sans y toucher. core/navigation,
   overlayMenu:"never" dans ce contexte : simple <ul>, aucun burger ni panneau
   propre à neutraliser. */
.menu-mobile-dialogue__liens {
	flex: 1;
	display: flex;
	align-items: flex-start;
	
	Padding-top:10rem;
}

/* ≤640px (même seuil que .menu-mobile-dialogue__barre juste en dessous) :
   padding réduit au gutter racine du site (md, 32px) — trouvé par mesure, pas
   deviné : à "lg" (60px), il ne restait que 255px de large à 375px, pas assez
   pour "Questions fréquentes" sur une seule ligne (voir la règle de taille de
   police juste en dessous, qui dépend elle aussi de cette largeur). */
@media (max-width: 640px) {
	.menu-mobile-dialogue__liens {
		padding-inline: var(--wp--preset--spacing--md);
	}
}

.menu-mobile-dialogue__liens .wp-block-navigation__container {
	display: flex;
	flex-direction: column;
	/* align-items:flex-start — trouvé par mesure (chaque lien plus court que le
	   plus long se retrouvait centré horizontalement, "mal calé" signalé par
	   Yvan) : align-items:center venait du layout natif du bloc (is-layout-flex),
	   jamais posé par ce fichier — "flex-start" ferre chaque lien à gauche,
	   contrairement au "stretch" qui les aurait tous élargis à la largeur du
	   plus long. */
	align-items: flex-start;
	gap: var(--wp--preset--spacing--sm);
	list-style: none;
	margin: 0;
	padding: 0;
}

.menu-mobile-dialogue__liens .wp-block-navigation-item__content {
	display: block;
	padding-block: 0;
	font-size: clamp(1.75rem, 8vw, 2.25rem);
	line-height: 1.2;
	color: inherit;
	text-decoration: none;
}

/* Filigrane décoratif (monogramme YR, lettres seules) : position/débordement
   repris tels quels de l'ancien overlay (15 %/15 % hors cadre, mesuré symétrique
   aux trois largeurs de référence — voir l'historique dans PASSATION.md). Jamais
   besoin ici d'un display:none conditionnel : ce SVG n'existe QUE dans ce dialogue
   (inc/menu-mobile-dialogue.php), jamais dans la pilule desktop. */
.menu-mobile-dialogue__filigrane {
	position: absolute;
	right: 0;
	bottom: 0;
	width: 75vw;
	height: auto;
	transform: translate(15%, 15%);
	color: color-mix(in oklab, var(--wp--preset--color--accent-2) 80%, white);
	pointer-events: none;
}

/* Bouton "Contour accent" (styles/bouton-contour-accent.json) : le survol global
   (fond accent assombri, même teinte que le fond transparent) ferait perdre le
   contour ; fond accent et texte contrast (blanc) à la place, pour un effet plus marqué.
   Le picto du Contact de l'en-tête (≤1080px, currentColor) passe en blanc avec lui. */
.is-style-contour-accent .wp-block-button__link:hover {
	background-color: var(--wp--preset--color--accent);
	color: var(--wp--preset--color--contrast);
}
.is-style-contour-accent .wp-block-button__link .bouton-filler{
	background-color:var(--wp--preset--color--accent);

}

/* Footer en deux zones (parts/footer.html) : zone CTA (le pattern cta-panneau, tel quel,
   dans .pied-de-page__cta) puis zone navigation (.pied-de-page__zone-nav : grille, filet,
   barre basse). Le footer occupe au moins la hauteur de l'écran, la zone CTA prend
   l'espace restant et centre son contenu verticalement ; si le contenu dépasse, le footer
   grandit. width:100% sur les enfants de la racine : en flex column, les marges inline auto
   du layout constrained du cœur les réduiraient sinon à leur largeur de contenu. */
.wp-site-blocks > footer {
	display: flex;
	flex-direction: column;
	min-height: 100svh;
}

.pied-de-page {
	flex: 1 0 auto;
	display: flex;
	flex-direction: column;
}

.pied-de-page > * {
	width: 100%;
}

.pied-de-page__cta {
	flex: 1 0 auto;
	display: flex;
	flex-direction: column;
	justify-content: center;
	/* Espace supplémentaire au-dessus du panneau, en plus du padding haut (lg) du footer. */
	padding-block-start: var(--wp--preset--spacing--md);
}

/* Panneau du CTA dans le footer : filet 1px --etude-filet, même filet que la barre basse et
   les boutons du footer (règle de contexte, le pattern reste sans bordure ailleurs). */
.pied-de-page__cta .cta-panneau {
	border: 1px solid var(--etude-filet);
}

/* Le <section> alignfull du CTA (constrained, root padding) ajoute ses propres 32px aux 32px de
   .pied-de-page : neutralisé sur mobile, où ce double retrait réduisait le panneau à 247px (375px).
   Deux classes + élément pour battre .has-global-padding du cœur. */
@media (max-width: 781px) {
	.pied-de-page .pied-de-page__cta > section {
		padding-inline: 0;
	}
}

/* Pages sans hero (modèles page-legale et page-sans-titre, classe sans-hero sur <main>) :
   respiration entre l'en-tête et le premier contenu. Variable et non padding direct : sous
   has-footer-reveal (plus bas), <main> remonte sous l'en-tête et compense par un padding égal
   à sa hauteur ; la respiration s'y ajoute au lieu de la remplacer. Vécu : un padding-top en
   attribut de bloc (style en ligne) écrasait cette compensation et glissait le H1 des mentions
   légales sous l'en-tête, à partir de 782px. */
.wp-site-blocks > main.sans-hero {
	--main-respiration: var(--wp--preset--spacing--lg);
	padding-block-start: var(--main-respiration);
}

/* Footer dévoilé (inc/footer-reveal.php, assets/js/footer-reveal.js). Uniquement sous
   html.has-footer-reveal, posée par le script si la fenêtre fait au moins 782px de large
   ET que le footer y tient en hauteur ; sinon footer en flux normal.
   - <main> : feuille opaque (fond base) au-dessus du footer, découpée par un clip-path
     RECTANGULAIRE (et non overflow : overflow ne découpe pas les descendants position:fixed
     — grain, lettres en parallaxe —, clip-path si). Rectangulaire et pas arrondi, mesuré
     (Chromium, 3 passes de 120 pas de molette) : avec l'arrondi, 22 à 26 images de plus de
     33ms sur ~355 en haut de page et 5 à 12 pendant le dévoilement ; inset(0) sans arrondi,
     0 partout, comme sans clip-path. Les coins arrondis sont DESSINÉS (main::after, deux
     quarts de cercle accent-2 posés sur les coins, au-dessus du grain). Aucune ombre (retirée
     à la demande d'Yvan) : le clip-path couperait de toute façon une ombre portée de <main>.
   - footer : sticky bottom:0, sous la feuille. Aucun ancêtre en overflow hidden/clip. */
html.has-footer-reveal .wp-site-blocks > main {
	position: relative;
	z-index: 1;
	/* La feuille remonte sous le header (transparent, sticky, z-index 10 au-dessus d'elle) :
	   la bande du header est couverte, et le haut des lettres de fond n'est plus coupé.
	   Marge négative compensée par un padding égal : aucune section ne bouge. --entete-bloc
	   = hauteur du header dans le flux, mesurée par footer-reveal.js. */
	margin-block-start: calc(var(--entete-bloc, 0px) * -1);
	padding-block-start: calc(var(--entete-bloc, 0px) + var(--main-respiration, 0px));
	/* Respiration entre le dernier contenu et le bord bas (arrondi) de la feuille. */
	padding-block-end: var(--wp--preset--spacing--xl);
	background-color: var(--wp--preset--color--base);
	clip-path: inset(0);
}

/* Page du diagnostic : au chargement, la feuille laisse voir exactement 85px du footer en bas de
   la fenêtre (valeur littérale demandée par Yvan), quelle que soit la hauteur d'écran. Hauteur
   minimale et non padding : la feuille démarre en haut de la page (marge négative sous
   l'en-tête, plus haut), sa hauteur fixe donc son bord bas. Barre d'admin déduite (elle décale
   la page). Contenu plus haut que la fenêtre : aucun effet, le footer se dévoile au défilement.
   Respiration basse réduite de xl à lg : avec xl (~190px), le contenu seul dépassait cette hauteur
   à 1440×900 et seuls 30px du footer restaient visibles.
   Ciblée par le bloc du plugin (:has), pas par l'ID de la page. */
html.has-footer-reveal .wp-site-blocks > main:has(.wp-block-yvan-diagnostic) {
	box-sizing: border-box;
	min-height: calc(100svh - 85px - var(--wp-admin--admin-bar--height, 0px));
	padding-block-end: var(--wp--preset--spacing--lg);
}

/* Dernier écran du diagnostic (résultat, formulaire : classe is-done du bloc, posée par son
   script) : respiration basse xl, celle des autres pages, sous le bouton du formulaire. Avec ou
   sans footer dévoilé (mobile : 0 sans cette règle, le bouton touchait le footer). */
.wp-site-blocks > main:has(.wp-block-yvan-diagnostic.is-done),
html.has-footer-reveal .wp-site-blocks > main:has(.wp-block-yvan-diagnostic.is-done) {
	padding-block-end: var(--wp--preset--spacing--xl);
}

/* Coins bas arrondis de la feuille : deux quarts de cercle couleur du footer (accent-2) posés
   dans les coins, au-dessus du contenu, du grain et des lettres. Le coin d'un rectangle moins
   un disque de rayon lg : visuellement, la feuille s'arrondit et laisse voir le footer. Les
   coins tombent toujours sur une zone unie du footer (marge latérale, hors contenu). */
html.has-footer-reveal .wp-site-blocks > main::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	bottom: 0;
	height: var(--wp--preset--border-radius--lg);
	z-index: 2;
	pointer-events: none;
	background:
		radial-gradient(circle at 100% 0, transparent calc(var(--wp--preset--border-radius--lg) - 0.5px), var(--wp--preset--color--accent-2) var(--wp--preset--border-radius--lg)) left bottom / var(--wp--preset--border-radius--lg) var(--wp--preset--border-radius--lg) no-repeat,
		radial-gradient(circle at 0 0, transparent calc(var(--wp--preset--border-radius--lg) - 0.5px), var(--wp--preset--color--accent-2) var(--wp--preset--border-radius--lg)) right bottom / var(--wp--preset--border-radius--lg) var(--wp--preset--border-radius--lg) no-repeat;
}

html.has-footer-reveal .wp-site-blocks > footer {
	position: sticky;
	bottom: 0;
	z-index: 0;
}

/* <main> positionné devient le bloc conteneur des lettres de fond quand elles restent
   absolues (sous reduce, sans parallaxe) : décalage de la position de <main> dans le
   document (--fond-lettres-decalage, posé par footer-reveal.js) pour les garder calées sur
   le haut du document, comme avant. Sans effet en parallaxe (position:fixed). */
html.has-footer-reveal .fond-lettres-page:not(.is-parallaxe) {
	top: calc(var(--fond-lettres-decalage, 0px) * -1);
}

/* Repère de fin de contenu (scrollspy) : 1px, compensé, aucune marge de flux. */
.wp-site-blocks > main > .fin-de-main {
	height: 1px;
	margin: -1px 0 0;
}

/* Animation de la zone CTA pendant le dévoilement : légère montée + fondu, liée à la
   sortie de <main> (timeline nommée --page, portée par .wp-site-blocks pour être visible
   du footer, frère de <main>). Amélioration progressive : support, no-preference et
   footer dévoilé requis ; sinon le contenu est simplement là. Plage bornée par le seul BORD
   BAS de <main> (entry-crossing 100% = bord bas en bas de la fenêtre, exit-crossing 100% =
   bord bas en haut) : la progression vaut la part du footer découverte, y compris sur une
   page courte (404) où le haut de <main> n'atteint jamais le haut de la fenêtre — avec
   "exit 0% exit 100%", le CTA visible sous une feuille courte restait à opacité 0.
   view-timeline-inset: 0 :
   la plage suit la fenêtre entière, pas la zone réduite par scroll-padding-top. Le tampon
   n'est pas animé ici (sa rotation reste temporelle). */
@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		html.has-footer-reveal .wp-site-blocks {
			timeline-scope: --page;
		}

		html.has-footer-reveal .wp-site-blocks > main {
			view-timeline: --page block;
			view-timeline-inset: 0;
		}

		html.has-footer-reveal .pied-de-page__cta .cta-panneau > :not(.cta-panneau__tampon) {
			animation: pied-de-page-reveal linear both;
			animation-timeline: --page;
			animation-range: entry-crossing 100% exit-crossing 100%;
		}
	}
}

@keyframes pied-de-page-reveal {
	from {
		opacity: 0;
		transform: translateY(var(--wp--preset--spacing--md));
	}
}

/* Pied de page (parts/footer.html) : la classe .pied-de-page est sur le groupe
   racine, qui porte fond, filet haut et padding vertical. Grille CSS à 12
   colonnes égales sur .pied-de-page__grille (pas core/columns : le mobile veut
   deux colonnes côte à côte, que l'empilement natif ne sait pas faire). Les
   colonnes de liens démarrent sur une ligne de grille. Points de rupture du
   thème. Desktop : identité 1-5, Navigation 6-7, Ressources 8-9, Contact 10-12.
   Tablette : identité pleine largeur, puis 3/4/5 (Contact le plus large : le
   bouton email fait ~243px). */
.pied-de-page__grille {
	display: grid;
	grid-template-columns: repeat(12, minmax(0, 1fr));
	column-gap: var(--wp--preset--spacing--md);
	row-gap: var(--wp--preset--spacing--md);
}

.pied-de-page__grille > * {
	min-width: 0;
}

.pied-de-page__identite {
	grid-column: 1 / span 5;
}

.pied-de-page__navigation {
	grid-column: 6 / span 2;
}

.pied-de-page__ressources {
	grid-column: 8 / span 2;
}

.pied-de-page__contact {
	grid-column: 10 / span 3;
}

@media (min-width: 782px) and (max-width: 1080px) {
	.pied-de-page__identite {
		grid-column: 1 / -1;
	}

	.pied-de-page__navigation {
		grid-column: 1 / span 3;
	}

	.pied-de-page__ressources {
		grid-column: 4 / span 4;
	}

	.pied-de-page__contact {
		grid-column: 8 / span 5;
	}
}

/* Mobile : 12 gouttières md (352px) ne tiennent pas dans 311px, la grille passe
   à 2 colonnes égales. */
@media (max-width: 781px) {
	.pied-de-page__grille {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.pied-de-page__identite,
	.pied-de-page__contact {
		grid-column: 1 / -1;
	}

	.pied-de-page__navigation,
	.pied-de-page__ressources {
		grid-column: auto;
	}
}

/* Baseline : mesure en ch + équilibrage des deux lignes. */
.pied-de-page__identite p {
	max-width: 32ch;
	text-wrap: balance;
}

/* Boutons de contact : même largeur, toute la largeur de la colonne, texte
   centré. */
.pied-de-page address .wp-block-button {
	width: 100%;
}

.pied-de-page address .wp-block-button__link {
	display: block;
	width: 100%;
	text-align: center;
}

/* Titres de colonnes : règle de contexte (jamais .eyebrow, corail). Limitée à la zone
   navigation : le H2 du CTA, désormais dans le footer, garde son propre style. */
.pied-de-page__zone-nav h2 {
	font-size: var(--wp--preset--font-size--sm);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--foreground);
}

.pied-de-page address {
	font-style: normal;
}

.pied-de-page .wp-block-navigation-item__content {
	color: var(--wp--preset--color--foreground);
	text-decoration: none;
}

.pied-de-page .wp-block-navigation-item__content:hover {
	text-decoration: underline;
}

.pied-de-page .current-menu-item > .wp-block-navigation-item__content {
	font-weight: var(--wp--custom--font-weight--strong);
}

/* Filets des boutons du footer : contact (is-style-outline) et réseaux sociaux
   (LinkedIn, Substack, ronds), 1px --etude-filet, même filet que la barre basse.
   Règle de contexte plutôt qu'attribut d'instance. Le style natif de l'outline pose 2px
   currentColor ; les boutons sociaux n'ont pas de bordure hors du footer (border-width:0,
   battu ici : quatre classes contre trois). box-sizing:border-box : le cercle garde 44px. */
.pied-de-page .is-style-outline > .wp-block-button__link,
.pied-de-page .wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link {
	border: 1px solid var(--etude-filet);
}

/* Boutons sociaux du footer à 120 % (≥ 782px) : diamètre (--bouton-rs-taille) et picto (--bouton-rs-picto),
   lus par la règle de base ; le texte déployé garde sa taille. Par variable et non par max-width/height :
   une déclaration directe ici (même spécificité que l'état :hover) bloquerait le déploiement au survol.
   Sous 782px, taille d'origine : à 375px, LinkedIn déployé à 120 % poussait Substack 20px hors de l'écran. */
@media (min-width: 782px) {
	.pied-de-page .wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link {
		--bouton-rs-taille: calc(var(--wp--preset--dimension--cible) * 1.2);
		--bouton-rs-picto: calc(1.25em * 1.2);
	}

	.pied-de-page .wp-block-button:is(.is-style-bouton-linkedin, .is-style-bouton-substack) > .wp-block-button__link::before {
		width: var(--bouton-rs-picto);
		height: var(--bouton-rs-picto);
	}
}
.pied-de-page .is-style-outline > .wp-block-button__link .bouton-filler{
	background:var(--bouton-survol);
}

/* Un texte de bouton long (email) passe à la ligne plutôt que de faire
   déborder la page. */
.pied-de-page .wp-block-button__link {
	max-width: 100%;
	overflow-wrap: anywhere;
}


/* Logo du site (SVG) : largeur par token, jamais par l'attribut `width` du
   bloc (inopérant pour un SVG, et retire is-default-size). Ratio du viewBox
   de yr-monogram.svg (0 0 71 72). */
.wp-block-site-logo.is-default-size img {
	width: var(--wp--preset--dimension--logo);
	aspect-ratio: 71 / 72;
	height: auto;
}

/* Logo réduit jusqu'à 781px — 2.5rem comme l'enveloppe et le burger (voir "Logo et
   pictos" plus haut), plus le token logo-sm : --entete-hauteur et scroll-padding-top
   suivent (voir :root, bloc En-tête). Cette règle vit APRÈS celle du bloc "Logo et
   pictos" dans le fichier : sans ça, à spécificité égale, c'est elle qui gagnerait et
   ramènerait le logo à 48px (bug trouvé en vérifiant, pas en relisant le code). */
@media (max-width: 781px) {
	.wp-block-site-logo.is-default-size img {
		width: 2.5rem;
		height: 2.5rem;
	}
}

/* Flip du logo au survol / focus clavier : demi-tour sur l'axe vertical (rotateY 0 → 180deg),
   zoom pendant le mouvement. TRANSITION (pas @keyframes) d'une seule valeur, l'angle
   --logo-flip, enregistrée par @property pour être interpolable : une sortie en plein
   mouvement ne saute pas à l'aller. Tout en DÉRIVE, jamais une seconde horloge :
   - zoom = 1 + zoom × sin(angle) : 0 au départ et à l'arrivée, maximal à 90deg ;
   - motif jamais inversé : à 180deg on voit le DOS de l'image (miroir). scaleX vaut 1
     avant 90deg et -1 après — marche nette par clamp(-1, cos × 1000, 1), sans sign() —
     et bascule pile quand le logo est vu par la tranche (invisible). rotateY(180deg)
     scaleX(-1) = rendu identique au repos.
   Courbe "back" (y < 0 puis y > 1) : léger recul au départ, dépassement à l'arrivée
   (le sinus y passe un peu sous 0 : petit tassement, voulu, effet cartoon).
   Perspective DANS transform, jamais sur un parent (bloc conteneur des éléments fixes,
   voir CLAUDE.md). Seule l'image (feuille) bouge, jamais le header.
   Sans @property : l'angle saute de 0 à 180deg, scaleX à -1 en même temps — même rendu,
   aucun effet, rien de cassé. */
@property --logo-flip {
	syntax: "<angle>";
	inherits: false;
	initial-value: 0deg;
}

.wp-block-site-logo {
	--logo-flip-duree: 0.7s;
	--logo-flip-courbe: cubic-bezier(0.5, -0.35, 0.3, 1.35);
	--logo-flip-zoom: 0.15;
}

@media (prefers-reduced-motion: no-preference) {
	.wp-block-site-logo img {
		transform: perspective(400px) rotateY(var(--logo-flip)) scaleX(clamp(-1, cos(var(--logo-flip)) * 1000, 1));
		scale: calc(1 + var(--logo-flip-zoom) * sin(var(--logo-flip)));
	}

	/* Transition portée par l'état survolé SEULEMENT : à la sortie, l'angle revient à 0
	   instantanément, sans flip retour — invisible au repos (0 et 180deg rendent
	   pareil, mesuré au pixel). Limite : une sortie EN PLEIN flip coupe le mouvement. */
	.wp-block-site-logo a:hover img,
	.wp-block-site-logo a:focus-visible img {
		--logo-flip: 180deg;
		transition: --logo-flip var(--logo-flip-duree) var(--logo-flip-courbe);
	}
}

/* Études de cas (patterns/etudes-de-cas.php, inc/champs-client.php) — session 6a, rendu statique.
   Section > panneau indigo (.etudes) > en-tête + boucle de fiches (article.etude) > onglet + corps.
   Jamais de couleur en dur : le fond du corps est le token fond-etude (#483C9E) ; TOUS les filets de la
   section sont 1px en token surface (#D5CCE1, maquette), via --etude-filet. Exceptions documentées à la règle
   « aucune valeur en dur » : 1px (épaisseur des filets), -1px (recouvrement d'un filet par l'onglet), 1,625
   (line-height global de theme.json, dupliqué comme dans la FAQ). */
.wp-block-group.section-etudes {
	padding-inline: calc(var(--wp--preset--spacing--gutter));
}

/* Marges négatives du root padding (`.has-global-padding > .alignfull`, CSS du cœur, 0,2,0) neutralisées :
   la section reste dans le root padding de la page. Trois classes (0,3,0) pour battre la règle du cœur sans
   dépendre de l'ordre de chargement des feuilles. À partir de 782px seulement : en dessous, la section est bord à
   bord (marges négatives du cœur conservées), le panneau prend la gouttière en padding latéral (voir plus bas). */
@media (min-width: 782px) {
	.wp-block-group.alignfull.section-etudes {
		margin-inline: 0;
	}
}

/* Panneau : largeur de la section (fenêtre − 2 × gutter/2), ni contentSize ni wideSize. Le layout constrained
   du cœur (`.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull))`, CSS généré par
   WordPress) pose `max-width: contentSize` (1126px) sur tout enfant de la section : neutralisé ici, sans
   `alignfull` (qui ajouterait la marge négative du root padding et le cumulerait). Padding lg sur les quatre côtés,
   en CSS (il change sous 782px, un style inline ne se surcharge pas). */
.section-etudes > .etudes {
	max-width: none;
}

.etudes {
	padding: var(--wp--preset--spacing--lg);
}

/* En-tête (eyebrow, H2, chapô) : largeur maximale = N colonnes d'une grille de 12 sur la largeur intérieure du
   panneau, gouttière incluse : (100% − (12 − 1) × g) / 12 × N + (N − 1) × g. Le pourcentage se rapporte à la zone
   de la grille (ou au bloc conteneur sans JS) = la largeur intérieure du panneau. Pleine largeur sous 1080px. */
.etudes > header {
	max-width: calc((100% - (var(--etude-colonnes) - 1) * var(--etude-gouttiere)) / var(--etude-colonnes) * var(--etude-entete-colonnes) + (var(--etude-entete-colonnes) - 1) * var(--etude-gouttiere));
}

@media (max-width: 1080px) {
	.etudes > header {
		max-width: none;
	}
}

.etudes .wp-block-query {
	margin-block-start: var(--wp--preset--spacing--md);
}

/* Empilement des fiches : gap CSS, marge de repli du layout flow (24px) remise à 0 sur les <li>
   (piège du CSS Grid/flex sans layout de bloc, voir « Cartes ressources »). */
.etudes .wp-block-post-template {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--md);
	list-style: none;
	margin: 0;
	padding: 0;
}

.etudes .wp-block-post-template > li {
	margin: 0;
}

.etudes {
	/* Grille de référence du panneau, déclarée UNE fois (réglage unique) : 12 colonnes sur la largeur intérieure,
	   gouttière md ; l'en-tête occupe 7 colonnes. */
	--etude-colonnes: 12;
	--etude-gouttiere: var(--wp--preset--spacing--md);
	--etude-entete-colonnes: 7;
	/* Ratio de la capture (largeur / hauteur d'une capture d'écran de site) et part minimale de la hauteur du cadre
	   qu'elle doit occuper (80 %) : ils fixent la hauteur minimale du cadre au-dessus de 1080px. */
	--etude-ratio: 9 / 16;
	--etude-part-capture: 0.8;
	/* Diaporama des captures (assets/js/etudes-diaporama.js), réglage unique : durée d'affichage d'une image (= durée
	   de l'anneau de progression, seule horloge), durée du fondu, et taille des cibles des points (24px = minimum
	   WCAG 2.5.8, exception documentée aux tokens). */
	--diapo-duree: 5s;
	--diapo-fondu: 700ms;
	--diapo-point: 1.5rem;
	/* Animation de passage entre fiches (assets/js/etudes-onglets.js), réglage unique, lu par le script à chaque
	   changement. Fiche entrante : bascule autour de son bord haut de rotateX(angle) à 0, perspective déclarée DANS la
	   fonction transform (jamais la propriété perspective sur un parent : bloc conteneur pour les éléments fixes).
	   Angle POSITIF = le bas de la fiche est soulevé vers l'internaute (vérifié : le bas apparaît plus grand) ;
	   l'option inverse (bas en retrait vers l'arrière) est l'angle NÉGATIF. Sortante : fondu seul. Durées, courbes
	   (l'entrée décélère fortement à l'arrivée, la sortie accélère) ; fondu du texte du domaine : 150ms. */
	--etude-passage-angle: 8deg;
	--etude-passage-perspective: 1200px;
	--etude-passage-entree: 450ms;
	--etude-passage-sortie: 300ms;
	--etude-passage-texte: 150ms;
	--etude-passage-courbe-entree: cubic-bezier(0.22, 1, 0.36, 1);
	--etude-passage-courbe-sortie: cubic-bezier(0.4, 0, 1, 1);
	/* --etude-filet : déclaré sur :root (voir plus haut), partagé avec le filet du footer. */
	/* Hauteur de l'onglet (~50px) : 2 × xs + une ligne de texte md + le filet haut. Utilisée par l'onglet, par le
	   <p> qui le remplace avant le script et par la ligne de la grille qui le réserve. */
	--etude-onglet-h: calc(var(--wp--preset--spacing--xs) * 2 + var(--wp--preset--font-size--md) * 1.625 + 1px);
	/* Prolongement d'un onglet inactif sous son voisin (cartes empilées) : la largeur d'un filet. */
	--etude-onglet-recouvrement: calc(var(--wp--preset--spacing--stack) * 2);
}

/* Onglet (actif) : ~50px de haut, filet 1px surface, coins hauts arrondis au token md (12px, celui du cadre),
   texte 16px sans soulignement, padding sm ; pas de filet bas : il recouvre de 1px le filet haut du cadre
   (margin -1px, même fond) pour que les deux ne fassent qu'une forme, le filet du cadre est interrompu sous lui. */
.etude__onglet {
	display: inline-flex;
	align-items: center;
	box-sizing: border-box;
	min-height: var(--etude-onglet-h);
	position: relative;
	z-index: 1;
	margin: 0 0 -1px;
	padding-inline: var(--wp--preset--spacing--sm);
	background-color: var(--wp--preset--color--fond-etude);
	border: 1px solid var(--etude-filet);
	border-bottom: 0;
	border-radius: var(--wp--preset--border-radius--md) var(--wp--preset--border-radius--md) 0 0;
	font-size: var(--wp--preset--font-size--md);
	text-decoration: none;
}

.etude__onglet:empty {
	display: none;
}

/* Cadre de fiche : UNE boîte (fond-etude, filet 1px surface, radius md, overflow hidden : la capture suit les
   coins), aucun gap interne. Deux colonnes sans gouttière, séparées par le filet vertical (bordure de la colonne
   de droite) : la gauche = min(70 %, token narrow 950px), la droite = le reste. */
.etude__corps {
	display: grid;
	grid-template-columns: minmax(0, min(70%, var(--wp--preset--dimension--narrow))) minmax(0, 1fr);
	gap: 0;
	background-color: var(--wp--preset--color--fond-etude);
	border: 1px solid var(--etude-filet);
	border-radius: var(--wp--preset--border-radius--md);
	overflow: hidden;
}

/* Un onglet visible : le coin haut gauche du corps est droit, l'onglet s'y raccorde. */
.etude:has(.etude__onglet:not(:empty)) .etude__corps {
	border-top-left-radius: 0;
}

.etude__gauche,
.etude__droite {
	display: flex;
	flex-direction: column;
}

/* Colonne gauche : présentation (padding md sur les quatre côtés), filet horizontal, puis capture bord à bord
   (gauche, bas, droite), sans radius propre, qui remplit la hauteur restante (object-fit cover, calage haut). */
.etude__gauche > .champ-riche {
	padding: var(--wp--preset--spacing--md);
}

.etude__gauche > .champ--etude_presentation > * {
	max-width: var(--wp--preset--dimension--mesure-presentation);
}

/* Bouton « Voir le site » (lien-client) sous la présentation, dans la colonne gauche : écart stack (1rem)
   au-dessus du bouton, la présentation perd son padding bas quand le bouton la suit (sinon md + stack). Le
   bouton reprend le padding latéral et bas de la présentation. Supprimé par le filtre si client_url est vide :
   la présentation garde alors son padding complet. */
.etude__gauche > .champ--etude_presentation:has(+ .wp-block-buttons) {
	padding-block-end: 0;
}

.etude__gauche > .wp-block-buttons {
	margin-block: var(--wp--preset--spacing--stack) 0;
	padding: 0 var(--wp--preset--spacing--md) var(--wp--preset--spacing--md);
}

.etude__gauche > .champ-riche ~ .champ-image {
	border-block-end: 1px solid var(--etude-filet);
}

/* Captures : elles s'empilent dans UNE cellule de grille, à l'intérieur de `.champ-image__cadre` (qui a remplacé
   le lien vers l'image pleine taille, supprimé en 6d). Une seule image ou plusieurs : même mécanisme, seule l'image
   `.is-actuelle` est opaque quand le script du diaporama est actif. Sous 1080px la couche est dans le flux (la plus
   haute des captures donne la hauteur, chacune remplit la cellule, calage haut) ; au-dessus, voir plus bas. */
.champ--etude_visuels {
	position: relative;
}

.champ-image__cadre {
	display: grid;
	grid-template: minmax(0, 1fr) / minmax(0, 1fr);
}

.champ-image__cadre > img {
	grid-area: 1 / 1;
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: top;
}

/* `display:block` ci-dessus écraserait la règle [hidden] du navigateur : sans cette règle les images suivantes
   (hidden + loading="lazy" côté serveur) seraient affichées, empilées sur la première, et téléchargées. */
.champ-image__cadre > img[hidden] {
	display: none;
}

@media (min-width: 1081px) {
	/* Le cadre est un conteneur de requête (container-type: inline-size) : ses enfants disposent des unités cqw.
	   Hauteur minimale du cadre, portée par la colonne de gauche (une boîte ne peut pas utiliser ses PROPRES unités
	   cq, et la hauteur du cadre est celle de sa ligne de grille) :
	   min-height = (largeur de la colonne gauche × ratio de la capture) / part de la capture
	   avec largeur de la colonne gauche = min(70cqw, narrow). Ratio et part sont déclarés une seule fois sur
	   .etudes. Ainsi la capture, au ratio, occupe au moins 80 % de la hauteur du cadre ; sans JS. */
	.etude__corps {
		container-type: inline-size;
	}

	.etude__gauche {
		min-height: calc(min(70cqw, var(--wp--preset--dimension--narrow)) * var(--etude-ratio) / var(--etude-part-capture));
		flex-direction:column-reverse;
	}

	/* Colonne inversée : sans order, le bouton (après la présentation dans le DOM) passerait AU-DESSUS d'elle.
	   order -1 le place en premier dans l'ordre de flex, donc en bas une fois inversé : capture, présentation,
	   bouton, de haut en bas. */
	.etude__gauche > .wp-block-buttons {
		order: -1;
	}

	/* La capture n'impose aucune hauteur (position absolue) : c'est la colonne de droite ou la présentation qui
	   fixe celle du cadre. min-height = xl, pour qu'elle ne s'écrase jamais si la colonne de droite est courte. */
	.etude__gauche > .champ-image {
		flex: 1 1 auto;
		position: relative;
		min-height: var(--wp--preset--spacing--xl);
	}

	/* La couche des captures est en position absolue (inset 0), comme l'était le lien : les captures, empilées
	   dedans, n'imposent JAMAIS leur hauteur au cadre. */
	.etude__gauche > .champ-image .champ-image__cadre {
		position: absolute;
		inset: 0;
	}
}

/* Colonne droite : les volets sont en rangées pleine largeur (aucun padding de colonne), en haut ; le pied
   (`.etude__pied` = bouton « Voir le site » + pastilles, groupe du pattern) est calé en bas par
   margin-block-start:auto et porte le padding sm. Le filet vertical est la bordure gauche de la colonne. */
.etude__droite {
	padding: 0;
	border-inline-start: 1px solid var(--etude-filet);
}

.etude__droite > * {
	margin-block-start: 0;
}

.etude__droite > .etude__pied {
	margin-block-start: auto;
	padding: var(--wp--preset--spacing--sm);
}

/* Écart entre le bouton et les pastilles : posé ici, pas par le blockGap du groupe, car le `margin: 0` de
   .pastilles-liste (même spécificité, chargé avant) l'écraserait (piège de « Liste à pictos »). Le premier enfant
   n'a pas d'écart. */
.etude__pied > * + * {
	margin-block-start: var(--wp--preset--spacing--sm);
}

/* Picto "lien externe" sur le bouton "Voir le site" (lien-client, inc/champs-client.php, colonne gauche sous la
   présentation) : core/button
   par défaut, aucune variation (voir "Mise en page d'après la maquette") — ciblé par l'attribut
   target="_blank" que le filtre pose déjà, pas par une classe. Même technique mask-image/currentColor
   que les variations Télécharger/Formulaire/LinkedIn, scopée à cette seule section. */
.etude .lien-client .wp-block-button__link[target="_blank"] {
	display: inline-flex;
	align-items: center;
	gap: 0.5em;
	background:none;
	padding:0;
	text-decoration:underline;
}
.etude .lien-client .wp-block-button__link[target="_blank"]:hover{
	text-decoration:none;
}
.etude .lien-client .wp-block-button__link[target="_blank"] .bouton-filler{
	display:none;
}
.etude .lien-client .wp-block-button__link[target="_blank"]::after {
	content: "";
	display: block;
	width: 1.25em;
	height: 1.25em;
	background-color: currentColor;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
	-webkit-mask-image: url("assets/icons/lien-externe.svg");
	mask-image: url("assets/icons/lien-externe.svg");
}

/* Titre de volet : ambré sur fond indigo (4,62:1), règle de contexte comme l'eyebrow. */
.etude__corps h4 {
	color: var(--wp--preset--color--accent-3);
	margin: 0;
}

/* Marges des listes : seul le retrait, jamais margin (celle du blockGap du groupe s'y applique). */
.champ-riche :is(ul, ol) {
	padding-inline-start: 1.25em;
}

/* Volets de l'étude de cas (variation « Volets », 6b) : rangées de 44px (token cible) = 43px de ligne + 1px de
   filet bas porté par l'ITEM (un seul filet entre deux rangées, plus de recouvrement de -1px : les rangées sont
   pleines largeur entre le filet vertical de la colonne et celui du cadre, le filet haut de la première est celui
   du cadre). Titre 16px (md, --acc-taille), padding gauche sm ; cellule du chevron carrée (43px + le filet du
   cadre = 44px), séparée par le filet vertical ; panneau padding sm, texte md, sans largeur maximale. */
.etude .is-volets .wp-block-accordion-item {
	--faq-row-size: calc(var(--wp--preset--dimension--cible) - 1px);
	margin-block-start: 0;
	border-bottom: 1px solid var(--acc-filet);
}

.etude .is-volets .wp-block-accordion-heading {
	border: 0;
}

.etude .is-volets .wp-block-accordion-heading__toggle {
	--faq-chevron-size: var(--faq-row-size);
	/* Un pourcentage de background-position se rapporte à (largeur − largeur du dégradé, 1px) : +1px pour que le
	   trait vertical soit le PREMIER pixel de la cellule (43px) et non un pixel à sa gauche (mesuré : cellule de
	   45px avant, 44px avec le filet du cadre après). */
	background-position: calc(100% - var(--faq-chevron-size) + 1px) 0;
}

.etude .is-volets .wp-block-accordion-heading__toggle-title {
	padding-block: 0;
	padding-inline-start: var(--wp--preset--spacing--sm);
}

.etude .is-volets .wp-block-accordion-panel {
	padding: var(--wp--preset--spacing--sm);
	max-width: none;
	/* Filet qui ferme la rangée du titre quand le volet est ouvert : bordure haute du PANNEAU (un panneau fermé est
	   invisible, opacité 0, puis hidden), donc aucun double filet avec la bordure basse de l'item ; il tombe à
	   43px du haut de l'item, au pixel où le trait vertical de la cellule chevron finit. Sans JS tous les volets
	   sont ouverts : le filet y est aussi. */
	border-top: 1px solid var(--acc-filet);
}

/* Avant l'hydratation du cœur (Interactivity API), un volet fermé n'a pas encore `hidden="until-found"` : son panneau
   garde son padding et son filet (41px chacun) puis retombe à 0 quand le cœur pose `hidden`, ce qui décalait le
   contenu qui suit (mesuré : −123px à 375, −31px à 1440). On annule donc padding et filet SEULEMENT pour un panneau
   fermé SANS `hidden` : à l'ouverture (`is-open` posé, `hidden` retiré) et à la fermeture (`hidden` posé en même
   temps que `is-open` est retiré) la règle ne s'applique jamais, l'animation est inchangée (prouvé, voir CLAUDE.md). */
.js .etude .is-volets .wp-block-accordion-item:not(.is-open) .wp-block-accordion-panel:not([hidden]) {
	padding-block: 0;
	border-top-width: 0;
}

@media (max-width: 1080px) {
	.etude__corps {
		grid-template-columns: minmax(0, 1fr);
	}

	/* Une colonne : le filet vertical devient un filet horizontal entre présentation/capture et volets. */
	.etude__droite {
		border-inline-start: 0;
		border-block-start: 1px solid var(--etude-filet);
	}
}

@media (max-width: 781px) {
	.wp-block-group.section-etudes {
		padding-inline: 0;
	}

	/* Bord à bord, sans radius, filets haut et bas 1px surface. Padding : md en haut et en bas, gouttière sur les
	   côtés (même retrait que le root padding : le contenu s'aligne sur celui des sections voisines). */
	.etudes {
		padding: var(--wp--preset--spacing--md) var(--wp--preset--spacing--gutter);
		border-radius: 0;
		border-block: 1px solid var(--etude-filet);
	}
}

/* Diaporama des captures (assets/js/etudes-diaporama.js) — session 6d. Tout est sous .diapo, classe posée par le
   script : sans JS, la première capture seule reste affichée. Fondu par opacité (`--diapo-fondu`) ; l'image suivante
   est visible et décodée AVANT le fondu (script). L'anneau de progression est la SEULE horloge : son animation
   (`--diapo-duree`) finit, `animationend` déclenche l'image suivante. Pause = animation-play-state:paused
   (`.is-en-pause`, posée par le script pour toutes les pauses). Géométrie du SVG : viewBox 44 = token cible,
   pathLength 1 (le trait se pilote en fraction de tour) ; 2px / 3px de traits = exception documentée aux tokens. */
.diapo .champ-image__cadre > img {
	opacity: 0;
	transition: opacity var(--diapo-fondu) ease;
}

.diapo .champ-image__cadre > img.is-actuelle {
	opacity: 1;
}

/* Points, en bas à gauche : pastille semi-opaque (base à 75 %) pour que les points restent lisibles sur toute capture. */
.diapo__points {
	position: absolute;
	inset-block-end: var(--wp--preset--spacing--sm);
	inset-inline-start: var(--wp--preset--spacing--sm);
	z-index: 1;
	display: flex;
	align-items: center;
	padding: 2px;
	border-radius: var(--wp--preset--border-radius--pill);
	background-color: color-mix(in srgb, var(--wp--preset--color--base) 75%, transparent);
}

.diapo__point {
	display: grid;
	place-items: center;
	width: var(--diapo-point);
	height: var(--diapo-point);
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: none;
	color: var(--wp--preset--color--contrast);
	cursor: pointer;
}

.diapo__point::before {
	content: "";
	box-sizing: border-box;
	width: calc(var(--diapo-point) * 0.42);
	height: calc(var(--diapo-point) * 0.42);
	border: 2px solid currentColor;
	border-radius: 50%;
}

.diapo__point.is-actif {
	color: var(--wp--preset--color--accent-3);
}

.diapo__point.is-actif::before {
	background-color: currentColor;
}

/* Lecture / pause, en bas à droite : disque plein (base) sous le bouton, cible = token cible (44px), anneau autour. */
.diapo__lecture {
	position: absolute;
	inset-block-end: var(--wp--preset--spacing--sm);
	inset-inline-end: var(--wp--preset--spacing--sm);
	z-index: 1;
	width: var(--wp--preset--dimension--cible);
	height: var(--wp--preset--dimension--cible);
	border-radius: 50%;
	background-color: var(--wp--preset--color--base);
}

.diapo__anneau {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	transform: rotate(-90deg); /* le trait part du haut */
}

.diapo__piste,
.diapo__progression {
	fill: none;
	stroke-width: 3;
}

.diapo__piste {
	/* 40 % : la piste doit atteindre 3:1 sur le disque base (30 % ne donnait que 2,70:1, WCAG 1.4.11). */
	stroke: color-mix(in srgb, var(--wp--preset--color--contrast) 40%, transparent);
}

.diapo__progression {
	stroke: var(--wp--preset--color--accent-3);
	stroke-dasharray: 1;
	stroke-dashoffset: 1;
	animation: diapo-anneau var(--diapo-duree) linear forwards;
}

.diapo.is-en-pause .diapo__progression {
	animation-play-state: paused;
}

@keyframes diapo-anneau {
	from {
		stroke-dashoffset: 1;
	}

	to {
		stroke-dashoffset: 0;
	}
}

.diapo__bouton {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: none;
	color: var(--wp--preset--color--contrast);
	cursor: pointer;
}

.diapo__bouton::before {
	content: "";
	width: calc(var(--wp--preset--dimension--cible) * 0.45);
	height: calc(var(--wp--preset--dimension--cible) * 0.45);
	background-color: currentColor;
	-webkit-mask-image: url("assets/icons/pause.svg");
	mask-image: url("assets/icons/pause.svg");
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
}

.diapo__bouton[data-action="lecture"]::before {
	-webkit-mask-image: url("assets/icons/lecture.svg");
	mask-image: url("assets/icons/lecture.svg");
}

.diapo__point:hover::before,
.diapo__bouton:hover {
	color: var(--wp--preset--color--accent-3);
}

/* Focus visible sur n'importe quelle capture : filet clair + halo sombre (le halo garantit le contraste, quel que
   soit le fond de l'image). */
.diapo__point:focus-visible,
.diapo__bouton:focus-visible {
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: 2px;
	box-shadow: 0 0 0 4px var(--wp--preset--color--base);
}

/* Réduction de mouvement : changement instantané, anneau STATIQUE (piste pleine en accent) ; le trait animé reste la
   horloge d'une lecture demandée par l'internaute mais il est invisible. */
@media (prefers-reduced-motion: reduce) {
	.diapo .champ-image__cadre > img {
		transition: none;
	}

	.diapo__progression {
		opacity: 0;
	}

	.diapo__piste {
		stroke: var(--wp--preset--color--accent-3);
	}
}

/* Onglets de la section Étude de cas (assets/js/etudes-onglets.js) — session 6c.
   Sans .js (JavaScript absent) la section reste celle de la 6a/6b : fiches empilées, rien ne s'applique ici.
   Aucune animation ni transition : le changement de fiche est instantané.

   ÉTAT FINAL APPLIQUÉ DÈS LE CSS (correction de la session 6c : pas de décalage au chargement). Le sélecteur
   E = :is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) vaut pour
   « JS actif ET au moins deux fiches » (sans attendre le script) OU « le script a fini » : la mise en page est
   la même aux deux moments, le script n'ajoute que la barre et les flèches, sans déplacer le contenu qui suit.
   Une seule fiche : `li + li` n'existe pas, rien ne change. `:is()` est indulgent : dans un navigateur sans
   :has() (Firefox < 121) la première branche est ignorée, seule la seconde s'applique (le décalage
   réapparaît, le reste fonctionne).
   Ce qui tient la place avant le script :
   - la barre : le `p.etude__onglet` de la première fiche, dessiné comme l'onglet actif par les règles 6a
     (même hauteur que l'onglet créé par le script), masqué par .is-onglets une fois la barre créée ;
   - les flèches sous 1080px : la ligne 4 de la grille a une hauteur fixe (cible + md), celle que prend le bloc
     de flèches une fois créé ; au-dessus de 1080px elles sont dans la ligne de la barre (44px < hauteur de barre).
   .etudes est une grille ; les enfants directs (header, tablist, flèches, query, bouton final) sont placés par
   numéro de ligne, l'ordre du DOM n'est jamais modifié. Marges de repli du layout flow remises à 0 sur ces
   enfants (le bouton final garde son margin-top en style inline, qui l'emporte). */
:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	/* Ligne 2 = flèches (cible) + leur écart sm au-dessus du cadre, la barre est alignée sur son bas. Réservée
	   avant le script : le <p> de la fiche 1 (voir plus bas) y tient la place de l'onglet. */
	grid-template-rows: auto calc(var(--wp--preset--dimension--cible) + var(--wp--preset--spacing--sm)) auto auto auto;
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes > * {
	margin: 0;
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes > header {
	grid-area: 1 / 1 / 2 / -1;
	margin-block-end: var(--wp--preset--spacing--md);
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes__onglets {
	grid-area: 2 / 1;
	align-self: end; /* aucun écart entre la barre et le cadre */
	margin-block-end: -1px; /* l'onglet actif recouvre le filet haut du corps */
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes__fleches {
	grid-area: 2 / 2;
	align-self: end;
	justify-self: end; /* alignées à droite */
	margin-block-end: var(--wp--preset--spacing--sm); /* bas des flèches à sm au-dessus du cadre */
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes > .wp-block-query {
	grid-area: 3 / 1 / 4 / -1;
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes > .wp-block-buttons {
	grid-area: 5 / 1 / 6 / -1;
}

/* Fiches empilées dans la même cellule : hauteur = fiche la plus haute, donc aucun saut au changement.
   Inactives : invisibles ET inertes (attribut posé par le script, hors arbre d'accessibilité et du focus). */
:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .wp-block-post-template {
	display: grid;
	gap: 0;
}

:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .wp-block-post-template > li {
	grid-area: 1 / 1;
	min-width: 0;
}

/* Avant que le script ait posé `inert` : seule la première fiche est visible (c'est aussi la fiche active à
   l'initialisation). Limite : JS actif (classe .js) mais script en échec = fiches 2 et suivantes masquées. */
.js .section-etudes:has(.wp-block-post-template > li + li):not(.is-onglets) .wp-block-post:nth-child(n+2) {
	visibility: hidden;
}

.js .section-etudes:has(.wp-block-post-template > li + li):not(.is-onglets) .wp-block-post {
	position: relative;
}

/* Le <p> de l'onglet, hors flux, au-dessus du cadre, à la place exacte de l'onglet du script (bas 1px sous le
   haut du cadre) : la ligne 2 de la grille est déjà réservée, le total ne bouge pas. */
.js .section-etudes:has(.wp-block-post-template > li + li):not(.is-onglets) .etude__onglet {
	position: absolute;
	inset-inline-start: 0;
	bottom: calc(100% - 1px);
	margin: 0;
}

.section-etudes.is-onglets .wp-block-post[inert] {
	visibility: hidden;
}

/* Ombre du passage entre fiches : pré-dessinée sur le ::after de l'article (qui a la forme du cadre ; .etude__corps a
   overflow:hidden et la couperait), box-shadow STATIQUE, couleur par token (base). Seule son OPACITÉ s'anime (de son
   maximum au début de l'entrée à 0 à la fin, script) : au repos elle vaut 0, le rendu est strictement inchangé, et un navigateur sans l'option pseudoElement de
   element.animate() la laisse à 0. isolation:isolate : contexte d'empilement (z-index -1 sous le cadre), pas un bloc
   conteneur pour un élément fixe. */
.section-etudes.is-onglets .etude {
	position: relative;
	isolation: isolate;
}

/* Charnière de la bascule d'entrée : le bord haut du <li>, qui coïncide avec le haut du cadre (mesuré). Sans effet au
   repos (aucun transform). */
.section-etudes.is-onglets .wp-block-post {
	transform-origin: 50% 0;
}

.section-etudes.is-onglets .etude::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: var(--wp--preset--border-radius--md);
	box-shadow: 0 var(--wp--preset--spacing--sm) var(--wp--preset--spacing--lg) calc(var(--wp--preset--spacing--xs) * -1) color-mix(in srgb, var(--wp--preset--color--base) 70%, transparent);
	opacity: 0;
	pointer-events: none;
}

/* Le domaine est repris par l'onglet créé par le script (le <p> reste dans le DOM, source du texte). */
.section-etudes.is-onglets .etude__onglet {
	display: none;
}

/* Barre : les onglets d'avant l'actif sont à sa gauche, ceux d'après à sa droite (ordre du DOM, aucun calcul).
   Un onglet inactif est un FILET DE PILE : bande de 16px (token stack), de la hauteur de l'onglet, dessinée par
   son ::before (fond de la fiche, filets haut et droit 1px surface, coin haut droit arrondi md) ; il s'arrête 1px
   au-dessus du cadre (margin bas), dont le filet reste visible dessous.
   - à DROITE de l'onglet actif : ce dessin tel quel ;
   - à GAUCHE : le même dessin en miroir, `transform: scaleX(-1)` (coin haut GAUCHE arrondi, trait vertical à
     gauche). Le miroir est posé sur le ::before, pas sur le bouton : le libellé (étiquette au survol, nom
     accessible) ne serait pas retourné. Un filet de gauche n'a pas de bord droit : aucun trait doublé avec le bord
     gauche de l'onglet actif (un seul trait à la jonction).
   La zone cliquable est le pas entier (16px), sans chevauchement (flex, aucune marge négative).
   EXCEPTION WCAG 2.5.8 (taille de cible 24px, AA) : ces bandes font 16px de large. Exception « commande
   équivalente » : la même action (passer à une autre étude) est offerte par les flèches, cibles de 44px, sur la
   même page. */
.etudes__onglets {
	display: flex;
	align-items: stretch;
	position: relative;
	z-index: 2;
}

.etudes__onglet {
	flex: 0 0 calc(var(--wp--preset--spacing--stack) * 2);
	box-sizing: border-box;
	position: relative;
	margin: 0 0 1px;
	padding: 0;
	border: 0;
	background: none;
	color: inherit;
	font: inherit;
	cursor: pointer;
}

.etudes__onglet::before {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	/* Onglet complet (coins hauts arrondis, filet haut + côtés), de la largeur de l'onglet actif
	   (--etude-onglet-largeur, mesurée par etudes-onglets.js ; repli sans mesure : prolongement d'un filet), qui
	   passe sous ses voisins vers l'actif : les onglets inactifs se lisent comme des cartes empilées derrière lui. À gauche de l'actif, le
	   prolongement part vers la droite ; à droite, vers la gauche (règle suivante). */
	left: 0;
	width: var(--etude-onglet-largeur, calc(100% + var(--etude-onglet-recouvrement)));
	box-sizing: border-box;
	background-color: var(--wp--preset--color--fond-etude);
	border: 1px solid var(--etude-filet);
	border-bottom: 0;
	border-radius: var(--wp--preset--border-radius--md) var(--wp--preset--border-radius--md) 0 0;
	transition: background-color 0.2s ease, top 0.2s ease;
}

.etudes__onglet[aria-selected="true"] ~ .etudes__onglet::before {
	left: auto;
	right: 0;
}

/* Empilement : l'actif devant tout, puis chaque carte devant celle qui est plus loin de lui. À gauche, l'ordre
   du DOM suffit (le plus proche est le dernier peint) ; à droite il faut l'inverser explicitement (4 fiches au
   plus, perPage du query : 3 cartes au plus d'un même côté). */
.etudes__onglet[aria-selected="true"] {
	z-index: 4;
}

.etudes__onglet[aria-selected="true"] + .etudes__onglet {
	z-index: 3;
}

.etudes__onglet[aria-selected="true"] + .etudes__onglet + .etudes__onglet {
	z-index: 2;
}

.etudes__onglet[aria-selected="true"] + .etudes__onglet + .etudes__onglet + .etudes__onglet {
	z-index: 1;
}

/* Survol / focus clavier : la carte dépasse vers le haut (de xs) et prend la couleur du filet, en plus de
   l'étiquette. Seul le dessin bouge, la zone cliquable reste le bouton. */
.etudes__onglet:not([aria-selected="true"]):is(:hover, :focus-visible)::before {
	top: calc(var(--wp--preset--spacing--xs) * -1);
	background-color: var(--etude-filet);
}

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

.etudes__onglet[aria-selected="true"]::before {
	display: none;
}

.etudes__onglet[aria-selected="true"] {
	margin-inline-start: -1px;
}

.etudes__onglet[aria-selected="true"] {
	flex: 0 1 auto;
	/* Grille à une cellule : le libellé et les copies invisibles des autres libellés (.etudes__onglet-mesure) sont
	   empilés dedans, la cellule prend la largeur du plus large, donc du plus long domaine. Le libellé reste ferré à
	   gauche (justify-items:start). Quand le plus long ne tient pas, le flex réduit l'onglet à la place disponible
	   (largeur de la barre − les filets, la même pour tous les rangs : n − 1 filets quelle que soit la fiche active),
	   donc la même largeur (tronquée par une ellipse) pour toutes. */
	display: grid;
	grid-template-columns: minmax(0, max-content);
	align-content: center;
	justify-items: start;
	min-width: 0;
	min-height: var(--etude-onglet-h);
	margin: 0;
	padding-inline: var(--wp--preset--spacing--sm);
	/* Le fond recouvre le filet haut du cadre sous l'onglet (le filet est interrompu exactement sous lui). */
	background-color: var(--wp--preset--color--fond-etude);
	border: 1px solid var(--etude-filet);
	border-radius: var(--wp--preset--border-radius--md) var(--wp--preset--border-radius--md) 0 0;
	font-size: var(--wp--preset--font-size--md);
	text-decoration: none;
	cursor: default;
}

.etudes__onglet[aria-selected="true"] > * {
	grid-area: 1 / 1;
}

.etudes__onglet[aria-selected="true"] .etudes__onglet-texte {
	display: block;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Copies de mesure : invisibles, sans hauteur, hors arbre d'accessibilité ; inutiles (donc retirées) dans un filet. */
.etudes__onglet-mesure {
	height: 0;
	overflow: hidden;
	visibility: hidden;
	white-space: nowrap;
	pointer-events: none;
}

.etudes__onglet:not([aria-selected="true"]) .etudes__onglet-mesure {
	display: none;
}

/* Étiquette d'un filet : positionnée (aucune variation de largeur, aucun reflow), au survol et au focus
   clavier. Elle reste dans l'arbre d'accessibilité (nom du bouton) : opacity, jamais display:none. */
.etudes__onglet:not([aria-selected="true"]) .etudes__onglet-texte {
	position: absolute;
	bottom: calc(100% + var(--wp--preset--spacing--xs));
	inset-inline-start: 0;
	z-index: 3;
	padding: var(--wp--preset--spacing--xs) var(--wp--preset--spacing--sm);
	border-radius: var(--wp--preset--border-radius--sm);
	background-color: var(--wp--preset--color--base);
	color: var(--wp--preset--color--contrast);
	font-size: var(--wp--preset--font-size--sm);
	white-space: nowrap;
	opacity: 0;
	pointer-events: none;
}

.etudes__onglet[aria-selected="true"] ~ .etudes__onglet .etudes__onglet-texte {
	inset-inline-start: auto;
	inset-inline-end: 0;
}

.etudes__onglet:not([aria-selected="true"]):is(:hover, :focus-visible) .etudes__onglet-texte {
	opacity: 1;
}

.etudes__onglet:focus-visible,
.etudes__fleche:focus-visible,
.temoignages__suivant:focus-visible,
.temoignages__precedent:focus-visible {
	outline: 2px solid var(--wp--preset--color--accent-3);
	outline-offset: 2px;
}

/* Flèches : pastilles rondes (token cible, 44px), fond surface, flèche accent-2 en mask-image (contraste
   4,96:1 ; le corail sur surface tombe à 2,1:1, refusé, WCAG 1.4.11). Survol : fond contrast (7,9:1, le
   contraste monte). « Précédent » = la même flèche tournée de 180°, sur le ::before (feuille, aucun
   descendant fixe). Groupe et compteur partagés avec la navigation de l'étude de cas (etudes-onglets.js),
   même motif repris pour les témoignages (temoignages.js, session V1). */
.etudes__fleches,
.temoignages__fleches {
	display: flex;
	align-items: center;
}

.etudes__fleches {
	gap: var(--wp--preset--spacing--stack);
}

/* .5rem (8px) : pas de token à cette valeur — calc(stack / 2), jamais une longueur en dur. */
.temoignages__fleches {
	gap: calc(var(--wp--preset--spacing--stack) / 2);
}

.etudes__fleche,
.temoignages__suivant,
.temoignages__precedent {
	display: grid;
	place-items: center;
	width: var(--wp--preset--dimension--cible);
	height: var(--wp--preset--dimension--cible);
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background-color: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--accent);
	cursor: pointer;
}

.etudes__fleche:hover,
.temoignages__suivant:hover,
.temoignages__precedent:hover {
	background-color: var(--wp--preset--color--contrast);
}

.etudes__fleche::before,
.temoignages__suivant::before,
.temoignages__precedent::before {
	content: "";
	width: 1.5em;
	height: 1.5em;
	background-color: currentColor;
	-webkit-mask-image: url("assets/icons/fleche-droite.svg");
	mask-image: url("assets/icons/fleche-droite.svg");
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
}

.etudes__fleche--precedent::before,
.temoignages__precedent::before {
	transform: rotate(180deg);
}

.etudes__compteur,
.temoignages__compteur {
	margin: 0;
	font-size: var(--wp--preset--font-size--sm);
	text-align: center;
}

/* Compteur des témoignages : toujours visible (contrairement à celui de l'étude de cas, jamais redondant
   avec des onglets ici — c'est la seule pagination de la navigation), largeur minimale pour ne pas
   décaler les flèches quand le chiffre change. */
.temoignages__compteur {
	min-width: 4ch;
	color: var(--wp--preset--color--foreground);
}

/* Au-dessus de 1080px le compteur est retiré de l'affichage ET du flux (sinon il compterait comme une
   troisième case du flex et doublerait l'écart entre les flèches) : rendu « sr-only », la région aria-live
   reste dans l'arbre d'accessibilité. */
@media (min-width: 1081px) {
	.etudes__compteur {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		padding: 0;
		overflow: hidden;
		clip: rect(0, 0, 0, 0);
		white-space: nowrap;
	}

	.etudes__compteur-visible {
		display: none;
	}
}

/* En dessous : une seule colonne, les flèches et leur compteur passent dans la ligne qui suit la fiche. */
@media (max-width: 1080px) {
	:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes {
		grid-template-columns: minmax(0, 1fr);
		/* Ligne 2 = la barre seule (hauteur de l'onglet moins son recouvrement), ligne 4 = flèches et leur marge
		   (cible + md) ; toutes deux réservées avant même que le script crée la barre et les flèches. */
		grid-template-rows: auto calc(var(--etude-onglet-h) - 1px) auto calc(var(--wp--preset--dimension--cible) + var(--wp--preset--spacing--md)) auto;
	}

	:is(.js .section-etudes:has(.wp-block-post-template > li + li), .section-etudes.is-onglets) .etudes__fleches {
		grid-area: 4 / 1;
		align-self: start;
		justify-self: center;
		margin-block: var(--wp--preset--spacing--md) 0;
	}

	.etudes__compteur {
		min-width: 4ch;
	}
}

#cmplz-manage-consent .cmplz-manage-consent {
	display: none;
}

/* Ombres portées ponctuelles, scopées par section (demande explicite) : aucune de ces sections
   n'avait d'ombre propre jusqu'ici, aucun conflit avec la règle "pas d'ombre sur un bouton en panneau"
   (qui ne cible que .wp-block-button__link, jamais le panneau ou l'image eux-mêmes). */
#a-propos .is-style-panel-indigo,
#a-propos .wp-block-image,
#etude-de-cas .is-style-panel-indigo,
#ressources .cartes-ressources__carte,
#contact .is-style-panel-indigo {
	box-shadow: var(--wp--preset--shadow--sm);
}

/* Déclencheur de contact : ombre PORTÉE PAR UN CALQUE STATIQUE (::before du bouton), jamais par le SVG
   lui-même. Le blob ne remplit pas tout le rectangle du viewBox (146x151) : box-shadow directement sur
   le SVG dessinait un rectangle décalé de la forme réelle (corrigé une première fois en drop-shadow) —
   mais drop-shadow doit rester sur le MÊME élément que celui qui bouge pour suivre son alpha, ce qui
   aurait fait grossir/trembler l'ombre en même temps que le pulse ci-dessous (filtre calculé sur le
   rendu déjà mis à l'échelle de son contenu). Approximation en cercle (border-radius:50%, le blob est
   presque circulaire) : suffisant pour une ombre floutée (20px de flou dans le preset), jamais
   transformée — elle reste visuellement "au sol" pendant que le SVG s'approche. */
#a-propos .about-contact-trigger::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: 50%;
	box-shadow: var(--wp--preset--shadow--sm);
	pointer-events: none;
}

/* Survol/focus : simple grossissement tenu tant que le survol dure, réduction à la sortie — par la
   transition de .about-contact-trigger svg (base), dans les deux sens. Remplace l'ancien "pulse"
   (keyframes à rebond), qui se combinait mal avec l'effet magnétique (boutons-survol.js, transform
   sur le bouton) : deux mouvements autonomes superposés. Seul le SVG est mis à l'échelle, jamais
   l'ombre (::before ci-dessus) ni le bouton (porteur du magnétique). */
.about-contact-trigger:is(:hover, :focus-visible) svg {
	transform: scale(1.15);
}

@media (prefers-reduced-motion: reduce) {
	.about-contact-trigger svg {
		transition: none;
	}

	.about-contact-trigger:is(:hover, :focus-visible) svg {
		transform: none;
	}
}

/* Dialogue de contact (inc/contact-dialogue.php, assets/js/contact-dialogue.js) : coquille de
   mise en couverture plein écran UNIQUEMENT — position, taille, marge de sécurité des encoches.
   Toute l'identité visuelle (fond corail, typographie, champs...) vit dans blocks/contact/style.css
   et s'applique aussi bien ici que sur la page /contact autonome (même bloc, même CSS) : le
   <dialog> lui-même reste transparent, c'est .wp-block-mcleod-contact qui porte le fond. Remplace
   l'ancien panneau centré (session étape 4) — design intégré depuis les maquettes
   `_docs/maquettes/formulaires/ecran-{1-4}.png`, voir docs/formulaire-contact.md. */
/* box-sizing:border-box explicite, par précaution : ce dialogue est injecté par wp_footer en
   dehors de .wp-site-blocks, hors de portée d'une éventuelle règle sitewide (voir le même piège,
   documenté en détail, dans blocks/contact/style.css). */
.contact-dialogue,
.contact-dialogue * {
	box-sizing: border-box;
}

.contact-dialogue {
	position: fixed;
	inset: 0;
	margin: 0;
	padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
		env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
	border: 0;
	background: transparent;
	color: inherit;
	width: 100%;
	height: 100%;
	max-width: none;
	max-height: none;
	overflow-y: auto;
}

/* Ouverture/fermeture en cercle depuis le point d'origine (groupe 7, docs/formulaire-contact.md) :
   aucune règle de transition ici — animée entièrement par assets/js/contact-dialogue.js via l'API
   Web Animations (clip-path sur le <dialog> SEUL), prefers-reduced-motion compris (vérifié en JS
   avant de lancer quoi que ce soit, jamais ici). Ne jamais ajouter de transition CSS en parallèle :
   un <dialog> qui vient d'apparaître (showModal) n'anime pas de façon fiable sa toute première
   peinture sans "starting style", piège déjà rencontré ailleurs dans ce thème avec des éléments
   nouvellement affichés — la bascule par classe + double rAF aurait le même risque, l'API Web
   Animations l'évite par construction.
   ::backdrop SANS AUCUNE couleur, volontairement (retour d'Yvan, deux essais précédents) : un fond
   coloré ici — plein dès l'ouverture, OU animé séparément en fondu — crée un second mouvement que
   l'œil distingue du cercle (« le fond vient recouvrir la page, PUIS le contenu apparaît dans le
   masque » : la page change de couleur d'un coup avant même que le cercle ait fini de grandir,
   perçu comme un raté plutôt qu'un seul geste). Le cercle du panneau (qui porte déjà le fond corail
   via .wp-block-mcleod-contact, blocks/contact/style.css) est le SEUL élément qui doit recouvrir
   quoi que ce soit : en dehors de lui, la vraie page reste visible jusqu'à ce que le cercle
   l'atteigne — exactement l'effet "reveal" voulu, un seul mouvement, pas deux superposés. Le rayon
   (rayonCouverture(), JS) couvre tout le viewport à la fin de l'animation : le ::backdrop n'a donc
   jamais besoin de peindre quoi que ce soit lui-même — aucune règle pour lui ici, intentionnellement
   (sa seule fonction restante : cible de clic pour "fermer au clic sur le fond", déjà gérée en JS
   par `event.target === dialogue`, indépendante de tout style). */

/* Bouton de fermeture : position fixe (ancré au viewport, pas de recoupement sticky avec la
   progression — voir blocks/contact/style.css — qui vit dans une branche DOM distincte du bloc).
   Cercle à contour blanc, fond transparent (maquette écran 1 : pas un disque plein comme l'ancienne
   version). */
.contact-dialogue__barre {
	position: fixed;
	top: var(--wp--preset--spacing--lg);
	right: var(--wp--preset--spacing--lg);
	z-index: 2;
}

.contact-dialogue__fermer {
	width: var(--wp--preset--dimension--cible);
	height: var(--wp--preset--dimension--cible);
	display: grid;
	place-items: center;
	background-color: transparent;
	color: var(--wp--preset--color--contrast);
	border: 1px solid var(--wp--preset--color--contrast);
	border-radius: var(--wp--preset--border-radius--pill);
	cursor: pointer;
}

.contact-dialogue__fermer:focus-visible {
	outline: 2px solid var(--wp--preset--color--base);
	outline-offset: 2px;
}

.contact-dialogue__fermer::before {
	content: "";
	display: block;
	width: 0.875rem;
	height: 0.875rem;
	background-color: currentColor;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
	-webkit-mask-image: url("assets/icons/fermer.svg");
	mask-image: url("assets/icons/fermer.svg");
}

@media (max-width: 640px) {
	.contact-dialogue__barre {
		top: var(--wp--preset--spacing--sm);
		right: var(--wp--preset--spacing--sm);
	}
}

/* Scroll de la page bloqué tant que le dialogue est ouvert. Scopé à CE dialogue (classe), jamais
   à "dialog[open]" en général : un futur dialogue du thème ne doit pas hériter de cette règle sans
   qu'on l'ait décidé pour lui. */
body:has(.contact-dialogue[open]) {
	overflow: hidden;
}
@media (max-width: 781px) {
	.cta-panneau .cta-panneau__tampon{
		display:none;
	}
}

#cmplz-manage-consent .cmplz-manage-consent {
	display: none;
}
/* Page 404 : plan d'étage du « bâtiment 31 » (templates/404.html, patterns/plan-404.php).
   Deux SVG dans le même <nav> : horizontal au-delà de 781px, vertical en dessous. Celui qui est
   masqué l'est en display:none, donc hors de l'arbre d'accessibilité (pas de liens en double).
   Couleurs par tokens seulement (palette + custom.plan) ; les coordonnées du SVG (viewBox, murs,
   portes) sont de la géométrie, écrites en dur dans le pattern. Traits en vector-effect:
   non-scaling-stroke : même épaisseur à l'écran quelle que soit la largeur du plan. Les textes
   du SVG, eux, suivent l'échelle du viewBox (tailles des tokens en unités du viewBox). */
.wp-site-blocks > main.plan-404 {
	padding-block-end: var(--wp--preset--spacing--xl);
}

/* Footer dévoilé (≥ 782px) : même hauteur de feuille que la page du diagnostic (85px de footer
   visibles au chargement, voir plus haut), respiration basse lg = la respiration haute de
   sans-hero, et contenu centré verticalement dans la feuille (flex en colonne ; le grain est
   fixe, hors flux, et .fin-de-main est vide). Contenu plus haut que la fenêtre : aucun effet. */
html.has-footer-reveal .wp-site-blocks > main.plan-404 {
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	justify-content: center;
	min-height: calc(100svh - 85px - var(--wp-admin--admin-bar--height, 0px));
	/* Centrage optique : le bord visible de l'en-tête est le bas du logo, pas le bas de sa boîte
	   (padding sm sous le logo, attribut de .entete). Puis remontée réglée à l'œil par Yvan
	   (custom.plan.remontee) : ôtée du padding haut, ajoutée au padding bas, le contenu remonte
	   d'autant sans changer la hauteur de la feuille. */
	padding-block-start: calc(var(--entete-bloc, 0px) + var(--main-respiration, 0px) - var(--wp--preset--spacing--sm) - var(--wp--custom--plan--remontee));
	padding-block-end: calc(var(--wp--preset--spacing--lg) + var(--wp--custom--plan--remontee));
}

/* Le groupe de contenu n'est pas le premier enfant de <main> (le grain l'est) : le repli de flux
   du cœur (:root :where(.is-layout-flow) > *) lui ajoute 24px au-dessus et décentrait le contenu. */
html.has-footer-reveal .wp-site-blocks > main.plan-404 > .wp-block-group {
	margin-block-start: 0;
}

.plan-404__legende {
	color: var(--wp--preset--color--muted);
	font-variant-caps: all-small-caps;
	letter-spacing: 0.08em;
}

.plan-404__legende p {
	margin: 0;
}

/* Texte à gauche (35 %), plan à droite (65 %) au-delà de 1080px seulement : plus étroit, le plan
   ne ferait que ~480px et ses libellés deviendraient illisibles. En dessous, une colonne : grille
   (ignore le flex-basis en ligne et le nowrap !important du cœur, comme « À propos »). */
@media (max-width: 1080px) {
	.wp-block-columns.plan-404__colonnes {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		row-gap: var(--wp--preset--spacing--lg);
	}
}

.plan-404__svg {
	display: block;
	width: 100%;
	height: auto;
	overflow: visible;
}

.plan-404__svg--vertical {
	display: none;
}

@media (max-width: 781px) {
	.plan-404__svg--horizontal {
		display: none;
	}

	.plan-404__svg--vertical {
		display: block;
		max-width: var(--wp--custom--plan--largeur-verticale);
		margin-inline: auto;
	}
}

.plan-404__murs path {
	fill: none;
	stroke: var(--wp--custom--plan--trait);
	stroke-width: var(--wp--custom--plan--epaisseur);
	vector-effect: non-scaling-stroke;
}

.plan-404__hachure {
	stroke: var(--wp--custom--plan--hachure);
	stroke-width: var(--wp--custom--plan--epaisseur);
}

.plan-404__svg text {
	font-family: var(--wp--preset--font-family--body);
}

.plan-404__numero {
	fill: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--sm);
	letter-spacing: 0.05em;
}

.plan-404__libelle {
	fill: var(--wp--preset--color--foreground);
	font-size: var(--wp--preset--font-size--chapo);
}

/* Plan horizontal dans sa colonne de 65 % : textes agrandis (unités du viewBox, mis à l'échelle). */
.plan-404__svg--horizontal .plan-404__numero {
	font-size: var(--wp--preset--font-size--md);
}

.plan-404__svg--horizontal .plan-404__libelle {
	font-size: var(--wp--preset--font-size--h-3);
}

/* Plan vertical : bureaux de 125 unités de large, « Témoignages » y dépassait en chapo. */
.plan-404__svg--vertical .plan-404__libelle {
	font-size: var(--wp--preset--font-size--md);
}

.plan-404__libelle--mure {
	fill: var(--wp--preset--color--muted);
}

/* Bureau = lien. Le trait de surbrillance est un rectangle en retrait des murs (les murs sont
   partagés entre bureaux : un trait posé dessus serait recouvert par le bureau voisin). Fond
   transparent, jamais "none" : sinon seul le texte serait cliquable. */
.plan-404__bureau {
	text-decoration: none;
}

.plan-404__bureau:focus-visible {
	outline: none;
}

.plan-404__surbrillance {
	fill: transparent;
	stroke: transparent;
	stroke-width: var(--wp--custom--plan--epaisseur);
	vector-effect: non-scaling-stroke;
}

.plan-404__bureau:is(:hover, :focus-visible) .plan-404__surbrillance {
	fill: color-mix(in srgb, var(--wp--preset--color--accent) 8%, transparent);
	stroke: var(--wp--preset--color--accent);
}

.plan-404__bureau:focus-visible .plan-404__surbrillance {
	stroke-width: calc(var(--wp--custom--plan--epaisseur) * 2);
}

.plan-404__bureau:is(:hover, :focus-visible) .plan-404__libelle,
.plan-404__bureau--contact .plan-404__libelle {
	fill: var(--wp--preset--color--accent);
}

@media (prefers-reduced-motion: no-preference) {
	.plan-404__surbrillance,
	.plan-404__libelle {
		transition-property: fill, stroke;
		transition-duration: var(--wp--custom--transition--duration);
		transition-timing-function: var(--wp--custom--transition--easing);
	}
}

/* Repère « vous êtes ici » : corail = le visiteur (indigo = moi, ailleurs sur le site). */
.plan-404__ici-point {
	fill: var(--wp--preset--color--accent);
}

.plan-404__ici-anneau {
	fill: none;
	stroke: var(--wp--preset--color--accent);
	stroke-width: var(--wp--custom--plan--epaisseur);
	vector-effect: non-scaling-stroke;
}

.plan-404__svg .plan-404__ici-libelle {
	fill: var(--wp--preset--color--accent);
	font-family: var(--wp--preset--font-family--script);
	font-size: var(--wp--preset--font-size--intro);
}

.plan-404__svg--horizontal .plan-404__ici-libelle {
	font-size: var(--wp--custom--plan--ici-horizontal);
}
