/* =====================================================
   JOYCE MARTENS PHOTOGRAPHIE — Styles
   ===================================================== */

/* -------------------------------------------------------
   Polices auto-hébergées (#35) — UN SEUL endroit, jamais recopié par page.
   Fichiers woff2 versionnés dans fonts/ (licence OFL de chaque famille à côté),
   sous-ensembles latin + latin-ext (le français en a besoin). Les deux familles sont des
   fichiers VARIABLES : un fichier par (famille, style, sous-ensemble), et la plage
   `font-weight` déclarée est l'AXE RÉEL du fichier — mêmes octets que ceux que servait
   Google. Déclarer l'axe entier ne coûte rien : c'est ce qui empêche le navigateur de
   SUBSTITUER (300 rendu en 400) ou de SYNTHÉTISER (faux gras, oblique calculée).
   Graisses et styles rendus : polices-auto-hebergees.test.cjs, qui croise les règles de
   TOUTES les feuilles avec ces faces. Un @font-face déclaré mais non rendu n'est pas téléchargé.
   `font-display: swap` conservé.
   ------------------------------------------------------- */
/* latin-ext */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/cormorant-garamond-latin-ext.9dc38267.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/cormorant-garamond-latin.5d618c46.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/cormorant-garamond-italic-latin-ext.06bc9a8c.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/cormorant-garamond-italic-latin.e6d6d1d7.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/jost-latin-ext.b2a539cd.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/jost-latin.235d8f89.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Jost';
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/jost-italic-latin-ext.a81ef0fa.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Jost';
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/jost-italic-latin.a3d71240.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* -------------------------------------------------------
   Polices de REPLI ajustées (#89) — calculées par scripts/polices-repli.mjs (bucket 3, valeurs
   imprimées par le script, jamais retouchées à la main ; un H1 changé dans content.json déplace
   l'optimum : relancer). Pendant la fenêtre `swap`, une police locale (Times New Roman / Arial sur
   Apple et Windows, Noto Serif / Roboto sur Android) prend la largeur moyenne et la boîte verticale
   de la police web. Ce que ces faces FONT (mesuré #149, texte servi des 10 pages) : largeur moyenne
   calée (repli Arial = 100,1 % de Jost à 1440 px, 100,0 % à 412 px) et boîte verticale calée.
   Ce qu'elles ne font PAS : garder le nombre de lignes. La bascule en change encore, paragraphe par
   paragraphe (15 couples page × largeur sur 50 au premier écran, dans les 3 moteurs), et aucun
   réglage global de `size-adjust` ne l'efface (balayage de 92,5 à 96,5 % : jamais zéro). Cause
   principale mesurée : une colonne bornée en `ch` suit le « 0 » de la police RENDUE (0,600 em en
   Jost, 0,526 em en Arial ajusté) et rétrécit de 12 % en repli ; `size-adjust` agit à la fois sur le
   texte et sur `ch`, il n'y peut rien.
   Pas de face Jost italique de repli : un texte Jost italique prend `Jost Repli`, penché par le
   navigateur pendant la seule fenêtre de repli (choix déclaré #89 D2).
   Garde : polices-repli.test.cjs.
   ------------------------------------------------------- */
@font-face {
  font-family: 'Cormorant Garamond Repli';
  font-style: normal;
  font-weight: 300 700;
  src: local('Times New Roman'), local('TimesNewRomanPSMT');
  size-adjust: 94.40%;
  ascent-override: 97.89%;
  descent-override: 30.40%;
  line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Cormorant Garamond Repli Android';
  font-style: normal;
  font-weight: 300 700;
  src: local('Noto Serif'), local('NotoSerif-Regular');
  size-adjust: 81.51%;
  ascent-override: 113.37%;
  descent-override: 35.21%;
  line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Cormorant Garamond Repli';
  font-style: italic;
  font-weight: 300 700;
  src: local('Times New Roman Italic'), local('TimesNewRomanPS-ItalicMT');
  size-adjust: 84.13%;
  ascent-override: 109.83%;
  descent-override: 34.11%;
  line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Cormorant Garamond Repli Android';
  font-style: italic;
  font-weight: 300 700;
  src: local('Noto Serif Italic'), local('NotoSerif-Italic');
  size-adjust: 72.38%;
  ascent-override: 127.66%;
  descent-override: 39.65%;
  line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Jost Repli';
  font-style: normal;
  font-weight: 100 900;
  src: local('Arial'), local('ArialMT');
  size-adjust: 94.66%;
  ascent-override: 113.03%;
  descent-override: 39.61%;
  line-gap-override: 0.00%;
}
@font-face {
  font-family: 'Jost Repli Android';
  font-style: normal;
  font-weight: 100 900;
  src: local('Roboto'), local('Roboto-Regular');
  size-adjust: 98.46%;
  ascent-override: 108.67%;
  descent-override: 38.09%;
  line-gap-override: 0.00%;
}

/* -------------------------------------------------------
   Variables CSS
   ------------------------------------------------------- */
:root {
  --color-bg:         #F7F4EF;
  --color-surface:    #EFEAE3;
  --color-text:       #1C1915;
  --color-muted:      #9A8E82;
  --color-muted-text: #6B5F54;
  /* Terre cuite assombrie pour tenir AA sur les deux fonds : 5,43:1 sur --color-bg,
     4,97:1 sur --color-surface (calculé par contrast-core, pas estimé). #B07550 ne donnait
     que 3,48:1 — sous le seuil, sur les 5 libellés de section que l'écart A rend visibles.
     Valeur nommée par le done-criterion du programme 2026-08-20. */
  --color-accent:     #8A5835;
  /* MÊME terre cuite, remontée en clarté pour le fond sombre du pied de page. Ce n'est pas une
     seconde couleur de marque : #8A5835 est H 25° S 45% L 37%, #C08A63 est H 25° S 43% L 57% —
     même teinte, même saturation, une clarté. Elle existe parce que #8A5835 sur #1C1915 ne
     donne que 2,94:1, sous AA : sur le pied de page la terre cuite ne se voyait tout simplement
     pas, et l'accent y paraissait « pas la même couleur ». 5,88:1 avec l'encre comme avec le
     crème (calculé par contrast-core, pas estimé).
     ⚠️ RÉSERVÉE AUX FONDS SOMBRES : sur le crème elle ne donne que 2,71:1. */
  --color-accent-dark: #C08A63;

  /* Entrée du sommaire des collections (#133, « le rideau ») : la courbe, et la course — plus que
     la hauteur d'une ligne, quel que soit le point de départ de l'élément dans la ligne. Portée à
     12rem entre 601 et 900 px (règle du sommaire), où la signature passe sous le nom. */
  --ease-entree:   cubic-bezier(0.22, 1, 0.28, 1);
  --rideau-course: 9rem;

  /* Encre du chiffre géant du panneau (§3.1). Exposé en jeton parce que c'est le SEUL réglage
     subjectif de la composition : trop pâle il ne traverse plus les trois plans, trop dense il
     concurrence le titre. Une valeur, un endroit — réglable sans toucher au geste. */
  --coll-num-ink: rgba(28, 25, 21, 0.09);

  --font-title: 'Cormorant Garamond', 'Cormorant Garamond Repli', 'Cormorant Garamond Repli Android', serif;
  --font-body:  'Jost', 'Jost Repli', 'Jost Repli Android', sans-serif;

  /* Planchers PROPORTIONNELS sous 375 px (#95, C123, 26/09/2026) : `min(Xrem, Yvw)` avec
     Y = X × 16 / 375 × 100 — la même taille à 375 px et au-dessus, une taille proportionnelle en
     dessous. H1 de l'accueil : son 1er segment (« L'art de vous voir », 6,66 em mesurés dans
     Chromium) passait à 2 lignes de 260 à 360 px au plancher fixe de 3rem (320 px pour 310,8 à 360) ;
     à 12.8vw il tient sur UNE ligne jusqu'à ~310 px. Titres de section : 8.534vw (32,002 px à 375,
     donc 2rem pile dès 375). */
  --hero-title-size:    clamp(min(3rem, 12.8vw), 8vw, 6rem);
  --hero-title-align:   left;
  --section-title-size: clamp(min(2rem, 8.534vw), 5vw, 3.5rem);

  --max-w:     1200px;
  --gutter:    clamp(1.5rem, 5vw, 4rem);
  --transition: 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* -------------------------------------------------------
   Reset
   ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  background-color: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}
img   { display: block; max-width: 100%; height: auto; }
a     { color: inherit; text-decoration: none; }
/* `font: inherit` n'emporte PAS la couleur. Sur iOS Safari, un <button> sans `color` explicite
   retombe sur la couleur systeme des controles : le titre des cartes portfolio s'affichait en
   BLEU sur iPhone et en noir partout ailleurs. `color: inherit` + neutralisation de l'apparence
   native ferment le cas pour TOUS les boutons du site, pas seulement les cartes. */
button {
  cursor: pointer;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  -webkit-text-fill-color: currentColor;
  -webkit-appearance: none;
  appearance: none;
}

/* -------------------------------------------------------
   Typographie partagée
   ------------------------------------------------------- */
h1, h2, h3 {
  font-family: var(--font-title);
  font-weight: 400;
  line-height: 1.1;
}

.section-label {
  display: block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-accent);
  /* Le libellé passe SOUS le titre (écart A). Les jambages de « Reportages » (p, g) plongent
     sous la ligne de base : sans cette respiration, ils viennent toucher les capitales du
     libellé. Volontairement léger — 0.35rem, pas un blanc. Uniforme sur les 5 sections. */
  margin-top: 0.35rem;
  margin-bottom: 0.75rem;
}

.section-title {
  font-family: var(--font-title);
  font-size: var(--section-title-size);
  font-style: italic;
  font-weight: 300;
  /* 1.1 (hérité de h1,h2,h3) serre la boîte sur les jambages d'un Cormorant italique. */
  line-height: 1.2;
}

/* Même boîte que les pistes de contenu (`.reportages-track`, `.plate`, `.temoignages-stage`) :
   sans `max-width`, l'en-tête se calait sur le bord de la fenêtre pendant que son contenu se
   centrait dans les 1200 px — au-delà de cette largeur, TOUS les H2 de la page dérivaient d'une
   vingtaine de pixels vers la gauche par rapport à ce qu'ils annoncent. */
.section-intro {
  padding: 0 var(--gutter);
  max-width: var(--max-w);
  margin: 0 auto 3rem;
}

/* -------------------------------------------------------
   Header
   ------------------------------------------------------- */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  transition: background var(--transition), box-shadow var(--transition);
}
/* `backdrop-filter: blur(8px)` RETIRE (24/08/26). Le fond est opaque a 95 % : le flou ne
   travaillait donc que sur les 5 % restants — invisible a l'oeil. En echange, il obligeait le
   navigateur a refiltrer toute la bande du header, qui est FIXE et donc a l'ecran pendant la
   totalite du defilement. C'est la propriete la plus couteuse du CSS sur mobile, payee en
   permanence pour un effet qu'on ne voit pas. Si le flou revient un jour, il faudra d'abord
   baisser l'opacite du fond — sinon il n'y a rien a flouter. */
.site-header.scrolled {
  background: rgba(247, 244, 239, 0.95);
  box-shadow: 0 1px 0 rgba(28, 25, 21, 0.08);
}

/* ── Le header SUR le hero : encre crème, tant que `.scrolled` n'est pas posé ────────────────
   ⚠️ SCOPÉ À L'ACCUEIL, jamais au header en général. Sur toutes les autres pages le header
   démarre sur fond crème et l'encre sombre y est correcte. Le scope passe par `body.home` et
   NON par `body:not(.sub)` : `.sub` manque sur /mentions-legales/, /politique-confidentialite/
   et /404 — un `:not(.sub)` y aurait posé du crème sur du crème, soit un header invisible sur
   trois pages, en corrigeant une seule.

   Le header n'a pas de calque à lui : c'est la bande haute du dégradé du hero qui lui sert de
   fond, et c'est pour ça que cette bande est MESURÉE comme zone à part entière (zone `header`
   de HERO_VEIL). Un header non mesuré serait un texte illisible en production.

   `opacity: 1` et non 0,65 : l'atténuation était dessinée pour de l'encre sombre sur crème.
   Surtout, une opacité < 1 rendrait FAUSSE la couleur déclarée au calcul — la mesure porte sur
   #F7F4EF, donc c'est #F7F4EF qui doit être servi. Le survol garde son filet animé. */
body.home .site-header:not(.scrolled) .nav-link,
body.home .site-header:not(.scrolled) .logo-link {
  color: var(--color-bg);
  opacity: 1;
}
body.home .site-header:not(.scrolled) .hamburger span { background: var(--color-bg); }

.site-nav {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 1.5rem var(--gutter);
}

/* Grille 3 colonnes plutôt que trois flex:1. Avec le flex, les deux côtés se partageaient la
   place à parts égales et le côté le plus chargé cassait juste au-dessus du seuil hamburger
   (cause racine §2.5). Ici la colonne centrale se dimensionne au logo (`auto`) et les deux
   côtés reçoivent le reste, symétriquement — le logo reste optiquement centré. */
.nav-left, .nav-right { display: flex; gap: clamp(1.1rem, 2.2vw, 2rem); align-items: center; }
.nav-right { justify-content: flex-end; }
.nav-logo  { justify-self: center; }

.nav-link {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--color-text);
  opacity: 0.65;
  position: relative;
  transition: opacity var(--transition);
}
.nav-link::after {
  content: '';
  position: absolute;
  bottom: -3px;
  left: 50%;
  transform: translateX(-50%);
  /* `scaleX` et non une largeur qui grandit : une largeur animee relance un reflow a chaque
     frame. C'est le meme contrat que le reste de l'accueil — ce filet-ci y echappait encore. */
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: translateX(-50%) scaleX(0);
  transform-origin: center;
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-link:hover { opacity: 1; }
.nav-link:hover::after { transform: translateX(-50%) scaleX(1); }

.logo-link {
  font-family: var(--font-title);
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-text);
}

.hamburger {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 4px;
}
.hamburger span {
  display: block;
  width: 22px; height: 1px;
  background: var(--color-text);
  transition: transform var(--transition), opacity var(--transition);
}
.hamburger.is-open span:first-child { transform: translateY(6px) rotate(45deg); }
.hamburger.is-open span:last-child  { transform: translateY(-6px) rotate(-45deg); }

/* ── Menu burger PLEIN ÉCRAN (#95 lot 3) — structure JMB, direction artistique JMP ───────────────
   `#mobileMenu` est le FRÈRE SUIVANT de `<header>`, jamais son enfant : sur les sous-pages,
   `body.sub .site-header` porte `backdrop-filter`, qui fait du header le bloc conteneur de tout
   descendant `position: fixed` — un menu resté dedans serait CONFINÉ à la bande de l'en-tête.
   Il couvre la fenêtre SOUS l'en-tête (90 < 100) : logo et X restent au-dessus, cliquables.
   Il n'existe que sous 1000 px, là où le hamburger existe : `.is-open` n'affiche rien au-delà
   (js/nav.js le referme en plus au passage du seuil, et rend le défilement). */
.mobile-menu {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 90;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2rem;
  background: var(--color-bg);
}
/* `.mobile-menu.is-open { display: flex }` vit dans le bloc « Responsive » (max-width: 1000px), à
   côté du hamburger qu'il accompagne. */
/* Menu ouvert en haut de l'accueil : l'en-tête y est transparent, logo et traits en CRÈME — la
   couleur même du fond du menu. js/nav.js pose `.is-menu-open` sur l'en-tête ; l'encre revient au
   texte. Même portée que la règle crème (accueil, non défilé), une classe de plus : elle gagne. */
body.home .site-header.is-menu-open:not(.scrolled) .logo-link { color: var(--color-text); }
body.home .site-header.is-menu-open:not(.scrolled) .hamburger span { background: var(--color-text); }
.mobile-link {
  font-family: var(--font-title);
  font-weight: 300;
  font-size: 1.5rem;
  font-style: italic;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
}

/* -------------------------------------------------------
   Hero
   ------------------------------------------------------- */
.hero-section {
  position: sticky;
  top: 0;
  z-index: 0;
  height: 100vh;
  min-height: 600px;
  display: flex;
  align-items: center;
  overflow: hidden;
  background: var(--color-text);   /* encre : le fondu de .hero-bg part de là, pas du crème du body */
}

/* Tactile : le hero REDEVIENT un bloc normal. Le `sticky` est un effet de BUREAU — les sections
   glissent par-dessus une photo tenue au fond. Sur mobile il coute un calque plein ecran compose
   en permanence (le scroll saccade), et la hauteur de viewport changeant a chaque apparition de
   la barre d'URL iOS, Safari ne repeint pas toujours : la photo du hero reapparait alors
   PAR-DESSUS les sections. Pas de colle, pas de calque, pas d'artefact.
   Benefice collateral : `href="#hero"` redevenait une ancre fiable — un element colle est ancre
   a sa position COURANTE, pas a sa place dans le flux (cf. la fleche de retour en haut, qui est
   malgre tout reprise en JS pour valoir a toutes les largeurs). */
@media (pointer: coarse), (max-width: 900px) {
  .hero-section { position: relative; }
}

/* ── LE VOILE DU HERO ───────────────────────────────────────────────────────────────────────
   Sans lui, `.hero-bg` posait la photo et RIEN d'autre : titre, étiquette et liens du header
   s'affichaient en encre sombre (#1C1915) directement sur la photo. Ce n'était pas un réglage
   à ajuster, il n'existait pas.

   ⚠️ CE CSS NE DÉCIDE RIEN. `--hero-veil-alpha` est MESURÉE au build (generate.js échantillonne
   la photo servie, hero-veil-core calcule la plus petite opacité qui tient AA au pire point de
   chaque zone). Le CSS la LIT. Ne jamais recopier une valeur ici : deux sources pour une même
   opacité, c'est une opacité qui diverge — et celle-ci se recalcule à CHAQUE changement de photo.

   ⚠️ LES STOPS SONT UN CONTRAT ARITHMÉTIQUE. Les `ramp` déclarées dans render-core.js
   (HERO_VEIL) sont la lecture exacte de ces dégradés : la fraction de `--hero-veil-alpha`
   réellement présente au PIRE point de chaque zone mesurée. Déplacer un stop ci-dessous sans
   corriger la rampe produit un voile qui se croit conforme et ne l'est pas. Les deux bougent
   ensemble, ou pas du tout. `home-css.test.cjs` monte la garde.

   Repli 0,62 : la valeur n'est écrite que lorsqu'une variante a pu être mesurée. Un aperçu
   navigateur ou un build sans média rendrait sinon un hero sans voile — un texte illisible est
   un défaut plus grave qu'un voile un peu trop marqué. */
.hero-bg {
  position: absolute;
  inset: 0;
  --hero-veil-alpha: 0.62;
  --hero-ink: 28, 25, 21;                    /* --color-text, en composantes : rgba() a besoin des 3 */
  /* D3 — LE FOND SOUS LA PHOTO. Sans lui, `.hero-bg` et `.hero-section` n'ont aucune couleur
     propre : pendant le téléchargement de la variante entrante (au franchissement du point de
     bascule, ou au premier chargement), le fond visible est le #F7F4EF du `body` — un flash crème
     derrière un texte lui-même crème. L'encre du voile est le bon repli : c'est déjà la couleur
     que la photo portera une fois le fondu posé, et elle garde le texte lisible entre-temps.
     #26 ne supprime pas la bascule (elle passe de la largeur au ratio), donc D3 ne disparaît pas
     avec elle : il se corrige, et ça coûte une déclaration. */
  background-color: rgb(var(--hero-ink));
  /* Indirection VOLONTAIRE. Les dégradés lisent `--veil-a`, jamais `--hero-veil-alpha` directement.
     Raison : les opacités mesurées arrivent en style INLINE, qui bat toute règle de feuille — le
     bloc mobile ne pourrait donc pas basculer sur sa propre valeur en redéfinissant
     `--hero-veil-alpha`. Il redéfinit `--veil-a`, qui n'existe qu'ici : la bascule gagne sans
     `!important`. Même geste que les points focaux. */
  --veil-a: var(--hero-veil-alpha);

  /* Couche 1 — LA BANDE DU HEADER, présente sous toutes les formes de fondu sauf « aucun ».
     Les liens du header sont sur la photo tant que `.scrolled` n'est pas posé ; ils se mesurent
     donc comme le texte (zone `header`, y ∈ [0 ; 0,09]) et il leur faut un fond. Plein jusqu'à
     9 %, éteint à 26 % → rampe 1 sur toute la zone mesurée. */
  --hero-veil-header: linear-gradient(to bottom,
      rgba(var(--hero-ink), var(--veil-a)) 0%,
      rgba(var(--hero-ink), var(--veil-a)) 9%,
      rgba(var(--hero-ink), 0) 26%);

  /* Couche 2 — LA FORME, choisie par `data-veil` (ci-dessous). `none` par défaut : une forme
     inconnue ne doit pas produire un dégradé au hasard. */
  --hero-veil-form: none;

  /* ⚠️ PAS d'`image-set` ICI, et c'est un correctif. `image-set` arbitre sur le DPR, JAMAIS sur
     la largeur : sur un écran de 1440 px à DPR 1 — un portable non-Retina, un aperçu dans une
     iframe transformée — le navigateur prend le candidat `1x`. Avec `image-set(sm 1x, lg 2x)`
     c'était donc la variante MOBILE (1400 px, 95 Ko, qualité 85) qui était étirée sur tout le
     hero : grain visible dans le ciel, exactement le défaut remonté. Le choix se fait par
     MEDIA-QUERY (la largeur), qui est la vraie question posée. Le bloc mobile plus bas sert la
     petite variante ; ici, toujours la grande — jamais d'agrandissement, c'était tout l'objet. */
  background-image:
    var(--hero-veil-form),
    var(--hero-veil-header),
    var(--hero-src-lg);
  /* Les deux voiles remplissent exactement la boîte → leur `background-position` n'a aucun effet,
     ce qui laisse l'attribut inline (le point focal, écrit par le build) piloter la seule couche
     qui s'en soucie : la photo. */
  background-size: 100% 100%, 100% 100%, cover;
  background-repeat: no-repeat;
  /* Les deux voiles remplissent leur boîte (100% 100%) : leur position est sans effet. Seule la
     photo lit le point focal, posé en variable par le build. Aucun `background-position` inline
     n'existe plus sur `.hero-bg` — c'est ce qui permet à la surcharge mobile ci-dessous de gagner
     SANS `!important`. */
  background-position: 0 0, 0 0, var(--hero-fp, center);
}

/* Une seule source disponible (chemin legacy du repo, aucune variante `cover`) : le build écrit
   la même URL dans les deux variables. Rien à écrire ici — la dégradation est dans la donnée,
   pas dans une règle de repli. */

/* ── Les trois formes (§3.2). Le preset choisit la FORME, la mesure choisit la FORCE. ────────
   « Fondu bas » — dégradé vertical plein derrière le texte, qui s'efface AUX DEUX BOUTS. Le bloc
   de texte est centré verticalement (y ∈ [0,32 ; 0,68]) : il est donc entièrement dans la partie
   pleine → rampe texte = 1. Transparent avant 12 %, il ne s'empile pas avec la bande du header ;
   transparent après 72 %, il RE-DONNE le dernier quart de la photo, qui ne porte aucun texte —
   assombrir du sable et des reflets pour rien, c'est payer le voile sans rien lui demander. */
.hero-bg[data-veil="fondu-bas"] {
  --hero-veil-form: linear-gradient(to bottom,
      rgba(var(--hero-ink), 0) 12%,
      rgba(var(--hero-ink), var(--veil-a)) 30%,
      rgba(var(--hero-ink), var(--veil-a)) 72%,
      rgba(var(--hero-ink), 0) 100%);
}

/* « Fondu latéral » — pour un sujet situé du côté OPPOSÉ au texte. Plein jusqu'à 58 %, éteint à
   88 %. Le texte s'arrête à x = 0,68 : il n'y reçoit plus que (0,88−0,68)/(0,88−0,58) = 2/3 de
   l'opacité nominale, ce que la rampe déclarée dit exactement.
   Il recouvre la bande du header sur la moitié gauche : deux calques de MÊME couleur s'empilent
   en 1−(1−α)², soit PLUS d'opacité, jamais moins. Le calcul reste donc conservateur. */
.hero-bg[data-veil="fondu-cote"] {
  --hero-veil-form: linear-gradient(to right,
      rgba(var(--hero-ink), var(--veil-a)) 0%,
      rgba(var(--hero-ink), var(--veil-a)) 58%,
      rgba(var(--hero-ink), 0) 88%);
}

/* « Sans fondu » — rien, pas même la bande du header. C'est une vraie option, pour une photo
   déjà sombre : la mesure la CONFIRME (verdict vert) ou ALERTE (verdict rouge dans l'éditeur et
   avertissement au build). Elle ne la rattrape pas en douce. */
.hero-bg[data-veil="aucun"] {
  --hero-veil-form: none;
  --hero-veil-header: none;
}

/* ⚠️ CE BLOC DOIT RESTER APRÈS `.hero-bg` ET APRÈS LES TROIS `[data-veil]`.
   Une media-query n'ajoute AUCUNE spécificité : à sélecteur égal, c'est la règle la PLUS BAS
   dans le fichier qui gagne. Ces surcharges vivaient d'abord dans le bloc `@media` du hero,
   posé une cinquantaine de lignes PLUS HAUT que `.hero-bg` — elles étaient donc intégralement
   écrasées, en silence : cadrage mobile ignoré, opacité mobile ignorée, et le téléphone qui
   téléchargeait quand même la grande variante. Rien ne le signalait, la page s'affichait.
   `home-css.test.cjs` vérifie désormais l'ORDRE, pas seulement la présence.

   ⚠️ LA CONDITION EST UN RATIO, PAS UNE LARGEUR, ET SURTOUT PAS `pointer: coarse`.
   La question posée ici est « la boîte du hero est-elle plus étroite que le crop servi ? » —
   c'est-à-dire la FORME du viewport, jamais le tactile ni la largeur. `pointer: coarse` est vrai
   pour toute surface tactile quelle que soit son ORIENTATION : une tablette EN PAYSAGE y
   recevrait un crop portrait, soit un crop de crop, et un agrandissement de ×1,33 sur des
   appareils qui vont très bien aujourd'hui. Une largeur ne dit rien non plus de la forme.
   `4/5` est la LECTURE EXACTE du ratio de la recette `heroportrait` (image-pipeline.js) : c'est
   précisément le point au-delà duquel ce serait la LARGEUR qui contraindrait, et où le crop
   perdrait donc de la matière pour rien. `home-css.test.cjs` garde les deux alignés.
   Le seuil hamburger (1000 px) n'est pas concerné : ceci n'est plus une règle de largeur. */
@media (max-aspect-ratio: 4/5) {
  /* Ne JAMAIS servir la grande variante ici : elle est PAYSAGE, et `height: 100vh` + `cover` sur
     un viewport portrait fait contraindre la HAUTEUR — la grande variante y agrandit encore
     (×1,06 à ×1,31 mesuré). La variante `heroportrait` (1920×2400) est recadrée pour cette forme.
     La condition est recopiée à l'identique dans les `media` du `<link rel="preload">`
     (render-core.js, tplPreload) : précharger une source que le CSS n'utilisera pas, c'est payer
     deux fois la ressource la plus critique de la page. */
  .hero-bg {
    background-image:
      var(--hero-veil-form),
      var(--hero-veil-header),
      var(--hero-src-sm);
    /* Le voile MOBILE a sa propre opacité, mesurée sur son propre cadrage : `cover` ne montre pas
       la même part de la photo en portrait qu'en paysage, et le mobile a en plus son propre point
       focal. Repli sur la valeur bureau si le build n'a pas pu mesurer le second cadrage. */
    --veil-a: var(--hero-veil-alpha-m, var(--hero-veil-alpha));

    /* Cadrage MOBILE dédié — LA CAUSE DU §3.7. `--hero-fp-m` était émis par le build depuis le
       port du hero et n'avait AUCUN consommateur dans le CSS de JMP : le point focal mobile
       proposé par l'éditeur était un champ sans effet, et l'aperçu mobile montrait le cadrage
       DESKTOP. Le mécanisme d'aperçu (APERCU_VIEWPORTS / layoutApercuFrame / DEVICE_ICONS) n'y
       était pour rien — il est identique à JMB au caractère près. C'est la règle CI-DESSOUS qui
       manquait au fork ; chez JMB elle vit dans `@media (max-width: 768px)`.
       Repli en cascade : pas de point mobile ⇒ point desktop ⇒ centre. */
    background-position: 0 0, 0 0, var(--hero-fp-m, var(--hero-fp, center));
  }
}

/* Le bloc de texte du hero est calé à GAUCHE, pas centré dans une colonne de 1200 px.
   Raison : la photo du hero a son sujet au centre-droit ; un bloc centré fait courir la
   deuxième ligne du titre par-dessus lui. Le retrait est proportionnel (7vw) et BORNÉ aux deux
   bouts — sans le clamp, un très large écran éloignerait le texte du bord et un téléphone l'y
   collerait. Le résultat tient le même retrait relatif (~7 %) de 390 px à 2560 px.
   ⚠️ Ce retrait est la moitié d'un contrat : la zone MESURÉE du voile (HERO_VEIL.presets[].
   zones.text, render-core.js) décalque ce bloc. Déplacer le texte sans déplacer la zone ferait
   mesurer le contraste là où il n'y a plus de lettres. Les deux bougent ensemble. */
.hero-content {
  position: relative;
  z-index: 1;
  padding-left: clamp(var(--gutter), 7vw, 10rem);
  padding-right: var(--gutter);
  max-width: none;
  width: 100%;
}

.hero-tag {
  display: block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  /* Crème, pas terre cuite. `--color-accent` (#8A5835) sur une photo n'est pas récupérable :
     même voilée, la photo reste un fond de luminance variable, et l'accent n'y tient AA nulle
     part. L'accent reste ce qu'il est — une couleur de surface claire. */
  color: var(--color-bg);
  margin-bottom: 1.5rem;
}

.hero-title {
  font-family: var(--font-title);
  font-size: var(--hero-title-size);
  font-weight: 300;
  line-height: 1;
  color: var(--color-bg);
  display: flex;
  flex-direction: column;
  text-align: var(--hero-title-align);
  margin-bottom: 2.5rem;
}

/* Masque vertical — le conteneur est générique, l'ÉTAT INITIAL ne l'est pas.
   `transform: translateY(100%)` est l'état de départ d'une animation : posé sur `.word-inner`
   nu, il s'applique à tout réemploi du markup, y compris à des blocs dont aucun JS ne remet le
   transform à zéro. C'est exactement ce qui est arrivé aux 3 citations de témoignages entre P1
   et P2 : décalées de 46 px, superposées à leur ligne d'attribution.
   L'état initial est donc scopé au contexte que `js/site.js` pilote réellement. */
.word-mask   { overflow: hidden; display: block; }
.word-inner  { display: block; }
/* #28 — l'entrée du hero est pilotée par le CSS : elle démarre au premier paint et ne dépend plus
   de `js/site.js` (defer, ~350 ms de latence mesurée). Si le JS échoue, le titre n'est plus masqué
   à vie. Natif avant JS, un seul mécanisme par geste. */
@keyframes heroMot {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
@keyframes heroPhoto {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes heroCta {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Entrée du hero — BUREAU SEULEMENT. Les animations ne sont DÉCLARÉES que sous cette condition :
   sur mobile il n'y a rien à désactiver, donc aucun ordre de règles ne peut les ressusciter (mesuré
   le 19/09 : un « animation: none » posé AVANT des règles de même spécificité était écrasé, et le
   bouton restait invisible 1,2 s). Sur mobile, le <h1> entier et le CTA sont peints au premier
   paint, sans JS.
   RYTHME sans dépendance au contenu : `renderH1` émet la 1re ligne en `.word-mask` nu et les
   suivantes en `.word-mask.italic-mask`. Compter les `span` (nth-of-type) comptait le sur-titre.
   #37 — UNE FOIS PAR SESSION. Chaque déclaration porte `html:not([data-hero-vu])` : un script inline du
   <head> de index.html pose `data-hero-vu` quand l'entrée a déjà joué dans la session (sessionStorage),
   AVANT la 1re frame. Signal présent = rien n'est déclaré, l'état final est l'état par défaut : pas de
   `animation: none` à faire gagner sur la spécificité. Un flag lu par js/site.js (defer) arriverait après
   le premier paint et laisserait les @keyframes peindre l'état de départ.
   ⚠️ Ces sélecteurs sont ÉNUMÉRÉS dans home-css.test.cjs (HERO_ENTREE), et chacun doit être
   neutralisé par `animation: none !important` sous reduced-motion ET couvert par
   `body.editor-mode *`. La garde SUIT LE MÉCANISME animé : c'est `animation`. */
@media (pointer: fine) and (min-width: 901px) {
  html:not([data-hero-vu]) .hero-bg                                              { animation: heroPhoto 0.9s linear both; }
  html:not([data-hero-vu]) .hero-title .word-inner                               { animation: heroMot 1.1s cubic-bezier(0.16, 1, 0.3, 1) both; }
  html:not([data-hero-vu]) .hero-title .word-mask:not(.italic-mask) .word-inner  { animation-delay: 0.4s; }
  html:not([data-hero-vu]) .hero-title .italic-mask .word-inner                  { animation-delay: 0.6s; }
  html:not([data-hero-vu]) .hero-title .hero-tag                                 { animation: heroCta 0.9s cubic-bezier(0.16, 1, 0.3, 1) 1.05s both; }
  html:not([data-hero-vu]) .hero-section .hero-cta                               { animation: heroCta 0.9s cubic-bezier(0.16, 1, 0.3, 1) 1.2s both; }
}
/* P7, 2026-09-17 — `.hero-tag` est le sur-titre du <h1> et `renderH1` l'émet sur toute page qui a
   un `subtitle` : l'accueil, le hub /portfolio/ et les cinq univers. Aucun état de départ ne vit sur
   la classe nue, ni statique nulle part : l'unique état masqué est le `from` de @keyframes heroCta,
   déclenché par le rendu, sous la condition bureau, et il se termine visible sans JS. Ailleurs il
   rendait un sur-titre présent dans le DOM (donc lu par Google et par l'audit de l'éditeur) et
   invisible pour un humain. Il a mordu trois fois avant d'être scopé. */
.italic-mask .word-inner {
  font-style: italic;
  padding-left: 2.5rem;
}


.btn-outline {
  display: inline-block;
  position: relative;
  overflow: hidden;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  padding: 0.9rem 2.2rem;
  border: 1px solid var(--color-text);
  color: var(--color-text);
  transition: color 0.35s ease 0.12s, border-color 0.35s ease 0.12s;
  z-index: 0;
}
.btn-outline::before { display: none; }
.btn-outline::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-accent);
  clip-path: circle(0% at 50% 50%);
  transition: clip-path 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: -1;
}
.btn-outline:hover::after  { clip-path: circle(150% at 50% 50%); }
.btn-outline:hover { color: var(--color-bg); border-color: transparent; }

/* ── Le bouton DU HERO (§3.6) — la forme de `.form-submit`, sur la photo ─────────────────────
   ⚠️ SCOPÉ À `.hero-cta`, et c'est délibéré. `.btn-outline` sert aussi /tarifs/, /a-propos/ et
   /contact/, où il est posé sur du CRÈME : lui donner un libellé crème globalement le rendrait
   invisible sur trois pages. Le commentaire de bas de fichier qui affirme que `.btn-outline`
   « reste au HERO, et à lui seul » décrit une intention, pas le HTML servi — vérifié.

   Une seule différence assumée avec `.form-submit`, demandée explicitement : le disque de
   remplissage est en `--color-accent` (#8A5835, le brun du hero) et non en `--color-accent-dark`
   (réservé au formulaire) ; et le libellé RESTE crème au survol au lieu de passer en encre. */
.hero-cta .btn-outline {
  /* Bordure en crème, pas en --color-accent-dark (#C08A63) : sur le sable du hero, le beige
     ressort peu et tombe sous le seuil non-texte WCAG 1.4.11 (mesuré 1,72:1 sur la photo voilée).
     Le crème est la MÊME paire fg/photo que le titre, qui tient déjà l'AA texte (≥ 4,5:1, plus
     strict) sur la même zone — le 3:1 non-texte est donc garanti par construction, sans remesure. */
  border-color: var(--color-bg);
  color: var(--color-bg);
}
.hero-cta .btn-outline::after { background: var(--color-accent); }
.hero-cta .btn-outline:hover  { color: var(--color-bg); border-color: transparent; }

.scroll-down {
  position: absolute;
  bottom: 2.5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  opacity: 0;
}
.scroll-arrow {
  display: block;
  width: 1px;
  height: 50px;
  /* Sur le hero elle est posée sur la photo voilée, comme le titre : encre crème. */
  background: linear-gradient(to bottom, transparent, var(--color-bg));
  margin: 0 auto;
  animation: scrollPulse 2s ease-in-out infinite;
}
/* #140 D6 — la pulsation est infinie : elle ne tourne pas quand le hero est recouvert (bureau, hero
   collé) ou sorti (mobile). `is-hors-ecran` est posé par js/site.js (observateur, aucun JS par frame). */
.scroll-down.is-hors-ecran .scroll-arrow { animation-play-state: paused; }
@keyframes scrollPulse {
  0%, 100% { opacity: 0.4; transform: scaleY(1); }
  50%       { opacity: 0.9; transform: scaleY(1.2); }
}

/* -------------------------------------------------------
   Portfolio — 5 catégories
   ------------------------------------------------------- */
/* Sections post-hero : passent par-dessus le hero sticky.
   Le hero est `position: sticky; top: 0; z-index: 0` — une section sans fond opaque laisse voir
   la photo au travers et son texte devient illisible. Toute section ajoutée à <main> entre ICI. */
.categories-section,
.reportages-section,
.collections-section,
.about-section,
.temoignages-section,
.site-footer {
  position: relative;
  z-index: 1;
}

/* ── SAFARI : le z-index ne suffit pas, il faut un CALQUE ────────────────────────────────────
   Défaut MESURÉ sur Safari 26 / macOS le 29/08/26, page servie : le hero et son voile se
   peignent PAR-DESSUS les sections pendant le défilement — jusqu'à 99 % de l'écran couvert,
   mesuré en peignant le hero d'une couleur repère et en comptant les pixels. Invisible sur
   Chrome, Firefox et en tactile.

   ⚠️ CE N'EST PAS UN PROBLÈME DE Z-INDEX, et c'est ce qui a coûté le plus cher à trouver :
   monter ces sections à `z-index: 10` ne change RIEN (99 % de couverture, identique au témoin).
   WebKit promeut le sticky en calque composé et ne le compare plus aux sections restées dans le
   calque de défilement — l'ordre z n'est simplement plus consulté. Neuf variantes posées sur le
   hero lui-même (overflow, contain, backface-visibility, calque du voile, overflow-x du body)
   ont toutes échoué pour cette raison : elles corrigeaient le mauvais élément.
   Ont échoué aussi, mesurés : `contain: paint`, `contain: layout`, `backface-visibility`,
   `isolation: isolate`. Seule une PROMOTION RÉELLE en calque de composition remet l'ordre.

   ⚠️ TOUTES les sections doivent y etre, sans exception. `.collections-section` en a ete
   exclue un temps parce qu'elle contient les panneaux `.coll-drawer` en `position: fixed`,
   qu'un ancetre transforme piege (mesure : 1252 px de haut au lieu des 848 px du viewport,
   decales de 47 px). Ce trou se payait comptant : le hero traversait cette seule section,
   surtout EN REMONTEE — 99 % de l'ecran couvert a y~3000 et y~3500, alors que la descente
   etait propre. Une mesure qui ne remonte pas ne prouve rien.
   Les panneaux sont donc SORTIS de <main> par `js/site.js` au chargement, comme la galerie et
   la lightbox le sont deja dans le HTML. Avant d'ajouter une section ici, verifier qu'elle ne
   contient AUCUN `position: fixed` — ou le sortir de <main> de la meme facon. */
.categories-section,
.reportages-section,
.collections-section,
.about-section,
.temoignages-section,
.site-footer {
  transform: translateZ(0);
}

/* ── Couture inter-calques — un fond D'UN SEUL TENANT sous les sections ────────────────────
   Constat sur FIREFOX (bureau) : une ligne d'environ un pixel sous le hero, à la jointure de
   deux calques, où l'on voit l'ENCRE du hero sticky sur fond crème. Les sections portent
   pourtant chacune un fond opaque et sont jointives — le trou n'est pas dans la mise en page,
   il est dans la RASTÉRISATION : promues en calques composés séparés (bloc ci-dessus), elles
   arrondissent leurs bords chacune de son côté, et Gecko laisse par moments une ligne non
   peinte entre deux voisines. Même défaut, même géométrie que JMB, recette portée de JMB
   `481e4a7`.

   REFUSÉ — élargir une section d'un pixel. C'est une couture d'UN PIXEL PHYSIQUE : elle ne
   grandit pas avec la page. Un colmatage calibré pour un facteur de zoom serait faux à tous
   les autres.

   Le remède ne bouche pas les jointures, il supprime CE QU'ELLES LAISSENT VOIR : une seule
   surface opaque, continue, posée entre le hero et les sections. N'ayant aucune jointure
   interne, elle ne peut pas s'ouvrir. Propre à JMP : les sections alternent `--color-bg` et
   `--color-surface` ; une couture future y montrerait donc une ligne `--color-bg` entre deux
   sections `--color-surface` — du fond, plus jamais l'encre du hero.

   `top` suit la hauteur du hero — `height: 100vh` bornée par `min-height: 600px`, donc
   `max(100vh, 600px)`. LES DEUX VALEURS DOIVENT RESTER D'ACCORD : un hero redimensionné sans
   ce calque masquerait son propre bas, ou découvrirait le haut de page. `home-css.test.cjs`
   lit les deux et sort en erreur si elles divergent. Sur mobile le hero n'est plus sticky,
   les sections le suivent dans le flux : le calque commence là où il finit, sans effet.

   Le débordement bas est FRANC, jamais calibré : il ferme la jointure </main> ↔ <footer> sans
   dépendre d'un arrondi, et reste sous le footer (plus haut que lui, peint après), donc il
   n'allonge pas la page. `pointer-events: none` n'est pas décoratif : cette surface couvre
   toute la page sous le hero, et `C16` rappelle que le rendu et l'atteignabilité sont deux
   mesures — rien de cliquable ne doit passer derrière elle. */
main:has(> .hero-section) { position: relative; }
main:has(> .hero-section)::before {
  content: '';
  position: absolute;
  top: max(100vh, 600px);
  left: 0;
  right: 0;
  bottom: -40px;
  background: var(--color-bg);
  z-index: 1;
  pointer-events: none;
  transform: translateZ(0);
}
/* Seule jointure surface|surface de l'accueil : la couture y montrait le calque `--color-bg` en
   ligne claire (vu sur Firefox Retina). La section du dessous RECOUVRE la couture avec marge —
   8 px CSS ≥ 1 px physique à tout zoom — et son bord se pose sur la même couleur. Ce n'est PAS le
   « +1 px » refusé ci-dessus, qui bouchait au pixel et restait calibré pour un zoom. Un
   `box-shadow` n'est pas une cible de clic (`C16`). Garde générique : `home-css.test.cjs`. */
.about-section + .temoignages-section { box-shadow: 0 -8px 0 var(--color-surface); }
.categories-section  { background: var(--color-bg); }
.reportages-section  { background: var(--color-surface); }
.collections-section { background: var(--color-bg); }
.temoignages-section { background: var(--color-surface); }

.categories-section { padding: 6rem 0; }

.categories-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
  padding: 0 var(--gutter);
  max-width: var(--max-w);
  margin: 0 auto;
}

.categories-row-2 {
  grid-column: 1 / -1;
  display: flex;
  justify-content: center;
  gap: 1.5rem;
}
/* Cette largeur ne vaut QUE sur bureau, ou la grille fait 3 colonnes et ou la rangee de 2 se
   centre dessous. Posee en regle de base, elle survivait au `display: contents` de la bande
   sous 1000 px : les deux dernieres cartes gardaient 33 % de leur cellule et se retrouvaient
   trois fois plus petites que les trois premieres. Le defaut etait invisible sur bureau et
   sautait aux yeux sur tablette et telephone — d'ou son passage en `min-width`. */
@media (min-width: 1001px) {
  .categories-row-2 .category-card { width: calc(33.333% - 0.5rem); }
}

.category-card {
  overflow: hidden;
  cursor: pointer;
}

/* Le clip d'entrée vit ICI, sur le VOLET, et surtout PAS sur `.category-card` : c'est la carte
   qui est passée à `io.observe()`, et Chrome rogne le rectangle d'un élément observé par son
   PROPRE `clip-path`. Quand la carte portait le clip, son `intersectionRatio` valait 0 en
   permanence (mesuré le 29/08/26 sur la prod : ratio 0 avec le clip, 1 sans, MÊME rectangle
   341×512) : les cinq cartes du portfolio restaient blanches pour tout visiteur Chrome, sans
   erreur console. Firefox ignore ce clip et révélait normalement — c'est ce qui a fait vivre le
   défaut en production, le poste de travail étant sous Firefox.
   RÈGLE : l'élément OBSERVÉ ne doit pas être l'élément CLIPPÉ.
   Le volet couvre exactement la boîte de la carte (`width: 100%`, et le reset global annule le
   padding UA du bouton) : déplacer le clip ne change pas le geste d'un pixel.
   #140 D1 — le volet est OUVERT par défaut. L'état fermé n'existe que sous `.category-card.is-armed`,
   que SEUL js/site.js pose (carte sous le pli au moment d'armer) : sans JS, sous mouvement réduit,
   dans l'éditeur, les cinq photos sont visibles (C9 ext., C143). Aucune transition sur l'état armé :
   armer ne fait rien glisser ; l'ouverture porte la sienne, à l'arrivée.
   #140 Lot 2 (M2) — DEUX variantes du même volet, choisies par le JS et ÉCRITES sur la carte
   (`is-doigt`) : la feuille ne les redécide jamais par un média (un hybride mettrait JS et feuille
   en désaccord, C9).
   · pointeur fin : le volet est CLIPPÉ ; le script pose en ligne la transition et le clip ouvert ;
   · au doigt : AUCUN `clip-path` (non composé, repeint pendant le défilement — REFUSÉ, Q5 hub). Un
     calque de couverture, couleur du fond, recouvre le volet et se retire vers le HAUT sous
     l'`overflow: hidden` de la carte : la photo se découvre du bas vers le haut, comme au bureau, et
     seul un `transform` bouge. L'horloge vit sur l'état d'ARRIVÉE (`is-ouvert`, C143) ; le volet fini,
     le script désarme : le calque n'existe plus. Aucun `will-change` : rien ne reste promu.
   Les filets editor-mode et reduced-motion couvrent les deux variantes en `!important` : le préfixe
   d'armement bat une neutralisation nue (C90). */
.card-btn { width: 100%; display: block; }
.category-card.is-armed:not(.is-doigt) .card-btn { clip-path: inset(100% 0 0 0); }
.category-card.is-armed.is-doigt .card-btn { position: relative; }
.category-card.is-armed.is-doigt .card-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-bg);
}
.category-card.is-armed.is-doigt.is-ouvert .card-btn::after {
  transform: translateY(-100%);
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

.card-image {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
}
.card-image img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.6s ease;
}
.category-card:hover .card-image img { transform: scale(1.04); }

.card-overlay {
  position: absolute;
  inset: 0;
  background: rgba(28, 25, 21, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity var(--transition);
}
.category-card:hover .card-overlay { opacity: 1; }

.card-view {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: #F7F4EF;
  border: 1px solid rgba(247, 244, 239, 0.6);
  padding: 0.6rem 1.2rem;
}

.card-info { padding: 1.25rem 0 0.5rem; }
.category-name {
  font-family: var(--font-title);
  font-size: 1.4rem;
  font-style: italic;
  font-weight: 400;
  margin-bottom: 0.25rem;
}
.category-label {
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-accent);
}

/* -------------------------------------------------------
   À propos
   ------------------------------------------------------- */
.about-section {
  padding: 6rem 0 4rem;
  background: var(--color-surface);
}

/* #103 ②b — mise en page v3 (maquette validée par Luca le 27/09). Deux lignes de force : la ligne
   de gauche de la page porte la photo (puis « Paroles » juste en dessous), la ligne de la colonne
   texte porte le titre, l'ouverture et le bouton. Le titre occupe la rangée du dessus, colonne texte
   seule : le haut de la photo tombe ainsi sur la PREMIÈRE LIGNE DE L'OUVERTURE — c'est le décalage
   photo/texte, réglé sur le texte au lieu de l'ancienne marge fixe `clamp(0rem, 6vw, 4.5rem)`.
   La photo ne prend plus la colonne entière : `clamp(16rem, 24vw, 22rem)`, calée à gauche.
   Même côté et même ordre de lecture à toutes les largeurs (variante B REFUSÉE, decisions 27/09).
   Pas d'état initial d'animation ici : aucun reveal n'est câblé sur ce bloc. */
.about-inner {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  grid-template-areas: ". intro" "photo body";
  column-gap: clamp(2rem, 5vw, 4.5rem);
  row-gap: 0;
  align-items: start;
  padding: 0 var(--gutter);
  max-width: var(--max-w);
  margin: 0 auto;
}
/* `padding: 0` + marge sans `auto` : dans la colonne texte, le `margin: 0 auto` de `.section-intro`
   centrait le H2 « Joyce Martens » au lieu de l'aligner sur le texte (constat de l'étape ①). */
.about-inner > .section-intro {
  grid-area: intro;
  padding: 0;
  margin: 0 0 2.5rem;
  max-width: none;
}
.about-figure {
  grid-area: photo;
  justify-self: start;
  width: clamp(16rem, 24vw, 22rem);
}
.about-photo-wrap { overflow: hidden; }
.about-photo {
  width: 100%;
  aspect-ratio: 4/5;
  object-fit: cover;
}
/* Légende facultative (`about.caption`) : le filet vertical des témoignages, la même attribution. */
.about-caption {
  border-left: 1px solid var(--color-accent);
  padding-left: 0.85rem;
  margin-top: 1.1rem;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-muted-text);
}

.about-corps {
  grid-area: body;
  max-width: 34rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

/* -------------------------------------------------------
   Galerie parallax (calque plein écran, hors flux)
   Piste horizontale · parallax interne par photo · pilotée par js/site.js
   ------------------------------------------------------- */
.lb-gallery {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--color-bg);
  display: flex;
  align-items: center;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
}
.lb-gallery.is-open { opacity: 1; pointer-events: auto; }

.lb-gallery-topbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 5;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.5rem var(--gutter);
}
.lb-gallery-label {
  font-family: var(--font-title);
  font-weight: 300;
  font-size: 1.6rem;
  font-style: italic;
  clip-path: inset(0 100% 0 0);
}
.lb-gallery.is-open .lb-gallery-label {
  clip-path: inset(0 0% 0 0);
  transition: clip-path 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.25s;
}
.lb-gallery-close {
  font-size: 1.1rem;
  color: var(--color-text);
  opacity: 0.55;
  transition: opacity var(--transition);
}
.lb-gallery-close:hover { opacity: 1; }

/* « Voir tout » de la catégorie ouverte — il a REMPLACÉ l'invite de défilement (`.lb-hint`) le
   16/09/2026 : même place, même casse, même couleur, même apparition différée à l'ouverture.
   Un seul écart, volontaire : plus de `pointer-events: none` — c'est un LIEN, il doit se cliquer.
   Il vit dans `.lb-gallery` et surtout PAS dans `.lb-track` : la piste est une géométrie mesurée
   (cf. la réserve de fin documentée juste en dessous), où un enfant de plus déplacerait la butée.
   Son `href` est posé par js/site.js depuis `data-univers-href`, et lui seul décide s'il existe. */
.lb-voir-tout {
  position: absolute;
  bottom: 1.75rem; left: 50%;
  transform: translateX(-50%);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-muted);
  opacity: 0;
  transition: color var(--transition);
}
.lb-voir-tout:hover { color: var(--color-text); }
.lb-gallery.is-open .lb-voir-tout {
  opacity: 1;
  transition: opacity 0.6s ease 0.9s, color var(--transition);
}

/* Piste : mouvement via translate3d posé en JS (transform inline).

   ⚠️ LA RÉSERVE DE FIN (le `38vw` de droite) NE TIENT QUE PARCE QUE CETTE BOÎTE EST LIBRE.
   Mesuré le 03/09/2026 sur la page servie, Chrome headed 1440×900 ET Firefox 155 :
       scrollWidth = 3312,0 px = padL 547,2 + Σ6 photos 2073,6 + 5×gap 28,8 + padR 547,2
       largeur de la boîte (rect) = 3311,9 px   ·   ecart_butee en butée = 0,1 px
   `.lb-track` est un flex-item de `.lb-gallery`, SANS conteneur intermédiaire : sa taille
   minimale automatique (`min-width: auto` d'un flex-item) l'empêche de se comprimer sous sa
   largeur min-content. Sa boîte fait donc 3312 px, la réserve de fin vit DEDANS, et
   `scrollWidth` — qui vaut au moins la largeur de la boîte de padding — la compte.

   Chez l'île voisine (JMB) la même piste vit dans un wrapper `overflow: hidden` : sa boîte y
   est contrainte, le padding de fin tombe dans l'aire de débordement, et Blink en EXCLUT toute
   boîte d'aire nulle — la dernière photo n'y atteignait jamais le centre, il a fallu déplacer
   la réserve dans un flex-item `::after`. NE PAS PORTER CE REMÈDE ICI : il n'y a rien à
   corriger, et la même sonde l'a vérifié à l'endroit (`::after` posé → +547 px, réserve
   comptée DEUX fois).
   Ce qui importerait le défaut, mesuré en le réintroduisant sur cette page le 03/09/2026 :
       `#lb-track { min-width: 0 }`            → scrollWidth 3312 → 2765 px   (−547)
       `#lb-track { width: 100%; min-width:0 }` → scrollWidth 3312 → 2765 px   (−547)
       un wrapper `overflow: hidden` autour de la piste (structure JMB)
   Les quatre formes sont interdites par `reserve-fin-piste.test.cjs`, et les deux instruments
   qui ont produit ces chiffres sont versionnés (`scripts/chrome/mesure-piste.mjs`,
   `scripts/firefox/sonde-reserve.mjs`). */
.lb-track {
  display: flex;
  align-items: center;
  gap: 2vw;
  padding: 0 38vw;
  will-change: transform;
  cursor: grab;
}
.lb-track:active { cursor: grabbing; }

.lb-photo {
  position: relative;
  flex: 0 0 auto;
  width: 24vw;
  height: 70vh;
  overflow: hidden;
  cursor: pointer;
  opacity: 0;
  transform: translateX(60px);
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.lb-photo.is-entered { opacity: 1; transform: none; }

/* Image 20 % plus large que le cadre → 10 % de mou de chaque côté pour le parallax. */
.lb-photo-inner {
  position: absolute;
  top: 0; left: -10%;
  width: 120%; height: 100%;
  will-change: transform;
}
.lb-photo-inner img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Réglage validé : ne pas hijacker la molette si l'utilisateur réduit les animations.
   ⚠️ `overflow-x: auto` fait de `.lb-gallery` un CONTENEUR DE DÉFILEMENT — et un enfant
   `position: absolute` s'y cale alors sur la zone SCROLLABLE (2448 px mesurés), pas sur la part
   visible : la croix de fermeture dérivait vers le centre et l'invite « défiler » sortait du
   champ par la gauche dès qu'on faisait défiler. Les deux commandes passent donc en `fixed` :
   `.lb-gallery` étant elle-même `position: fixed; inset: 0`, le viewport et sa boîte coïncident
   — même placement à l'œil, mais désolidarisé du défilement. */
@media (prefers-reduced-motion: reduce) {
  .lb-gallery { overflow-x: auto; }
  .lb-track { transform: none !important; padding: 0 8vw; }
  .lb-photo { opacity: 1; transform: none; transition: none; }
  .lb-photo-inner { transform: none !important; }
  .lb-gallery-label { clip-path: none; }
  .lb-gallery-topbar { position: fixed; }
  .lb-voir-tout { position: fixed; }
}

/* MÊME repli, second déclencheur : l'APERÇU de l'éditeur. `js/site.js` y bascule déjà sur son
   chemin « mouvement réduit » (window.JMB_PREVIEW → `reduce`), qui coupe le RAF ET le
   détournement de la molette. Mais ce drapeau est du JS : la media-query ci-dessus, elle, ne
   matche pas — la piste restait donc en `overflow: hidden` sans personne pour la déplacer.
   Mesuré avant correction : 0 px de déplacement après 30 crans de molette, 2 photos sur 6
   atteignables. Les deux moitiés d'un même repli doivent écouter le MÊME signal ; un test de
   parité (home-css.test.cjs) interdit aux deux listes de diverger. */
:root[data-jmb-preview="true"] .lb-gallery { overflow-x: auto; }
:root[data-jmb-preview="true"] .lb-track { transform: none !important; padding: 0 8vw; }
:root[data-jmb-preview="true"] .lb-photo { opacity: 1; transform: none; transition: none; }
:root[data-jmb-preview="true"] .lb-photo-inner { transform: none !important; }
:root[data-jmb-preview="true"] .lb-gallery-label { clip-path: none; }
:root[data-jmb-preview="true"] .lb-gallery-topbar { position: fixed; }
:root[data-jmb-preview="true"] .lb-voir-tout { position: fixed; }

/* Tactile : la galerie immersive n'existe pas (doctrine perf iOS). Le tap carte → lightbox. */
@media (pointer: coarse) {
  .lb-gallery { display: none; }
}
/* En APERÇU, c'est l'appareil VISÉ qui décide de son existence, jamais le pointeur de l'HÔTE.
   L'éditeur ouvert sur un téléphone fait matcher la media-query ci-dessus DANS l'iframe, y
   compris quand on prévisualise le bureau : le JS ouvrait alors une galerie que la feuille
   masquait, laissant un calque invisible et `overflow: hidden` sur le body — aperçu figé, plus
   rien de cliquable. Réciproquement, prévisualiser une tablette depuis un Mac doit MASQUER la
   galerie que le pointeur fin laisserait passer. La surcharge joue donc DANS LES DEUX SENS.
   `estTactile()` (js/site.js) lit le même attribut : un seul signal, deux lecteurs.
   Une media-query n'ajoute AUCUNE spécificité : ce qui fait gagner l'attribut, c'est (0,2,0)
   contre (0,1,0) — pas sa position. Il est quand même placé après, pour que les deux raisons
   tiennent. */
:root[data-jmb-preview-pointer="fine"] .lb-gallery { display: flex; }
:root[data-jmb-preview-pointer="coarse"] .lb-gallery { display: none; }

/* -------------------------------------------------------
   Lightbox
   ------------------------------------------------------- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(28, 25, 21, 0.95);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.lightbox.is-open { opacity: 1; pointer-events: auto; }
.lightbox-img { max-height: 90vh; max-width: 88vw; object-fit: contain; }

.lightbox-prev, .lightbox-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: 2.5rem;
  color: rgba(247, 244, 239, 0.6);
  padding: 1.5rem;
  transition: color var(--transition);
}
.lightbox-prev { left: 0; }
.lightbox-next { right: 0; }
.lightbox-prev:hover, .lightbox-next:hover { color: #F7F4EF; }

.lightbox-close {
  position: absolute;
  top: 1.5rem; right: 1.5rem;
  font-size: 1.2rem;
  color: rgba(247, 244, 239, 0.6);
  transition: color var(--transition);
}
.lightbox-close:hover { color: #F7F4EF; }

.lightbox-counter {
  position: absolute;
  bottom: 1.5rem; left: 50%;
  transform: translateX(-50%);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: rgba(247, 244, 239, 0.4);
}

/* -------------------------------------------------------
   Footer / Contact
   ------------------------------------------------------- */
.site-footer { background: var(--color-text); color: var(--color-bg); }

.footer-inner {
  padding: 4rem var(--gutter) 4.5rem;
  max-width: var(--max-w);
  margin: 0 auto;
}

/* L'accroche « L'art de vous voir autrement » a été RETIRÉE le 24/08/26. Elle reprenait mot pour
   mot le H1 du hero : le visiteur qui arrivait en bas relisait la phrase d'accueil, et surtout
   elle coûtait près de 9rem de hauteur (5rem de titre + 4rem de marge) AVANT le formulaire —
   une section de contact dont on ne voyait pas le formulaire en arrivant. */
.footer-content-grid {
  display: grid;
  /* Le formulaire est l'action, les réseaux l'accompagnent : ils ne méritent pas la même
     largeur. Le filet vertical remplace la gouttière vide — c'est une double page, pas deux
     colonnes posées côte à côte. */
  grid-template-columns: 1.45fr 1fr;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: start;
}
.social-block {
  padding-left: clamp(0rem, 3vw, 3rem);
  border-left: 1px solid rgba(247, 244, 239, 0.1);
}

.block-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  margin-bottom: 0.75rem;
}
/* Agrandi : la section a perdu son titre géant, ce sont ces deux-là qui portent désormais la
   page. À 1,5rem ils se lisaient comme des sous-titres d'un titre absent. */
.block-title {
  font-family: var(--font-title);
  font-size: clamp(min(1.9rem, 8.1067vw), 2.8vw, 2.5rem);
  font-style: italic;
  font-weight: 400;
  line-height: 1.15;
  color: var(--color-bg);
  margin-bottom: 1.75rem;
}

/* Form */
.contact-form { display: flex; flex-direction: column; gap: 1rem; }

/* GRILLE de 3, et non un flex qui enroule : avec cinq seances, trois colonnes donnent 3 + 2 —
   deux lignes franches au lieu d'un enroulement qui dependait de la largeur des mots et donnait
   4 + 1 ou 3 + 2 selon l'ecran. Les colonnes sont egales, donc les pastilles aussi : c'est ce
   qui les fait lire comme un jeu de choix et non comme cinq etiquettes de longueurs differentes.
   La 2e ligne reste calee a GAUCHE (elle occupe les colonnes 1 et 2) : centrer deux pastilles
   sous trois casserait l'alignement de la colonne de formulaire. */
.form-services {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.service-pill {
  font-size: clamp(0.62rem, 1.7vw, 0.75rem);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* 44px de haut : ce sont les premiers controles du formulaire, ils se tapent au doigt. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Padding reduit et corps fluide : a trois par ligne sur un telephone de 390px, chaque
     pastille dispose d'environ 105px — « NAISSANCE » doit y tenir sans se couper. */
  padding: 0 0.7rem;
  border: 1px solid rgba(247, 244, 239, 0.25);
  color: rgba(247, 244, 239, 0.62);
  border-radius: 999px;
  /* `all` animait aussi `border-radius` et la couleur de fond au meme rythme : on nomme. */
  transition: color var(--transition), border-color var(--transition), background var(--transition);
}
.service-pill:hover { border-color: rgba(247, 244, 239, 0.6); color: var(--color-bg); }
/* Encre sur terre cuite claire = 5,88:1 (calcule). Le creme sur la terre cuite SOMBRE tenait
   AA lui aussi, mais sur un pied de page presque noir un brun sombre ne se voit pas : la
   pastille active ne se distinguait pas des autres, ce qui est tout ce qu'on lui demande. */
.service-pill.is-active {
  background: var(--color-accent-dark);
  border-color: var(--color-accent-dark);
  color: var(--color-text);
}

.form-field input,
.form-field textarea,
.form-field select {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(247, 244, 239, 0.2);
  padding: 0.75rem 0;
  color: var(--color-bg);
  font-family: var(--font-body);
  font-size: 0.88rem;
  font-weight: 400;
  outline: none;
  transition: border-color var(--transition);
}
.form-field textarea { resize: none; }
.form-field input::placeholder,
.form-field textarea::placeholder { color: rgba(247, 244, 239, 0.35); }
.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus { border-color: rgba(247, 244, 239, 0.6); }
.form-field select option { background: var(--color-text); }


.form-submit {
  align-self: flex-start;
  position: relative;
  overflow: hidden;
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  /* Cible tactile tenue par une hauteur minimale, pas par la somme de deux paddings — qui,
     elle, suit la taille de police et se derobe au premier reglage. */
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  padding: 0 2.2rem;
  border: 1px solid var(--color-accent-dark);
  color: var(--color-bg);
  margin-top: 1rem;
  transition: color 0.45s ease 0.1s, border-color 0.45s ease 0.1s;
  z-index: 0;
}
.form-submit::before { display: none; }
.form-submit::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-accent-dark);
  clip-path: circle(0% at 50% 50%);
  transition: clip-path 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: -1;
}
.form-submit:hover::after  { clip-path: circle(150% at 50% 50%); }
/* Encre sur terre cuite claire : le libelle reste lisible PENDANT et APRES le remplissage.
   L'ancienne regle gardait le creme sur un brun sombre — correct en contraste, mais le bouton
   s'assombrissait au survol au lieu de s'allumer. */
.form-submit:hover { color: var(--color-text); border-color: transparent; }

/* DEPLIE d'emblee depuis le 24/08/26. Les champs etaient masques (`opacity: 0`) ET inertes
   (`pointer-events: none`) tant qu'aucun type de seance n'etait choisi : un formulaire de
   contact qui ne montre pas ses champs se lit comme un formulaire cassé, et le clic
   supplementaire n'achetait rien. Une pastille est active par defaut, le champ hidden part
   rempli — il n'y a plus d'etat « rien choisi » a gerer. */
.form-fields-group {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Le message de confirmation vit UNIQUEMENT sur le pied de page sombre (une seule occurrence
   dans tout le dépôt) : il prend donc l'accent des fonds sombres. En terre cuite foncée il
   donnait 2,94:1 — le seul message que le formulaire adresse à la visiteuse était le moins
   lisible de la page. Défaut trouvé par le garde-fou d'accent, pas à l'œil. */
.form-success {
  font-size: 0.88rem;
  color: var(--color-accent-dark);
  margin-top: 0.5rem;
}

/* #145 (A8) — ligne de confidentialité sous le bouton du formulaire de l'ACCUEIL (pied de page
   sombre ; celle de /contact/ vit dans css/contact.css). Encre crème α 0,62 sur --color-text =
   6,79:1 (contrast-core), l'encre des pastilles au repos — gardée par home-css.test.cjs. Le lien
   est souligné : le `a` global ne souligne rien, et un lien DANS une phrase doit se distinguer
   du texte autrement que par sa seule couleur. */
.site-footer .form-note { font-size: 0.76rem; line-height: 1.55; color: rgba(247, 244, 239, 0.62); }
/* R2-3 (iPhone, 05/10) : le souligné en `text-decoration` ne paraissait pas sur iPhone (Safari et Firefox iOS),
   alors que celui de /contact/ y paraît. Cause NON reproduite sous WebKit 26.6 (Playwright, macOS : l'ancien
   raccourci y souligne). On prend donc l'écriture PROUVÉE sur l'iPhone de Luca, celle de /contact/ (`.ct-lien`) :
   un filet de 1px peint en fond, qui suit le lien sur ses deux lignes à 390. */
.site-footer .form-note a { background: linear-gradient(currentColor, currentColor) 0 100% / 100% 1px no-repeat; padding-bottom: 1px; }

/* Social links */
.social-cards { display: flex; flex-direction: column; }
/* Le handle est l'information, pas la plateforme : c'est lui qu'on recopie ou qu'on cherche.
   Il etait a 1rem sous un libelle de plateforme presque aussi visible. La composition
   s'inverse — plateforme en surtitre discret, handle en corps de titre. */
.social-link {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  min-height: 44px;
  justify-content: center;
  padding: 1.15rem 0;
  border-bottom: 1px solid rgba(247, 244, 239, 0.12);
  text-decoration: none;
  color: inherit;
  transition: opacity var(--transition);
}
.social-link:first-child { border-top: 1px solid rgba(247, 244, 239, 0.12); }
.social-link:hover { opacity: 0.55; }
.social-link-platform {
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
}
.social-link-handle {
  font-family: var(--font-title);
  font-weight: 300;
  font-size: clamp(1.2rem, 2vw, 1.55rem);
  font-style: italic;
  line-height: 1.2;
  color: var(--color-bg);
  /* `anywhere` et pas `break-all` : le handle ne se coupe QUE s'il ne tient pas, et il se coupe
     alors n'importe ou plutot que de deborder de la colonne sur un telephone. */
  overflow-wrap: anywhere;
}

/* Footer bottom */
.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.75rem var(--gutter);
  border-top: 1px solid rgba(247, 244, 239, 0.09);
}
.footer-bottom p { font-size: 0.7rem; color: rgba(247, 244, 239, 0.35); line-height: 1.8; }
.footer-bottom-left, .footer-bottom-right { flex: 1; }
.footer-bottom-right { text-align: right; }
.footer-bottom-center { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
.footer-logo {
  font-family: var(--font-title);
  font-weight: 300;
  font-size: 0.85rem;
  letter-spacing: 0.18em;
}
.back-to-top { font-size: 1rem; color: rgba(247, 244, 239, 0.35); transition: color var(--transition); }
.back-to-top:hover { color: var(--color-bg); }

/* -------------------------------------------------------
   Editor mode (preview dans iframe éditeur)
   ------------------------------------------------------- */
body.editor-mode *                   { animation: none !important; transition: none !important; }
body.editor-mode .word-inner         { transform: none !important; }
body.editor-mode .scroll-down        { opacity: 1 !important; }
body.editor-mode .card-btn,
body.editor-mode .about-photo-wrap   { clip-path: none !important; }
body.editor-mode .card-btn::after    { display: none !important; }
body.editor-mode .about-corps         { opacity: 1 !important; transform: none !important; }

/* -------------------------------------------------------
   Responsive
   ------------------------------------------------------- */
@media (max-width: 1000px) {
  .nav-left, .nav-right { display: none; }
  .hamburger             { display: flex; }
  .mobile-menu.is-open   { display: flex; }

  /* Les deux côtés disparus, il ne reste que deux enfants dans la grille : sans cette
     redéclaration ils occuperaient les colonnes 1 et 2 d'un `1fr auto 1fr`, poussant le logo
     au centre-gauche et le hamburger au milieu. On revient au rendu attendu — logo à gauche,
     hamburger à droite — que le `space-between` du flex donnait gratuitement. */
  .site-nav  { grid-template-columns: 1fr auto; }
  .nav-logo  { justify-self: start; }

  .categories-grid          { grid-template-columns: 1fr 1fr; }
  /* Les 5 univers du portfolio se rangeaient 2 + 1 + 2 : les 3 premières cartes vivent dans la
     grille, les 2 dernières dans une rangée flex séparée — un artefact de structure, pas une
     intention. La planche, qui montre LES MÊMES 5 univers deux sections plus bas, se range
     2 + 2 + 1. Deux rythmes différents pour la même liste se lisent comme un défaut.
     `display: contents` fait disparaître la boîte de la seconde rangée : ses deux cartes
     rejoignent la grille parente et les cinq se rangent d'une seule coulée. */
  .categories-row-2         { display: contents; }

  /* #103 ②b — une colonne « intro / photo / body » : photo à gauche, texte pleine largeur. */
  .about-inner              { grid-template-columns: minmax(0, 1fr); grid-template-areas: "intro" "photo" "body"; }
  .about-inner > .section-intro { margin-bottom: 2rem; }
  .about-figure             { width: clamp(15rem, 64vw, 22rem); }
  .about-corps              { margin-top: 2.5rem; max-width: 36rem; }

  .footer-content-grid      { grid-template-columns: 1fr; gap: 3rem; }
}

@media (max-width: 600px) {
  .categories-grid                  { grid-template-columns: 1fr; }

  .footer-bottom                    { flex-direction: column; gap: 1.25rem; text-align: center; }
}

/* ── SOUS 375 px SEULEMENT (#95, 26/09/2026) — au-dessus, rien ne bouge ─────────────────────────
   Logo : taille pleine tant qu'il tient, sinon réduit en PROPORTION pour garder ≥ 16 px d'air avant
   le hamburger — (100vw − 2×24 gouttières − 30 hamburger − 16 air) / 10,27 em (« JOYCE MARTENS »,
   Cormorant 500, letter-spacing 0.2em : 164,3 px à 16 px, mesuré Chromium). Pleine taille jusqu'à
   ~258 px. La grille de `.site-nav` n'a pas d'espace fantôme à retirer ici (`gap` nul, `.nav-left` /
   `.nav-right` en `display: none` ne sont pas des cellules) : à 260-280 px, l'en-tête s'étirait parce
   que la PAGE débordait (commandes des reportages, plus bas), pas de lui-même.
   Sur-titre `.hero-tag` (taille fixe, hors C123) : « Photographe · Province du Hainaut » = 19,48 em de
   glyphes + 33 × letter-spacing (Jost 500, mesuré Chromium). À 0.2em : 313 px, 2 lignes jusqu'à
   360 px. À 0.12em (23,45 em) et une taille qui garde 8 px d'air sur 2 × 24 de gouttières, il tient
   sur UNE ligne jusqu'à 300 px (12 px dès 331 px ; 11,3 px à 320) ; plancher 10 px, 2 lignes sous ~290. */
@media (max-width: 374.98px) {
  .logo-link { font-size: min(1rem, calc((100vw - 94px) / 10.27)); }
  .hero-tag  { letter-spacing: 0.12em; font-size: clamp(0.625rem, calc((100vw - 56px) / 23.45), 0.75rem); }
}

/* ── Liens légaux footer ── */
.footer-legal-links {
  font-size: 0.72rem;
  margin-top: 0.4rem;
  letter-spacing: 0.03em;
}

.footer-legal-links a {
  color: var(--color-muted);
  text-decoration: none;
  transition: color 0.2s ease;
}

.footer-legal-links a:hover { color: var(--color-bg); }

.footer-legal-links span {
  color: var(--color-muted);
  margin: 0 0.35rem;
}

/* ═══════════════════ SOUS-PAGES JMP (tarifs · à-propos · contact · légales) ═══════════════════ */
/* L'accueil garde le header fixe transparent sur le hero ; les sous-pages (body.sub) ont un header
   opaque sticky pour ne pas masquer le contenu. */
body.sub .site-header { position: sticky; background: rgba(247,244,239,0.95); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); box-shadow: 0 1px 0 rgba(28,25,21,0.08); }

/* ⚠️ `.nav-links` a été RETIRÉ le 2026-09-11. C'était la nav des sous-pages : logo à gauche,
   liens poussés à droite — une SECONDE identité de navigation, à côté de celle de l'accueil
   (nav-left / logo centré / nav-right). Le visiteur qui cliquait « Portfolio » depuis l'accueil
   croyait changer de site. Les quatre sous-pages portent désormais le markup de l'accueil, et
   héritent donc de ses règles (.nav-left/.nav-right, l. 180) sans une ligne de CSS en plus,
   seuil de bascule compris. Le contrôle vit dans chrome-partage.test.cjs : il compare les pages
   entre elles plutôt que de décrire ce qu'elles devraient contenir. */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

.section { max-width: var(--max-w); margin: 0 auto; padding: clamp(3.5rem,9vw,7rem) var(--gutter); position: relative; z-index: 1; background: var(--color-bg); }
.section-eyebrow { display: block; font-size: 0.72rem; font-weight: 500; letter-spacing: 0.22em; text-transform: uppercase; color: var(--color-accent); margin-bottom: 0.6rem; }
.section-cta { margin-top: 2.5rem; }

/* Tarifs */
.tarifs-page .sp-intro, .tarifs-intro p { color: var(--color-muted-text); max-width: 60ch; margin-bottom: 0.9rem; }
.tarif-section { padding: 2.2rem 0; border-top: 1px solid rgba(28,25,21,0.1); }
.tarif-section-head { margin-bottom: 1.4rem; }
.tarif-num { display: block; font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-accent); margin-bottom: 0.4rem; }
.tarif-title { font-family: var(--font-title); font-weight: 300; font-size: clamp(min(1.6rem, 6.8267vw),4vw,2.3rem); font-style: italic; }
.tarif-tagline { color: var(--color-muted-text); margin-top: 0.5rem; max-width: 55ch; }
.tarif-cta-label { display: inline-block; margin-top: 0.6rem; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-muted); }
.tarif-rows { display: grid; gap: 1rem; margin-top: 1.2rem; }
.tarif-row { display: grid; gap: 0.4rem; padding: 1.2rem 1.3rem; background: var(--color-surface); border-radius: 4px; }
.tarif-row-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
.tarif-row-name { font-family: var(--font-title); font-weight: 300; font-size: 1.25rem; letter-spacing: 0.04em; }
.tarif-row-price { font-family: var(--font-title); font-size: 1.05rem; font-weight: 500; color: #8A5835; white-space: nowrap; }
.tarif-row-price .unit { font-size: 0.8rem; color: var(--color-muted); }
.tarif-row-detail { color: var(--color-muted-text); font-size: 0.92rem; white-space: pre-line; }
.tarif-row-note { font-size: 0.8rem; color: var(--color-muted); font-style: italic; }
/* Sous 300 px SEULEMENT (#95, C127) : « professionnel » et les libellés longs imposaient leur
   largeur à la ligne (page de 278 px à 260). `anywhere` ne coupe un mot que s'il ne tient pas, et le
   RETIRE du calcul de largeur minimale — sans borne, la grille le couperait bien plus tôt (constaté
   chez JMB dès 360 px). Le prix reste en `nowrap`. */
@media (max-width: 299.98px) {
  .tarif-row { overflow-wrap: anywhere; }
}
.tarifs-note { margin-top: 2rem; font-size: 0.85rem; color: var(--color-muted); font-style: italic; }
@media (min-width: 760px) { .tarif-rows { grid-template-columns: repeat(3,1fr); align-items: start; } }

/* prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }
  .word-inner { transform: none !important; }
  .hero-bg, .hero-title .word-inner, .hero-title .hero-tag, .hero-section .hero-cta { animation: none !important; }
  .scroll-down { opacity: 1 !important; }
  .card-btn, .about-photo-wrap { clip-path: none !important; }
  /* Volet au doigt (#140 Lot 2) : le JS n'arme jamais ici ; la feuille ne dépend pas de lui. Le
     calque de COUVERTURE disparaît — `transform: none` le laisserait couvrir la carte. */
  .card-btn::after { display: none !important; }
  /* Le sommaire est statique : le JS n'arme jamais l'entrée dans ce cas (l'état caché n'existe
     pas), mais on ne dépend pas de lui pour que la page soit correcte. `!important` : la règle
     d'entrée est préfixée `.plate.is-armed`, une neutralisation nue la perdrait. */
  .plate :is(.plate-num, .plate-name, .plate-signature),
  .plate .plate-link::after { transform: none !important; }
  /* Le panneau s'ouvre a plat : le cache est deja retire, la photo ne contre-glisse pas. La
     visibilite reste portee par [hidden], jamais par le cadrage — le CONTENU affiche est
     identique, la reduction porte sur le mouvement seul (§2.4).
     ⚠️ Cette garde SUIT LA PROPRIETE ANIMEE. Tant que le geste etait un pivot elle neutralisait
     `transform` ; le geste est un `clip-path` depuis P3.5, elle neutralise `clip-path`. Changer
     le geste sans changer la garde la transforme silencieusement en CSS MORT QUI A L'AIR D'UNE
     GARDE.
     ⚠️ Ces deux dernieres lignes annoncaient « un test derive la liste des proprietes animees du
     CSS et exige qu'elles soient toutes neutralisees ici ». Ce test N'EXISTAIT PAS — l'annonce ne
     gardait rien, et c'est ainsi que les 5 animations de reportage sont restees nues jusqu'au
     2026-09-17. Il existe depuis (`css-reduced-motion.test.mjs`) mais son perimetre est PLUS
     ETROIT que la phrase d'origine : il couvre les `animation` PORTANT UN DELAI (le seul cas que
     la regle `*` ci-dessus ne rattrape pas, puisqu'elle ecrase la duree et pas le delai), et
     verifie en plus que la neutralisation est posee APRES la regle animee. Les `transition` et
     les `clip-path` ci-dessous, eux, restent gardes a la main — personne ne les derive. */
  .coll-drawer-panel { clip-path: none !important; }
  .coll-drawer-photo { transform: none !important; }
}

/* =====================================================
   ACCUEIL — refonte 2026-08-20, phase 2
   Le CSS LIT ce que le rendu a décidé : il ne recopie aucune donnée de contenu.
   ===================================================== */

/* -------------------------------------------------------
   À propos (accueil) — ouverture + paragraphes (#103 ②b)
   ------------------------------------------------------- */
.about-lede {
  font-family: var(--font-title);
  font-weight: 300;
  font-size: clamp(1.15rem, 2.2vw, 1.5rem);
  font-style: italic;
  line-height: 1.5;
  color: var(--color-text);
}

.about-p {
  font-size: 0.95rem;
  line-height: 1.85;
  color: var(--color-muted-text);
}

/* `.btn-filet` ici aussi (24/08/26). C'était un lien souligné en terre cuite qui pâlissait au
   survol — un quatrième dessin pour la même intention que les deux autres, et le seul geste de
   la page où le survol ÉTEINT au lieu d'allumer. Cette règle ne porte que le placement. */
.about-cta { margin-top: 1rem; }

/* -------------------------------------------------------
   Collections — « le sommaire » (#133, maquette validée par Luca le 01/10/26, variante « rideau »)
   Une table des matières typographique à TOUTES les largeurs : numéro, nom, signature. Zéro image
   dans la section — le portfolio vient de montrer les cinq univers en photos ; ici on passe de
   l'IMAGE à l'OFFRE. La photo vit au tiroir (> 900 px), chargée à l'intention (js/site.js).
   Base = le sommaire mobile prouvé (24/08/26), promu à toutes les largeurs ; au-dessus de 600 px
   une composition en colonnes ; au-dessus de 900 px l'affordance « En savoir plus » et le tiroir.
   Le markup est celui du renderer, sans figure ni géométrie en ligne.
   ------------------------------------------------------- */
.collections-section { padding: 6rem 0; overflow: hidden; }

/* Boîte du sommaire : la largeur du contenu de page, SANS padding — le survol d'une gouttière vide
   ne doit pas estomper les cinq entrées (la respiration écoute `.plate:hover`). */
.plate {
  width: calc(100% - 2 * var(--gutter));
  max-width: calc(var(--max-w) - 2 * var(--gutter));
  margin: 0 auto;
}

.plate-item { position: relative; }

/* L'entrée du sommaire. Elle porte le SURVOL (respiration) et le recul quand un tiroir est ouvert,
   jamais l'entrée : une même propriété ne porte pas deux horloges. Les déclarations du recul sont
   dans CETTE règle et ses variantes de média — jamais dans une seconde règle `.plate-link` posée
   ailleurs, où elles se recouvriraient en silence. */
.plate-link {
  display: block;
  position: relative;
  /* La gouttière du numéro (≤ 600 px). Le folio se pose à gauche, le nom et la signature
     s'alignent à droite : la mise en page d'un sommaire, pas d'une liste à puces. */
  padding: 1.15rem 0 1.25rem 3.4rem;
  border-bottom: 1px solid rgba(28, 25, 21, 0.14);
  color: var(--color-text);
  transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.plate-item:first-child .plate-link { border-top: 1px solid rgba(28, 25, 21, 0.14); }

/* Les enfants du corps deviennent les items de la grille (≥ 601 px) sans changer le markup. */
.plate-body { display: contents; }

/* Le folio reprend EXACTEMENT celui des reportages — Cormorant italique en terre cuite. Même
   fonction (numéroter une entrée dans une suite), donc même signe. `line-height: 1` et pas moins :
   Cormorant compose des chiffres elzéviriens, dont le 3, le 4 et le 5 descendent sous la ligne de
   base — une boîte plus serrée leur couperait le jambage. La gouttière du lien (3,4rem) est
   calculée pour lui : les deux se règlent ensemble. */
.plate-num {
  position: absolute;
  left: 0;
  top: 0.95rem;
  font-family: var(--font-title);
  font-size: 1.85rem;
  font-style: italic;
  font-weight: 300;
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--color-accent);
}
.plate-name {
  display: block;
  font-family: var(--font-title);
  font-size: clamp(1.4rem, 6.5vw, 1.8rem);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.05;
}
.plate-rule { display: none; }
/* Graisse 400 (#133, E3) : à 300 l'italique se lisait comme une note de bas de page. */
.plate-signature {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--font-title);
  font-weight: 400;
  font-size: 0.95rem;
  font-style: italic;
  line-height: 1.4;
  color: var(--color-muted-text);
}

/* Focus clavier visible — jamais d'outline supprimée sans remplacement (contrat SEO §a11y). */
.plate-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }

/* ----- 601 px et plus : composition en colonnes ----- */
@media (min-width: 601px) {
  .plate-link {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    column-gap: 2rem;
    align-items: baseline;
    padding: 1.6rem 0 1.7rem;
    border-bottom: 0;                   /* le filet devient un élément : il ne bouge pas avec le texte */
  }
  .plate-item:first-child .plate-link { border-top: 0; }
  /* Les deux filets : celui du bas (élément du markup) et, pour la première entrée seule, celui du
     haut (pseudo). Pleine largeur, absolus, hors grille — immobiles pendant l'entrée : c'est
     derrière eux que le texte monte. */
  .plate-rule,
  .plate-item:first-child .plate-link::before {
    content: '';
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    height: 1px;
    background: var(--color-text);
    opacity: 0.14;
  }
  .plate-rule { bottom: 0; }
  .plate-item:first-child .plate-link::before { top: 0; }

  .plate-num { position: static; grid-column: 1; grid-row: 1; font-size: clamp(1.9rem, 2.6vw, 2.4rem); }
  .plate-name { grid-column: 2; grid-row: 1; font-size: clamp(1.9rem, 2.9vw, 2.75rem); line-height: 1; }
  .plate-signature { grid-column: 2; grid-row: 2; margin-top: 0.55rem; font-size: 1.25rem; }

  /* Respiration : l'entrée survolée (ou focalisée au clavier) garde son encre, les autres reculent.
     Même objet et même propriété que `.is-dimmed` (tiroir ouvert) : une seule horloge.
     ⚠️ JAMAIS `:hover` dans un sélecteur qui porte aussi `:has()` (#133 RETOUR 2) : Safari 26.1
     réel n'y recalculait pas le style au changement de ligne survolée — `matches()` juste,
     opacité calculée figée sur la 1re ligne survolée. Le survol dépend donc d'un `:hover` sur
     l'élément stylé ou son ancêtre ; le clavier a ses propres règles, sans `:hover`, et reste
     `:focus-visible` (pas `:focus-within`, qui matcherait le focus rendu après un geste souris).
     Ordre voulu, à spécificité égale : le survol APRÈS le clavier, pour que la ligne survolée
     garde son encre même quand une autre porte l'anneau. */
  .plate:has(.plate-link:focus-visible) .plate-link { opacity: 0.42; }
  .plate:has(.plate-link:focus-visible) .plate-link:focus-visible { opacity: 1; }
  .plate:hover .plate-link { opacity: 0.42; }
  .plate:hover .plate-link:hover { opacity: 1; }
  .plate-item.is-dimmed .plate-link { opacity: 0.3; }
}

/* ----- 901 px et plus : bureau, le tiroir existe ----- */
@media (min-width: 901px) {
  .plate-link {
    grid-template-columns: 5.5rem minmax(0, 1.1fr) minmax(0, 1fr) max-content;
    column-gap: 2.5rem;
    padding: 1.7rem 0 1.8rem;
  }
  .plate-signature { grid-column: 3; grid-row: 1; margin-top: 0; }
  /* Affordance (#133, E2) : la ligne OUVRE un panneau. Hors du nom accessible (syntaxe `/ ''`,
     avec repli pour les moteurs qui ne la lisent pas) : le nom du lien reste celui de la
     collection et sa signature. Elle ne porte AUCUN geste de survol : elle fait partie de la ligne
     et entre AVEC elle (sinon cinq libellés flotteraient seuls pendant l'entrée). Sous 900 px il
     n'y a pas de tiroir (drawer-core.BREAKPOINT), donc pas de libellé : il promettrait une
     ouverture qui n'a pas lieu. */
  .plate-link::after {
    content: 'En savoir plus \2009\2192';
    content: 'En savoir plus \2009\2192' / '';
    grid-column: 4;
    grid-row: 1;
    justify-self: end;
    font-family: var(--font-body);
    font-size: 0.62rem;
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--color-muted-text);
  }
}

@media (min-width: 601px) and (max-width: 900px) {
  :root { --rideau-course: 12rem; }
}

/* ----- Entrée « le rideau » — chaque ligne monte derrière son filet, une fois -----
   · L'état caché n'existe que si le JS a ARMÉ la section (`.plate.is-armed`, js/site.js) : il
     n'arme que mouvement non réduit, IntersectionObserver présent, et section encore SOUS le pli
     (au doigt aussi depuis #140 Lot 2 : même rideau). Sans JS, ou dans tous les autres cas, l'état final est l'état par défaut.
   · `transform` seul : la hauteur de la section est définitive au premier rendu.
   · Les porteurs d'entrée (folio, nom, signature, affordance) ne portent AUCUN geste de survol ;
     le porteur de survol (`.plate-link`) ne porte AUCUNE entrée.
   · Le rang de chaque ligne (`--i`) est posé ICI par `:nth-child`, jamais en ligne (CSP) ; les
     délais en découlent. Un sixième univers s'ajoute en une règle. */
.plate-item:nth-child(1) { --i: 0; }
.plate-item:nth-child(2) { --i: 1; }
.plate-item:nth-child(3) { --i: 2; }
.plate-item:nth-child(4) { --i: 3; }
.plate-item:nth-child(5) { --i: 4; }

.plate-link { overflow: hidden; }   /* le masque, immobile */
.plate.is-armed .plate-item:not(.is-entered) :is(.plate-num, .plate-name, .plate-signature),
.plate.is-armed .plate-item:not(.is-entered) .plate-link::after {
  transform: translateY(var(--rideau-course));
}
/* L'horloge est portée par l'état ENTRÉ, jamais par l'état de base : une transition se lit sur le
   style d'ARRIVÉE. Posée sur la base, l'armement lui-même glissait vers l'état caché (mesuré
   Firefox et WebKit : la mesure du pli fige le style avant l'armement, la ligne descendait en
   1 s au lieu d'être posée sous son filet) ; ici l'armement est instantané, seule l'entrée anime. */
.plate.is-armed .plate-item.is-entered :is(.plate-num, .plate-name, .plate-signature),
.plate.is-armed .plate-item.is-entered .plate-link::after {
  transition: transform 1.05s var(--ease-entree) calc(var(--i, 0) * 0.09s);
}

/* -------------------------------------------------------
   Panneau de collection — « le cache qui se retire » (P3.5, spec 2026-08-22)
   Le renderer émet chaque panneau avec [hidden] : sans JS, rien ne s'affiche et le lien du
   tirage reste la vraie destination. C'est aussi le comportement mobile voulu (§2.2 : pas de
   panneau sous 900 px).

   CE QUI A ÉTÉ ABANDONNÉ, et pourquoi — pour que ça ne soit pas re-proposé dans six mois comme
   une idée neuve : le panneau pivotait (`rotateY(-89.9deg) → 0` sur une perspective de 1500 px),
   sous un voile sombre plein écran. Deux rondes de réglage l'avaient rendu fluide ; ce n'est pas
   un échec technique mais un arbitrage de direction artistique tranché le 22/08/26 — le pivot
   lit comme une interface d'application, pas comme une page imprimée, et il arrive DEPUIS LE
   BORD DE L'ÉCRAN au lieu de naître de la carte.

   LE GESTE ACTUEL : le panneau ne se déplace pas. Il est déjà composé, immobile, à sa position
   finale ; ce qui bouge est un CACHE qui se retire de dessus lui. Un glissement dirait « un
   élément d'interface arrive » ; un dévoilement dit « quelque chose était là, on l'a
   découvert » — le geste de l'imprimé, où l'on retire un passe-partout de dessus un tirage.
   Il produit GRATUITEMENT l'ordre de lecture : le cache partant vers la droite, l'œil reçoit le
   chiffre, puis le nom, puis le texte, puis la photo, sans qu'aucun stagger soit écrit.
   Ce n'est pas un pari : `.category-card`, `.about-photo-wrap` et `.lb-gallery-label` s'ouvrent
   déjà ainsi — c'est la grammaire de la feuille.
   ------------------------------------------------------- */
/* Explicite, jamais implicite : la règle UA `[hidden] { display: none }` se fait écraser par
   n'importe quelle règle de classe portant un display. On la réaffirme ici. */
.coll-drawer[hidden] { display: none; }

/* Le conteneur reste plein écran, mais TRANSPARENT : il ne sert plus qu'à capter le clic hors
   panneau (js/site.js). Le voile sombre est parti avec le pivot — ce sont désormais les tirages
   eux-mêmes qui reculent (§2.3), ce qui garde la planche lisible pendant la lecture du panneau.
   Aucune `perspective` : plus rien ne pivote. */
.coll-drawer {
  position: fixed;
  inset: 0;
  z-index: 300;
}

/* `.collections-section` porte `position: relative; z-index: 1` : c'est un contexte
   d'empilement, et le z-index 300 du panneau n'y vaut QUE face à ses frères de section. Face au
   `.site-header` (z-index 100, hors de ce contexte), c'est le 1 de la section qui est comparé —
   le header passerait donc par-dessus le panneau ouvert. On fait remonter la section elle-même
   tant qu'un panneau est ouvert.
   ⚠️ Toujours nécessaire après P3.5 : la disparition du voile ne change RIEN à l'empilement,
   c'est le `position: fixed` du panneau dans une section à z-index qui crée le piège. */
.collections-section:has(.coll-drawer:not([hidden])) { z-index: 400; }

.coll-drawer-panel {
  /* Padding exposé en jeton : le chiffre géant s'en sert pour calculer son propre débord et
     être rogné par l'arête gauche (§3.1). Deux clamp() recopiés divergeraient au premier
     réglage — un seul est réglable sans rien casser. */
  --coll-pad: clamp(1.75rem, 3.2vw, 3rem);
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  /* Elargi le 24/08/26 : a 44rem la colonne photo ne pouvait pas depasser 320 px, largeur ou
     aucune vraie photo ne tient. On ne resserre PAS le texte pour autant (contrainte explicite
     de Luca) — c'est le panneau qui gagne 9rem, et les DEUX colonnes s'elargissent. */
  width: min(53rem, 96vw);
  /* Colonne flex : la composition doit pouvoir REVENDIQUER toute la hauteur du panneau, sinon
     la photo ne peut pas s'étirer et retombe à la taille de son contenu. */
  display: flex;
  flex-direction: column;
  /* `overflow-x: hidden` et PAS le clip-path : c'est lui qui rogne le chiffre géant sur l'arête
     gauche. Confier ce rognage au clip-path le ferait disparaître sous `prefers-reduced-motion`
     (où le clip-path est neutralisé) et le chiffre déborderait hors du panneau. Deux rôles,
     deux propriétés. */
  overflow-x: hidden;
  overflow-y: auto;
  padding: var(--coll-pad);
  background: var(--color-bg);
  /* Un filet, pas une ombre. L'ombre portée du pivot (`-24px 0 60px`) est retirée : le clip-path
     rogne aussi les ombres, elle serait donc coupée à l'arête du cache pendant tout le
     dévoilement — exactement le bruit que ce geste existe pour éviter. */
  border-left: 1px solid rgba(28, 25, 21, 0.14);
  /* Le cache. L'arête droite part repliée sur l'arête gauche : le panneau se découvre depuis la
     tranche, du côté de la planche. UNE seule horloge sur cet objet (leçon de P3 : trois durées
     sur un même objet faisaient lire trois arrivées). Les deux autres durées de §2.2 portent sur
     d'AUTRES objets — les tirages et la photo. */
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.62s cubic-bezier(0.22, 1, 0.36, 1) 0.08s;
}
/* `will-change` seulement tant qu'un panneau est à l'écran : posé en permanence, il réserverait
   un calque GPU pour chacun des cinq panneaux, tous fermés, en pure perte. */
.coll-drawer:not([hidden]) .coll-drawer-panel { will-change: clip-path; }

.coll-drawer.is-open .coll-drawer-panel { clip-path: inset(0 0 0 0); }

/* Composition — révision du 22/08/26 (retour Luca sur le déployé).
   Ce qui n'allait pas dans la première version, dit franchement : la photo était une vignette
   de 216 px en haut à droite, et le chiffre géant se posait AU-DESSUS du titre au lieu de
   passer DERRIÈRE lui. Deux boîtes de grille distinctes ne se recouvrent jamais — la page de
   catalogue lisait comme une fiche produit.

   Le panneau ne s'élargit pas (contrainte explicite) : c'est le TEXTE qui se resserre pour
   donner sa place à la photo. Colonne de texte étroite et lignes courtes, c'est la mise en
   page d'un magazine, pas une concession.

   Trois plans, toujours : le fond crème, PUIS le chiffre, PUIS la photo. */
.coll-drawer-compo {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 50%;
  column-gap: clamp(1.25rem, 2.8vw, 2.25rem);
  align-items: stretch;
  /* `flex: 1 0 auto` et pas `1 1 auto` : la compo ne doit jamais être COMPRIMÉE sous la
     hauteur de son texte — sur un écran court, c'est le panneau qui défile. */
  flex: 1 0 auto;
}
/* Cover absente (jamais le cas aujourd'hui, mais la donnée est éditable) : une colonne vide
   laisserait le texte serré sur la moitié gauche d'un panneau à moitié blanc. */
.coll-drawer-compo:not(:has(.coll-drawer-figure)) { grid-template-columns: minmax(0, 1fr); }

/* Le texte se CENTRE verticalement contre la photo pleine hauteur : aligné en haut, il laissait
   un pied vide qui faisait retomber toute la composition. */
.coll-drawer-body {
  grid-column: 1;
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(1.25rem, 4vh, 2.75rem) 0;
}

/* Chaque bloc de texte devient POSITIONNE. Sans ca, le chiffre — qui est positionne, lui —
   passerait par-dessus la signature et le chapo : dans l'ordre de peinture CSS, un element
   positionne a rang 0 est au-dessus du texte NON positionne, meme d'un rang plus eleve en
   apparence. En posant `z-index: 1` sur chaque enfant direct, on rend la regle vraie pour tout
   le bloc et pas seulement pour le titre. */
.coll-drawer-body > * {
  position: relative;
  z-index: 1;
}

/* Le chiffre et le titre dans la MÊME boîte : c'est la condition pour que l'un passe derrière
   l'autre. Le chiffre est en position absolue — il ne pousse rien, il ne réserve rien, il est
   une texture d'encre sous le nom. */
.coll-drawer-titrage { position: relative; }

.coll-drawer-num {
  position: absolute;
  /* Rogné par l'arête GAUCHE du panneau : on annule le padding, puis on déborde encore d'un
     vingt-cinquième de cadratin. Le rognage est fait par `overflow-x: hidden` du panneau —
     jamais par le clip-path, qui disparaît sous prefers-reduced-motion. */
  left: calc(-1 * var(--coll-pad) - 0.04em);
  /* Le chiffre doit MONTER au-dessus du nom et DESCENDRE sous lui, sinon il se lit comme un
     numero de page pose a cote.
     REMONTE d'un quart de cadratin le 24/08/26 (retour Luca sur le deploye) : simplement centre
     sur le titre, le nom traversait son PREMIER tiers et le haut du panneau restait vide. Le
     nom croise desormais le DERNIER tiers — la position d'un folio de magazine — et la masse
     d'encre remonte occuper ce vide. L'offset est en `em`, donc solidaire de la taille du
     chiffre : en pixels il se deregleraient au premier reclamp.
     `translateY` porte DEUX roles ici (le centrage -50% et le releve) : c'est voulu, une seule
     propriete transform existe par element — les separer en poserait une qui ecrase l'autre. */
  top: 50%;
  transform: translateY(calc(-50% - 0.24em));
  z-index: 0;
  font-family: var(--font-title);
  /* Assez grand pour porter la page, borne pour ne jamais devenir criard : a sa taille maximale
     le chiffre reste plus etroit que la colonne de texte, il ne peut donc pas atteindre la photo. */
  font-size: clamp(10rem, 19vw, 15.5rem);
  font-weight: 400;
  line-height: 0.74;
  letter-spacing: -0.06em;
  color: var(--coll-num-ink);
  pointer-events: none;
  white-space: nowrap;
}

/* Les deux chiffres sont DEUX boites : le second descend d'un cinquieme de cadratin et se glisse
   sous le premier. Un « 01 » aligne est une pagination ; desaligne, c'est une signature de
   maquette. Le decalage est le MEME pour les cinq (01 a 05) — c'est un geste de composition,
   pas un accident de rendu. */
.coll-drawer-num-a,
.coll-drawer-num-b { display: inline-block; }
.coll-drawer-num-b { transform: translate(-0.05em, 0.2em); }

.coll-drawer-title {
  position: relative;
  z-index: 1;
  font-family: var(--font-title);
  font-size: clamp(min(2.1rem, 8.96vw), 4vw, 3rem);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
}

/* Le filet du titre, repris de la planche : même geste de magazine, même largeur. */
.coll-drawer-rule {
  display: block;
  width: 3rem;
  height: 1px;
  margin: 1.3rem 0 1.4rem;
  background: var(--color-text);
  opacity: 0.4;
}

.coll-drawer-signature {
  font-family: var(--font-title);
  font-weight: 400;                     /* #133, E3 : même graisse qu'au sommaire */
  font-size: 1.15rem;
  font-style: italic;
  color: var(--color-muted-text);
}

.coll-drawer-teaser {
  margin-top: 1.3rem;
  font-size: 0.9rem;
  line-height: 1.9;
  color: var(--color-muted-text);
}

/* La photo tient toute la hauteur du panneau et la moitié de sa largeur. `align-self: stretch`
   plutôt qu'un `aspect-ratio` figé : le cadre suit la hauteur de la fenêtre, ce qui donne le
   format vertical franc d'une pleine page. Le `min-height` couvre le cas de l'écran très court,
   où un cadre étiré deviendrait une bande. */
.coll-drawer-figure {
  grid-column: 2;
  /* CENTREE, plus etiree (revision du 24/08/26). Etiree sur toute la hauteur du panneau, la
     photo montait sous la croix de fermeture — qui disparaissait derriere elle, ce cadre etant
     au-dessus dans l'empilement — et son rapport tombait vers 0,45 : une bande, pas un portrait.
     Le rapport 3/4 est celui de TOUTES les images du site (cartes, planche, reportages) : le
     panneau parle enfin la meme langue, et le haut du panneau redevient du creme ou la croix se
     detache. `max-height` borne le cas de l'ecran tres haut, ou 3/4 deborderait. */
  align-self: center;
  aspect-ratio: 3 / 4;
  max-height: 84vh;
  z-index: 2;
  overflow: hidden;
  margin: 0;
}

/* Contre-glissement (§2.2). La photo est dévoilée PROGRESSIVEMENT par le cache : immobile, elle
   paraîtrait collée à lui. À 900 ms contre 620, elle continue de se placer après que le cache a
   fini — l'œil ne lit pas une arrivée mais un CALAGE, le parallaxe d'un dépliant de magazine.
   L'image est 28 px plus large que son cadre : à -28px elle le remplit exactement (débord à
   gauche), à 0 elle le remplit encore (débord à droite). Aucun état ne laisse de vide. */
.coll-drawer-photo {
  display: block;
  width: calc(100% + 28px);
  height: 100%;
  object-fit: cover;
  transform: translateX(-28px);
  transition: transform 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.08s;
}
.coll-drawer.is-open .coll-drawer-photo { transform: translateX(0); }

/* `z-index` OBLIGATOIRE : la figure porte 2 et vient APRES dans le DOM — sans rang explicite,
   la croix passait dessous des qu'elles se recouvraient. Le fait qu'elles ne se recouvrent plus
   depuis que la photo est centree ne dispense pas de la regle : c'est le rang qui garantit
   qu'une sortie reste cliquable, pas la geometrie du moment. */
.coll-drawer-close {
  position: absolute;
  z-index: 5;
  top: 1rem;
  right: 1rem;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  color: var(--color-text);
  opacity: 0.55;
  transition: opacity var(--transition);
}
.coll-drawer-close:hover { opacity: 1; }

/* Formules — EMPILÉES, en serif de titre (révision du 22/08/26).
   Trois états successifs, et il faut dire pourquoi on a bougé deux fois : d'abord un `<ol>`
   numéroté (`01 LIN / 02 SOIE`), qui lisait comme une carte de restaurant et dont le chiffre
   entrait en concurrence avec celui de la collection ; puis une ligne à points médians
   (`LIN · SOIE · VELOURS`), qui réglait la concurrence mais rendait les noms secondaires —
   une énumération, pas une offre. Empilés et composés en Cormorant, ils redeviennent ce
   qu'ils sont : trois propositions qu'on lit une par une.

   VOCABULAIRE : « formules », jamais « collections » — dans la nomenclature figée le 20/08/26,
   « collections » désigne les 5 univers. Employer le même mot pour les deux niveaux sur le même
   écran, à quarante centimètres d'écart, casse le langage de marque. */
.coll-drawer-formules {
  margin-top: 2rem;
  padding-top: 1.4rem;
  border-top: 1px solid rgba(28, 25, 21, 0.12);
}

.coll-formules-intro {
  font-family: var(--font-body);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--color-muted-text);
}

.coll-formules-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  margin-top: 0.85rem;
}

/* Interlignage SERRÉ (1.18) : c'est lui qui fait bloc typographique plutôt que liste à puces.
   Le corps reste au-dessus de la taille de lecture confortable — éditorial ne veut pas dire
   illisible, et ces trois mots sont une information tarifaire, pas un ornement. */
.coll-formule {
  font-family: var(--font-title);
  font-size: clamp(1.35rem, 2.5vw, 1.8rem);
  font-weight: 400;
  letter-spacing: 0.09em;
  line-height: 1.18;
  text-transform: uppercase;
  color: var(--color-text);
}

/* Prix d'accroche (#133, D3) — la donnée éditoriale DÉCLARÉE (`collections[].prixAccroche`),
   jamais calculée. Au tiroir SEULEMENT, juste avant les sorties : l'accueil fermé n'affiche aucun
   montant. Même famille que l'annonce des formules (Jost capitales espacées, encre muted-text) :
   une information, pas une accroche commerciale. Vide ⇒ la ligne n'est pas émise. */
.coll-drawer-prix {
  margin-top: 2rem;
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--color-muted-text);
}

/* Sorties COURTES (#133, E4) : « Les tarifs » toujours, « Le déroulement » quand la page de séance
   existe. Une rangée, pas une pile : deux mots chacun, ils referment le panneau ensemble.
   Le §2.2 n'en voulait qu'une (un panneau trop satisfaisant retient sur l'accueil au lieu
   d'envoyer vers la page poussée) : les deux ENVOIENT, aucune ne retient.
   FORME : `.btn-filet`, partagée avec les reportages et « En savoir plus » (24/08/26). Trois
   dessins ont été essayés puis écartés ICI, et le chemin vaut d'être gardé : deux filets (ils
   se confondaient avec le filet du titre et la règle des formules), un cartouche plein noir
   (lisible, mais un sixième dessin de bouton), puis le rectangle du hero (`.btn-outline`) —
   correct, mais posé au bas d'une colonne de texte étroite il faisait bloc et coupait la
   lecture. Le bouton à filet est la seule des quatre formes qui referme le paragraphe au lieu
   de l'interrompre. Cette règle ne porte QUE le placement. */
.coll-drawer-actions {
  display: flex;
  flex-wrap: wrap;
  column-gap: 2.5rem;
  row-gap: 0.5rem;
  margin-top: 2.2rem;
}
.coll-drawer-prix + .coll-drawer-actions { margin-top: 1rem; }

.coll-drawer-close:focus-visible,
.coll-drawer-cta:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

/* -------------------------------------------------------
   Reportages — double page de magazine
   Desktop : image plein-bleed à gauche (~55 %), colonne texte à droite. Mobile : une slide par
   écran, accrochée au scroll — le swipe fonctionne SANS JS. Le carrousel piloté (flèches,
   pastilles actives, substitution du compteur par masque vertical) arrive en phase 3.
   ------------------------------------------------------- */
.reportages-section { padding: 6rem 0; }

/* Une slide à la fois (§5.2). P2 empilait les 3 : la section mangeait 3,21 écrans (mesure
   Chrome 1440×900) et les flèches ne pilotaient rien du tout. Sous 900 px, le bloc responsive
   plus bas rebascule la piste en scroll-snap horizontal — là, les 3 slides redeviennent
   présentes et c'est le doigt qui navigue. */
.reportages-track {
  display: block;
  padding: 0 var(--gutter);
  max-width: var(--max-w);
  margin: 0 auto;
}
.reportage-slide:not(.is-active) { display: none; }

/* Repli SANS JS : aucune slide ne porte `is-active` dans le markup. Masquer les inactives sans
   ce repli cacherait les TROIS reportages sur une page dont le JS n'a pas tourné — et les
   crawlers rendent le CSS. Tant que rien n'est actif, la première reste visible. */
.reportages-track:not(:has(.is-active)) .reportage-slide:first-child { display: grid; }

.reportage-slide {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: clamp(2rem, 4vw, 4rem);
  align-items: center;
}

.reportage-media { overflow: hidden; }

/* PASSE-PARTOUT (24/08/26). La photo est POSÉE sur un carton clair qui la déborde de tous les
   côtés — le geste des carrousels Instagram de Joyce, et celui d'un tirage monté. Le fond de la
   section est le beige `--color-surface` ; le carton est le crème `--color-bg`, plus clair :
   c'est ce contraste de DEUX beiges, et non un cadre dessiné, qui fait lire le montage. Un
   cadre tracé aurait ajouté une ligne de plus à une section qui en compte déjà beaucoup.
   La marge est en `clamp` — elle grandit avec l'écran comme une vraie marge de tirage, au lieu
   de devenir un liseré sur grand écran et un bourrelet sur téléphone. */
.reportage-passe {
  background: var(--color-bg);
  padding: clamp(0.85rem, 1.7vw, 1.6rem);
}
.reportage-media img {
  display: block;   /* sans quoi la ligne de base de l'inline laisse un jour sous la photo */
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

/* La reserve des commandes (voir `.reportages-scene`). En `clamp` : sur un ecran court elle se
   resserre plutot que de pousser le texte hors du cadre. */
.reportage-body {
  display: flex;
  flex-direction: column;
  padding-bottom: clamp(2.5rem, 6vh, 4.5rem);
}

/* Compteur éditorial : le chiffre porte la composition, le dénominateur reste un filet.
   Le « 01 / 03 » uniforme en petites capitales se lit comme une pagination de site ; un grand
   numéral italique adossé à un petit total, c'est le geste de double page de magazine. La
   démesure est bornée par le clamp — exagérée, jamais criarde. */
.reportage-counter {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  /* PAS de marge négative ici : les chiffres elzeviriens de Cormorant descendent (3, 4, 5, 7,
     9) et leur jambage viendrait percuter les capitales du titre. Le « 01 » passait, le « 03 »
     non — un emboîtement calibré sur un seul chiffre n'est pas un emboîtement. La consigne est
     « exagéré mais maîtrisé » : le rapprochement se fait par le corps du titre, pas en faisant
     collisionner deux lettres. */
  margin-bottom: 0;
  font-family: var(--font-title);
  font-size: 0.95rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  color: var(--color-muted-text);
}
.reportage-counter-num {
  font-family: var(--font-title);
  font-size: clamp(4.2rem, 9.5vw, 8rem);
  font-style: italic;
  font-weight: 300;
  /* Cormorant compose des chiffres ELZEVIRIENS : le 3 et le 4 descendent sous la ligne de base,
     le 0 tient la hauteur d'x. C'est précisément ce qui rend la numérotation éditoriale — mais
     un line-height serré coupe le jambage, et le clip-path de l'animation finissait de le
     trancher. La boîte doit contenir le jambage, et le clip doit déborder. */
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--color-accent);
}

/* Le titre s'ADOSSE au numéral au lieu de le suivre : c'est l'interverrouillage des doubles
   pages de magazine (le « 05 » de LOOK A RULES). La marge négative ne dépend que du corps du
   titre (em), donc l'emboîtement tient à toutes les tailles — desktop comme mobile. */
.reportage-title {
  margin-top: 0.16em;
  font-family: var(--font-title);
  font-size: clamp(min(2.1rem, 8.96vw), 4.6vw, 3.6rem);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
}

.reportage-meta {
  margin-top: 0.6rem;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-muted-text);
}

/* Agrandie le 24/08/26 : c'est LA phrase qui vend le reportage, et elle était composée plus
   petit que la ligne d'accroche d'un panneau de collection. Même police, même italique — c'est
   le corps qui change, pas le registre. */
.reportage-teaser {
  margin-top: 1.35rem;
  font-family: var(--font-title);
  font-weight: 300;
  font-size: clamp(1.15rem, 1.45vw, 1.4rem);
  font-style: italic;
  line-height: 1.65;
  color: var(--color-text);
}

/* Geste signature du §5.2 : à chaque changement de slide, l'image et la colonne texte entrent
   à des VITESSES DIFFÉRENTES — c'est ce décalage qui fait la double page, pas un fondu. Le
   compteur, lui, se substitue par masque vertical. Les animations rejouent à chaque bascule
   parce que la slide sort de `display: none` : le navigateur relance l'animation tout seul. */
/* ═══ LE SWITCH — révision du 24/08/26 (retour Luca : « ça fait très flash ») ═══
   Ce qui n'allait pas : les deux moitiés de la slide glissaient de quelques rem en fondu
   d'opacité. Un fondu, c'est un REMPLACEMENT — la slide sortante et l'entrante se
   superposent une fraction de seconde et l'œil lit un clignotement, pas une arrivée.
   Ce qu'on veut à la place : un teaser d'histoire qui se pose devant le lecteur.

   Deux gestes, un seul vocabulaire — celui du site : le cache qui se retire.
   · La PHOTO se lève du bas, avec un contre-mouvement d'échelle : un tirage qui apparaît.
   · Le TEXTE se découvre de la gauche vers la droite, bloc après bloc et dans l'ordre de
     lecture — titre, lieu, accroche, bouton. En `linear` et non en courbe douce : c'est la
     régularité qui donne l'illusion de l'écriture ; une courbe qui ralentit en fin de course
     donnerait un volet, pas une phrase.

   POURQUOI PAS LIGNE À LIGNE : dévoiler un paragraphe ligne par ligne exigerait de le
   découper en lignes, ce qui dépend du rendu (police chargée ou non, largeur, césure). Ça
   casserait au premier redimensionnement. Le bloc se découvre donc d'un seul balayage.

   COÛT : `clip-path` et `transform` uniquement, composés par le GPU. Aucun JS par frame,
   aucune bibliothèque, aucun octet de plus. Aucun `will-change` (#140 D6) : posé sur la slide
   active, il réservait un calque À VIE — bien après la fin d'une animation de 1,15 s. Le moteur
   promeut de lui-même l'élément le temps que son `transform` s'anime. */
@keyframes jmpRevelePhoto {
  from { clip-path: inset(100% 0 0 0); transform: scale(1.05); }
  to   { clip-path: inset(0 0 0 0);    transform: scale(1); }
}
/* Le cadrage DÉBORDE la boîte (valeurs négatives) : sans ça il tranche les jambages de
   l'italique et les accents au moment précis où l'animation se termine. Même piège, même
   remède que le compteur ci-dessous. */
@keyframes jmpEcritLigne {
  from { clip-path: inset(-0.22em 100% -0.28em 0); }
  to   { clip-path: inset(-0.22em -0.08em -0.28em 0); }
}
@keyframes jmpSlideNum {
  /* Le clip d'arrivée DÉBORDE la boîte (valeurs négatives) : sinon il tranche le jambage des
     chiffres elzeviriens au moment même où l'animation se termine. */
  from { transform: translateY(0.55em); clip-path: inset(0 -10% 100% -10%); }
  to   { transform: none; clip-path: inset(-15% -10% -20% -10%); }
}

.reportage-slide.is-active .reportage-media img {
  animation: jmpRevelePhoto 1.15s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* Le geste du numéro est INCHANGÉ — c'est le seul que le retour du 24/08 valide tel quel. */
.reportage-slide.is-active .reportage-counter-num {
  display: inline-block;
  animation: jmpSlideNum 0.65s cubic-bezier(0.16, 1, 0.3, 1) 0.14s both;
}
/* Le « / 03 » n'est PAS animé, et c'est délibéré : il est identique sur les trois slides. Lui
   rejouer une apparition ferait annoncer un changement là où rien ne change. Il reste le point
   d'ancrage fixe — le folio de la page — sur lequel le reste vient s'inscrire. */
.reportage-slide.is-active .reportage-title  { animation: jmpEcritLigne 0.66s linear 0.22s both; }
.reportage-slide.is-active .reportage-meta   { animation: jmpEcritLigne 0.34s linear 0.52s both; }
.reportage-slide.is-active .reportage-teaser { animation: jmpEcritLigne 0.90s linear 0.64s both; }
.reportage-slide.is-active .reportage-cta    { animation: jmpEcritLigne 0.40s linear 0.95s both; }

/* Le réglage global plus haut dans cette feuille écrase la DURÉE (`animation-duration: 0.001ms`)
   mais pas le DÉLAI, et ces cinq animations sont déclarées `both` : l'état `from` s'applique
   pendant toute l'attente. Or les deux keyframes partent CLIPPÉS — jusqu'à 0,95 s d'invisibilité
   chez quelqu'un qui a demandé qu'on lui épargne le mouvement. Le shorthand tue durée ET délai ;
   c'est le patron du tronc (JMB `css/style.css`), porté ici le 2026-09-17.

   PAS d'`opacity: 1` ici, contrairement au tronc : l'invisibilité de départ vient du `clip-path`
   du keyframe, que `animation: none` retire — et ces éléments ne portent AUCUNE opacité de base
   à remonter. Le tronc en a besoin parce que SES keyframes partent à `opacity: 0`.

   POSITION : ce bloc doit rester APRÈS les quatre lignes ci-dessus. Il neutralise les sélecteurs
   animés eux-mêmes, donc à poids égal (0,3,0) — c'est la position qui tranche. Remonté dans le
   bloc `prefers-reduced-motion` général de la ligne ~1338, il serait servi et jamais appliqué.
   `css-reduced-motion.test.mjs` dérive cette exigence de la feuille et vérifie aussi l'ordre. */
@media (prefers-reduced-motion: reduce) {
  .reportage-slide.is-active .reportage-counter-num,
  .reportage-slide.is-active .reportage-title,
  .reportage-slide.is-active .reportage-meta,
  .reportage-slide.is-active .reportage-teaser,
  .reportage-slide.is-active .reportage-cta { animation: none; }
}

/* #140 D4 — section ARMÉE (bureau, sous le pli au chargement, posé par js/site.js) : les animations de
   la slide active sont tenues à leur première image. Remplissage `both` ⇒ l'état de départ s'applique,
   c'est l'état caché — il n'existe donc que si le script a armé. Il les libère à la 1ʳᵉ entrée, photo
   peinte. Une pause, pas une suppression : la même animation part de zéro, rien n'est redéclaré, et
   la règle ne fait rien là où l'animation n'existe pas (mouvement réduit, éditeur, sous 900 px). */
.reportages-section.is-armed .reportage-slide.is-active .reportage-media img,
.reportages-section.is-armed .reportage-slide.is-active .reportage-counter-num,
.reportages-section.is-armed .reportage-slide.is-active .reportage-title,
.reportages-section.is-armed .reportage-slide.is-active .reportage-meta,
.reportages-section.is-armed .reportage-slide.is-active .reportage-teaser,
.reportages-section.is-armed .reportage-slide.is-active .reportage-cta { animation-play-state: paused; }

/* ═══ LE BOUTON À FILET — forme partagée de toute la page d'accueil (24/08/26) ═══
   Né dans les reportages, étendu aux 5 panneaux de collection et à « En savoir plus » sur
   arbitrage de Luca le même jour. Le raisonnement de départ vaut pour les trois : ces boutons
   CLÔTURENT un paragraphe, dans une colonne de texte. Un rectangle encadré y fait bloc et
   coupe la lecture juste après l'accroche — c'est ce qu'on a vu sur le panneau de collection.
   La forme garde donc le registre du LIEN — libellé lettré, filet dessous — avec la cible et le
   geste d'un BOUTON : un filet clair court sur toute sa largeur, et un filet d'ENCRE le
   recouvre de gauche à droite. C'est « le cache qui se retire » du panneau réduit à un trait :
   le site décline son propre vocabulaire au lieu d'en inventer un de plus.

   `.btn-outline` (rectangle, disque d'accent) reste au HERO, et à lui seul : un bouton posé
   dans une photo plein écran n'a pas de colonne de texte à ménager, il a besoin d'un contour
   pour exister sur une image. Deux registres, deux formes — pas cinq.

   Le filet est peint par un `scaleX` et non par une largeur qui grandit : une largeur animée
   relance un reflow à chaque frame (contrat de build de cette page). */
.btn-filet {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  align-self: flex-start;
  gap: 0.55rem;
  min-height: 44px;
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-text);
}
.btn-filet-texte { transition: color 0.4s ease; }
.btn-filet-filet {
  position: relative;
  display: block;
  height: 1px;
  background: rgba(28, 25, 21, 0.22);
}
.btn-filet-filet::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
.btn-filet:hover .btn-filet-filet::after,
.btn-filet:focus-visible .btn-filet-filet::after { transform: scaleX(1); }
.btn-filet:hover .btn-filet-texte,
.btn-filet:focus-visible .btn-filet-texte { color: var(--color-accent); }

/* Les trois emplois ne portent QUE leur placement. Une seule d'entre elles qui redéclarerait
   une couleur ou une bordure suffirait à faire diverger un bouton des deux autres. */
.reportage-cta { margin-top: 1.9rem; }

.reportage-link { border-bottom: 1px solid transparent; transition: border-color var(--transition); }
.reportage-link:hover { border-color: currentColor; }

/* Navigation — inerte tant que la phase 3 ne l'a pas câblée, mais déjà dimensionnée et
   accessible : une cible tactile ne se corrige pas après coup. */
/* La navigation reprend EXACTEMENT la grille de la slide — mêmes colonnes, même gouttière,
   même boîte — pour que les commandes tombent sous la COLONNE DE TEXTE et non sous la photo.
   Posées à gauche sous l'image, elles flottaient : elles ne commentaient ni l'un ni l'autre.
   Une pagination de magazine se pose au pied de la colonne qu'on vient de lire.
   ⚠️ Les trois valeurs de grille sont recopiées de `.reportage-slide` : elles DOIVENT rester
   identiques, un décalage d'un pixel ici se voit immédiatement comme un défaut d'alignement. */
/* Les commandes REMONTENT au bas de la photo (24/08/26). Posees sous la composition, elles
   etaient a plus de 140 px du bas du cadre : l'oeil qui les cherche descend, et redescendre
   c'est quitter la zone ou l'animation se joue — on ne voyait donc plus le geste qu'on venait
   de declencher. La piste et la navigation partagent desormais UNE cellule de grille ; les
   commandes s'y calent en bas. Elles restent en colonne 2, donc jamais au-dessus de la photo.
   La colonne de texte RESERVE leur hauteur (`padding-bottom` du corps) : sans cette reserve
   elles se poseraient sur le texte des qu'il descend bas, et le texte, lui, remonte de la
   moitie de cette reserve — ce qui recentre le bloc face a la photo. */
.reportages-scene { display: grid; }
.reportages-track,
.reportages-nav { grid-area: 1 / 1; }
/* La piste REMPLIT sa cellule, comme la navigation (`width: 100%` plus bas) — #140 RETOUR 4, T2.
   Un élément de grille à `margin: auto` ne s'étire pas : la piste prenait la largeur max-content de
   la slide affichée. Photo pas encore chargée (largeur naturelle nulle) et texte court, elle
   tombait à 838 px au lieu de 1014 à 1440, et la photo en 4/5 perdait 120 px de haut — tout ce qui
   suit remontait, sans défilement (mesuré : slide 2, 1re visite, Chromium et WebKit). Photos
   chargées, la mise en page est inchangée (3 moteurs, 901 à 1920 px) : la piste atteignait déjà sa
   largeur pleine, à 0,1 px près à 901 px. */
.reportages-track { width: 100%; }
.reportages-nav {
  align-self: end;
  /* Le conteneur couvre les DEUX colonnes, dont celle de la photo : sans cette neutralisation
     il intercepterait les clics sur le bas du cadre. Seules les commandes restent cliquables. */
  pointer-events: none;
}
.reportages-commandes { pointer-events: auto; }

.reportages-nav {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: clamp(2rem, 4vw, 4rem);
  padding: 0 var(--gutter);
  max-width: var(--max-w);
  margin: 0 auto;
  width: 100%;
}
.reportages-commandes {
  grid-column: 2;
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

/* Les flèches étaient les caractères « ← » et « → » posés à 55 % d'opacité. Un glyphe hérite de
   la graisse et de la chasse de sa police : il ne se règle pas, et il change d'aspect d'un
   système à l'autre. Elles deviennent un DISQUE FILAIRE contenant un trait dessiné de 1 px, qui
   se remplit d'encre au survol par le même disque que le bouton du hero — le site décline son
   vocabulaire au lieu d'en inventer un. Le cercle donne aussi la cible tactile sans l'inventer. */
.reportages-prev,
.reportages-next,
.temoignages-prev,
.temoignages-next {
  position: relative;
  overflow: hidden;
  width: 46px;
  height: 46px;
  min-width: 46px;
  min-height: 46px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(28, 25, 21, 0.18);
  border-radius: 50%;
  color: var(--color-text);
  transition: color 0.35s ease 0.08s, border-color 0.35s ease 0.08s;
  z-index: 0;
}
.reportages-prev::after,
.reportages-next::after,
.temoignages-prev::after,
/* Remplissage en TERRE CUITE et non en encre (24/08/26) : c'est l'accent du site, et le disque
   du bouton du hero se remplit déjà de cette couleur-là. Un disque noir faisait de ces deux
   commandes le seul geste de la page à ne pas parler la couleur de marque.
   Crème sur #8A5835 = 5,43:1 (calculé) : la flèche reste lisible une fois le disque plein. */
.temoignages-next::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-accent);
  clip-path: circle(0% at 50% 50%);
  transition: clip-path 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: -1;
}
.reportages-prev:hover::after,
.reportages-next:hover::after,
.temoignages-prev:hover::after,
.temoignages-next:hover::after { clip-path: circle(71% at 50% 50%); }
.reportages-prev:hover,
.reportages-next:hover,
.temoignages-prev:hover,
.temoignages-next:hover { color: var(--color-bg); border-color: transparent; }
.nav-fleche { display: block; width: 25px; height: 16px; }

/* Trois traits séparés par un écart, dont un s'allumait : ça se lit comme trois cases à cocher.
   Devenus UN RAIL CONTINU sur lequel un segment d'encre GLISSE d'un tiers à l'autre — un seul
   objet, et le déplacement raconte le sens de la navigation au lieu de simplement la signaler.
   Le rail est en `transform` (composé par le GPU) et sa largeur vient du nombre de tirages baké
   dans le markup : il a la bonne taille au premier rendu, sans attendre le script. */
/* `flex: 1` : le rail file jusqu'au bord droit de la colonne au lieu de rester un moignon de
   132 px collé aux flèches. Le segment d'encre parcourt alors toute la largeur du texte —
   le geste devient ample, et chaque tiers dépasse largement la cible tactile de 44 px. */
.reportages-dots {
  position: relative;
  flex: 1;
  display: flex;
  gap: 0;
}

.reportages-dot {
  min-width: 44px;
  min-height: 44px;
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
/* Le trait clair du rail. Pleine largeur du tiers et sans écart : les trois se touchent, donc
   ils forment une ligne, pas une série. La cible tactile reste à 44 px autour. */
.reportages-dot::after {
  content: '';
  width: 100%;
  height: 1px;
  background: var(--color-text);
  opacity: 0.16;
  transition: opacity var(--transition);
}
/* Le tiers actif s'efface SOUS le segment d'encre : sans ça, deux traits se superposent et le
   rail paraît plus épais à un endroit. */
.reportages-dot.is-active::after { opacity: 0; }

.reportages-rail {
  position: absolute;
  top: 50%;
  left: 0;
  margin-top: -0.5px;
  height: 1px;
  width: calc(100% / var(--reportage-n, 3));
  background: var(--color-text);
  transform: translateX(calc(var(--reportage-i, 0) * 100%));
  transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}

/* Aligne sur `.btn-filet` le 24/08/26, alors qu'il est encore ETEINT (aucun reportage publie).
   Cette regle ne porte que le placement : il est le seul bouton de la page a vivre hors d'une
   colonne de texte, d'ou la marge au gouttiere. */
.reportages-cta { margin: 2rem var(--gutter) 0; }

.reportages-prev:focus-visible,
.reportages-next:focus-visible,
.reportages-dot:focus-visible,
.reportage-cta:focus-visible,
.reportages-cta:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

@media (max-width: 900px) {
  /* Une slide ≈ un écran, swipe horizontal — en CSS pur, donc valable sans JS.
     Ici le doigt navigue : aucune slide n'est masquée, elles doivent toutes être présentes
     pour que le scroll-snap ait de quoi accrocher. */
  .reportage-slide:not(.is-active) { display: grid; }
  .reportages-track {
    display: flex;
    flex-direction: row;
    /* Les slides se TOUCHAIENT : la dernière ligne de l'accroche du 01 arrivait au contact du
       titre du 02, qu'on aperçoit volontairement pour appeler le swipe. Le teasing ne marche
       que s'il reste lisible comme une slide SUIVANTE, pas comme la suite du paragraphe. */
    gap: clamp(1.75rem, 7vw, 3rem);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .reportages-track::-webkit-scrollbar { display: none; }
  .reportage-slide {
    grid-template-columns: 1fr;
    gap: 1.25rem;
    flex: 0 0 100%;
    scroll-snap-align: center;
  }
  .reportage-media img { aspect-ratio: 3 / 4; }

  /* La colonne image est BORNEE, pas etiree a la largeur de la slide. Sans borne, a 768 px la
     photo faisait 768 x 1024 — une image d'un ecran entier — alors que dans la MEME bande la
     planche et les cartes du portfolio restent autour de 350 x 470. C'est cette disproportion
     que Luca a vue le 24/08/26, et elle ne concerne pas que la tablette : elle vaut pour toute
     fenetre de 600 a 900 px.
     C'est la LARGEUR qu'on borne, jamais le rapport : le 3/4 est la grammaire d'image du site.
     La borne est inoperante en dessous de 24rem de large, donc le rendu telephone — valide le
     24/08 — ne bouge pas d'un pixel. Le corps de texte prend la MEME borne : desaligne de la
     photo, il ferait retomber la double page en bloc de blog. */
  .reportages-section { --reportage-borne: 24rem; }
  .reportage-media,
  .reportage-body,
  .reportages-commandes {
    width: 100%;
    max-width: var(--reportage-borne);
    margin-inline: auto;
  }
  /* La grille de la nav n'a plus de colonne de texte à suivre : la slide est passée à une
     colonne. Les commandes se recalent sur la même borne que la photo et le corps — sans ça
     elles traverseraient tout l'écran sous une composition large de 24rem. */
  /* Plus de superposition : la piste defile au doigt, des commandes posees par-dessus
     tomberaient au milieu d'une slide en cours de glissement. Elles reprennent leur place
     SOUS la composition, et la reserve du corps n'a plus d'objet. */
  .reportages-scene { display: block; }
  .reportages-nav {
    grid-template-columns: 1fr;
    padding-top: 1.75rem;
  }
  .reportages-commandes { grid-column: 1; }
  .reportage-body { padding-bottom: 0; }

  /* AUCUNE animation d'entrée au doigt (règle Lumyor gravée le 24/08/26). Deux raisons, et la
     seconde suffirait seule : la piste est un scroll-snap CSS où les TROIS slides sont
     présentes — animer « celle qui est active » ferait rejouer un dévoilement sur une slide
     que le doigt est en train de faire glisser, à contretemps du geste réel. Et un dévoilement
     par clip-path sur une image plein écran promeut un calque pendant tout le défilement. */
  .reportage-slide.is-active .reportage-media img,
  .reportage-slide.is-active .reportage-counter-num,
  .reportage-slide.is-active .reportage-title,
  .reportage-slide.is-active .reportage-meta,
  .reportage-slide.is-active .reportage-teaser,
  .reportage-slide.is-active .reportage-cta {
    animation: none;
    will-change: auto;
  }
}
/* Sous 300 px SEULEMENT (#95) : 2 flèches de 46 px + 2 écarts de 20 px + 3 tiers du rail à 44 px
   minimum = 264 px incompressibles — à 260-280 px la PAGE s'élargissait à 288 px (mesuré Chromium),
   étirant avec elle l'en-tête fixe et poussant le hamburger hors de l'écran. Les tiers partagent la
   place restante (26,7 px à 260 px, cible toujours ≥ 24 px, 44 px de haut) ; les flèches ne bougent pas. */
@media (max-width: 299.98px) {
  .reportages-dot { min-width: 0; }
}

/* -------------------------------------------------------
   Témoignages — une citation à l'écran
   Les 3 figures occupent la MÊME cellule de grille : seule celle qui porte .is-active est
   visible. L'état initial est correct sans JS (la première s'affiche). La rotation et le
   masque vertical arrivent en phase 3.
   ------------------------------------------------------- */
.temoignages-section { padding: 5rem 0; }

/* Composition alignée à gauche, pas centrée : une citation centrée se lit comme un bandeau de
   site ; adossée à une marge, elle se lit comme un bloc de magazine. La scène reste bornée en
   largeur pour que la ligne ne dépasse jamais la longueur de lecture confortable. */
.temoignages-stage {
  display: grid;
  padding: 0 var(--gutter);
  max-width: var(--max-w);
  margin: 0 auto;
  text-align: left;
}
/* Largeur de lecture en rem, pas en ch : `ch` se calcule sur la police du CONTENEUR (le corps
   à 16 px), pas sur le grand Cormorant de la citation — 46ch ne donnait que 368 px et cassait
   une phrase courte en quatre lignes. 36rem tient les trois citations réelles en deux lignes. */
.temoignage { max-width: 36rem; }

/* Passage de relais, pas fondu croisé. Les 3 citations partagent la même cellule : avec une
   durée identique dans les deux sens, la sortante et l'entrante sont toutes deux à mi-opacité
   au même instant et on lit les deux textes superposés. La sortante part donc VITE et sans
   délai ; l'entrante attend qu'elle ait disparu avant de monter. */
.temoignage {
  grid-area: 1 / 1;
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.26s;
}
.temoignage:not(.is-active) {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease 0s;
}

.temoignage-quote {
  font-family: var(--font-title);
  font-size: clamp(1.3rem, 3vw, 2rem);
  font-style: italic;
  font-weight: 300;
  line-height: 1.45;
}

/* Guillemet ouvrant en pleine chasse, posé AU-DESSUS de la citation — traitement de pull-quote
   de magazine. Il vit hors du masque vertical, donc il reste immobile pendant que le texte
   monte : c'est la citation qui bouge, pas l'ornement.
   ⚠️ Choix créatif — le « » français plutôt que le “ ” anglo-saxon, cohérent avec la langue du
   site. Une ligne à changer si Joyce préfère l'autre. */
.temoignage-quote::before {
  /* Guillemet ouvrant anglais (U+201C) : ses deux virgules hautes donnent le geste de
     pull-quote de magazine, là où le chevron français « se lit comme un signe de navigation.
     ⚠️ Choix créatif — une ligne à changer si Joyce préfère le « ». */
  content: '\201C';
  display: block;
  font-family: var(--font-title);
  font-weight: 300;
  font-size: clamp(4rem, 8vw, 7rem);
  font-style: normal;
  line-height: 0.62;
  color: var(--color-accent);
  opacity: 0.65;
  margin-bottom: -0.35rem;
}

/* Masque vertical des citations (§5.5) — MÊME geste que le H1, pas un effet de plus.
   #140 D5 — l'état masqué n'existe que sous `.temoignages-stage.is-armed`, que SEUL js/site.js pose
   (rotation démarrée, ou section armée sous le pli). Avant #140 il vivait sur `.temoignage
   .word-inner` nu et attendait le script pour remonter : sans JS, la citation restait sous son
   masque (C9 ext.). `is-attente` (bureau, avant l'entrée) tient aussi l'active sous son masque, SANS
   transition : armer ne fait rien glisser ; l'horloge vit sur l'état d'arrivée (C143). Sélecteurs
   CONTEXTUELS, jamais nus — c'est la règle posée à la Task 1. */
.temoignages-stage.is-armed .temoignage .word-inner { transform: translateY(100%); }
.temoignages-stage.is-armed:not(.is-attente) .temoignage .word-inner {
  /* Même délai que l'opacité : le texte ne remonte qu'une fois la citation précédente partie. */
  transition: transform 0.62s cubic-bezier(0.16, 1, 0.3, 1) 0.26s;
}
.temoignages-stage.is-armed:not(.is-attente) .temoignage.is-active .word-inner { transform: none; }

/* Filet vertical plutôt que tiret cadratin : le trait reprend la verticale des filets de la
   planche et du portfolio. Le tiret venait du renderer — il en a été retiré, un caractère de
   ponctuation ne devant pas figer une décision de style dans la donnée rendue. */
.temoignage-author {
  border-left: 1px solid var(--color-accent);
  padding-left: 0.85rem;
  margin-top: 1.5rem;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-muted-text);
}

.temoignages-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 2rem;
}

.temoignages-dots { display: flex; gap: 0.15rem; }

.temoignages-dot {
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.temoignages-dot::after {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-text);
  opacity: 0.25;
  transition: opacity var(--transition);
}
.temoignages-dot.is-active::after { opacity: 1; }

.temoignages-prev:focus-visible,
.temoignages-next:focus-visible,
.temoignages-dot:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

@media (max-width: 600px) {
  /* #140 D7 — PLUS de plafond de hauteur sur la scène. Le « budget de scroll » (§5.5 : un tiers
     d'écran, `max-height: 30vh; overflow: hidden`) tronquait la citation sans le dire : mesuré sur la
     page servie, 3 moteurs, la longueur max du Studio (citation 240, prénom 40, contexte 60) perdait
     172 px à 320×568, 94 px à 375×667, 41 px à 390×844. Les citations publiées tiennent (167 px dans
     170 à 320) — de 3 px, une citation un peu plus longue de Joyce aurait été coupée en ligne.
     La scène prend la hauteur de la plus longue citation, rien n'est jamais caché. */
  .temoignages-section { padding: 3.5rem 0; }
}

/* -------------------------------------------------------
   Doctrine tactile Lumyor — le mouvement ne dépend JAMAIS du survol
   -------------------------------------------------------
   Au tap, Safari iOS active un `:hover` collant : le zoom est rejoué et RESTE, avec son repaint.
   Cette feuille ne portait AUCUNE `@media (hover: none)` alors que `.category-card:hover
   .card-image img` (l. 694) zoome depuis toujours — une page où certaines images zooment au tap
   et d'autres non serait une incohérence visible. Ce bloc couvre donc la carte EXISTANTE ; les
   surfaces du Lot 2 portent leurs propres règles dans css/portfolio-univers.css.

   Même spécificité que la règle neutralisée (0,3,1) : c'est la POSITION en fin de feuille qui
   fait gagner celle-ci, pas son poids. `:where()` maintient le garde à spécificité nulle.
   La surcharge par attribut joue DANS LES DEUX SENS : en aperçu, c'est l'appareil VISÉ qui
   décide, jamais le pointeur de l'hôte — même contrat que `data-jmb-preview-pointer` sur
   `.lb-gallery` (l. 944). Le nom porte `jmb` par héritage de fork : c'est un contrat technique
   éditeur↔page, pas une identité de marque ; le renommer casse l'aperçu. */
@media (hover: none) {
  :where(html:not([data-jmb-preview-pointer])) .category-card:hover .card-image img { transform: none; }
}
html[data-jmb-preview-pointer="coarse"] .category-card:hover .card-image img { transform: none; }
