/* Accueil française (accueil photo, novembre 2026) : le hero d'un écran, puis la feuille qui passe par-dessus lui au
   défilement et ses sections (Réalisations, Studio, Compétences, Avis). Liée par la seule page d'accueil, après
   commun.css (jetons, .cadre, .bouton, surfaces). Même charte : seuls transform et opacity s'animent, durées et
   courbes en jetons, mouvement réduit doublé pour la classe .mouvement-reduit (?motion=reduce). Contrôles :
   tests/charte-fr.test.mjs, tests/navigateur/accueil-fr.test.mjs, reel-fr.test.mjs et avis-fr.test.mjs. */

/* Hero : un écran au moins, fond Onyx (.sur-onyx), sous la barre du haut, transparente à cet endroit. Ce qui dépasse sur
   les côtés (les épaules du portrait sur un écran étroit) est coupé. --place-barre : la hauteur que la barre fixe
   recouvre en haut du hero, que son contenu laisse libre. */
.hero { --place-barre: var(--barre); position: relative; display: grid; min-height: 100vh; min-height: 100svh; overflow: hidden; overflow: clip; }
/* Sans JavaScript, sous 900px (la condition de la barre, commun.css), la barre n'est plus fixe : elle a sa place dans
   la page, au-dessus du hero, qui ne lui en réserve plus. */
@media not all and (min-width: 900px) { html:not(.js) .hero { --place-barre: 0px; } }

/* La suite de la page : la feuille (dans main) et le pied sont opaques et passent au-dessus du hero. */
.feuille { position: relative; z-index: 1; background: var(--onyx); }
.pied { position: relative; z-index: 1; }

/* Hero collé : il reste en place pendant que la feuille le recouvre. Seulement dans une fenêtre d'au moins 600px de
   haut (37,5em : le seuil suit la taille de texte choisie par le visiteur, comme le contenu du hero) : dans une fenêtre
   basse (téléphone couché, zoom fort) le hero est plus haut qu'elle, et collé il cacherait son propre bouton.
   Pendant le recouvrement son contenu recule (échelle 1 à 0,96, opacité 1 à 0,6), au rythme du défilement : animation
   liée à la position de la page, sans écouteur ni script ; sans prise en charge, pas de recul. La condition porte sur
   l'animation liée au défilement et sur sa plage : un navigateur qui comprendrait la première sans la seconde étalerait
   le recul sur toute la page. Le recul porte sur le conteneur, l'entrée sur ses enfants : deux animations sur un même
   élément se gêneraient. animation-timeline s'écrit après le raccourci animation, qui la réinitialise. */
@media (min-height: 37.5em) {
  .hero { position: sticky; top: 0; }
  @supports ((animation-timeline: scroll()) and (animation-range: 0 100svh)) {
    .hero-recul { animation: hero-recul var(--courbe-recul) both; animation-timeline: scroll(root); animation-range: 0 100svh; }
  }
}
@keyframes hero-recul { to { opacity: 0.6; transform: scale(0.96); } }
/* Focus au clavier revenu dans le hero alors que la feuille le recouvre : collé, il serait dans la fenêtre pour le
   navigateur (qui ne défilerait pas jusqu'à lui) et pourtant caché. Tant qu'il porte un focus visible, le hero reprend
   sa place dans la page : le navigateur y ramène l'élément qui a le focus. Pas pour le focus d'un clic ou d'un toucher :
   le hero se déroberait sous le pointeur entre l'appui et le relâchement. */
.hero:has(:focus-visible) { position: relative; }

/* Contenu du hero, dans .cadre : le bloc de marque et, s'il y en a un, le portrait. --logo : largeur du logotype (180px au
   moins, guide du logo), dont dépendent la signature et l'espace qui la sépare de lui. */
.hero-recul { --logo: max(180px, clamp(12.5rem, 62vw, 21.25rem)); display: grid; padding-top: var(--place-barre); }

/* Bloc de marque : le logotype et sa signature forment un seul signe, puis le titre de la page (corps modeste : la marque
   passe avant lui), puis les actions. */
.hero-marque { display: grid; justify-items: start; min-width: 0; }
.hero-logo { width: var(--logo); }
.hero-logo-voile { fill: var(--glacier); opacity: 0; }
/* Signature : Archivo largeur 125, graisse 600, sur une ligne, à 1 h sous le logotype (h, la moitié de la hauteur du B :
   7,63 % de la largeur du logotype), large d'environ 60 % de celui-ci (corps de 4,36 % de sa largeur), sans descendre sous
   0,75rem. */
.hero-signature { margin-top: calc(var(--logo) * 0.0763); font-family: var(--font-display); font-size: max(0.75rem, calc(var(--logo) * 0.0436)); font-weight: 600; letter-spacing: 0; line-height: 1; white-space: nowrap; }
.hero h1 { max-width: 32rem; margin-top: clamp(1.5rem, 5svh, 3rem); font-size: clamp(1.125rem, 0.95rem + 0.75vw, 1.5rem); font-weight: 600; letter-spacing: var(--tracking-m); line-height: var(--leading-m); }
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 24px; margin-top: clamp(1.25rem, 4svh, 2.25rem); }

/* Sans photo : le bloc de marque, centré dans l'espace sous la barre, à toutes les largeurs. */
.hero-sans-photo .hero-recul { place-items: center; }
.hero-sans-photo .hero-marque { justify-items: center; text-align: center; }
.hero-sans-photo .hero-actions { justify-content: center; }

/* Portrait : détouré sur l'Onyx exact du site, il sort du fond sans cadre, sans filet ni masque. Il garde ses proportions
   (celles de ses attributs width et height), n'est jamais rogné en hauteur et son bas touche le bas du hero. */
.hero-photo { display: block; }
.hero-photo img { width: auto; max-width: none; height: 100%; }

/* Jusqu'à 899px (la condition du cadrage mobile de l'image, dans le HTML) : le bloc de marque en haut, centré ; le
   portrait dessous, centré, dans la hauteur qui reste. L'image, plus large que la fenêtre sur un écran étroit, déborde de
   sa case d'autant des deux côtés (marges automatiques entre deux bords repoussés hors de la case) : seules les épaules
   sortent.
   Collé, le hero ne défile pas : plus haut que la fenêtre, il garderait le bas du portrait hors de l'écran. La colonne
   tient donc dans la fenêtre dès que le hero est collé (37,5em de haut), et d'abord à la hauteur réelle d'une fenêtre de
   téléphone, barres du navigateur visibles (390 × 664, 360 × 640, 375 × 629), par deux réglages :
   --portrait-min, la hauteur sous laquelle le portrait ne descend pas (15rem) : dans une fenêtre plus basse encore, où le
   hero n'est plus collé, il grandit plutôt que d'écraser le portrait ;
   --manque, ce qu'il manque à la fenêtre pour faire 47,5rem de haut : les quatre espaces de la colonne (sous la barre,
   avant le titre, avant les actions, avant le portrait) en cèdent chacun une part, jusqu'à leur plancher, atteint à
   37,5rem. À partir de 47,5rem de haut, rien ne se resserre. */
@media (max-width: 899px) {
  .hero-avec-photo .hero-recul { --portrait-min: 15rem; --manque: max(0px, 47.5rem - 100svh); grid-template-rows: auto minmax(var(--portrait-min), 1fr); row-gap: clamp(0.5rem, 3svh - 0.0625 * var(--manque), 2rem); padding-top: calc(var(--place-barre) + clamp(0.5rem, 4svh - 0.1 * var(--manque), 2.5rem)); }
  .hero-avec-photo h1 { margin-top: clamp(1.25rem, 5svh - 0.0625 * var(--manque), 3rem); }
  .hero-avec-photo .hero-actions { margin-top: clamp(1rem, 4svh - 0.05 * var(--manque), 2.25rem); }
  .hero-marque { justify-items: center; text-align: center; }
  .hero-actions { justify-content: center; }
  .hero-photo { position: relative; }
  .hero-photo img { position: absolute; inset: 0 -100vw; margin-inline: auto; }
}

/* Au-delà : deux colonnes. Le bloc de marque à gauche, centré en hauteur sous la barre ; le portrait à droite, son bas au
   bas du hero, son haut sous la barre. Sa hauteur : celle de la fenêtre moins la barre et une marge, sans que sa largeur
   dépasse 54vw ni 58 % du cadre de 1280px (avec ses proportions, 1611 × 1860 : 62vw et 856px de haut), pour que le bloc de
   marque garde sa colonne. */
@media not all and (max-width: 899px) {
  .hero-recul { --logo: max(180px, clamp(15rem, 26vw, 25rem)); }
  .hero-avec-photo .hero-recul { grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: clamp(1.5rem, 4vw, 4rem); }
  .hero-photo { align-self: end; height: min(calc(100svh - var(--barre) - clamp(1rem, 3svh, 2rem)), 62vw, 856px); }
}

/* Entrée du hero, une fois, au chargement, sans script : l'état de repos est l'état final, chaque animation part d'un
   état antérieur (fill-mode backwards) ; sans elles tout est en place. Le logotype fait son geste (guide du logo) : le
   bas du B part aligné sur le haut, 8,68 unités du tracé à gauche de sa place, et la rejoint, pendant que le tracé
   Glacier posé sur lui s'efface (il passe du Glacier au Bleu dragée). Puis la signature, le titre et les actions montent
   de 8px en fondu, l'un après l'autre. Fin : 720 ms.
   La photo, elle, s'affiche sans fondu : c'est l'image principale de la page, et Chrome ne la compte comme affichée (LCP)
   qu'à la fin d'un fondu parti de zéro (mesuré le 13/11/2026 : 704 ms au lieu de 36 ms sur une connexion rapide). */
.hero-logo-bas { animation: hero-logo-bas var(--dur-logo) var(--ease) backwards; }
.hero-logo-voile { animation: hero-logo-voile var(--dur-logo) var(--ease) backwards; }
.hero-signature, .hero h1, .hero-actions { animation: hero-monte var(--dur-entree) var(--ease) var(--dur-entree) backwards; }
.hero h1 { animation-delay: calc(var(--dur-entree) + var(--pas-hero)); }
.hero-actions { animation-delay: calc(var(--dur-entree) + 2 * var(--pas-hero)); }
@keyframes hero-fondu { from { opacity: 0; } }
@keyframes hero-monte { from { opacity: 0; transform: translateY(8px); } }
@keyframes hero-logo-bas { from { transform: translateX(-8.68px); } }
@keyframes hero-logo-voile { from { opacity: 1; } }

/* Mouvement réduit : le hero n'est plus collé, il défile avec la page, sans recul ; ni déplacement ni échelle à l'entrée,
   il reste des fondus courts ; le logotype est d'emblée dans son état final (guide du logo). */
@media (prefers-reduced-motion: reduce) {
  .hero { position: relative; }
  .hero-recul, .hero-logo-bas, .hero-logo-voile { animation: none; }
  .hero-signature, .hero h1, .hero-actions { animation: hero-fondu var(--dur-2) var(--ease) backwards; }
}
:root:where(.mouvement-reduit) .hero { position: relative; }
:root:where(.mouvement-reduit) :is(.hero-recul, .hero-logo-bas, .hero-logo-voile) { animation: none; }
:root:where(.mouvement-reduit) :is(.hero-signature, .hero h1, .hero-actions) { animation: hero-fondu var(--dur-2) var(--ease) backwards; }

/* Sections de la feuille : une bande pleine largeur ; le style des titres vient de site.css, leur corps d'ici. */
.bloc { padding-block: var(--section); }
.bloc h2 { font-size: var(--step-2); }

/* Réalisations (surface Glacier) : titre, grille et lien centrés. Deux colonnes, quatre à partir de 1024px (les attributs
   sizes des images, dans le gabarit, suivent ces largeurs). Moins de quatre projets (.vitrine-courte) : une rangée
   centrée de vignettes aux mêmes largeurs ; sur deux colonnes, une troisième se centre sous les deux premières. */
.realisations { text-align: center; }
.vitrine { --colonnes: 2; --ecart: clamp(12px, 2vw, 24px); display: grid; grid-template-columns: repeat(var(--colonnes), minmax(0, 1fr)); gap: var(--ecart); margin-top: clamp(2rem, 5vw, 3.5rem); }
@media (min-width: 1024px) { .vitrine { --colonnes: 4; } }
.vitrine-courte { display: flex; flex-wrap: wrap; justify-content: center; }
.vitrine-courte > li { flex: none; width: calc((100% - (var(--colonnes) - 1) * var(--ecart)) / var(--colonnes)); }
.realisations-suite { margin-top: clamp(1.5rem, 4vw, 2.5rem); }

/* Vignette : un lien entier au format 4:3. L'image remplit le cadre, qui coupe ce qui en dépasse ; l'étiquette est posée
   dessus, en bas, centrée. Le cadre coupe sans faire défiler (overflow: clip) : devant une étiquette plus haute que lui,
   il grandit au lieu de la couper (un navigateur qui ne connaît que overflow: hidden la couperait). Appui : voile
   immédiat, retour en fondu, comme les autres liens ; l'anneau de focus, Onyx sur cette surface, vient de site.css. */
.vignette { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); aspect-ratio: 4 / 3; overflow: hidden; overflow: clip; text-decoration: none; transition: opacity var(--dur-1) var(--ease); }
.vignette:active { opacity: 0.6; transition: none; }
.vignette picture { position: absolute; inset: 0; }
.vignette img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-2) var(--ease); }
/* Étiquette : rectangle Onyx à texte Glacier, le signe d'ouverture et le nom du projet sur une ligne, la mention dessous.
   Un nom long passe à la ligne, il n'est jamais tronqué. Hiérarchie par la graisse seule. */
.vignette-etiquette { position: relative; align-self: end; justify-self: center; margin: 0.5rem; padding: 0.375rem 0.625rem; background: var(--onyx); border: 1px solid var(--onyx); color: var(--glacier); font-size: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem); line-height: 1.3; overflow-wrap: anywhere; text-wrap: balance; }
.vignette-icone { display: inline-block; width: 0.75em; height: 0.75em; margin-right: 0.4em; fill: none; stroke: currentColor; stroke-width: 1.5; }
.vignette-nom { font-weight: 600; }
.vignette-mention { display: block; }
/* Sous 480px de large, les deux colonnes sont étroites : l'étiquette est compacte (marge, remplissage et interligne
   réduits), pour laisser voir la capture. Jusque sur un écran de 320px, où un nom prend deux lignes, elle couvre moins de
   la moitié de la hauteur de sa vignette. */
@media not all and (min-width: 480px) {
  .vignette-etiquette { margin: 0.25rem; padding: 0.0625rem 0.375rem; font-size: 0.75rem; line-height: var(--interligne-titre); }
  .vignette-nom { font-size: 0.8125rem; }
}
/* Survol, vrais pointeurs seulement : l'image grandit dans son cadre, l'étiquette s'inverse d'un coup (une couleur ne
   s'anime pas) et garde son filet Onyx. En mouvement réduit, l'inversion seule. */
@media (hover: hover) and (pointer: fine) {
  .vignette:hover img { transform: scale(1.03); }
  .vignette:hover .vignette-etiquette { background: var(--glacier); color: var(--onyx); }
  @media (prefers-reduced-motion: reduce) { .vignette:hover img { transform: none; } }
  :root:where(.mouvement-reduit) .vignette:hover img { transform: none; }
}

/* Studio et Instagram (surface Onyx). Une colonne : le texte, puis le bloc Instagram. À partir de 900px, deux : le texte
   à gauche, centré en hauteur sur le cadre ; à droite le bloc Instagram, dans une colonne de la largeur du cadre. */
.studio { display: grid; grid-template-columns: minmax(0, 1fr); gap: clamp(2.5rem, 8vw, 4rem) clamp(2rem, 6vw, 6rem); }
@media (min-width: 900px) { .studio { grid-template-columns: minmax(0, 1fr) 400px; align-items: center; } }

/* Colonne de texte. L'accroche : un paragraphe en capitales (celles du contenu), en police de titre, une phrase par
   ligne. Son corps suit la largeur de la colonne (5,8 % : la plus longue phrase, large de 16,8 fois le corps, y tient
   sur une ligne), sans dépasser 2rem : sous le titre, au-dessus du texte. Sans unités de conteneur, il suit la largeur
   de la fenêtre ; une phrase trop longue pour sa colonne passe à la ligne. Puis les deux paragraphes, sur une largeur de
   lecture. */
.studio-texte { container-type: inline-size; }
.studio-accroche { margin-top: clamp(1.25rem, 3vw, 2rem); font-family: var(--font-display); font-size: clamp(1rem, 5vw, 2rem); font-size: clamp(1rem, 5.8cqi, 2rem); font-weight: 650; letter-spacing: var(--tracking-m); line-height: var(--interligne-grand); overflow-wrap: anywhere; }
.studio-accroche span { display: block; }
.studio-texte > p:not(.studio-accroche) { max-width: var(--lecture); margin-top: 1rem; font-size: var(--step-0); line-height: var(--interligne); text-wrap: pretty; }
.studio-texte > .studio-accroche + p { margin-top: clamp(1.25rem, 3vw, 2rem); }
/* Types de sites : une liste en ligne, en petites capitales (celles du contenu, en petit corps, espacées), qui passe à
   la ligne entre deux types, jamais dans un type. Une barre fine devant chacun : le premier de chaque ligne pousse la
   sienne hors de la liste, qui la coupe (la liste déborde à gauche, pour que son texte reste au bord de la colonne).
   La barre de tête est cachée à 2px du bord de coupe, pas au ras de lui : sur un écran à 3 pixels par point (Safari
   d'iPhone), l'arrondi en laissait voir un liseré. D'où les 2px du calcul : la liste commence 2px plus à droite, chaque
   type recule de 2px de plus et les rend à sa droite, la liste les rend à la sienne pour garder sa largeur (les passages
   à la ligne ne changent pas). Textes et barres visibles restent où ils seraient sans ces 2px. */
.studio-types { --pas: 0.875rem; display: flex; flex-wrap: wrap; row-gap: 0.75rem; margin: clamp(1.75rem, 4vw, 2.5rem) -2px 0 calc(-1 * var(--pas) + 2px); overflow: hidden; overflow: clip; color: var(--text-muted); font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.08em; line-height: 1; }
.studio-types li { margin-left: -3px; padding-inline: var(--pas) calc(var(--pas) + 2px); border-left: 1px solid var(--text-muted); white-space: nowrap; }

/* Bloc Instagram. Le cadre : 9:16, 400px de large au plus, à angles droits. Plus étroit que 394px, il prend la hauteur
   dont l'intégration d'Instagram a besoin (1,25 fois sa largeur plus 208px), pour qu'elle n'y soit jamais coupée : sa
   largeur se lit sur la colonne, en unités de conteneur (sans elles, le 9:16 seul). Sa taille ne dépend pas de ce
   qu'il montre (couverture, cadre d'Instagram, logotype) : rien ne bouge au clic. */
.studio-instagram { container-type: inline-size; }
.reel { display: block; width: min(100%, 400px); margin: 0; }
.reel-cadre { position: relative; display: block; aspect-ratio: 9 / 16; background: var(--onyx); }
@supports (width: 1cqi) {
  .reel-cadre { --largeur-cadre: min(100cqi, 400px); aspect-ratio: auto; height: max(var(--largeur-cadre) * 16 / 9, 1.25 * var(--largeur-cadre) + 208px); }
}
/* Avec un Reel : la couverture remplit le cadre ; le lien de lecture (un bouton dès que js/reel.js est passé) le
   recouvre en entier, son signe au centre ; au clic, le cadre d'Instagram se pose par-dessus la couverture. Tant qu'il
   n'a pas reçu sa page, Chrome le laisse transparent : pendant cette attente (aria-busy, posé par le script), la
   couverture reste dessous et s'estompe de moitié sur l'Onyx de la boîte, pour dire que cela charge ; elle part quand
   le cadre est chargé. Une opacité seule : le voile est le même en mouvement réduit. (WebKit peint en blanc un cadre
   qui attend : la couverture y est cachée dès le clic.) */
.reel-couverture, .reel-cadre iframe { position: absolute; inset: 0; width: 100%; height: 100%; }
.reel-couverture { transition: opacity var(--dur-2) var(--ease); }
.reel-cadre[aria-busy="true"] .reel-couverture { opacity: 0.5; }
.reel-couverture img { width: 100%; height: 100%; object-fit: cover; }
.reel-cadre iframe { border: 0; }
.reel-lecture { position: absolute; inset: 0; display: grid; place-items: center; padding: 0; background: none; border: 0; color: var(--glacier); cursor: pointer; }
/* Signe de lecture : un carré Onyx aux angles arrondis (rayon des boutons, commun.css), à triangle Glacier (le Bleu
   dragée ne va pas sur une photo), de 72px. Appui : léger recul, qui repart de la valeur affichée. Survol, vrais
   pointeurs : il s'inverse d'un coup et garde son filet Onyx. */
.reel-icone { width: 4.5rem; height: 4.5rem; padding: 1.4375rem; background: var(--onyx); border: 1px solid var(--onyx); fill: currentColor; transition: transform var(--dur-appui) var(--ease), opacity var(--dur-appui) var(--ease); }
.reel-lecture:active .reel-icone { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .reel-lecture:hover .reel-icone { background: var(--glacier); color: var(--onyx); }
}
@media (prefers-reduced-motion: reduce) { .reel-lecture:active .reel-icone { transform: none; opacity: 0.7; } }
:root:where(.mouvement-reduit) .reel-lecture:active .reel-icone { transform: none; opacity: 0.7; }
/* Sous le cadre : la légende, puis la ligne d'information, discrète, puis le lien vers le profil. Avec un Reel, la
   légende est un lien vers sa page, souligné comme les liens de la section, à la graisse de la légende ; sa cible fait
   44px de haut, et la marge de la légende lui cède ce qu'elle ajoute : le texte reste à la même distance du cadre. */
.reel-legende { display: block; margin-top: 0.75rem; font-weight: 600; line-height: var(--interligne); }
figcaption .reel-legende { margin-top: 0.125rem; }
.reel-legende .lien { font-weight: inherit; }
.reel-info { margin-top: 0.25rem; color: var(--text-muted); font-size: var(--step--1); line-height: var(--interligne); text-wrap: pretty; }
.reel-suivre { margin-top: 0.25rem; }
/* Sans Reel : le cadre et sa légende sont un seul lien vers le profil. Le logotype du site, au centre d'un cadre Onyx
   bordé d'un filet Glacier. Appui : voile immédiat, retour en fondu, comme les autres liens ; survol : légende
   soulignée, d'un coup. */
.reel-profil { text-decoration: none; transition: opacity var(--dur-1) var(--ease); }
.reel-profil:active { opacity: 0.6; transition: none; }
.reel-profil .reel-cadre { display: grid; place-items: center; border: 1px solid var(--glacier); }
.reel-profil .reel-cadre svg { width: min(56%, 13.75rem); }
@media (hover: hover) and (pointer: fine) {
  .reel-profil:hover .reel-legende { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
}

/* Compétences (surface Glacier) : titre et phrase d'introduction alignés à gauche, sur le bord des listes ; une colonne
   par pôle à partir de 900px, l'une sous l'autre en dessous. Listes typographiques : un filet fin au-dessus de chaque
   ligne, ni carte ni icône ; un libellé sur deux lignes les prend de longueurs voisines (jamais un mot seul, ni la
   coupe de « e-mail » à son trait d'union en bout de ligne). Puis la ligne des outils, sous un filet : le libellé et
   les noms à la suite, qui passent à la ligne quand la place manque. */
.competences-intro { max-width: var(--lecture); margin-top: 1rem; font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.3125rem); line-height: var(--interligne); text-wrap: pretty; }
.competences-colonnes { display: grid; gap: 2.5rem clamp(1.5rem, 3vw, 3rem); margin-top: clamp(2.5rem, 6vw, 4rem); }
@media (min-width: 900px) { .competences-colonnes { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.competences-colonne h3 { font-size: var(--step-1); letter-spacing: var(--tracking-m); line-height: var(--leading-m); }
.competences-liste { margin-top: 1rem; line-height: var(--interligne); }
.competences-liste li { padding-block: 0.75rem; border-top: 1px solid var(--rule); text-wrap: balance; }
.competences-outils { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 2rem; margin-top: clamp(2.5rem, 6vw, 4rem); padding-top: 1.25rem; border-top: 1px solid var(--rule); line-height: var(--interligne); }
.competences-outils-titre { font-weight: 600; }
.competences-outils ul { display: flex; flex-wrap: wrap; gap: 0.25rem 1.5rem; }

/* Avis clients (surface Onyx), entre les compétences et le formulaire ; la section n'existe pas sans avis. Le titre à
   gauche, sur le bord des cartes. --visibles : cartes côte à côte (une sous 700px, deux jusqu'à 1099px, trois au-delà) ;
   --ecart : l'espace entre deux cartes ; --sous-titre : l'espace entre le titre et les cartes. Jusqu'à trois avis : des
   cartes fixes, dans cette grille. */
.avis { --visibles: 1; --ecart: clamp(16px, 2vw, 24px); --sous-titre: clamp(2rem, 5vw, 3.5rem); }
@media (min-width: 700px) { .avis { --visibles: 2; } }
@media (min-width: 1100px) { .avis { --visibles: 3; } }
.avis-cartes { display: grid; grid-template-columns: repeat(var(--visibles), minmax(0, 1fr)); gap: clamp(2rem, 5vw, 3rem) var(--ecart); margin-top: var(--sous-titre); }

/* Carte d'avis : ni boîte ni fond, un filet fin en tête. La citation dans le corps du texte ; puis le nom, en gras ;
   puis l'origine (entreprise, ville) et la source, en gris, chacune sur sa ligne. Hiérarchie par la graisse et la
   couleur. Un avis long allonge sa carte, il n'est jamais coupé ; un mot plus long que la carte passe à la ligne. */
.avis-carte { padding-top: 1.25rem; border-top: 1px solid var(--rule); overflow-wrap: anywhere; }
.avis-carte figure, .avis-carte blockquote { margin: 0; }
.avis-carte blockquote { font-size: clamp(1.0625rem, 1rem + 0.25vw, 1.1875rem); line-height: var(--interligne); text-wrap: pretty; }
.avis-carte figcaption { margin-top: 1.25rem; line-height: var(--interligne); }
.avis-nom, .avis-origine, .avis-source { display: block; }
.avis-nom { font-weight: 600; }
.avis-origine, .avis-source { color: var(--text-muted); font-size: var(--step--1); }

/* Carrousel (à partir de quatre avis). Rangée d'en-tête : le titre, puis les commandes, sous lui ; à partir de 700px,
   sur sa ligne, à droite. Elles sont au-dessus de la piste, près du titre : sous elle, haute de l'avis le plus long,
   elles se perdraient sur téléphone. */
.carrousel-tete { display: grid; justify-items: start; gap: 1rem 2rem; }
@media (min-width: 700px) { .carrousel-tete { grid-template-columns: minmax(0, 1fr) auto; align-items: center; } }
/* La piste : défilement natif horizontal, accroché carte par carte, sans barre de défilement ; chaque carte y prend sa
   part de la largeur. Elle défile au doigt, au pavé tactile et au clavier, avec ou sans JavaScript ; elle ne laisse pas
   le geste déborder sur la page (retour arrière au pavé tactile).
   Le carrousel est la fenêtre de sa piste : il coupe ce qui en dépasse sur les côtés quand elle se décale (résistance,
   plus bas), à --retrait de ses bords, la place d'un anneau de focus ; la page ne s'élargit jamais. */
.carrousel { --retrait: 0.5rem; margin: calc(var(--sous-titre) - var(--retrait)) calc(-1 * var(--retrait)) calc(-1 * var(--retrait)); padding: var(--retrait); overflow: hidden; overflow: clip; }
.carrousel-piste { display: flex; gap: var(--ecart); overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; transition: transform var(--dur-entree) var(--ease); }
.carrousel-piste::-webkit-scrollbar { display: none; }
.carrousel-piste > .avis-carte { flex: none; width: calc((100% - (var(--visibles) - 1) * var(--ecart)) / var(--visibles)); scroll-snap-align: start; }
/* Glisser à la souris (js/carrousel.js), une fois le module en service (.est-actif) : la piste se prend. Pendant le
   geste (.en-glisse) elle suit le pointeur sans transition, sélection de texte coupée ; l'accroche est suspendue du début
   du geste à l'arrivée (.sans-accroche). Aux deux bouts elle résiste : le script la décale (transform) ; relâchée, elle
   revient à sa place par la transition de la piste, qui repart de la valeur affichée si le geste reprend. En mouvement
   réduit, le script ne la décale pas : la transition n'a rien à jouer. */
.est-actif .carrousel-piste { cursor: grab; }
.carrousel-piste.sans-accroche { scroll-snap-type: none; }
.carrousel-piste.en-glisse { cursor: grabbing; transition: none; -webkit-user-select: none; user-select: none; }

/* Commandes, dans la rangée d'en-tête : précédent, position, suivant. Masquées tant que le script n'est pas passé
   (attribut hidden) : sans JavaScript elles sont absentes (site.css) ; avec lui (html.js, posé avant le premier
   affichage), elles gardent leur place sans se montrer ni prendre le focus, pour que rien ne bouge quand le module les
   affiche, d'un coup. La position, en chiffres tabulaires, garde sa largeur d'une carte à l'autre. Les boutons : des
   carrés de 48px (44px au moins) aux angles arrondis (rayon des boutons, commun.css), au filet gris, signe Glacier.
   Appui : léger recul, qui repart de la valeur affichée.
   Survol, vrais pointeurs : le bouton s'inverse d'un coup. Au bout de la piste (aria-disabled), le bouton ne répond
   plus à l'appui ni au survol et il garde le focus : ce sont donc son signe (en fondu) et son filet (celui des listes)
   qui s'estompent, pas le bouton entier, dont l'anneau de focus pâlirait avec lui. */
.carrousel-commandes { display: flex; align-items: center; gap: 0.5rem; }
.js .carrousel-commandes[hidden] { display: flex !important; visibility: hidden; }
.carrousel-position { min-width: 7ch; font-variant-numeric: tabular-nums; line-height: var(--interligne); text-align: center; }
.carrousel-bouton { display: grid; place-items: center; width: 3rem; min-width: 44px; height: 3rem; min-height: 44px; padding: 0; background: none; border: 1px solid var(--bordure); cursor: pointer; transition: transform var(--dur-appui) var(--ease), opacity var(--dur-appui) var(--ease); }
.carrousel-icone { width: 1rem; height: 1rem; fill: none; stroke: currentColor; stroke-width: 1.5; transition: opacity var(--dur-appui) var(--ease); }
.carrousel-bouton:not([aria-disabled="true"]):active { transform: scale(0.97); }
.carrousel-bouton[aria-disabled="true"] { border-color: var(--rule); cursor: default; }
.carrousel-bouton[aria-disabled="true"] .carrousel-icone { opacity: 0.4; }
@media (hover: hover) and (pointer: fine) {
  .carrousel-bouton:not([aria-disabled="true"]):hover { background: var(--glacier); border-color: var(--glacier); color: var(--onyx); }
}
@media (prefers-reduced-motion: reduce) { .carrousel-bouton:not([aria-disabled="true"]):active { transform: none; opacity: 0.7; } }
:root:where(.mouvement-reduit) .carrousel-bouton:not([aria-disabled="true"]):active { transform: none; opacity: 0.7; }

/* Formulaire (#contact, gabarit et styles de site.css, inchangés) : son conteneur prend la largeur de contenu de l'accueil
   (1280px, celle de .cadre), pas les 1320px des autres pages. Titre et champs s'alignent sur les autres sections, la
   barre et le pied ; jusqu'à 1376px de fenêtre, où tout tient déjà dans les marges, rien ne change. */
.page-accueil .section-final > .wrap { max-width: calc(var(--largeur) + 2 * var(--gutter)); }

/* Révélation au défilement (js/reveles.js) : les enfants directs d'un conteneur [data-revele] situé sous la fenêtre
   attendent masqués (.revele-attente), puis apparaissent quand il arrive (.est-revele) : opacité et 8px, l'un après
   l'autre selon leur rang (.revele-1 à .revele-8). L'état caché n'existe que par ces classes, posées par le script :
   sans lui, tout est visible. En mouvement réduit et à l'impression, elles restent sans effet : rien n'est masqué ni
   déplacé. */
.revele-attente { opacity: 0; transform: translateY(8px); }
.revele-attente.est-revele { opacity: 1; transform: none; transition: opacity var(--dur-entree) var(--ease), transform var(--dur-entree) var(--ease); }
.revele-2.est-revele { transition-delay: var(--pas-liste); }
.revele-3.est-revele { transition-delay: calc(2 * var(--pas-liste)); }
.revele-4.est-revele { transition-delay: calc(3 * var(--pas-liste)); }
.revele-5.est-revele { transition-delay: calc(4 * var(--pas-liste)); }
.revele-6.est-revele { transition-delay: calc(5 * var(--pas-liste)); }
.revele-7.est-revele { transition-delay: calc(6 * var(--pas-liste)); }
.revele-8.est-revele { transition-delay: calc(7 * var(--pas-liste)); }
@media (prefers-reduced-motion: reduce) { .revele-attente { opacity: 1; transform: none; } }
:root:where(.mouvement-reduit) .revele-attente { opacity: 1; transform: none; }

/* Impression : la révélation ne masque rien ; le carrousel sort sans ses commandes, et sa piste passe à la ligne au lieu
   de défiler : tous les avis s'impriment, par rangées, pas les seuls que la fenêtre de la piste montrait. Les couleurs
   des sections Onyx ne sont pas reprises ici (écart connu : sans les fonds, leur texte sort pâle, comme sur les autres
   pages). */
@media print {
  .revele-attente { opacity: 1; transform: none; }
  .carrousel-commandes { display: none !important; }
  .carrousel-piste { flex-wrap: wrap; overflow: visible; }
}

/* Couleurs forcées (thèmes de contraste du système) : une couleur posée dans un élément svg n'est pas forcée. Le voile
   du logotype du hero suit les tracés du logotype (commun.css) : la couleur du texte du canevas. Le carré du signe
   de lecture, resté Onyx sous un triangle passé à la couleur du texte (noir sur noir avec une palette claire), prend
   les couleurs système d'un bouton. */
@media (forced-colors: active) {
  .hero-logo-voile { fill: CanvasText; }
  .reel-icone { background: ButtonFace; border-color: ButtonText; }
}
