/* ==========================================================================
   Cybersecurity-Law.be — feuille de style unique
   Aucune requête externe (pas de CDN, pas de Google Fonts) : conforme RGPD.
   ========================================================================== */

:root {
  /* Encres */
  --ink: #16130F;
  --ink-soft: #5D524B;
  --ink-faint: #9A8F84;

  /* Fonds pastel (palette dérivée de microsoft.ai) */
  --paper: #FEF9ED;
  --cream: #FEF9ED;
  --beige: #F7ECD9;
  --sand: #F7ECD9;
  --butter: #F6EBC6;
  --peach: #FBD3BE;
  --peach-deep: #F4C8AE;
  --blush: #F4D4D2;
  --sage: #E3EFDD;
  --sage-deep: #D7E6D6;
  --mist: #E6EFEE;

  --espresso: #2E2620;
  --accent: #B0533A;
  --accent-hover: #8E402C;
  --line: rgba(93, 82, 75, .18);
  --line-soft: rgba(93, 82, 75, .1);


  /* ---- Gamme typographique, rapport 1,2 (tierce mineure), base 15 px ----
     Toutes les tailles du site sont des degres de cette gamme. Deux blocs
     peuvent partager un degre : ils se distinguent alors par la graisse ou
     la police, jamais par un ecart d'un ou deux pixels — trop petit pour
     se lire comme une intention, assez grand pour desunir la page. */
  --t-3: .54rem;    /*   8,7 px */
  --t-2: .65rem;    /*  10,4 px */
  --t-1: .78rem;    /*  12,5 px */
  --t0:  .94rem;    /*  15   px */
  --t1:  1.125rem;  /*  18   px */
  --t2:  1.35rem;   /*  21,6 px */
  --t3:  1.62rem;   /*  25,9 px */
  --t4:  1.95rem;   /*  31,1 px */
  --t5:  2.33rem;   /*  37,3 px */
  --t6:  2.8rem;    /*  44,8 px */
  --t7:  3.36rem;   /*  53,8 px */
  --t8:  4.03rem;   /*  64,5 px */
  --t9:  4.84rem;   /*  77,4 px */
  --t10: 5.81rem;   /*  92,9 px */

  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, "Times New Roman", serif;
  --sans: "Segoe UI Variable Display", "Segoe UI", Inter, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;

  --gutter: clamp(20px, 5vw, 72px);
  --maxw: 1320px;
  --radius: 20px;
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

/* Grain papier sur tout le document (SVG inline, aucune requête réseau) */
body::after {
  content: ""; position: fixed; inset: 0; z-index: 200; pointer-events: none;
  opacity: .055; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
}

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.narrow { max-width: 900px; }

/* ---------- Typographie ---------- */

.display {
  font-family: var(--serif);
  font-weight: 400;
  letter-spacing: -.035em;
  line-height: .96;
  margin: 0;
  font-size: clamp(var(--t5), 6.6vw, var(--t10));
}
.display--sm { font-size: clamp(var(--t4), 4.3vw, var(--t7)); line-height: 1.02; }

h2, h3 { font-family: var(--serif); font-weight: 400; letter-spacing: -.025em; margin: 0; }

.eyebrow {
  font-family: var(--sans);
  font-size: var(--t-2);
  letter-spacing: .18em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-faint);
  margin: 0 0 1.6rem;
}

.lede { font-size: clamp(var(--t0), 1.5vw, var(--t1)); line-height: 1.5; color: var(--ink-soft); }
.lede b { font-weight: 600; }

p { margin: 0 0 1.15em; }
p:last-child { margin-bottom: 0; }

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

.btn {
  display: inline-flex; align-items: center; gap: .55em;
  padding: .95em 1.7em;
  border-radius: 999px;
  font-size: var(--t-1); font-weight: 550;
  text-decoration: none;
  border: 1px solid transparent;
  transition: background-color .25s, color .25s, border-color .25s, transform .25s;
}
.btn:hover { transform: translateY(-1px); }
.btn--solid { background: var(--ink); color: var(--paper); }
.btn--solid:hover { background: var(--accent); }
.btn--ghost { border-color: var(--line); color: var(--ink); }
.btn--ghost:hover { border-color: var(--ink); background: rgba(22,19,15,.04); }
/* Les fleches ont ete retirees de tous les boutons ; leur regle de survol
   part avec elles. Le `gap` de `.btn` reste utile : d'autres boutons portent
   encore une icone. */

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

/* Barre flottante plutot que bandeau pleine largeur : elle se detache du
   bord de l'ecran et laisse voir le lavis passer derriere elle. */
/* La barre ne prend plus de hauteur dans le flux : la marge negative annule
   la sienne, si bien que le heros commence en haut de page et que le lavis
   anime passe DERRIERE la pilule. C'est ce mouvement qu'on voit au travers. */
.nav {
  position: sticky; top: .7rem; z-index: 60;
  padding-inline: var(--gutter);
  margin-bottom: calc(-62px - .7rem);
  background: none; border: 0;
}
.nav__in {
  position: relative;
  max-width: var(--maxw); margin-inline: auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 2rem; height: 62px;
  padding-inline: clamp(1rem, 2.2vw, 1.75rem);
  border-radius: 999px;
  /* Assez transparente pour laisser voir le lavis bouger derriere, assez
     opaque pour que les liens restent lisibles quand la barre passe sur la
     bande sombre. Le flou empeche le fond de concurrencer le texte. */
  background: rgba(254, 249, 237, .6);
  border: 1px solid var(--line-soft);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  transition: background-color .35s, border-color .35s, box-shadow .35s;
}
/* Au defilement, la pilule s'affirme legerement au lieu de tracer un filet. */
.nav.is-stuck .nav__in {
  background: rgba(254, 249, 237, .88);
  border-color: var(--line);
  box-shadow: 0 14px 34px -22px rgba(22, 19, 15, .45);
}
/* Marque : le mot dans le serif du site, encadre de deux chevrons dont les
   trois sommets se terminent par un cercle ouvert. Le motif reprend le
   glyphe noeuds-liens du logo d'origine, qui dessinait deja un `<`.
   En em, donc solidaire de la taille du texte. */
.brand {
  font-family: var(--serif); font-size: var(--t1); letter-spacing: -.03em;
  text-decoration: none; white-space: nowrap;
}
.brand span { color: var(--accent); }
/* En inline-block et non en flex : un conteneur flex ferait de chaque
   fragment de texte un element separe, et le `gap` s'insererait aussi
   entre « Cybersecurity », le trait d'union et « Law ». */
.brand::before, .brand::after {
  content: ""; display: inline-block; vertical-align: -.04em;
  width: .82em; height: .82em;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23B0533A' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M17.1 5.2C11.4 7.9 8 9.9 6.7 12c1.3 2.1 4.7 4.1 10.4 6.8'/%3E%3Ccircle cx='19.5' cy='4.1' r='2.2'/%3E%3Ccircle cx='19.5' cy='19.9' r='2.2'/%3E%3Ccircle cx='4.2' cy='12' r='2.2'/%3E%3C/svg%3E") no-repeat center / contain;
  transition: transform .45s cubic-bezier(.16, 1, .3, 1);
}
.brand::before { margin-right: .42em; }
.brand::after { margin-left: .42em; transform: scaleX(-1); }
/* Au survol, les chevrons s'ecartent legerement du mot. */
.brand:hover::before { transform: translateX(-.12em); }
.brand:hover::after { transform: scaleX(-1) translateX(-.12em); }
@media (prefers-reduced-motion: reduce) {
  .brand::before, .brand::after { transition: none; }
  .brand:hover::before { transform: none; }
  .brand:hover::after { transform: scaleX(-1); }
}
.nav__links { display: flex; align-items: center; gap: 2.1rem; }
.nav__links a { text-decoration: none; font-size: var(--t-1); color: var(--ink-soft); transition: color .2s; }
.nav__links a:hover, .nav__links a[aria-current="page"] { color: var(--ink); }

/* Soulignement qui se deploie depuis la gauche, et se retire vers la droite
   quand on quitte le lien — le trait suit la lecture dans les deux sens.
   Le bouton d'appel a l'action en est exclu : il a deja sa forme. */
.nav__links a:not(.nav__cta) { position: relative; }
.nav__links a:not(.nav__cta)::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -.42em;
  height: 1px; background: currentColor;
  transform: scaleX(0); transform-origin: right;
  transition: transform .42s cubic-bezier(.16, 1, .3, 1);
}
.nav__links a:not(.nav__cta):hover::after,
.nav__links a:not(.nav__cta)[aria-current="page"]::after {
  transform: scaleX(1); transform-origin: left;
}
@media (prefers-reduced-motion: reduce) {
  .nav__links a:not(.nav__cta)::after { transition: none; }
}
.nav__cta { padding: .62em 1.25em; font-size: var(--t-1); }
/* En blanc, et avec la specificite necessaire : `.nav__links a` (0,2,0)
   l'emportait sur `.btn--solid` (0,1,0) et imposait au bouton le brun des
   liens sur son fond noir — 2,45 de contraste, illisible. */
.nav__links .nav__cta,
.nav__links .nav__cta:hover { color: #fff; }

.burger { display: none; background: none; border: 0; padding: 8px; cursor: pointer; }
.burger span { display: block; width: 22px; height: 1.5px; background: var(--ink); margin: 5px 0; transition: transform .3s, opacity .3s; }
.nav.is-open .burger span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav.is-open .burger span:nth-child(2) { opacity: 0; }
.nav.is-open .burger span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

@media (max-width: 860px) {
  .burger { display: block; }

  /* --- La barre, pleine largeur ---------------------------------------
     Sur telephone la pilule flottante coute cher : elle mange une gouttiere
     de chaque cote, ce qui resserre la marque et le bouton sur une largeur
     deja etroite. Elle se cale donc bord a bord, sans rayon.
     Le filet ne subsiste qu'en bas : une bordure sur les quatre cotes
     tracerait deux traits verticaux au ras de l'ecran, la ou il n'y a plus
     rien a delimiter. */
  .nav { top: 0; padding-inline: 0; margin-bottom: -62px; }
  .nav__in {
    max-width: none;
    border-radius: 0;
    border-width: 0 0 1px;
    padding-inline: var(--gutter);
  }
  .nav.is-stuck .nav__in { box-shadow: 0 10px 26px -20px rgba(22, 19, 15, .45); }

  .nav__links {
    /* Le menu suit la barre : plus de carte flottante posee dessous, mais un
       panneau accroche a son bord inferieur, de la meme largeur qu'elle.
       `top: 100%` et non `calc(100% + .55rem)` : le moindre jeu laisserait
       voir la page entre la barre et son menu. */
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: flex-start;
    gap: 0; padding: 1.1rem var(--gutter) 1.6rem;
    /* Opaque : ce panneau recouvre le contenu, et le titre du heros
       transparaissait au travers. */
    background: var(--paper); border: 1px solid var(--line-soft);
    border-width: 0 0 1px;
    border-radius: 0;
    box-shadow: 0 26px 50px -26px rgba(22, 19, 15, .4);
    backdrop-filter: saturate(180%) blur(16px);
    -webkit-backdrop-filter: saturate(180%) blur(16px);
    transform-origin: top center;
    transform: translateY(-12px) scale(.97); opacity: 0; pointer-events: none;
    transition: transform .4s cubic-bezier(.16,1,.3,1), opacity .3s ease;
  }
  .nav.is-open .nav__links { opacity: 1; pointer-events: auto; }
  .nav.is-open .nav__links { transform: none; }
  .nav__links a { font-family: var(--serif); font-size: var(--t2); padding: .5rem 0; letter-spacing: -.02em; }
  /* La regle ci-dessus vise TOUS les liens du menu, bouton compris : elle lui
     retirait ses marges laterales et le texte touchait les deux bords de la
     pilule. On les lui rend — un peu resserrees par rapport au `.btn` de base,
     la police etant ici plus petite. */
  /* `.nav__links a.nav__cta` et non `.nav__cta` seul : a specificite egale ou
     moindre, la regle ci-dessus l'emporte et le retrait reste a zero. */
  .nav__links a.nav__cta {
    margin-top: 1rem;
    padding: .8em 1.5em;
    font-family: var(--sans) !important; font-size: var(--t-1) !important;
  }
}

/* ---------- Héros ---------- */

/* Le retrait haut integre la hauteur de la barre, qui flotte par-dessus. */
.hero {
  position: relative; overflow: hidden;
  padding: calc(62px + .7rem + clamp(46px, 7vw, 96px)) 0 clamp(60px, 9vw, 120px);
}
.hero::before {
  content: ""; position: absolute; inset: -30% -10% auto -10%; height: 130%;
  background:
    radial-gradient(44% 60% at 88% 10%, var(--peach-deep) 0%, transparent 70%),
    radial-gradient(42% 58% at 60% 48%, var(--butter) 0%, transparent 70%),
    radial-gradient(38% 55% at 8% 12%, var(--sage) 0%, transparent 68%),
    radial-gradient(34% 50% at 97% 88%, var(--blush) 0%, transparent 68%),
    radial-gradient(34% 50% at 26% 92%, var(--mist) 0%, transparent 68%),
    radial-gradient(30% 46% at 44% 4%, var(--blush) 0%, transparent 66%);
  filter: blur(26px);
  z-index: -3;
  animation: drift 30s ease-in-out infinite alternate;
}

/* --- Heros court -------------------------------------------------------
   Le lavis du heros est dimensionne en pourcentage de la section. Sur
   l'accueil, haute de 872 px, les six nappes ont la place de se fondre.
   Sur Services, haute de 625 px, la meme pile est comprimee d'un tiers :
   les nappes se resserrent et leurs bords se lisent comme deux barres
   contrastees, en haut et en bas.
   Un plancher en pixels lui rend son etalement.
   Ce degrade sert de repli quand le canvas ne peint pas ; les barres que
   l'on voyait, elles, venaient du canvas lui-meme — voir `.hero__canvas`.
   L'accueil n'est pas concerne : 130 % y valent deja 1134 px.            */
.hero--court::before {
  height: max(130%, 900px);
}
/* Et le lavis SVG s'eteint avant les bords.
   C'est lui, et non le canvas ni le degrade, qui faisait les barres : pose
   en `multiply` par-dessus le lavis anime, il est cadre en `cover` sur une
   boite aux marges fixes en pourcentage. Sur un heros court cette boite se
   resserre, ses zones denses arrivent au ras du haut et du bas de la
   section, et la multiplication y creuse deux bandes franches.
   Verifie couche par couche : ni le canvas seul ni le SVG seul ne les
   produit — c'est leur superposition aux extremites qui les fabrique.
   Le fondu supprime la cause : plus rien a multiplier sur les bords.     */
.hero--court::after {
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 14%, #000 82%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 14%, #000 82%, transparent 100%);
}
/* Lavis anime (canvas 2D). Absent si le navigateur ne le supporte pas :
   le degrade CSS ci-dessus fait alors office de repli. */
.hero__canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: -2; display: block; pointer-events: none;
  filter: blur(7px) saturate(1.12);
  /* Debordement porte de 1,05 a 1,18.
     Le lavis est peint sur un canvas de basse resolution — 317 x 142 px
     pour 1600 px de large — puis etire d'un facteur cinq. Ses bords, deja
     adoucis a la peinture, le sont donc cinq fois plus a l'ecran, et le
     flou CSS de 7 px s'y ajoute. A 1,05 le debordement ne faisait que
     18 px : la zone de fondu depassait, on voyait le lavis s'eteindre et
     le degrade pale reapparaitre dessous — deux barres contrastees en haut
     et en bas de la section. A 1,18 il y a 64 px de marge, et le fondu
     reste hors cadre. */
  transform: scale(1.18);
  opacity: .96;
}
/* Quand le canvas peint, le degrade CSS et le lavis SVG s'effacent :
   la couleur vient desormais du canvas, le SVG ne garde qu'un role de grain. */
.hero.hero--live::before { opacity: .3; }
.hero.hero--live::after  { opacity: .3; }
/* Lavis aquarelle du héros — couvre toute la bande, à toute largeur d'écran */
.hero::after {
  content: ""; position: absolute; z-index: -1;
  inset: -20% -12% -14% -12%;
  background: url("art/hero.svg?v=ffc72561") no-repeat center center / cover;
  opacity: .7; mix-blend-mode: multiply; pointer-events: none;
  animation: float 34s ease-in-out infinite alternate;
}
@keyframes drift { to { transform: translate3d(-3%, 2%, 0) scale(1.06); } }
@keyframes float { to { transform: translate3d(-2.5%, 3%, 0) rotate(-2.5deg) scale(1.05); } }
@media (prefers-reduced-motion: reduce) {
  .hero::before, .hero::after { animation: none; }
}
@media (max-width: 700px) { .hero::after { opacity: .6; right: -30%; top: -6%; } }

/* Voile de lisibilite.
   La composition du lavis est aleatoire a chaque chargement : sans ce voile,
   le contraste du texte dependrait du tirage. Un degrade creme couvre la
   colonne de texte et s'efface vers la droite, ou le lavis reste pleinement
   visible. Le contraste devient ainsi garanti, quel que soit le tirage. */
.hero--live .wrap { position: relative; }
.hero--live .wrap::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  inset: -18% -24% -20% -26%;
  /* Halo elliptique et non rectangle : un degrade lineaire dans une boite
     laisse des aretes franches en haut, en bas et a gauche, ou l'opacite
     tombe d'un coup a zero. Ici l'attenuation est douce dans toutes les
     directions, donc le voile ne se voit pas comme une forme. */
  background:
    radial-gradient(ellipse 66% 60% at 30% 48%,
      rgba(254, 249, 237, .62) 0%,
      rgba(254, 249, 237, .44) 42%,
      rgba(254, 249, 237, .16) 70%,
      rgba(254, 249, 237, 0) 88%),
    /* seconde ellipse, basse et large : la ligne de reperes s'etend au-dela
       de la premiere. Faible opacite, donc invisible comme forme. */
    radial-gradient(ellipse 88% 30% at 42% 86%,
      rgba(254, 249, 237, .34) 0%,
      rgba(254, 249, 237, .20) 50%,
      rgba(254, 249, 237, 0) 84%);
}

/* Le surtitre et la ligne de reperes s'etendent au-dela du voile, sur le
   lavis nu : ils passent a l'encre pleine. Mesure sur leur fond le plus
   sombre : 1,3:1 et 3,2:1 avec les gris d'origine, 7,6:1 et 7,9:1 ici. */
.hero .eyebrow,
.hero .lede,
.hero .hero__meta { color: var(--ink); }

/* ---------- Echelle typographique du heros ----------
   Une gamme de rapport 1,2 (tierce mineure) plutot que six tailles posees
   au jugé. Trois degres seulement : 12,5 / 15 / 18 px. Deux blocs peuvent
   partager un degre — ils se distinguent alors par la graisse ou par la
   police, non par un ecart de taille trop faible pour se lire.
     surtitre                12,5 px
     reperes, boutons        15 px    (x 1,2)
     paragraphe, legende     18 px    (x 1,2)
   Le titre prolonge la meme gamme : 7 rem vaut 112 px, soit exactement
   18 x 1,2^10. */

/* La gamme entiere ayant ete descendue d'un degre, le heros n'a plus de
   reglage propre : il retrouve les valeurs de la gamme commune. Seul
   subsiste l'ecart entre reperes, resserre pour qu'ils tiennent sur une
   ligne. */
.hero .hero__meta { gap: clamp(1.25rem, 3vw, 3rem); }

/* ---------- Heros sur deux colonnes : texte et portrait ---------- */

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(240px, 25vw, 380px);
  gap: clamp(1.5rem, 5vw, 4.5rem);
  align-items: center;
}
.portrait { margin: 0; position: relative; }
/* La lemniscate est large et basse la ou un portrait est etroit et haut :
   sa colonne s'elargit, et la figure se centre sur la hauteur du texte au
   lieu de le suivre du haut. */
.hero__grid--large { grid-template-columns: minmax(0, 1fr) clamp(260px, 32vw, 440px); }
/* Sans figure en vis-a-vis, la seconde colonne n'a plus lieu d'etre : la
   laisser vide creuserait un blanc a droite du texte. */
.hero__grid--seul { grid-template-columns: minmax(0, 1fr); }

/* Les figures d'ouverture — le cadenas sur l'accueil, la marque de conformite
   sur Services — ne vivent que sur grand ecran. Sur une colonne elles
   passeraient sous le titre et repousseraient le chapeau, les boutons et la
   ligne de reperes hors de la premiere vue : un ornement couterait alors ce
   que vaut le texte. La regle est en `max-width` et non l'inverse — c'est
   bien le retrait qui est conditionnel, la presence etant l'etat normal.
   Le portrait d'About n'en est pas : il porte le sujet de la page, il reste
   sur telephone. */
@media (max-width: 900px) {
  .cadenas,
  .conformite { display: none; }
}
.boucle { align-self: center; }

/* Chaque passe monte en fondu pendant qu'elle se trace, au lieu de
   s'allumer d'un coup. Dix passes qui paraissent ainsi donnent un degrade
   continu la ou dix traits francs auraient fait dix apparitions.
   Le nom de l'animation est le seul changement : le module continue de
   poser duree et retard en style en ligne, et le portrait de l'accueil
   garde `tracer`, inchange. */
/* La figure nait du centre et s'elargit.
   Les passes continuent de se tracer dessous, dans leur ordre ; par-dessus,
   un masque circulaire s'ouvre depuis le croisement — le point exact ou la
   bille est posee. On voit donc le dessin gagner vers les deux lobes a la
   fois, comme une tache d'encre qui s'etend, et non un rideau qui passe.
   Les pourcentages d'un degrade radial se comptent sur le rayon de sa forme
   d'arrivee : `farthest-corner` garantit qu'a 100 % toute la boite est
   couverte, y compris les angles.
   La frange de 14 % est ce qui distingue une main d'un obturateur : un bord
   net decouperait l'image, un bord fondu la fait apparaitre. */
@property --crayon {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 100%;
}
.boucle .portrait__svg .mine {
  -webkit-mask-image: radial-gradient(circle farthest-corner at 50% 50%,
    #000 var(--crayon, 100%), transparent calc(var(--crayon, 100%) + 20%));
  mask-image: radial-gradient(circle farthest-corner at 50% 50%,
    #000 var(--crayon, 100%), transparent calc(var(--crayon, 100%) + 20%));
  animation: crayonner var(--trace, 4.2s) linear forwards;
  /* La bezier precedente (.3 .1 .3 1) ouvrait le masque en trombe : mesure
     faite, il etait deja aux quatre cinquiemes a 45 % du temps, si bien que
     les six dernieres passes se posaient sur une figure entierement
     decouverte. L'elargissement s'arretait donc a mi-parcours.
     Cette courbe-ci progresse regulierement du debut a la fin, avec juste
     assez d'amorti aux deux bouts pour qu'on ne voie ni depart brusque ni
     arret net. La figure continue de gagner du terrain jusqu'au dernier
     trait. */
  animation-timing-function:
    linear(0, .055 8%, .135 16%, .235 25%, .345 34%, .455 43%, .555 52%,
           .655 61%, .745 70%, .825 78%, .895 86%, .945 92%, .98 96%, 1);
}
@keyframes crayonner {
  from { --crayon: 0%; }
  to   { --crayon: 100%; }
}

.boucle .portrait__svg path:not([data-libre]) { animation-name: tracer-doux; }
@keyframes tracer-doux {
  from { stroke-dashoffset: 100; opacity: 0; }
  24%  { opacity: 1; }
  to   { stroke-dashoffset: 0; opacity: 1; }
}

/* La lueur qui tourne. Un seul tiret de 13 unites sur 100, glisse sans fin
   le long du chemin : c'est le decalage du pointille qui bouge, pas le
   trait. `linear` et rien d'autre — la moindre acceleration donnerait un
   objet lance puis freine, alors qu'on veut une circulation egale.
   Elle attend que les dix passes soient posees pour partir : `--trace`,
   publie par le module, porte cette duree.
   Le depart a 13 et non a 0 : avec `dasharray 13 87` et un `dashoffset` D,
   le tiret occupe les positions [-D, -D+13]. A D = 13 il couvre [-13, 0],
   donc sa tete est exactement en 0 — la ou la bille commence sa course. La
   lueur trainee derriere elle se lit alors comme une comete, au lieu de
   courir devant sans rien tirer. */
.boucle .portrait__svg path.court {
  stroke-dasharray: 13 87;
  stroke-dashoffset: 13;
  /* Deux animations, deux proprietes : le pointille glisse sans fin, et
     l'opacite fait son eveil une seule fois. */
  animation: courir 7.5s linear .25s infinite,
             eveil var(--trace, 4.2s) ease-out forwards;
}
@keyframes courir { to { stroke-dashoffset: -87; } }

/* L'eveil. La bille et sa trainee sont la des le depart, mais a peine —
   un quart d'encre, le temps que la boucle se construise. Elles ne prennent
   leur pleine valeur qu'au dernier trait.
   Sans cela, deux traits pleins tournaient sur un dessin encore absent :
   l'oeil allait a la comete et manquait la construction. */
@keyframes eveil {
  0%   { opacity: 0; }
  5%   { opacity: .24; }
  62%  { opacity: .3; }
  100% { opacity: 1; }
}

/* La bille. Son deplacement est porte par le SVG lui-meme (`animateMotion`
   suit le chemin en fraction de longueur, la meme unite que le pointille) ;
   le CSS ne fait que la reveler quand le dessin est pose. */
/* La bille parait la premiere et part aussitot : c'est elle qui mene, le
   crayon suit. Elle et sa trainee sont hors du groupe masque, sinon elles
   s'effaceraient des qu'elles quittent le centre.
   Meme eveil que la trainee : discrete pendant la construction, pleine a
   l'arrivee. */
.boucle .portrait__svg .bille {
  opacity: 0;
  animation: eveil var(--trace, 4.2s) ease-out forwards;
}

@media (prefers-reduced-motion: reduce) {
  /* `animateMotion` est porte par le SVG et ne repond pas aux preferences
     du systeme : on masque la bille plutot que d'esperer l'arreter. */
  .boucle .portrait__svg path.court { animation: none; }
  .boucle .portrait__svg .bille { display: none; }
  /* Le balayage s'arrete lui aussi — mais le masque doit rester ouvert,
     sinon la figure resterait coupee a -16 %. */
  .boucle .portrait__svg .mine { animation: none; --crayon: 100%; }
}
@media (max-width: 880px) {
  /* Sur une colonne, la boucle passe sous le texte et se resserre : a
     pleine largeur elle prendrait plus de place que le titre. */
  .boucle { max-width: 330px; margin-inline: auto; }
}

.portrait__img,
.portrait__svg { display: block; width: 100%; height: auto; }

/* Legende : une profession de foi placee sous le portrait, donc en serif
   italique plutot qu'en petite capitale — elle se lit comme une parole,
   pas comme une etiquette. */
/* La legende s'ecrit caractere par caractere, comme une signature posee
   sous le dessin. Traitement volontairement different de celui du titre :
   pas de curseur, et une encre qui se precise en sortant du flou plutot
   qu'un caractere qui surgit. L'etat masque n'existe que si le script a
   decoupe le texte et pose lui-meme `cap-ready`. */
.js .portrait__cap.cap-ready .sg {
  opacity: 0; filter: blur(2.5px);
  animation: signer .6s cubic-bezier(.2, .75, .3, 1) forwards;
}
@keyframes signer { to { opacity: 1; filter: blur(0); } }
/* La legende est volontairement EXCLUE du filet global `reveal-now` : celui-ci
   se declenche a 2,5 s, soit bien avant le depart de la signature a 7 s, et
   la revelait donc d'un bloc. Elle a son propre filet, dans main.js, cale
   apres la fin de la sequence. */
@media (prefers-reduced-motion: reduce) {
  .js .portrait__cap.cap-ready .sg { opacity: 1; filter: none; animation: none; }
}

.portrait__cap {
  margin-top: 1.1rem; text-align: right;
  font-family: var(--serif); font-style: italic;
  font-size: var(--t0); line-height: 1.4;
  letter-spacing: -.01em; color: var(--ink);
}
@media (max-width: 900px) {
  .portrait__cap { text-align: left; }
  .portrait__cap br { display: none; }
}

/* Chaque trait a sa propre longueur normalisee a 100 par `pathLength` :
   un seul jeu de valeurs suffit donc pour tous, quelle que soit la taille
   reelle du trait. La duree et le retard sont poses par le script. */
.portrait__svg path {
  stroke-dasharray: 100; stroke-dashoffset: 100;
  animation-name: tracer; animation-fill-mode: forwards;
  animation-timing-function: cubic-bezier(.25, .6, .35, 1);
}
@keyframes tracer { to { stroke-dashoffset: 0; } }

@media (max-width: 900px) {
  .hero__grid { grid-template-columns: 1fr; gap: 2rem; }
  .portrait { max-width: 230px; }
}

.hero__title { max-width: 15ch; }

/* ---------- Titre a la machine a ecrire ---------- */

.hero__title b { font-weight: 700; }
.hero__title i { font-style: italic; }

/* L'etat masque n'existe que si le script a decoupe le texte en caracteres
   et pose `tw-ready` lui-meme. Sans JavaScript, ou si le decoupage echoue,
   le texte reste simplement affiche.
   Regle commune au heros et au paragraphe de la section 5 : seule la
   geometrie du curseur differe, la police n'etant pas la meme. */
.js .tw-ready .tw { opacity: 0; position: relative; }
.js .tw-ready .tw.on { opacity: 1; transition: opacity .45s ease-out; }

/* Le curseur est dessine PAR le caractere courant, et non par un element
   distinct place a sa suite : un element de largeur nulle bascule a la ligne
   suivante des que la ligne est pleine, ce qui faisait clignoter le trait
   sous le point final au lieu de le suivre. Rattache au caractere, il ne
   peut plus s'en detacher. */
/* Cale par le bas, puis remonte de la profondeur du jambage : `bottom: 0`
   correspond au bas de la boite du caractere, jambages compris, soit 19 px
   sous la ligne de base a cette taille. Le decalage de .294em — valeur
   mesuree sur la police — pose le trait exactement au niveau du point. */
.js .hero__title .tw.is-cursor::after {
  content: ""; position: absolute; left: 100%; top: auto; bottom: .294em;
  width: .045em; height: .74em; background: var(--accent);
  animation: caret 1.05s steps(1) infinite;
}
/* La phrase frappee est en gras — meme graisse que les autres gras du site.
   Regle a part et non attribut : le script decoupe ce paragraphe en un span
   par caractere, et une balise ajoutee dans le HTML se retrouverait a
   envelopper cent quatre-vingt-quatre elements pour rien. */
/* Elle passe a la taille du chapeau qui la precede — meme echelon fluide,
   donc les deux paragraphes montent et descendent ensemble avec la largeur.
   Elle etait un degre en dessous : la phrase de chute se lisait comme une
   note alors qu'elle porte la conclusion de la section. A graisse egale
   elle l'emporterait sur le chapeau ; c'est le gras, deja pose, qui la
   distingue, pas la taille. */
.frappe {
  font-weight: 600;
  font-size: clamp(var(--t0), 1.5vw, var(--t1));
  line-height: 1.5;
}

/* Meme curseur, autre police, donc autre calage. La valeur du heros
   (.294em) vaut pour le serif d'affichage ; ici le paragraphe est en sans
   serif a 15 px. Mesure faite sur la ligne de base reelle — une sonde en
   ligne de hauteur nulle posee apres le point final : elle tombe 4 px
   au-dessus du bas de la boite, soit .266em. C'est cette valeur qui met le
   pied du trait exactement au niveau du point.
   Le trait est aussi proportionnellement plus epais : a cette taille,
   .045em ne ferait pas un pixel plein. */
.js .frappe .tw.is-cursor::after {
  content: ""; position: absolute; left: 100%; top: auto; bottom: .266em;
  width: .07em; height: .72em; background: var(--accent);
  animation: caret 1.05s steps(1) infinite;
}
/* Troisieme calage, pour le titre de la section contact : meme serif que
   le heros mais a la taille `display--sm`, donc un autre rapport entre la
   ligne de base et le bas de la boite. Valeur relevee a la sonde, comme
   les deux autres — voir le commentaire du paragraphe frappe. */
.js .frappe--titre .tw.is-cursor::after {
  content: ""; position: absolute; left: 100%; top: auto; bottom: .291em;
  width: .05em; height: .70em; background: var(--accent);
  animation: caret 1.05s steps(1) infinite;
}
@keyframes caret { 0%, 50% { opacity: 1 } 50.01%, 100% { opacity: 0 } }

@media (prefers-reduced-motion: reduce) {
  .js .tw-ready .tw { opacity: 1; }
  .js .tw.is-cursor::after { display: none; }
}
.hero__sub { margin-top: 2rem; max-width: 46ch; }
/* Les quatre casquettes en liste. Puce dessinee plutot que `list-style` :
   le marqueur natif ne se cale pas sur la ligne de base et sa couleur suit
   celle du texte. Ici le point est en terracotta — le seul accent du site —
   et son `top` est en em, donc solidaire de la taille fluide du texte. */
/* Liste a fleches. Trois propositions espacees plutot qu'un paragraphe
   continu : chacune se lit pour elle-meme, et la fleche marque la
   progression de l'une a l'autre.
   L'ecart est plus large que celui des puces du profil — ce sont des
   phrases entieres, souvent sur deux lignes ; a interligne serre, on ne
   verrait plus ou l'une finit et l'autre commence. */
/* Mot d'appel, au-dessus de la liste. Il ne porte pas de fleche : ce n'est
   pas un element de l'enumeration, c'est ce qui l'annonce.
   La taille se place entre la liste (18 px) et le grand titre : le meme
   serif, un degre au-dessus du corps, de sorte qu'il se lise comme un
   sous-titre de la section et non comme une seconde manchette. */
.sur-titre {
  font-family: var(--serif);
  font-size: clamp(var(--t2), 2.1vw, var(--t3));
  line-height: 1.15; letter-spacing: -.02em;
  margin: 0 0 1.05rem;
}
.band--ink .sur-titre { color: var(--cream); }

/* Bande du printemps : le bloc de droite se centre sur la hauteur au lieu
   de se caler en haut. C'est la ou l'arc passe — le texte s'y installe
   plutot que de le surplomber. La citation, plus haute, reste la reference
   verticale de la rangee. */
.saison--printemps .split { align-items: center; }

/* Bande des services : le chapeau de droite se cale sur le TITRE de gauche,
   non sur la surtitre. Le retrait vaut exactement la hauteur de cette
   surtitre — sa ligne, puis sa marge — et il est ecrit dans les memes
   unites qu'elle : `--t-2` pour le corps, 1,6rem pour la marge. Une valeur
   en pixels aurait glisse des que l'echelle typographique bouge.
   Sous 880 px la grille passe sur une colonne : le retrait n'a plus de
   sens et disparait. */
@media (min-width: 881px) {
  .band--sage .split > :nth-child(2),
  /* Meme calcul pour les publications : la colonne de droite n'y porte plus
     qu'un bouton depuis que le paragraphe a saute, et il se calait a hauteur
     du surtitre — donc au-dessus du titre, sans rien pour l'y rattacher. Le
     retrait vaut la hauteur du surtitre plus sa marge. */
  #publications .split > :nth-child(2) {
    padding-top: calc(var(--t-2) * 1.65 + 1.6rem);
  }
}

/* Encre pleine sur cette bande, et non le gris du texte secondaire.
   Le bloc de droite tombe en plein sur l'arc : sur ses bandes froides, le
   gris ne tenait que 4,7:1 — au-dessus du seuil, mais sur un fond colore
   et changeant, la mesure flatte ce que l'oeil ressent. En encre pleine on
   passe a 11,6:1, et le texte cesse de dependre de la couleur qui se
   trouve derriere lui. */
.saison--printemps .lede,
.saison--printemps .frappe { color: var(--ink); }

/* Second rang de lecture : le detail qui suit l'essentiel. Un degre plus
   bas que le chapeau — 15 px la ou celui-ci monte a 18 — et un ton plus
   sourd. L'ecart d'un seul degre suffit : deux auraient fait note de bas de
   page, alors que ces dates doivent rester lisibles.
   Contraste mesure sur l'encre : 7,3:1, contre 8,6:1 pour le chapeau. */
.mineur { font-size: var(--t0); line-height: 1.55; }
.band--ink .mineur { color: rgba(254, 249, 237, .68); }

.fleches { list-style: none; padding: 0; display: grid; gap: 1.05rem; }
.fleches li { position: relative; padding-left: 1.85rem; }
.fleches li::before {
  content: "→";
  position: absolute; left: 0; top: 0;
  font-family: var(--sans);
  color: var(--accent);
}
/* Sur l'encre, la terracotta tombe a 2,9:1 — trop sourde pour un repere.
   La peche de la bande, celle des grands nombres, tient 9,7:1. */
.band--ink .fleches li::before { color: var(--peach); }

.hero__points { list-style: none; padding: 0; display: grid; gap: .55rem; }
.hero__points li { position: relative; padding-left: 1.15rem; }
.hero__points li::before {
  content: ""; position: absolute; left: .1rem; top: .62em;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent);
}
.hero__actions { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: 2.6rem; }

.hero__meta {
  display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 4vw, 3.5rem);
  margin-top: clamp(3.5rem, 7vw, 6rem);
  padding-top: 1.8rem; border-top: 1px solid var(--line);
  font-size: var(--t-1); color: var(--ink-soft);
}
.hero__meta b { font-weight: 600; color: var(--ink); }

/* ---------- Sections ---------- */

section { position: relative; }
.band { padding: clamp(72px, 11vw, 168px) 0; }
/* Grain de la bande verte : plus gros et plus contraste que le grain de
   papier general. L'alpha du bruit est quantifie en paliers (`discrete`),
   ce qui agglomere les points au lieu de produire un voile continu — c'est
   ce qui donne l'aspect granule plutot que brumeux. */
/* Un lavis derive lentement SOUS le grain, qui reste immobile : sur du
   papier, le grain ne bouge pas, c'est la lumiere qui le traverse. La
   section prolonge ainsi le vocabulaire des deux precedentes — un lavis qui
   respire — sans reprendre leur mouvement a l'identique.
   Les trois sections ralentissent en descendant : 30 s pour le heros,
   64 s pour la section 2, 78 s ici. La page s'apaise a mesure qu'on la lit. */
.band--sage {
  position: relative;
  background-color: var(--sage);
  background-image:
    radial-gradient(38% 46% at 20% 16%, var(--mist) 0%, transparent 62%),
    radial-gradient(44% 48% at 84% 78%, var(--sage-deep) 0%, transparent 60%),
    radial-gradient(34% 40% at 62% 34%, var(--butter) 0%, transparent 58%);
  background-size: 162% 162%;
  background-position: 50% 50%;
  animation: respirer 78s ease-in-out infinite alternate;
}
@keyframes respirer {
  to { background-position: 36% 64%; background-size: 176% 176%; }
}
@media (prefers-reduced-motion: reduce) {
  .band--sage { animation: none; }
}
.band--sage::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: .5; mix-blend-mode: multiply;
  /* baseFrequency pilote la grosseur du grain : 0,38 donne ce piquete fin,
     0,16 un grain de papier a dessin, 0,075 une pierre. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.38' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='0 0 0 0.3 0.65 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 260px 260px;
}
/* Le contenu doit repasser au-dessus du grain, qui est positionne. */
.band--sage > .wrap { position: relative; z-index: 1; }

.band--sand,
.band--beige { background: var(--beige); }
.band--butter { background: var(--butter); }
/* `.band--sage` a son fond plus haut, avec son lavis : la forme raccourcie
   `background` remettrait `background-image` a zero. */
.band--mist { background: var(--mist); }
.band--blush { background: var(--blush); }
.band--ink { background: var(--espresso); color: var(--cream); }

/* Les lavis decoratifs en coin de bande ont ete retires : les bandes ne
   portent plus que leur aplat de couleur. Les memes dessins servent encore
   d'illustration dans les cartes (`.card__art`), ou ils sont poses en
   <img> et non en fond — les fichiers art/*.svg restent donc utilises. */
.band--ink .eyebrow { color: rgba(254,249,237,.52); }
.band--ink .lede { color: rgba(254,249,237,.75); }
/* Le corps du texte est a 75 % de creme sur l'encre ; un gras de la meme
   teinte ne se distinguerait que par sa graisse. Le passer en creme pleine
   lui donne aussi le contraste — 14,1:1 contre 8,6:1 — et c'est cet ecart,
   plus que le poids, qui le fait ressortir. */
.band--ink .lede b { color: var(--cream); }
.band--ink .rule { background: rgba(254,249,237,.16); }

.rule { height: 1px; background: var(--line); border: 0; margin: 0; }

.split { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: clamp(2rem, 6vw, 6rem); align-items: start; }
@media (max-width: 880px) { .split { grid-template-columns: 1fr; gap: 2.2rem; } }

/* ---------- Grille de services ---------- */

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(285px, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; margin-top: clamp(2.5rem, 5vw, 4.5rem); }
.card {
  background: rgba(254, 249, 237, .74);
  padding: clamp(1.8rem, 3vw, 2.6rem);
  display: flex; flex-direction: column; gap: .55rem; min-height: 250px;
  transition: background-color .35s;
}
.card:hover { background: rgba(254, 249, 237, .97); }
.card__art { width: 96px; height: 96px; margin-bottom: .4rem; margin-left: -6px; mix-blend-mode: multiply; transition: transform .5s cubic-bezier(.16,1,.3,1); }
.card:hover .card__art { transform: scale(1.09) rotate(-3deg); }
.card__n { font-family: var(--serif); font-size: var(--t-1); color: var(--accent); }
.card h3 { font-size: var(--t1); line-height: 1.2; margin-top: .3rem; }
.card p { font-size: var(--t0); color: var(--ink-soft); line-height: 1.55; }

/* ---------- Cartes a feuillet (accueil uniquement) ---------------------
   Au repos la carte ne montre que son dessin et son titre : le texte est
   recouvert d'une page de parchemin. Au survol, la page tourne sur son dos
   comme celle d'un livre et decouvre le paragraphe.

   Le modificateur `--feuilles` restreint l'effet a la grille de l'accueil ;
   `.card` sert aussi sur services.html et about.html, ou les cartes doivent
   rester lisibles d'un coup d'oeil.                                       */

/* `perspective` sur la carte : sans elle, `rotateY` produit un simple
   aplatissement horizontal — la page glisserait au lieu de tourner. La
   valeur est volontairement longue : une perspective courte donnerait une
   deformation de grand-angle, spectaculaire mais fausse a cette echelle. */
.cards--feuilles .card {
  position: relative; perspective: 1500px;
  /* Les quatre variables du tour de page sont portees par la CARTE, non par
     le feuillet : le paragraphe en dessous doit lire la meme duree pour
     apparaitre au rythme de sa propre page, et non d'une horloge commune. */
  --tour: -104deg;    /* angle final : juste au-dela de la verticale */
  --duree: 1.1s;
  --gite: -1.6deg;    /* la page n'est jamais parfaitement d'aplomb */
  --dos: 50%;         /* hauteur de la charniere sur le bord gauche */
  --attente: 42ms;    /* le temps que la main trouve le bord */
  --leve: 15px;       /* de combien le feuillet quitte la carte */
}
/* Huit pages, huit gestes. Les ecarts sont petits — quelques degres, une
   fraction de seconde — mais il suffit qu'ils existent : deux cartes
   voisines ne s'ouvrent jamais tout a fait de la meme facon. */
.cards--feuilles .card:nth-child(1) { --tour: -101deg; --duree: 1.16s; --gite: -1.2deg; --dos: 44%; --attente: 38ms; --leve: 13px; }
.cards--feuilles .card:nth-child(2) { --tour: -108deg; --duree: 1.0s;  --gite: -2.4deg; --dos: 57%; --attente: 52ms; --leve: 18px; }
.cards--feuilles .card:nth-child(3) { --tour: -103deg; --duree: 1.26s; --gite: -.8deg;  --dos: 39%; --attente: 34ms; --leve: 11px; }
.cards--feuilles .card:nth-child(4) { --tour: -110deg; --duree: 1.05s; --gite: -2.9deg; --dos: 61%; --attente: 58ms; --leve: 19px; }
.cards--feuilles .card:nth-child(5) { --tour: -99deg;  --duree: 1.2s;  --gite: -1.7deg; --dos: 47%; --attente: 41ms; --leve: 14px; }
.cards--feuilles .card:nth-child(6) { --tour: -106deg; --duree: .96s;  --gite: -2.1deg; --dos: 53%; --attente: 48ms; --leve: 17px; }
.cards--feuilles .card:nth-child(7) { --tour: -112deg; --duree: 1.12s; --gite: -1.4deg; --dos: 42%; --attente: 32ms; --leve: 12px; }
.cards--feuilles .card:nth-child(8) { --tour: -100deg; --duree: 1.3s;  --gite: -2.6deg; --dos: 59%; --attente: 62ms; --leve: 20px; }
/* Trois plans. Le feuillet couvre toute la case ; le dessin et le titre
   passent AU-DESSUS de lui, le paragraphe en dessous. C'est l'empilement,
   et non la geometrie du feuillet, qui laisse l'en-tete lisible au repos :
   la page peut ainsi prendre la case entiere sans rien masquer de ce qui
   doit rester visible, et elle s'echappe de sous le titre. */
.cards--feuilles .card__art,
.cards--feuilles .card h3 { position: relative; z-index: 2; }
.cards--feuilles .card__corps { z-index: 0; }
/* Le feuillet passe au-dessus des cartes voisines pendant son vol. */
.cards--feuilles .card:hover,
.cards--feuilles .card:focus-within { z-index: 2; }
.cards--feuilles .card:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

/* `flex: 1` : le corps occupe toute la hauteur restante, donc le feuillet
   couvre la meme surface sur les huit cartes malgre des textes de longueurs
   differentes. Sans cela, les feuilles seraient de tailles inegales. */
.cards--feuilles .card__corps { position: relative; flex: 1; }

/* Le texte se decouvre au rythme de SA page — d'ou la duree heritee de la
   carte. L'attente est calee sur la nouvelle courbe : la page n'a pris que
   le dixieme de son angle au tiers du temps, un texte qui paraitrait la
   se lirait a travers un feuillet encore a plat. Il attend donc la moitie
   du geste, quand l'angle est assez ouvert. */
.cards--feuilles .card__corps p {
  opacity: 0; transform: translateY(7px);
  transition: opacity calc(var(--duree) * .44) ease calc(var(--duree) * .46),
              transform calc(var(--duree) * .52) cubic-bezier(.16,1,.3,1) calc(var(--duree) * .46);
}
/* `.tournee` est pose par le script des que la page se souleve, et n'est
   jamais retire : une page tournee reste tournee. Le survol n'est plus que
   le declencheur du geste, pas la condition de son maintien — sans quoi le
   lecteur qui deplace la souris refermerait le cahier derriere lui. */
.cards--feuilles .card:hover .card__corps p,
.cards--feuilles .card:focus-within .card__corps p,
.cards--feuilles .card.tournee .card__corps p { opacity: 1; transform: none; }

/* Parchemin. Cinq couches, de la plus proche de l'oeil a la plus lointaine :
   la reglure du copiste, sa marge, les vergeures du papier vergé, la fibre,
   puis la lumiere et le fond. Le bruit reprend la recette du grain de la
   section — meme feTurbulence, frequence plus basse — pour que le feuillet
   et le fond sur lequel il retombe soient de la meme matiere. */
.card__page {
  position: absolute; inset: 0; z-index: 1;
  border-radius: 1px;
  background-color: #FBF4E4;
  background-image:
    repeating-linear-gradient(to bottom, transparent 0 25px, rgba(93,82,75,.075) 25px 26px),
    linear-gradient(to right, transparent 0 30px, rgba(176,83,58,.14) 30px 31px, transparent 31px),
    repeating-linear-gradient(to right, transparent 0 3px, rgba(93,82,75,.026) 3px 4px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='f' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.24' numOctaves='5' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='0 0 0 0.22 0.5 0.85'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)' opacity='0.5'/%3E%3C/svg%3E"),
    radial-gradient(118% 88% at 26% 16%, rgba(255,253,246,.92) 0%, transparent 64%),
    linear-gradient(166deg, #FDF8EA 0%, #F7EDD8 58%, #F1E3C8 100%);
  background-size: auto, auto, auto, 220px 220px, auto, auto;
  box-shadow: 0 1px 0 rgba(255,255,255,.55) inset,
              0 2px 7px -4px rgba(22,19,15,.24);

  /* --- Le tour de page -------------------------------------------------
     `--dos` place la charniere : c'est le dos du cahier. La page pivote
     autour de son bord gauche, jamais autour d'un coin — un coin donnerait
     un envol, pas une ouverture.                                          */
  transform-origin: 0% var(--dos);

  /* Repli pour les moteurs sans `linear()` : une bezier ne peut pas rendre
     un geste en trois temps, mais elle reste plus juste qu'un `ease`. */
  transition: transform var(--duree) cubic-bezier(.44,.02,.22,1) var(--attente),
              opacity calc(var(--duree) * .32) ease calc(var(--duree) * .5),
              box-shadow calc(var(--duree) * .6) ease var(--attente);

  /* Le geste du lecteur attentif, en trois temps. Une courbe de Bezier ne
     sait pas les enchainer : elle n'a que deux poignees, donc une seule
     inflexion. `linear()` autorise autant de points qu'on veut.

       0 -> 19 %   la pulpe cherche le bord, souleve a peine : 6 % d'angle.
                   C'est la que se joue toute la delicatesse — la page
                   hesite avant de partir.
       19 -> 71 %  elle cede et monte.
       71 -> 100 % elle se repose de l'autre cote, la fin freinee.

     La phase d'hesitation vaut environ 210 ms, quelle que soit la carte :
     c'est le seuil en dessous duquel l'oeil ne la voit plus et lit un
     declic. Le gain de vitesse a donc ete pris sur le milieu du geste, pas
     sur son debut — d'ou une pente maximale de 1,5 seulement, sans a-coup.

     Cette declaration ecrase la precedente sur les moteurs qui la lisent. */
  transition: transform var(--duree)
                linear(0, .012 7%, .032 13%, .062 19%, .115 27%,
                       .195 36%, .30 45%, .425 54%, .555 63%, .675 71%,
                       .78 79%, .865 86%, .935 92%, .975 96%, 1)
                var(--attente),
              opacity calc(var(--duree) * .32) ease calc(var(--duree) * .5),
              box-shadow calc(var(--duree) * .6) ease var(--attente);
  will-change: transform, opacity;
}
/* Le galbe. Une page qui tourne n'est pas un plan rigide : elle prend la
   lumiere pres du dos et s'assombrit vers le bord libre. Le degrade se
   renforce pendant le tour, ce qui suffit a suggerer la courbure sans
   decouper la page en segments. */
.card__page::after {
  content: ""; position: absolute; inset: 0;
  border-radius: inherit;
  /* L'axe du tour etant desormais oblique, la lumiere l'est aussi : elle
     descend du coin qui se leve le premier vers celui qui lache en
     dernier. */
  background: linear-gradient(112deg,
    rgba(255,255,255,.42) 0%,
    rgba(255,255,255,.06) 26%,
    rgba(22,19,15,.03) 68%,
    rgba(22,19,15,.10) 100%);
  opacity: .45;
  /* La lumiere suit la page, attente comprise : un galbe qui se creuserait
     avant que le feuillet ne bouge trahirait le truc. */
  transition: opacity var(--duree) ease var(--attente);
}
.cards--feuilles .card:hover .card__page::after,
.cards--feuilles .card:focus-within .card__page::after,
.cards--feuilles .card.tournee .card__page::after { opacity: 1; }

/* La page s'efface au moment ou elle passe de chant, avant de basculer
   par-dessus le dos : `.cards` masque son debordement pour tenir ses angles
   arrondis, et une page encore visible au-dela de la verticale serait
   tranchee net au bord de la grille. */
.cards--feuilles .card:hover .card__page,
.cards--feuilles .card:focus-within .card__page,
.cards--feuilles .card.tournee .card__page {
  /* Une fois de l'autre cote, le feuillet ne doit plus rien intercepter :
     invisible, il resterait sinon sous le curseur pour la selection. */
  pointer-events: none;
  /* L'axe n'est plus le Y pur. En l'inclinant d'environ 11 degres — une
     part de X pour cinq de Y — la page ne pivote plus comme une porte :
     un coin part avant l'autre et le feuillet se decolle en diagonale,
     comme sous un doigt qui feuillette. Le `translateZ` prealable le
     detache franchement de la carte au lieu de le faire glisser dessus.
     La duree et la courbe sont inchangees : seul le geste change. */
  transform: translateZ(var(--leve)) rotate3d(1, 5.2, .45, var(--tour)) rotateZ(var(--gite));
  opacity: 0;
  box-shadow: 0 30px 46px -24px rgba(22,19,15,.45);
}

/* Sans survol — tactile — l'effet n'aurait aucun declencheur : le texte
   doit etre lisible d'emblee et la feuille ne pas exister. */
@media (hover: none) {
  .cards--feuilles .card__corps p { opacity: 1; transform: none; transition: none; }
  .card__page { display: none; }
}
/* Mouvement reduit : la feuille disparait en fondu, sans vol. */
@media (prefers-reduced-motion: reduce) {
  .card__page { transition: opacity .3s ease; }
  .cards--feuilles .card:hover .card__page,
  .cards--feuilles .card:focus-within .card__page,
  .cards--feuilles .card.tournee .card__page { transform: none; }
  .cards--feuilles .card__corps p { transition: opacity .3s ease; transform: none; }
  .cards--feuilles .card:hover .card__corps p,
  .cards--feuilles .card:focus-within .card__corps p,
  .cards--feuilles .card.tournee .card__corps p { transform: none; }
}


/* ---------- Les trois saisons (accueil) --------------------------------
   Les trois dernieres bandes de la page marquent un passage : hiver sur la
   bande d'encre, printemps sur la bande de beurre, ete sur la brume. Rien
   n'est illustre — tout passe par la lumiere et le mouvement, en couches
   posees sous le contenu.

   Les trois effets sont assez legers pour ne jamais disputer la lecture :
   aucun ne depasse 16 % d'opacite sur le texte, et les trois se figent en
   `prefers-reduced-motion`.                                              */

/* Une propriete personnalisee n'est interpolable que si son type est
   declare : sans cela `--arc` basculerait d'un coup de 0 a 1 a la moitie de
   l'animation, au lieu de progresser. `initial-value: 1` est le filet — en
   mouvement reduit, ou si le moteur ignore `@property`, l'arc est
   simplement la, entier et immobile. */
@property --arc {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

.saison { position: relative; overflow: hidden; }
.saison::before {
  content: ""; position: absolute; pointer-events: none; z-index: 0;
}
/* Le contenu repasse au-dessus de la saison, qui est positionnee. */
.saison > .wrap { position: relative; z-index: 1; }

/* --- Hiver : une bruine fine ------------------------------------------
   Trois nappes de la meme tuile a trois echelles. La profondeur ne vient
   pas d'une opacite differente mais de la parallaxe : chaque nappe parcourt
   un multiple ENTIER de sa propre tuile par boucle — 3, 3 et 4 tuiles en
   7,5 s — ce qui la fait tourner sans couture tout en lui donnant sa
   vitesse propre. Les gouttes les plus grosses tombent le plus vite :
   c'est ce qui les place devant.

   Chute ralentie de moitie : 48, 68 et 123 px/s au lieu de 90, 170 et 287.
   L'ecart entre nappes a ete resserre en meme temps — une parallaxe trop
   ouverte donne une averse battante, ou l'oeil suit les gouttes de devant
   au lieu de percevoir un voile. L'inclinaison tombe de 7 a 4 degres :
   moins de vent, et les gouttes sont plus courtes et moins appuyees.      */
.saison--hiver {
  --pluie: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><line x1='34.9' y1='190.4' x2='34.9' y2='198.1' stroke='rgba(188,190,192,0.52)' stroke-width='0.9' stroke-linecap='round'/><line x1='34.9' y1='-9.6' x2='34.9' y2='-1.9' stroke='rgba(188,190,192,0.52)' stroke-width='0.9' stroke-linecap='round'/><line x1='14.6' y1='108.9' x2='14.6' y2='114.2' stroke='rgba(255,255,255,0.58)' stroke-width='0.9' stroke-linecap='round'/><line x1='76.1' y1='140.7' x2='76.1' y2='151.9' stroke='rgba(214,214,214,0.47)' stroke-width='0.7' stroke-linecap='round'/><line x1='190.8' y1='39.1' x2='190.8' y2='47.0' stroke='rgba(255,255,255,0.31)' stroke-width='0.7' stroke-linecap='round'/><line x1='157.5' y1='10.6' x2='157.5' y2='20.3' stroke='rgba(255,255,255,0.31)' stroke-width='0.6' stroke-linecap='round'/><line x1='77.3' y1='63.5' x2='77.3' y2='73.7' stroke='rgba(214,214,214,0.47)' stroke-width='0.9' stroke-linecap='round'/><line x1='106.5' y1='121.3' x2='106.5' y2='128.4' stroke='rgba(255,255,255,0.62)' stroke-width='0.7' stroke-linecap='round'/><line x1='170.8' y1='164.5' x2='170.8' y2='170.9' stroke='rgba(255,255,255,0.61)' stroke-width='0.9' stroke-linecap='round'/><line x1='195.0' y1='187.1' x2='195.0' y2='192.9' stroke='rgba(255,255,255,0.32)' stroke-width='0.6' stroke-linecap='round'/><line x1='180.9' y1='19.7' x2='180.9' y2='32.7' stroke='rgba(188,190,192,0.25)' stroke-width='0.7' stroke-linecap='round'/><line x1='115.2' y1='34.6' x2='115.2' y2='46.9' stroke='rgba(255,255,255,0.47)' stroke-width='0.9' stroke-linecap='round'/><line x1='54.1' y1='110.1' x2='54.1' y2='123.0' stroke='rgba(255,255,255,0.27)' stroke-width='0.9' stroke-linecap='round'/><line x1='55.6' y1='139.0' x2='55.6' y2='150.1' stroke='rgba(214,214,214,0.50)' stroke-width='0.6' stroke-linecap='round'/><line x1='169.9' y1='96.9' x2='169.9' y2='107.2' stroke='rgba(255,255,255,0.35)' stroke-width='0.6' stroke-linecap='round'/><line x1='94.1' y1='80.8' x2='94.1' y2='88.5' stroke='rgba(214,214,214,0.51)' stroke-width='0.6' stroke-linecap='round'/><line x1='63.0' y1='196.8' x2='63.0' y2='207.5' stroke='rgba(214,214,214,0.53)' stroke-width='0.7' stroke-linecap='round'/><line x1='63.0' y1='-3.2' x2='63.0' y2='7.5' stroke='rgba(214,214,214,0.53)' stroke-width='0.7' stroke-linecap='round'/><line x1='20.8' y1='116.1' x2='20.8' y2='125.4' stroke='rgba(255,255,255,0.55)' stroke-width='0.6' stroke-linecap='round'/><line x1='87.7' y1='105.7' x2='87.7' y2='117.3' stroke='rgba(188,190,192,0.48)' stroke-width='0.6' stroke-linecap='round'/><line x1='80.2' y1='134.4' x2='80.2' y2='144.4' stroke='rgba(255,255,255,0.39)' stroke-width='0.9' stroke-linecap='round'/><line x1='39.8' y1='0.9' x2='39.8' y2='12.3' stroke='rgba(255,255,255,0.33)' stroke-width='0.9' stroke-linecap='round'/><line x1='39.5' y1='66.1' x2='39.5' y2='79.2' stroke='rgba(188,190,192,0.61)' stroke-width='0.9' stroke-linecap='round'/><line x1='150.8' y1='90.8' x2='150.8' y2='103.1' stroke='rgba(188,190,192,0.21)' stroke-width='0.9' stroke-linecap='round'/><line x1='143.9' y1='89.7' x2='143.9' y2='95.3' stroke='rgba(255,255,255,0.46)' stroke-width='0.6' stroke-linecap='round'/><line x1='121.9' y1='59.3' x2='121.9' y2='72.3' stroke='rgba(255,255,255,0.53)' stroke-width='0.6' stroke-linecap='round'/><line x1='191.9' y1='41.1' x2='191.9' y2='47.9' stroke='rgba(214,214,214,0.27)' stroke-width='0.7' stroke-linecap='round'/><line x1='161.4' y1='120.3' x2='161.4' y2='131.0' stroke='rgba(255,255,255,0.48)' stroke-width='0.7' stroke-linecap='round'/><line x1='117.2' y1='145.2' x2='117.2' y2='154.9' stroke='rgba(255,255,255,0.55)' stroke-width='0.9' stroke-linecap='round'/><line x1='30.4' y1='29.6' x2='30.4' y2='35.3' stroke='rgba(255,255,255,0.29)' stroke-width='0.6' stroke-linecap='round'/><line x1='143.3' y1='92.4' x2='143.3' y2='102.9' stroke='rgba(214,214,214,0.30)' stroke-width='0.6' stroke-linecap='round'/><line x1='116.0' y1='105.5' x2='116.0' y2='118.3' stroke='rgba(188,190,192,0.50)' stroke-width='0.9' stroke-linecap='round'/></svg>");
}
.saison--hiver::before {
  /* Debordement large : la nappe est inclinee, un cadrage au ras laisserait
     deux coins vides. */
  inset: -22% -14%;
  transform: rotate(4deg);
  background-image: var(--pluie), var(--pluie), var(--pluie);
  background-size: 120px 120px, 170px 170px, 230px 230px;
  background-position: 0 0, 40px 0, 90px 0;
  /* .20 et non .42 : a mi-force les gouttes se comptent encore, et l'oeil
     suit celles de devant. Ici il ne reste qu'un grain qui descend — on
     percoit qu'il pleut avant de voir la pluie. */
  opacity: .2;
  /* La bruine s'eteint en haut et en bas de la bande au lieu de s'arreter
     net sur ses bords. Un rectangle de pluie pose sur une section se lit
     comme une texture appliquee ; une pluie qui n'a pas de limite franche
     se lit comme du temps qu'il fait. Le masque est fixe : il ne coute rien
     a chaque image, contrairement a un flou qu'il faudrait recalculer a
     chaque deplacement des nappes. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 22%, #000 74%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 22%, #000 74%, transparent 100%);
  /* Comme l'arc et le soleil : rien ne tourne hors du champ du lecteur. */
  animation: bruine 7.5s linear infinite;
  animation-play-state: paused;
}
.saison--hiver.en-vue::before { animation-play-state: running; }
@keyframes bruine {
  to { background-position: 0 360px, 40px 510px, 90px 920px; }
}

/* --- Printemps : un arc-en-ciel suggere -------------------------------
   Un arc trop net serait decoratif ; celui-ci n'est qu'une refraction, six
   bandes de 3 a 4 % d'ecart posees sur un cercle dont le centre est sous la
   page. Le flou acheve de le rendre incertain — on le voit sans le
   regarder.                                                               */
.saison--printemps::before {
  /* Deborde lateralement de 14 % : l'arc derive, et un cadrage au ras
     decouvrirait une bande nue du cote d'ou il vient. */
  inset: -40px -14%;
  /* Le centre est loin sous la page et le rayon plus grand qu'elle : seule
     la crete de l'arc traverse la bande, a mi-hauteur. Un cercle plus petit
     laissait l'arc ramper au ras du bas, ou l'on ne voyait que ses bandes
     chaudes — invisibles sur du beurre.
     L'ordre va du violet (au plus pres du centre, donc en bas) au rouge (au
     plus loin, donc en haut), comme dans un arc primaire. Les bandes
     froides sont les plus appuyees : ce sont les seules qui se detachent
     d'un fond deja jaune. */
  background: radial-gradient(78% 128% at 50% 158%,
    transparent 0 80.5%,
    rgba(150, 128, 184, .30) 80.5% 83.6%,
    rgba(116, 156, 196, .36) 83.6% 86.7%,
    rgba(130, 176, 126, .34) 86.7% 89.8%,
    rgba(220, 206, 124, .24) 89.8% 92.8%,
    rgba(224, 158, 80, .28) 92.8% 95.9%,
    rgba(180, 86, 60, .30) 95.9% 99%,
    transparent 99%);
  /* Le flou vaut maintenant plus que la largeur d'une bande — environ 20 px
     ici. C'est un choix, et l'inverse du reglage precedent : tant qu'il
     restait en dessous, les six couleurs gardaient leurs limites et la forme
     de l'arc se lisait franchement. A 22 px chaque bande deborde sur ses
     voisines ; il ne subsiste qu'un degrade colore, sans contour. La couleur
     est encore la, le dessin n'y est plus. */
  filter: blur(22px);

  /* L'arc ne doit qu'etre suggere. Plutot que de retoucher les six alphas un
     a un — ce qui deferait l'equilibre entre bandes froides et chaudes,
     regle sur un fond deja jaune — tout le calque est ramene a 32 %. La
     hierarchie interne est conservee, l'ensemble recule.
     La valeur est portee ici et non seulement dans `eclore` : sous mouvement
     reduit l'animation est supprimee, et l'arc reprendrait sinon sa pleine
     force. */
  opacity: .32;

  /* L'arc se developpe de gauche a droite. `--arc` va de 0 a 1 ; le masque
     est un degrade dont le bord avance avec lui : franc derriere,
     transparent devant, avec une frange de 26 % qui evite le coup de
     ciseaux. La course va jusqu'a 128 % pour que la frange elle-meme sorte
     du cadre et que l'arc finisse net. */
  --front: calc(var(--arc, 1) * 128%);
  -webkit-mask-image: linear-gradient(96deg,
    #000 0, #000 calc(var(--front) - 26%), transparent var(--front));
  mask-image: linear-gradient(96deg,
    #000 0, #000 calc(var(--front) - 26%), transparent var(--front));

  /* La derive. Elle ne tourne que lorsque la section est a l'ecran — d'ou
     `paused` par defaut et la classe posee par l'observateur. `play-state`
     plutot qu'une animation ajoutee puis retiree : l'arc reprend ou il en
     etait au lieu de repartir de son point de depart a chaque passage.
     58 s pour 14 % de largeur, soit environ 2 px par seconde sur un ecran
     courant : au seuil du perceptible, ce qui est le but — on ne voit pas
     l'arc bouger, on s'apercoit qu'il a bouge.
     `alternate` et non un bouclage : un retour au point de depart ferait
     un saut. Aux extremes la vitesse est nulle, le demi-tour ne se voit
     pas, et l'arc arrive toujours par la gauche. */
  transform: translateX(-7%);

  /* Deux animations sur le meme element, sans conflit : l'une porte
     `--arc` et `opacity`, l'autre `transform`.
     Elles n'ont pas le meme declencheur, d'ou la liste a deux valeurs de
     `animation-play-state` — un etat par animation :
       eclosion : au premier defilement de la section, et une seule fois ;
       derive   : tant que la section est a l'ecran.                     */
  animation: eclore 5.4s cubic-bezier(.45,.05,.25,1) forwards,
             derive 58s ease-in-out infinite alternate;
  /* Depart tres lent — 4,5 % d'avancement au dixieme du temps. L'arc ne
     doit pas s'allumer, il doit se mettre a exister. Une bezier ne sait pas
     tenir une tete aussi plate : cette declaration ecrase la precedente sur
     les moteurs qui lisent `linear()`. */
  animation-timing-function:
    linear(0, .014 8%, .045 16%, .1 25%, .19 35%, .31 45%, .46 55%,
           .62 65%, .765 75%, .878 84%, .95 91%, .987 96%, 1),
    ease-in-out;
  animation-play-state: paused, paused;
}
.saison--printemps.en-vue::before        { animation-play-state: paused, running; }
.saison--printemps.eclot::before         { animation-play-state: running, paused; }
.saison--printemps.eclot.en-vue::before  { animation-play-state: running, running; }

/* L'eclosion. La bande se peint d'un bout a l'autre pendant que ses
   couleurs montent — deux gestes simultanes, l'un qui l'etend, l'autre qui
   l'appuie, comme un arc qui se precise a mesure qu'il se forme.
   L'opacite acheve sa course avant la longueur : la queue de l'arc arrive
   deja pleinement coloree au lieu de palir. */
@keyframes eclore {
  from { --arc: 0; opacity: 0; }
  38%  { opacity: .32; }
  to   { --arc: 1; opacity: .32; }
}
@keyframes derive {
  from { transform: translateX(-7%); }
  to   { transform: translateX(7%); }
}

/* --- Ete : la lumiere du soleil ---------------------------------------
   Le soleil est hors cadre, en haut a droite. Les rais et le halo etaient
   sur un seul calque, donc condamnes a bouger ensemble : une lumiere qui
   pivote et faiblit d'un bloc, ce qui se lit comme un mecanisme.
   Ils sont maintenant separes — les rais sur `::before`, le halo sur
   `::after` — sur des periodes de 74 s et 46 s. En `alternate` leurs cycles
   complets valent 148 s et 92 s, dont le plus petit commun multiple est de
   3 404 s : les deux mouvements ne retombent en phase qu'au bout de
   cinquante-sept minutes. A l'echelle d'une visite, la lumiere ne se
   repete jamais.                                                          */
/* --- Le fond du heros, repris en sourdine ------------------------------
   La derniere bande se referme sur la matiere qui a ouvert la page : les
   memes six nappes de la palette, le meme lavis SVG, les memes derives.
   Seule l'intensite change — .34 et .3 d'opacite contre 1 et .7 dans
   l'ouverture. Assez pour qu'on reconnaisse le grain, pas assez pour
   rejouer l'entree.
   Deux pseudo-elements et non un seul : le flou de 26 px des nappes
   effacerait le grain du lavis s'ils partageaient le meme calque.        */
.saison--ete .lavis {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  overflow: hidden;
}
.saison--ete .lavis::before,
.saison--ete .lavis::after {
  content: ""; position: absolute; pointer-events: none;
  animation-play-state: paused;
}
.saison--ete .lavis::before {
  inset: -30% -10% auto -10%; height: 130%;
  background:
    radial-gradient(44% 60% at 88% 10%, var(--peach-deep) 0%, transparent 70%),
    radial-gradient(42% 58% at 60% 48%, var(--butter) 0%, transparent 70%),
    radial-gradient(38% 55% at 8% 12%, var(--sage) 0%, transparent 68%),
    radial-gradient(34% 50% at 97% 88%, var(--blush) 0%, transparent 68%),
    radial-gradient(34% 50% at 26% 92%, var(--mist) 0%, transparent 68%),
    radial-gradient(30% 46% at 44% 4%, var(--blush) 0%, transparent 66%);
  filter: blur(26px);
  opacity: .34;
  animation: drift 30s ease-in-out infinite alternate;
}
.saison--ete .lavis::after {
  inset: -20% -12% -14% -12%;
  background: url("art/hero.svg?v=ffc72561") no-repeat center center / cover;
  opacity: .3; mix-blend-mode: multiply;
  animation: float 34s ease-in-out infinite alternate;
}

/* Le soleil est descendu : il ne surplombe plus la bande depuis le coin, il
   se tient juste au-dessus de l'horizon, a droite. Les rais partent donc
   presque a l'horizontale — c'est ce qui distingue une lumiere de lever
   d'une lumiere de midi. Ils sont tres attenues depuis que le lavis porte
   la couleur : un eventail net sur une aquarelle, ce sont deux langages. */
.saison--ete::before {
  /* Debordement : les rais pivotent autour du soleil ; a 3,4 degres, le
     coin oppose se deplace d'environ 70 px et decouvrirait la bande. */
  inset: -8%;
  z-index: 2;
  /* Les rais partent du soleil lui-meme, et ne se lèvent qu'avec lui : au
     carre comme l'eclat, ils jaillissent quand le disque se degage. */
  /* .085 et non .16 : au-dessus, l'eventail se compte rai par rai et le
     mecanisme se voit. En dessous du dixieme, il ne reste qu'une inegalite
     de lumiere dans l'air. */
  background: repeating-conic-gradient(from 214deg at var(--sx) var(--sy),
    rgba(255, 222, 158, .085) 0deg 2.4deg,
    transparent 2.4deg 11deg);
  opacity: calc(var(--lever, 1) * var(--lever, 1));
  /* Les rais s'arretent a l'horizon. Sans ce masque ils rayonnaient dans
     toutes les directions, y compris vers le bas a travers les coordonnees
     — un soleil au centre d'une roue, pas une lumiere rasante. Sur l'eau,
     la lumiere ne diffuse pas : elle se reflechit, et c'est le sillage qui
     s'en charge. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0 44%, rgba(0,0,0,.3) 52%, transparent 58%);
  mask-image: linear-gradient(to bottom, #000 0 44%, rgba(0,0,0,.3) 52%, transparent 58%);
  transform-origin: var(--sx) var(--sy);
  transform: rotate(-2.6deg);
  animation: rais 74s ease-in-out infinite alternate;
  animation-play-state: paused;
}

/* La lumiere, en trois plans : le soleil, son sillage sur l'eau, et le
   refroidissement de l'eau elle-meme.
   Le ciel a disparu de cette liste — c'est le lavis qui le porte
   desormais. Et la ligne d'horizon franche a ete abandonnee : elle
   tranchait une aquarelle en deux. L'eau se refroidit maintenant par
   degres sur quarante pour cent de hauteur, sans arete. La separation
   d'avec les coordonnees est portee par un filet accroche au bloc de
   contact lui-meme (plus bas), qui tient a toutes les largeurs — un
   pourcentage ne se serait aligne sur rien. */
.saison--ete::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background:
    radial-gradient(28% 24% at var(--sx) var(--sy),
      rgba(255, 216, 150, .4) 0%,
      rgba(255, 232, 186, .18) 46%,
      transparent 76%),
    /* Le sillage tombe a l'aplomb du soleil, sur l'eau. */
    radial-gradient(6% 26% at var(--sx) 78%,
      rgba(255, 210, 148, .24) 0%,
      transparent 74%),
    linear-gradient(to bottom,
      transparent 0 52%,
      rgba(132, 172, 180, .16) 72%,
      rgba(120, 162, 174, .2) 100%);
  /* Le halo suit le disque : la lumiere monte avec lui au lieu d'attendre,
     eclairee a un tiers quand le soleil est encore sous l'eau. */
  /* Plafond ramene de 1 a 0,62 : le halo accompagne le disque sans le
     doubler d'une aureole qui, elle, redessinerait sa forme. */
  opacity: calc(.2 + var(--lever, 1) * .42);
  animation: lever 68s ease-in-out infinite alternate;
  animation-play-state: paused;
}

/* Le ressac. Quelques lignes tres pales sous l'horizon, qui glissent
   lentement de biais. C'est le seul element franchement mobile de la
   section, et il reste sous le seuil du remarquable : 1 px de haut, 4 %
   d'opacite, quatre-vingt-dix secondes pour traverser. */
.saison--ete .ressac {
  position: absolute; left: -6%; right: -6%; top: 58%; bottom: 0;
  pointer-events: none; z-index: 2;
  background: repeating-linear-gradient(177deg,
    transparent 0 26px,
    rgba(255, 255, 255, .5) 26px 27px,
    transparent 27px 52px);
  opacity: .5;
  animation: ressac 90s linear infinite;
  animation-play-state: paused;
}
.saison--ete.en-vue::before,
.saison--ete.en-vue::after,
.saison--ete.en-vue .ressac,
.saison--ete.en-vue .lavis::before,
.saison--ete.en-vue .lavis::after { animation-play-state: running; }

/* --- Le disque ---------------------------------------------------------
   `--lever` va de 0 (soleil sous l'eau) a 1 (pose sur la ligne). Il est
   ecrit par le script au fil du defilement ; sans script il vaut 1 et le
   soleil est simplement la.
   Aucun masque, aucun degrade de decoupe : le disque est un enfant de
   `.horizon`, dont le bord inferieur est la ligne d'eau et qui masque son
   debordement. La mer coupe le soleil parce qu'elle est devant lui. */
@property --lever {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
/* Une seule position de soleil, partagee par tous les calques : le disque,
   son eclat, les rais et le sillage. Sans ce point commun, chacun avait sa
   propre coordonnee et la lumiere ne partait pas tout a fait de l'astre. */
.saison--ete { --sx: 74%; --sy: 52%; }

.saison--ete .horizon {
  position: absolute; left: 0; right: 0; top: 0; height: 62%;
  overflow: hidden; z-index: 1; pointer-events: none;
}
.saison--ete .soleil {
  position: absolute; left: var(--sx); bottom: 0;
  width: clamp(104px, 12vw, 176px); aspect-ratio: 1;
  border-radius: 50%;
  /* Pas de bord franc : un disque net ferait pictogramme. Le coeur est
     dense, la couronne se dissout — c'est ainsi qu'un soleil bas se lit a
     travers l'air epais de l'horizon. */
  /* Le coeur perd les deux tiers de sa densite et la dissolution commence
     bien plus tot — des 24 % au lieu de 46. Le disque n'a plus de bord du
     tout : c'est une clarte plus dense au centre, dont on devine le rond
     sans pouvoir le detourer. */
  background: radial-gradient(circle at 50% 50%,
    rgba(255, 208, 140, .34) 0%,
    rgba(255, 214, 152, .28) 24%,
    rgba(255, 224, 172, .16) 52%,
    rgba(255, 232, 190, .05) 78%,
    rgba(255, 236, 200, 0) 100%);
  /* Un flou de plus, par-dessus le degrade : il efface la derniere trace de
     contour. Le disque ne bouge qu'au defilement, jamais image par image —
     le flou n'est donc recalcule qu'a ce moment-la. */
  filter: blur(10px);
  /* A `--lever` 0,5 le disque est exactement a moitie sorti : le demi-
     soleil pose sur l'eau, l'image meme du lever.
     `translateX(-50%)` centre le disque sur `--sx` : c'est ce qui permet
     aux autres calques de viser le meme point. */
  transform: translate(-50%, calc((1 - var(--lever, 1)) * 100%));
}

/* --- L'eblouissement ---------------------------------------------------
   Trois cercles concentriques de plus en plus larges et de plus en plus
   pales : le coeur brulant, la couronne, puis le voile qui gagne la moitie
   de la bande. C'est ce dernier qui fait l'eblouissement — pas la
   luminosite du centre, mais l'etendue de ce qu'il contamine.
   Le tout est indexe sur `--lever`, donc sur le defilement : la lumiere
   n'existe pas tant que le soleil est sous l'eau, et elle atteint sa pleine
   force au moment ou il s'en detache. Elevee au carre pour que l'essentiel
   arrive dans le dernier tiers de la montee — un soleil eblouit quand il
   est degage, pas quand il affleure. */
.saison--ete .eclat {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(20% 17% at var(--sx) var(--sy),
      rgba(255, 236, 196, .38) 0%,
      rgba(255, 232, 186, .18) 42%,
      rgba(255, 236, 198, 0) 74%),
    radial-gradient(46% 38% at var(--sx) var(--sy),
      rgba(255, 226, 172, .19) 0%,
      rgba(255, 234, 192, .07) 44%,
      rgba(255, 240, 208, 0) 76%),
    /* Le voile large est le moins reduit des trois : c'est lui qui porte
       l'eblouissement, et il n'a pas de forme. Les deux cercles serres,
       eux, cernaient le soleil — ce sont eux qu'on ramene le plus. */
    radial-gradient(115% 88% at var(--sx) var(--sy),
      rgba(255, 244, 220, .21) 0%,
      rgba(255, 246, 228, .07) 40%,
      rgba(255, 248, 234, 0) 70%);
  opacity: calc(var(--lever, 1) * var(--lever, 1));
}

/* Le contenu passe au-dessus de tous les calques de la saison. `.saison`
   posait `z-index: 1` sur le wrap, ce qui laissait les rais et le ressac
   — montes a 2 pour passer devant le lavis — se glisser par-dessus le
   texte. */
.saison--ete > .wrap { z-index: 4; }

/* La separation d'avec les coordonnees. Accrochee au bloc lui-meme et non
   posee a un pourcentage de la bande : elle reste ainsi calee sur le texte
   quelle que soit la largeur d'ecran. Le filet reprend la couleur de l'eau
   plutot qu'un gris neutre — c'est la ligne ou le regard passe du ciel a la
   surface. */
.saison--ete .contact {
  position: relative; z-index: 3;
  border-top: 1px solid rgba(120, 162, 174, .38);
  padding-top: clamp(2rem, 4vw, 3.2rem);
}

@keyframes rais { to { transform: rotate(2.6deg); } }
/* Le soleil monte de deux pour cent de la hauteur de bande — quelques
   pixels en plus d'une minute. On ne le voit pas se lever ; on constate,
   en revenant, qu'il est plus haut. */
/* L'opacite a ete retiree de ces images-cles : elle est desormais pilotee
   par `--lever` au defilement, et une animation l'emporterait sur la
   declaration. Il ne reste que la lente montee de la lumiere. */
@keyframes lever {
  to { transform: translateY(-2%); }
}
@keyframes ressac {
  to { background-position: 140px 0; }
}

/* Le grain est exactement celui de la bande verte : meme bruit, meme URI,
   meme opacite, meme `multiply`. La couleur de la section 5 vient de son
   arc, pas de sa matiere — les deux bandes partagent donc le meme papier. */
.saison--printemps::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.38' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='0 0 0 0.3 0.65 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 260px 260px;
  opacity: .5;
  mix-blend-mode: multiply;
}

@media (prefers-reduced-motion: reduce) {
  .saison--hiver::before,
  .saison--printemps::before,
  .saison--ete::before { animation: none; }
}

/* ---------- Statistiques ---------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: clamp(2rem, 5vw, 4rem); margin-top: clamp(3rem, 6vw, 5rem); }
.stat__n { font-family: var(--serif); font-size: clamp(var(--t6), 5.4vw, var(--t9)); line-height: 1; letter-spacing: -.04em; }
.stat:nth-child(1) .stat__n { color: var(--peach); }
.stat:nth-child(2) .stat__n { color: var(--butter); }
.stat:nth-child(3) .stat__n { color: var(--sage-deep); }
.stat p { margin-top: .9rem; font-size: var(--t0); color: rgba(254,249,237,.68); max-width: 32ch; }

/* ---------- Liste de liens (publications, etc.) ---------- */

.rows { margin-top: clamp(2.5rem, 5vw, 4rem); border-top: 1px solid var(--line); }
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1.5rem; align-items: baseline; padding: 1.6rem 0; border-bottom: 1px solid var(--line); text-decoration: none; transition: padding-left .3s, background-color .3s; }
.row:hover { padding-left: .8rem; }
.row h3 { font-size: var(--t1); line-height: 1.3; }
.row__meta {
  font-size: var(--t-2); color: var(--ink-soft); letter-spacing: .1em; text-transform: uppercase;
  white-space: nowrap; padding: .42em .95em; border-radius: 999px; background: rgba(254,249,237,.8);
  /* Le filet garantit la pastille quelle que soit la bande qui la porte. Sans
     lui, l'etiquette ne se voit que par le contraste entre son fond et celui
     de la section — et si les deux coincident, elle disparait purement et
     simplement. C'etait le cas ici : la bande est en `--blush`, et une
     etiquette sur cinq recevait exactement cette couleur. */
  border: 1px solid rgba(93, 82, 75, .22);
}
/* La rotation evite desormais `--blush` : sur cette bande, la cinquieme
   etiquette — CONTRACTS, puis DIGITAL au tour suivant — se peignait de la
   couleur du fond et n'existait plus. `--sand` s'en detache franchement tout
   en restant de la meme famille chaude. */
.row:nth-child(5n+1) .row__meta { background: var(--peach); }
.row:nth-child(5n+2) .row__meta { background: var(--butter); }
.row:nth-child(5n+3) .row__meta { background: var(--sage-deep); }
.row:nth-child(5n+4) .row__meta { background: var(--mist); }
.row:nth-child(5n+5) .row__meta { background: var(--sand); }

/* ---------- Publications ---------- */

.pubs { list-style: none; margin: clamp(2.5rem, 5vw, 4rem) 0 0; padding: 0; border-top: 1px solid var(--line); }
.pubs li { display: grid; grid-template-columns: 5.5rem minmax(0, 1fr); gap: 1.5rem; padding: 1.15rem 0; border-bottom: 1px solid var(--line-soft); transition: background-color .25s; }
.pubs li:hover { background: rgba(247, 220, 201, .35); }
.pubs__y { font-family: var(--serif); font-size: var(--t0); color: var(--accent); padding-top: .05rem; }
.pubs p { font-size: var(--t0); line-height: 1.55; color: var(--ink-soft); }
.pubs i { color: var(--ink); font-style: italic; }
@media (max-width: 640px) {
  .pubs li { grid-template-columns: 1fr; gap: .3rem; }
}

/* ---------- Blog ---------- */
/* Meme grille date/contenu que .pubs, mais chaque entree porte un titre,
   un resume et un lien propres — .pubs n'avait qu'une seule ligne de texte. */
.blog-list { list-style: none; margin: clamp(2rem, 4vw, 3rem) 0 0; padding: 0; border-top: 1px solid var(--line); }
.blog-list li { display: grid; grid-template-columns: 5.5rem minmax(0, 1fr); gap: 1.5rem; padding: 1.5rem 0; border-bottom: 1px solid var(--line-soft); transition: background-color .25s; }
.blog-list li:hover { background: rgba(247, 220, 201, .35); }
.blog-list__d { font-family: var(--serif); font-size: var(--t0); color: var(--accent); padding-top: .05rem; }
.blog-list h3 { font-size: var(--t1); line-height: 1.3; margin: 0 0 .4rem; font-weight: 600; }
.blog-list p { font-size: var(--t0); line-height: 1.55; color: var(--ink-soft); margin: 0 0 .6rem; }
.blog-list a.doc-link { font-size: var(--t-1); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color .25s; }
.blog-list a.doc-link:hover { border-color: var(--accent); }
@media (max-width: 640px) {
  .blog-list li { grid-template-columns: 1fr; gap: .3rem; }
}

/* ---------- Citation ---------- */

.quote { font-family: var(--serif); font-size: clamp(var(--t4), 3.8vw, var(--t7)); line-height: 1.14; letter-spacing: -.03em; max-width: 20ch; }

/* ---------- Contact / pied de page ---------- */

.contact { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: clamp(2rem, 5vw, 4rem); margin-top: clamp(2.5rem, 5vw, 4rem); }
.contact h4 { font-family: var(--sans); font-size: var(--t-2); letter-spacing: .18em; text-transform: uppercase; font-weight: 600; color: var(--ink-faint); margin: 0 0 1rem; }
.contact a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--line); transition: border-color .25s; }
.contact a:hover { border-color: currentColor; }
.contact address { font-style: normal; color: var(--ink-soft); }

/* Pictogrammes de contact. `align-items: start` et non `center` : l'adresse
   tient sur quatre lignes, un centrage vertical poserait l'epingle au
   milieu du bloc au lieu de la premiere ligne. */
.ligne { display: flex; align-items: start; gap: .6rem; }
.pic {
  flex: none; width: 18px; height: 18px;
  /* Cale sur la premiere ligne de texte : la moitie de l'interligne, moins
     la moitie de ce qui depasse entre le pictogramme et la hauteur d'x. */
  margin-top: .16em;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
  /* `--ink-faint` ne tenait pas sur le bleu de la mer : 2,1:1. Le ton du
     texte secondaire donne 5,1:1, et a 1,5 px de trait sur 18 px le
     pictogramme reste un dessin au trait, pas une tache. */
  color: var(--ink-soft);
  transition: color .25s;
}
.ligne:hover .pic { color: var(--accent); }

/* ---------- Pied de page : la meme pilule que la barre du haut ---------
   Memes valeurs, ligne pour ligne — rayon, fond, filet, flou, gouttiere.
   Et la meme mecanique : une marge negative de la hauteur de la pilule
   plus son ecart au bord, pour qu'elle flotte au-dessus de la fin de la
   derniere section au lieu de se poser apres elle. En haut la barre passe
   sur le lavis du heros ; en bas elle passe sur le lever de soleil.
   La page s'ouvre et se ferme sur le meme objet.                        */
.pied {
  position: relative; z-index: 60;
  padding-inline: var(--gutter);
  margin-top: calc(-62px - .7rem);
  padding-bottom: .7rem;
}
.foot__in {
  position: relative;
  max-width: var(--maxw); margin-inline: auto;
  display: flex; align-items: center; justify-content: space-between;
  /* `flex-wrap` et `min-height` la ou la barre du haut fixe une hauteur :
     le pied porte une phrase entiere, qui doit pouvoir passer a la ligne
     sur les ecrans etroits sans deborder de la pilule. */
  flex-wrap: wrap; gap: .5rem 2rem;
  min-height: 62px;
  padding: .6rem clamp(1rem, 2.2vw, 1.75rem);
  border-radius: 999px;
  /* Les valeurs de la barre EN DEFILEMENT (.88 et `--line`), non celles de
     son repos (.6 et `--line-soft`). Le repos n'existe qu'en haut de page,
     avant le premier geste ; des qu'on voit la barre passer sur du contenu,
     c'est cet etat-la qu'on voit. Et il le fallait : a .6 sur la bande
     d'encre qui ferme la page About, le texte tombait a 3,1:1. A .88 il
     tient 5,7:1 sur l'encre et 6,9:1 sur le lever de soleil. */
  background: rgba(254, 249, 237, .88);
  border: 1px solid var(--line);
  box-shadow: 0 14px 34px -22px rgba(22, 19, 15, .45);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  /* `--ink-faint` donnait 2,8:1 — et deja 3,0:1 sur l'ancien fond papier,
     donc le defaut precedait la pilule. Le ton du texte secondaire monte
     a 6,9:1 sans alourdir la ligne. */
  font-size: var(--t-1); color: var(--ink-soft);
}
.foot__in a { text-decoration: none; color: inherit; }
.foot__in a:hover { color: var(--ink); }
.foot__in nav { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; }
/* Sur mobile la pilule s'arrondit moins : a 999px de rayon, un bloc de
   trois lignes prend la forme d'une gelule et le texte se decolle des
   bords. */
/* Le pied suit la barre du haut : vous aviez demande que les deux aient le
   meme format, et une pilule flottante sous un bandeau bord a bord romprait
   cet accord. Meme traitement, meme seuil — et le filet passe en haut, du
   cote ou il separe le pied du contenu. */
@media (max-width: 860px) {
  /* La devise ne parait plus sur telephone : dans une bande bord a bord elle
     prenait deux a trois lignes a elle seule, au-dessus des liens et de la
     mention legale. Elle reste sur ordinateur, ou elle tient sur une ligne
     en regard des liens. */
  .pied__devise { display: none; }
  .pied { padding-inline: 0; margin-top: -62px; padding-bottom: 0; }
  .foot__in {
    max-width: none;
    border-radius: 0;
    border-width: 1px 0 0;
    padding-inline: var(--gutter);
    justify-content: flex-start;
  }
}

/* ---------- Classes reprises des styles en ligne ----------
   Le site posait vingt-trois attributs `style="…"` dans son balisage. Ils
   fonctionnaient en local, mais etaient SILENCIEUSEMENT IGNORES en ligne : le
   `.htaccess` envoie une CSP avec `style-src 'self'`, sans `'unsafe-inline'`,
   et le navigateur refuse alors tout style attache a un element. D'ou des
   marges disparues et des boutons de la mauvaise couleur, visibles seulement
   sur le site publie.
   Ces huit regles reprennent exactement les memes valeurs. La CSP reste
   stricte : c'est le balisage qui s'y conforme, plutot que l'inverse.        */

/* Le paragraphe qui prolonge le precedent, dans un meme bloc. */
.p-suite { margin-top: 1.4rem; }
/* Le bloc qui suit, detache : un bouton apres un texte, par exemple. */
.p-bloc { margin-top: 2rem; }
/* Le chapeau du bloc de contact : un peu plus d'air, et une mesure courte. */
.p-contact { margin-top: 2.4rem; max-width: 52ch; }
/* Le bouton qui suit une grille : il lui faut le retrait des blocs denses,
   sans quoi la masse des cases l'ecrase. */
.p-apres-grille { margin-top: clamp(3.4rem, 6vw, 5.2rem); }
/* Une rangee de boutons. */
.actions { margin-top: 2rem; display: flex; gap: .8rem; flex-wrap: wrap; }
/* La description sous le titre d'une ligne de prestation. */
.row__desc { color: var(--ink-soft); font-size: .95rem; margin-top: .35rem; }
/* Les deux boutons poses sur une bande d'encre : l'encre et le papier
   s'inversent, sinon ils disparaissent dans le fond. */
.btn--clair { background: var(--paper); color: var(--ink); }
.btn--fantome-clair { border-color: rgba(254,249,237,.35); color: var(--cream); }

/* ---------- Bandeau de la version de test ----------
   Pose par build_csl.py sur les seules pages de staging. Ses regles vivent
   ici, et non dans le balisage injecte, pour la meme raison que ci-dessus :
   la CSP rejette tout style ecrit dans une page.
   Quelques lignes voyagent donc aussi en production, ou rien ne les invoque.
   C'est le prix d'une CSP stricte, et il est mince.                        */
.avis-test-actif { padding-bottom: 42px; }
.avis-test {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 9999;
  background: var(--espresso); color: var(--cream);
  font: 600 12px/1.4 var(--sans);
  letter-spacing: .14em; text-transform: uppercase; text-align: center;
  padding: 11px 12px;
}

/* ---------- Lecteur de PDF integre ----------
   Ouvrir un document ne doit pas chasser le lecteur du site : il parait dans
   un cadre pose par-dessus la page, et se referme sur place. C'est le lecteur
   PDF du navigateur qui l'affiche — aucune bibliotheque tierce, donc aucune
   requete vers un tiers, la propriete meme qui dispense ce site de banniere
   de cookies.
   Ce n'etait pas un `<div>` au depart mais un `<dialog>` ouvert par
   `showModal`, pour son piege a tabulation et sa touche Echap. Mais le
   lecteur PDF de Chrome vit dans un processus separe, et il ne peint pas son
   document dans le « top layer » ou le navigateur hisse les dialogues
   modaux : le cadre restait vide. D'ou ce simple calque en position fixe.
   Le voile, le piege au focus et Echap sont repris dans main.js.            */
.lecteur {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(12px, 3vw, 40px);
  /* L'encre du site, pas un noir pur : la page reste devinable dessous. */
  background: rgba(22,19,15,.58);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: lecteurVoile .3s ease both;
}
.lecteur[hidden] { display: none; }
@keyframes lecteurVoile { from { opacity: 0 } to { opacity: 1 } }

/* Le cadre. Colonne : la barre prend sa hauteur, le document occupe tout le
   reste. Une hauteur calculee sur une barre supposee de 50 px se decalerait
   des que le titre passe a la ligne. */
.lecteur__cadre {
  display: flex; flex-direction: column;
  width: min(1180px, 100%);
  height: min(90vh, 1100px);
  background: var(--paper);
  border-radius: clamp(14px, 1.6vw, 22px);
  box-shadow: 0 46px 90px -40px rgba(22,19,15,.6),
              0 0 0 1px rgba(22,19,15,.09);
  overflow: hidden;
  /* Le cadre monte de quelques pixels en paraissant : la meme entree que
     les blocs `rise` de la page, pour ne pas surgir comme une alerte. */
  animation: lecteurEntre .34s cubic-bezier(.16,1,.3,1) both;
}
@keyframes lecteurEntre {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.lecteur__barre {
  flex: none;
  display: flex; align-items: center; gap: 1rem;
  padding: .7rem .8rem .7rem clamp(1rem, 2.2vw, 1.5rem);
  border-bottom: 1px solid var(--line);
  /* `--cream` vaut exactement `--paper` : sur le fond du cadre la barre ne
     se detacherait pas. `--beige` lui donne le poids d'une barre d'outils. */
  background: var(--beige);
}
.lecteur__titre {
  font-family: var(--serif);
  font-size: var(--t1); line-height: 1.2;
  margin-right: auto;
}
.lecteur__lien {
  font-size: var(--t0); color: var(--ink-soft);
  text-decoration: none; white-space: nowrap;
  border-bottom: 1px solid transparent;
}
.lecteur__lien:hover { color: var(--ink); border-bottom-color: var(--line); }

/* Le telechargement est l'action principale de cette barre : il prend la
   pilule pleine, quand l'ouverture dans un onglet reste un lien discret. */
.lecteur__dl {
  flex: none;
  padding: .5em 1.15em;
  border-radius: 999px;
  background: var(--ink); color: var(--paper);
  font-size: var(--t-1); font-weight: 550;
  text-decoration: none; white-space: nowrap;
  transition: background-color .25s;
}
.lecteur__dl:hover { background: var(--espresso); }
.lecteur__dl:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Sous 560 px la barre porterait quatre elements sur une largeur de poche.
   Le lien « ouvrir dans un onglet » s'efface : le bouton de telechargement
   couvre le meme besoin, et le repli renvoie desormais vers lui. */
@media (max-width: 560px) {
  .lecteur__lien { display: none; }
  .lecteur__titre { font-size: var(--t0); }
}
.lecteur__fermer {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--paper); color: var(--ink);
  font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: background .2s ease, border-color .2s ease;
}
.lecteur__fermer:hover { background: var(--sand); border-color: var(--ink-soft); }
.lecteur__fermer:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* `display:block` : en `inline` l'iframe laisse sous elle la place d'une
   ligne de texte, qui ouvrirait une bande claire au bas du cadre. */
.lecteur__vue {
  display: block; width: 100%; border: 0;
  flex: 1; min-height: 0;
  background: var(--beige);
}

/* Le repli, quand le navigateur ne sait pas afficher un PDF dans un cadre —
   c'est le cas de plusieurs navigateurs mobiles, qui rendent une page
   blanche sans le dire. Le lecteur garde alors une porte de sortie. */
.lecteur__repli {
  display: none;
  padding: clamp(1.6rem, 4vw, 2.6rem);
  text-align: center; color: var(--ink-soft);
}
.lecteur--sansvue .lecteur__vue { display: none; }
.lecteur--sansvue .lecteur__repli { display: block; }

@media (max-width: 640px) {
  /* Plein ecran : sur un telephone, une marge autour du document ne coute que
     de la surface de lecture. */
  .lecteur { padding: 0; }
  .lecteur__cadre { width: 100%; height: 100%; border-radius: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lecteur, .lecteur::backdrop { animation: none; }
}

/* ---------- Sequence en plan fixe (page Services) ----------
   Meme mecanique que microsoft.ai : un conteneur colle en pleine hauteur
   d'ecran, et le contenu qui part legerement agrandi pour revenir a sa
   taille au fil du defilement. `--p` va de 0 a 1, pose par main.js. */

.zoom { position: relative; height: 230vh; }
.zoom__stage {
  position: sticky; top: 0; height: 100vh;
  display: flex; align-items: center; overflow: hidden;
  padding-block: 0;
}
.zoom__scale {
  position: relative; z-index: 1;
  transform: scale(calc(1 + .17 * (1 - min(var(--p, 0) * 2, 1))));
  transform-origin: 50% 50%;
}
/* Le plan fixe sans la loupe : le cadre et les colonnes de code restent, le
   contenu ne bouge plus. `transform: none` et non une echelle de 1 — une
   transformation, meme neutre, cree un contexte d'empilement et fait passer
   le texte par la composition GPU, ce qui l'adoucit inutilement. */
.zoom--sansloupe .zoom__scale { transform: none; }

/* Et sans la loupe, plus de raison de garder la piste. La section faisait
   2,3 hauteurs d'ecran, dont 1,3 de defilement pendant lequel le contenu
   restait colle en place — c'etait le prix du zoom, qui n'existe plus. Elle
   redevient une bande ordinaire : sa hauteur est celle de son contenu, on la
   descend et on la remonte d'une traite.
   Les colonnes de code continuent de deriver : main.js lit desormais la
   progression sur la TRAVERSEE de la section quand elle n'a plus de piste. */
.zoom.zoom--sansloupe { height: auto; }
.zoom.zoom--sansloupe .zoom__stage {
  position: static; height: auto;
  padding: clamp(72px, 11vw, 168px) 0;
}

/* Lignes de commande en fond : registre technique, contenu credible.
   Masque elliptique pour que rien ne bute sur les bords. */
.zoom__code {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: var(--t-2); line-height: 2; letter-spacing: .015em;
  color: var(--cream); opacity: .14;
  /* Fondu lateral seulement. L'ellipse d'origine estompait aussi le haut et
     le bas : les lignes s'arretaient avant les bords de la section. Le
     rectangle etant desormais opaque, rien n'oblige plus a degager le
     centre — le terminal court sur toute la hauteur. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 7%, #000 93%, transparent 100%);
}
/* Largeur fixe et debordement masque : sans cela les lignes, plus larges
   que l'ecart entre colonnes, se chevauchent et le fond devient illisible.
   Coupees net, elles evoquent un terminal tronque. */
.zoom__col {
  position: absolute; top: 0; width: 19%; overflow: hidden;
  white-space: pre; will-change: transform;
}
.zoom__col b { font-weight: 400; color: var(--peach); }

/* Voile entre les lignes de commande et le texte : le fond reste lisible en
   peripherie, le propos reste lisible au centre. La teinte suit la bande. */
.zoom__stage::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(ellipse 64% 60% at 34% 50%,
    rgba(46, 38, 32, .94) 0%,
    rgba(46, 38, 32, .78) 42%,
    rgba(46, 38, 32, .28) 68%,
    rgba(46, 38, 32, 0) 84%);
}

/* ---------- Variante claire : terminal imprime sur papier ---------- */

/* Un lavis sous les lignes : sans peinture, le code n'a rien ou s'inviter.
   C'est le meme materiau que le heros — un vrai lavis SVG aux bords
   deformes, non un degrade CSS — mais d'une autre humeur : le vert et le
   bleu-vert y dominent, la peche n'est plus qu'un accent. Section chaude,
   puis section froide : les deux s'accordent sans se repeter. */
/* Le lavis a laisse place a son aplat : `--mist` est le vert-bleu froid que
   wash2.svg faisait dominer. La section garde donc sa couleur et sa place
   dans la progression chaude -> froide, sans la texture peinte. */
.zoom--paper .zoom__stage { background-color: var(--mist); }
/* `multiply` : les caracteres se melent au lavis comme une encre absorbee,
   au lieu de flotter par-dessus. */
/* Deux couleurs, deux natures : vert profond pour la machine, terracotta
   pour le droit. Le couple vert/peche est celui de microsoft.ai ; il tient
   sur le lavis sans jurer avec lui. */
.zoom--paper .zoom__code {
  color: #35624A; opacity: .66; mix-blend-mode: multiply;
}
.zoom--paper .zoom__line.is-legal { color: #9C4227; }
/* Le fond etant charge, tout le texte de la section passe en encre pleine.
   Mesure sur un glyphe de code au pire endroit : 2,3:1 et 4,2:1 avec les
   gris d'origine, au-dela de 10:1 ici. */
.zoom--paper .zoom__scale .eyebrow,
.zoom--paper .zoom__scale p { color: var(--ink); }
/* Ellipse large et recentree : la mise en page est sur deux colonnes, un
   voile cale a gauche laisserait le paragraphe de droite illisible. */
/* Le voile creme ne servait qu'a calmer le lavis. Sur un aplat il ne fait
   plus que delaver la couleur et affadir les lignes de commande : supprime. */
.zoom--paper .zoom__stage::after { background: none; }

/* ---------- Zoom marque ---------- */

/* 130vh et non 165 : la piste de defilement ne vaut plus que 0,30 ecran au
   lieu de 0,65. Le repli ne se declenche qu'une fois la section entierement
   passee — on ne peut pas retracter un bloc encore visible sans faire bouger
   ce qu'il y a autour. Il reste donc une fenetre, entre la fin du zoom et le
   repli, ou le lecteur peut faire demi-tour : il retraverse alors la piste,
   pendant laquelle le cliquet tient la carte immobile. C'est le scroll en
   trop. La seule prise est la longueur de cette piste : a 0,30 ecran elle ne
   vaut plus une demi-molette, et l'effet garde sa course a la descente. */
.zoom--intense { height: 130vh; }
/* Zoom AVANT : le bloc entre en retrait puis grandit tout au long du
   parcours, au lieu d'arriver surdimensionne et de se poser. L'amplitude
   reste mesuree — 0,86 a 1,12 — car au-dela la carte butterait sur les
   bords de l'ecran, qui la decoupe. */
/* Le zoom s'acheve a l'echelle 1, taille naturelle de la mise en page.
   C'est ce qui permet de replier ensuite la section en bande ordinaire sans
   rien changer a l'apparence de la carte — a 1,12 elle deborderait du cadre
   et se ferait rogner. */
/* Le fondu d'entree a ete retire : `opacity` s'applique a tout le sous-arbre,
   fond compris, si bien qu'avant le zoom le rectangle etait translucide et
   laissait passer les lignes de commande. Il est desormais opaque des le
   depart — le terminal ne se lit qu'autour. Le grandissement seul porte
   l'effet, ce qui suffit. */
.zoom--intense .zoom__scale {
  /* 1,06 et non 1,15 : sur une piste courte, avancer la fin du zoom d'un
     septieme de la course la rendrait brusque. La carte atteint sa taille
     juste avant que le plan fixe se decroche. */
  transform: scale(calc(.84 + .16 * min(var(--p, 0) * 1.06, 1)));
}

/* Premier temps — la piste est consommee, le plan fixe se decroche. Le cadre
   redevient statique mais garde la hauteur d'un ecran : il occupe alors, au
   pixel pres, la place que tenait le plan fixe. Rien ne bouge a l'ecran, et
   la piste disparait : le lecteur qui fait demi-tour ici ne la retraverse
   plus. C'est ce qui supprime le scroll pour rien. */
.zoom.zoom--pose { height: 100vh; }
.zoom.zoom--pose .zoom__stage { position: static; }
.zoom.zoom--pose .zoom__scale { transform: none; opacity: 1; }

/* Second temps — la section est hors champ : elle se resserre en bande
   ordinaire, ce qui raccourcit d'autant le trajet de retour. */
.zoom.zoom--fige { height: auto; }
.zoom.zoom--fige .zoom__stage {
  position: static; height: auto;
  padding: clamp(72px, 11vw, 168px) 0;
}
.zoom.zoom--fige .zoom__scale { transform: none; opacity: 1; }

/* Le rectangle vert : il porte le texte, ce qui rend inutile le voile
   creme d'origine. Les lignes de commande continuent de courir autour. */
.zoom--paper .zoom__scale {
  background: linear-gradient(158deg, var(--sage) 0%, var(--sage-deep) 100%);
  border: 1px solid rgba(53, 98, 74, .2);
  border-radius: clamp(20px, 2.4vw, 34px);
  padding: clamp(1.8rem, 4vw, 3.4rem);
  box-shadow: 0 34px 70px -46px rgba(22, 19, 15, .5);
}

/* ---------- Terminal vivant : lignes qui s'ecrivent puis s'effacent ---------- */

.zoom__line {
  position: absolute; white-space: pre; overflow: hidden;
  width: 31%; text-overflow: clip;
}
.zoom__line.is-legal { color: var(--accent); }
.zoom--paper .zoom__line.is-legal { color: var(--accent); }
.zoom__caret { opacity: .75; }

@media (max-width: 860px) {
  /* Le plan fixe demande beaucoup de defilement : sur petit ecran la
     section redevient une bande ordinaire.
     Les deux etats du repli sont neutralises avec elle : `--pose` impose une
     hauteur d'ecran, ce qui rouvrait un blanc de 82 px sous la carte alors
     qu'il n'y a ici ni piste ni plan fixe a resorber. Deux classes dans le
     selecteur, pour l'emporter sur leur definition. */
  .zoom,
  .zoom.zoom--pose,
  .zoom.zoom--fige { height: auto; }
  .zoom__stage,
  .zoom.zoom--pose .zoom__stage,
  .zoom.zoom--fige .zoom__stage { position: static; height: auto; padding: clamp(72px, 11vw, 168px) 0; }
  /* Specificite alignee sur `.zoom--intense .zoom__scale`, sinon la mise a
     l'echelle et le fondu resteraient figes a leur valeur de depart. */
  .zoom .zoom__scale { transform: none; opacity: 1; }

  /* Replie, le plan fixe ne laisse plus qu'un bandeau de 72 px au-dessus et
     au-dessous de la carte. Traverse par les lignes de commande, celui du bas
     se lisait comme une bande etrangere glissee entre le vert de la carte et
     celui de la section suivante : le terminal disparait donc ici, ou il
     n'etait de toute facon pas lisible.
     La section garde en revanche sa couleur. La rendre transparente reglait
     la bande, mais ouvrait 581 px de papier nu autour de la carte — un vide
     entre le heros et la bande verte. Avec son aplat, elle redevient une
     bande a part entiere, comme sur grand ecran.
     `.zoom--paper .zoom__stage` et non `.zoom__stage` : la regle d'origine
     porte deux classes, une seule ne l'emporterait pas. */
  .zoom--paper .zoom__stage { background-color: var(--mist); }
  .zoom__code { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .zoom { height: auto; }
  .zoom__stage { position: static; height: auto; padding: clamp(72px, 11vw, 168px) 0; }
  /* Specificite alignee sur `.zoom--intense .zoom__scale`, sinon la mise a
     l'echelle et le fondu resteraient figes a leur valeur de depart. */
  .zoom .zoom__scale { transform: none; opacity: 1; }
}

/* ---------- Apparition au scroll ---------- */

/* Le contenu est visible par defaut. Il n'est masque que si le script a pu
   poser la classe `js` sur <html> -- et un garde-fou dans main.js le
   readfiche apres 2,5 s quoi qu'il arrive. Aucun texte ne peut donc rester
   invisible parce qu'une transition ou un script a echoue. */
.js .rise { opacity: 0; transform: translateY(26px); transition: opacity .85s cubic-bezier(.16,1,.3,1), transform .85s cubic-bezier(.16,1,.3,1); }
.js .rise.in { opacity: 1; transform: none; }
/* Garde-fou final : coupe les transitions et impose l'etat visible. Pose par
   main.js apres 2,5 s, ou immediatement si une erreur JS survient. Ne depend
   d'aucune animation, donc rien ne peut le bloquer. */
.reveal-now .rise { opacity: 1 !important; transform: none !important; transition: none !important; }
.rise[data-d="1"] { transition-delay: .09s; }
.rise[data-d="2"] { transition-delay: .18s; }
.rise[data-d="3"] { transition-delay: .27s; }
@media (prefers-reduced-motion: reduce) { .js .rise { opacity: 1; transform: none; transition: none; } }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.skip { position: absolute; left: -9999px; }
.skip:focus { left: var(--gutter); top: 10px; z-index: 100; background: var(--ink); color: var(--paper); padding: .7em 1.2em; border-radius: 999px; }
