/* ==========================================================================
   Career Launch — feuille unique.
   22 octobre 2026, Career Launch 4e édition. Jeune Consulting.

   La règle qui gouverne tout le fichier : UNE seule ligne, l'axe.
   Pas de bordure ailleurs, pas d'ombre, pas de dégradé, pas de carte.
   L'ambre n'apparaît qu'une fois : la confirmation.

   Une exception, et une seule : le dégradé blanc du fond photographique
   (body::after). Ce n'est pas une décoration, c'est un voile de lisibilité —
   il mesure le contraste, pas l'ambiance. Le texte garde ses aplats, la
   typographie et la grille sont celles d'avant.
   ========================================================================== */

:root {
  /* Sept valeurs, pas une de plus. */
  --encre: #0E1424;
  /* tout le texte primaire */
  --pierre: #4E5768;
  /* texte secondaire, jamais plus. 7.27:1 sur --papier */
  --papier: #FFFFFF;
  /* la page */
  --brume: #EEF1F7;
  /* le seul aplat : champs, confirmation */
  --bleu: #1427B8;
  /* l'axe, les liens, le focus, le bouton */
  --or: #FFB900;
  /* la confirmation. Une seule fois. */
  --rouge: #A4262C;
  /* les erreurs uniquement. 7.26:1 sur --papier. */

  /* L'axe est à une position fixe. Le texte commence toujours au même
     endroit, à 2px + 1.75rem du trait. */
  --axe: 1.75rem;
  --ecart: 1.75rem;
  --gouttiere: calc(var(--axe) + 2px + var(--ecart));

  --mesure: 62ch;
  /* le corps de texte */
  --rayon: 6px;

  /* Le cadre : la largeur de TOUTE la composition du desktop, et la seule
     quantité dont la rampe du voile, plus bas, aura besoin. 64rem, soit 1024px
     — un nombre pair parce que c'est une mesure de PAPIER, pas une mesure de
     texte : rien, dans le contenu, n'a cette largeur. Ce qui l'a, c'est la
     grille, et une grille à deux colonnes d'un nombre pair se lit sans
     effort ; 1024 = 2 × 464 + 96.

     Elle est déclarée dans la base et jamais lue en dessous de 701px : sur
     l'écran étroit, .main, .programme et .footer gardent leur 72rem, qui est
     de toute façon inerte à cette largeur. Elle est ici, et non dans le bloc
     @media de la fin de la feuille, pour une raison de cascade : la rampe du
     voile est dans la base — voir body::after — et une variable déclarée plus
     bas que son premier usage se résoudrait, sur ce même usage, à sa valeur
     initiale. */
  --cadre: 64rem;
  --colonne-ecart: 6rem;
  /* La gouttière ENTRE les deux colonnes du desktop : 96px, soit 12% du cadre.
     Assez large pour que le carré bleu d'un atelier respire contre le texte de
     la colonne de gauche, assez étroite pour que 464 + 96 + 464 fasse 1024. */
}

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--papier);
  color: var(--encre);
  font-family: "Helvetica Neue", Helvetica, "Inter", "Segoe UI", system-ui, sans-serif;
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0;
  /* Tout le texte se cale sur l'axe. Rien n'est centré, nulle part. */
  padding: 0 var(--axe) 0 var(--gouttiere);
  position: relative;
  /* Sans cette hauteur minimale, une page courte — 404, confirmation —
     laisse l'axe s'arrêter au bout du contenu et le trait se retrouve
     suspendu dans le blanc à mi-hauteur. body::before se mesure sur body. */
  min-height: 100vh;
  /* Puis `100dvh`, et c'est le problème iOS classique : `100vh` vaut la
     hauteur du viewport MESURÉE PORTILLET OUVERT, donc une page courte — 404,
     confirmation — peut déborder de la hauteur visible et faire défiler pour
     rien quand la barre d'adresse se rétracte. `dvh` suit la hauteur
     RÉELLEMENT visible, donc la page ne bouge plus.

     Progressif, et dans cet ordre précis : un moteur qui ne connaît pas `dvh`
     jette la ligne et garde `100vh`, qui est le comportement d'aujourd'hui.

     Le repli est donc réellement VIVANT sur deux des trois moteurs du plancher,
     et il ne faut pas le trouver Activé par hasard : `dvh` arrive à Chrome 108,
     Firefox 101 et Safari 15.4, contre un plancher à Chrome 86, Firefox 85 et
     Safari 15.4. Entre le plancher et 108, c'est-à-dire sur Chrome 86 à 107 et
     Firefox 85 à 100 — la majorité du parc réel de ce site — la ligne `dvh`
     est jetée et `100vh` reste. Safari est le seul à l'avoir dès le plancher.
     C'est aussi ce qui rend la règle sans risque au-dessus : au-delà de 108 et
     101, les deux déclarations valent la même chose. */
  min-height: 100dvh;
  /* Le rattachement du calque de fond (body::after, z-index: -1).

     position:relative avec z-index:auto ne crée pas de stacking context :
     sans cette ligne, ce calque à z-index négatif appartient au contexte
     racine, sur html. Il n'y est visible que par une accident de règles —
     le fond de body est PROPAGÉ vers le canvas tant que html n'a pas de
     fond à lui, donc ce blanc est peint comme le fond du canevas, avant les
     descendants à z-index négatif. Le jour où html reçoit le moindre
     `background`, la propagation cesse, le blanc de body redevient une boîte,
     et cette boîte est peinte AU-DESSUS d'un z-index négatif placé dans le
     contexte racine : le fond disparaît. Vérifié — en injectant
     `html { background }` par-dessus cette feuille, la marge droite passe à
     (255,255,255), 7020 pixels sur 7020.

     isolation: isolate énonce le contrat : le calque reste dans le contexte
     de body, donc au-dessus du fond de body que la propagation ait ou non
     lieu. Même test avec cette ligne : (206,204,200), 0 pixel blanc sur 7020.

     Ce que le nouveau contexte ne change pas, parce que l'ordre à
     l'intérieur reste le même : body::before (l'axe, z-index auto) passe
     toujours après le calque négatif, et le header (z-index: 1) comme
     .skip-link (z-index: 2) restent au-dessus de l'un et l'autre.
     Revérifié au pixel après coup : axe (20,39,184), bandeau opaque. */
  isolation: isolate;
}

/* L'axe : 2px, bleu, de haut en bas de la page. Le header le recouvre
   par son aplat blanc, donc le trait commence au contenu — à l'édition et à la date —
   et il se prolonge dans le pied de page.
   Couplage : le header DOIT rester opaque (background sur .header), sinon
   le trait remonte derrière lui. Ne pas le rendre transparent. */
body::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--axe);
  width: 2px;
  background: var(--bleu);
}

/* --- Le fond photographique ------------------------------------------------
   background_site.jpeg, la salle de l'événement. C'est un calque
   DÉCORATIF : il ne reçoit rien (pointer-events), il est sous tout le
   contenu (z-index: -1), et aucun texte ne se pose sur une surface
   transparente — le header garde son aplat blanc, les champs leur --brume.

   Pourquoi un calque à part plutôt qu'un background sur body : le voile et
   le flou doivent agir sur la photo SEULE. Les mettre sur body flouterait
   aussi l'axe bleu et décalerait sa position de un pixel — et un filtre
   crée un contexte d'empilement, donc body::before et ce calque se
   disputeraient l'ordre. Séparés, chacun dit où il se place : z-index: -1
   pour le fond, l'axe garde son z-index:auto et reste au-dessus.

   Pourquoi position: fixed et NON background-attachment: fixed : ce dernier
   fait repeindre le fond à chaque geste de défilement sur iOS Safari, sur
   une image de 1280x1706 — le site ramerait sur mobile. Une couche fixe,
   elle, est rastérisée une fois et réutilisée tant que le viewport ne
   change pas. Elle reste donc fixe à l'écran, ce qui est l'effet voulu.

   DEUX leviers de non-omniprésence, sur trois possibles, et ils ne se
   remplacent pas :

   1. Le flou (16px). La photo a un grain de pellicule très présent sur ses
      grandes surfaces lisses : mesuré, l'écart moyen au détail fin passe
      de 2.3 à 0.0 sur le plafond et de 3.3 à 0.0 sur le sol. Le flou ne
      fait pas que voiler, il supprime le bruit photographique et l'aliasing
      des croisillons de stores et des points de LED. Sans lui, un JPEG
      agrandi sur un écran dense scintille — c'est le pire pour la
      lisibilité, pas le mieux.

2. Le voile blanc, et c'est lui qui fait le travail. La photo n'a aucune
       zone uniforme : 11,6 % de ses pixels dépassent 200 et 11,3 % sont
       sous 20 (les bandes LED, les stores noirs, les vêtements). Un voile
       uniforme est donc obligatoire — et il ne peut pas être fin : mesuré
       sur l'image entière floutée, il faut 0.80 de blanc pour que le pixel
       LE PLUS SOMBRE de la photo reste lisible par --pierre (le texte
       secondaire : .session__text, .note, .footer__line) à 4.5:1, et 0.85
       pour 5.1:1. Sous le texte, le voile est donc à 0.95 — relevé, pas
       calculé : 6.50:1 pour --pierre et 16.4:1 pour --encre, contre 7.27:1
       pour --pierre sur papier blanc. Un voile plus fin n'apporterait rien
       sous le texte, la photo y étant déjà à cinq niveaux du blanc.

       RE-VÉRIFIÉ SUR LE RENDU, pas recalculé depuis une formule : capture faite
       sur 1440x900, contenu masqué, pixels comptés sur toute la surface que
       couvre le plateau — luminance de fond de 0.888 à 1.000, moyenne 0.936.
       Ramené aux sept couleurs de la page, ça donne --pierre 6.50:1 AU PIRE
       pixel et 6.84:1 en moyenne, --encre 16.39 et 17.51, --bleu 9.32 et 9.96,
       --rouge 6.49 et 6.93. Les quatre sont au-dessus du plafond AA de 4.5:1,
       et --encre reste à seize comme l'annonce ce fichier.

       Ces chiffres sont ceux de la géométrie d'AVANT la refonte, et ils
       tiennent toujours : la VALEUR du plateau est restée à 0.95, donc le
       pixel le plus sombre de la zone voilée est le même — 0.888, relevé, pas
       recalculé. Seule la MOYENNE a bougé, de 0.952 à 0.936 : le plateau
       fait 288px de plus, et il couvre donc aussi une part un peu plus sombre
       du milieu de l'image. Revérifié sur les trois largeurs après refonte,
       contenu masqué, pixels comptés de x=34 (bord de l'axe) à x=1082 (bord
       du cadre), sur toute la hauteur du viewport :

         1280x800   L min 0.888  max 1.000  moyenne 0.935  amplitude 29
                    --pierre 6.50:1 au pire pixel, 6.82:1 en moyenne
         1440x900   L min 0.888  max 1.000  moyenne 0.938  amplitude 29
                    --pierre 6.50:1 au pire pixel, 6.84:1 en moyenne
         1920x1080  L min 0.888  max 1.000  moyenne 0.935  amplitude 29
                    --pierre 6.50:1 au pire pixel, 6.83:1 en moyenne
         --encre 16.39:1 au pire pixel sur les trois.

       6.50:1 au pire pixel, c'est 44% de marge au-dessus du plafond AA, et
       29 niveaux d'amplitude seulement : sous le texte il ne reste de la
       photo qu'un voile, ce qui est exactement ce qu'on lui demande. Le
       plateau est à 0.95 dans le bloc comme dans le @supports de la fin de la
       feuille — les deux rampes sont la même chaîne, caractère pour
       caractère, et c'est vérifiable.

       ET LE TEXTE N'EST JAMAIS DANS LA RAMPE. Le plateau finit à 1082px du bord
       gauche, et le cadre de la composition -- .main, .programme, .footer,
       .header__inner, .page — se termine à 58 + 1024 = 1082px exactement. Le
       plateau n'est pas « à peu près » calibré sur la colonne : il finit dessus
       au pixel, comme avant, et il finit désormais sur le PLUS LARGE des blocs
       du site et non sur le plus étroit. Tout ce qui suit est donc de la marge
       : à x=1200..1380, --pierre tombe à 4.89:1 en moyenne, et bien en dessous
       au bord droit — sans importance, il n'y a là aucun glyphe : le seul
       contenu qui atteint cette zone, l'en-tête, porte son aplat blanc opaque et
       ne laisse rien passer.

       ⚠️  1082px N'EST PLUS UNE BORNE THÉORIQUE, c'est le premier pixel que
       l'encre atteint. Sur /inscription, le formulaire occupe la colonne de
       droite jusqu'à son bord : mesuré, l'encre de <main> y finit à 1082px, à
       1440 comme à 1920. C'est donc la première fois que le plateau est
       réellement utilisé jusqu'à son bout, et la garantie est désormais
       vérifiable page par page au lieu de l'être par construction. Contrôle
       refait sur cette vue, contenu masqué, de x=34 à x=1082 : luminance
       0.888 à 1.000, moyenne 0.938 à 1440, amplitude 29, --pierre 6.50:1 au
       pire pixel sous la colonne de gauche comme sous celle de droite,
       --encre 16.39:1. Rien n'a bougé — la valeur du plateau est la même qu'au
       moment de la mesure d'origine — mais la page où elle sert le plus est
       celle qui a le plus d'encre à en couvrir.

       Un mot sur ce qui n'est PAS sur le voile à cet endroit : .form__input et
       .choix portent --brume, un aplat opaque. Leurs textes — valeur saisie,
       placeholder en --pierre — sont donc lus sur un aplat, pas sur la photo,
       et le voile n'a rien à dire de leur contraste. Seuls les libellés
       (.form__label, --encre) et .form__aide (--pierre) sont sur le voile, et
       ils tiennent 6.50:1 au pire pixel comme le reste.

    Le voile est en outre décentré vers la droite : le cadre fait 64rem, soit
    1024px, et la page étant une grille calée à gauche sur l'axe, tout ce qui
    passe à droite du cadre est du vide. C'est là — et seulement là — que la
    photo apparaît : 0.45 à 1274px, 0.25 au bord droit. Un aplat fort sous le
    texte l'aurait effacée, un aplat faible partout l'aurait imposée.

    ⚠️  LE PLATEAU N'EST PLUS CALIBRÉ SUR .page, ET C'EST UN CHANGEMENT DE
    SENS. Il l'était sur 46rem = 736px, la largeur de .page, parce que c'était
    le seul bloc qui approchait du bord de la zone voilée : sur l'accueil, le
    texte s'arrêtait à 678px et le plateau tenait 116px de marge. La composition
    du desktop est désormais une grille de 1024px dont les ink dépassent
    678px — les descriptions du programme passent à 1082px sur leur deuxième
    ligne, et le lien du pied de page à ~800. Calibrer sur la largeur d'un bloc
    de TEXTE laissait donc la rampe passer sur de l'encre ; c'est la largeur du
    CADRE qu'il faut couvrir, parce qu'elle est une garantie structurelle et
    non une observation.

    C'est aussi ce qui décide du rôle de la photo sur le desktop. Elle n'occupe
    plus la moitié d'un écran comme une marge.graduelle : c'est un PANNEAU,
    à droite du cadre, de 198px à 1280, 358px à 1440 et 838px à 1920. Un objet
    posé, dont on voit où il commence, plutôt qu'une photographie qui s'étale
    depuis toujours. Et il n'y a plus de vide entre l'encre et lui : le panneau
    commence exactement où s'arrête la grille.

    Le 0.45 est à 12rem — 192px — du bord du cadre, et non à un pourcentage de
    la largeur comme avant. Un pourcentage de viewport ne peut pas suivre un
    plateau fixe : 22 % de 1440 font 317px de rampe, 22 % de 1920 en font 422,
    et la photo n'apparaîtrait qu'au tout dernier bord d'un écran large. Une
    distance en px la rend au contraire proportionnelle AU PANNEAU : 192px de
    rampe sur 198 à 1280, sur 358 à 1440, sur 838 à 1920 — le panneau se
    dégage du texte dans la même proportion à toutes les largeurs.

    0.25 au bord droit, c'est un choix du commanditaire, et il faut le dire tel
    qu'il est mesuré plutôt que le justifier après coup : la photo est alors
    franchement présente dans le panneau — mesuré sur le rendu, contenu masqué,
    luminance de fond de 0.061 à 0.989, soit 240 niveaux d'amplitude — et la
    salle se lit sans ambiguïté (bandeaux de LED, fenêtres, murs jaunes). Le
    panneau cesse d'être une zone vide pour devenir une image. La contrainte de
    non-omniprésence n'est plus tenue par le voile mais par la géométrie : la
    photo reste à droite de 1082px, donc jamais sous une ligne, et le texte
    garde son 0.95. Si un jour on veut un panneau plus discret, le curseur est
    le palier final : 0.45 donnait 102 niveaux d'amplitude, 0.30 en donnait
    133. Sous 0.25, le panneau cesse d'être un fond et devient une photo — ce
    qui reste défendable tant que le texte ne bouge pas.

    ⚠️  LE PANNEAU N'EST PAS LISIBLE, et il n'est pas censé l'être : --pierre
    y tombe à 4.84:1 en moyenne à 1280, 3.79:1 à 1440 et 3.45:1 à 1920. Il n'y a
    là AUCUN glyphe, par construction — le cadre s'arrête à 1082px et le
    plateau suit — donc ces chiffres mesurent une image, pas un texte. Ils
    sont notés ici pour que personne ne les découvre en croyant que c'est un
    défaut de contraste : c'est le contenu du panneau, pas une surface de
    lecture.

   Pas d'opacité sur le calque en plus du voile : ce serait le même
   curseur, réglé deux fois.

   Les positions sont en px depuis la gauche, pas en pourcentage de la
   largeur : comme elles suivent la mesure du texte, sur un écran étroit
   le plateau fort recouvre toute la largeur et la photo s'efface au lieu
   de passer sous les lignes. C'est le desktop qui décide de la valeur, et
   sur un écran étroit ce réglage-ci est trop fort : mesuré, 0.95 ne laisse
   que 6 niveaux d'écart au blanc sur toute la largeur, 10 d'amplitude —
   aucun nuage, aucune forme. D'où le @media en fin de feuille, qui
   ramène le voile à 0.85 sur les écrans étroits : 17.6 d'écart moyen, 30
   d'amplitude, et 5.30:1 pour --pierre, toujours au-dessus de 4.5:1.
   Sous 0.80 on descend à 4.72:1 — encore conforme, mais à 0.2 de la
   limite ; à 0.70 c'est 3.72:1, hors AA. 0.85 est le plancher retenu.
   --------------------------------------------------------------------- */
body::after {
  content: "";
  position: fixed;
  /* Les quatre décalages en longhand AVANT `inset`, et c'est le seul endroit de
     la feuille qui mérite ce genre de repli. La raison est ARITHMÉTIQUE, pas
     esthétique : un pseudo-élément VIDE en `position: fixed` dont les quatre
     décalages valent `auto` — ce que laisse un moteur qui ne connaît pas
     `inset` — n'occupe AUCUNE place. Mesuré sur ce build, fenêtre 900x700,
     Gecko 157 : `width: 0px; height: 0px`, la boîte disparaît entièrement.
     C'est sa largeur ajustée qui vaut zéro, un pseudo sans contenu n'ayant rien
     à mesurer. La même règle reçoit ses quatre décalages et fait 900x700.
     Concrètement, c'est-à-dire TOUTE la couche photographique, voile compris,
     sur toute la page.

     Le repli ne peut pas se doubler : `inset: 0` réaffirme exactement les mêmes
     quatre valeurs, et sur un moteur qui le comprend le longhand devient
     simplement redondant — mesuré, la boîte calculée est identique avec et
     sans. `inset` arrive à Chrome 87, Firefox 66 et Safari 14.1 : sous le
     plancher du site il ne reste donc que Chrome 86, une seule version. Le
     repli est donc mince — et il est gardé quand même, pour la raison qui
     décide de tout le reste de cette feuille : le remède coûte quatre lignes,
     il est impossible à casser au-dessus du plancher, et l'échec qu'il évite
     n'est pas une dégradation mais la disparition d'un calque de lisibilité
     dont le contraste a été calibré. */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--papier);

  /* Le voile et la photo : UNE déclaration, et le JPEG en url().

     Il y avait ici deux déclarations, la seconde portant image-set(), la
     première étant le repli. Ce n'était pas une précaution excessive, c'était
     une BOGUE — et elle a été MESURÉE, pas supposée. Sur 1440x900, Chromium
     153 calcule `background-image: none` sur ce calque et ne demande AUCUNE des
     cinq images : 0 requête, 0 octet, et une page entièrement blanche à droite
     de la colonne de texte. Le voile part avec la photo, parce que le voile
     EST dans background-image.

     Le repli « deux déclarations, la mauvaise d'abord » ne protège que d'un
     ÉCHEC À LA PARSE. image-set() échoue ici APRÈS l'analyse, au CALCUL de la
     valeur : la déclaration devient « unset », donc initial pour une propriété
     qui ne s'hérite pas, donc background-image: none — et c'est elle qui gagne
     la cascade, donc le JPEG du dessous n'est jamais utilisé. C'est le piège
     décrit plus bas, mais pour une raison que personne n'avait regardée : le
     DESCREPTEUR DE LARGEUR. Vérifié sur ce build, cinq variantes : image-set()
     avec `2x` est compris (→ 2dppx), avec `1280w` il ne l'est pas — même sans
     type(), même avec une largeur de 12800w, même avec `1280px`.

D'où le bloc @supports en fin de feuille : c'est lui, et lui seul, qui
   réécrit background-image avec image-set(), et seulement là où les
   descripteurs de largeur sont compris. Sur un moteur qui les ignore — et ils
   sont ignorés par les DEUX moteurs vérifiés, Gecko 157 et Chromium 153, voir
   la note de ce bloc — le JPEG ci-dessous s'affiche : 118 Ko, ce que ces
   écrans paient déjà aujourd'hui dans les mêmes moteurs.

     ⚠️  NE JAMAIS factorier le voile (le linear-gradient) dans une variable CSS
     pour éviter de le dupliquer ailleurs. Une valeur invalide à la
     CALCUL COMPUTÉ ne retombe pas sur la déclaration précédente : elle vaut
     « unset », donc initial pour une propriété qui ne s'hérite pas, donc
     background-image: none. On perdrait le voile ET la photo, au lieu de ne
     perdre que la photo. Le doublon est le prix à payer. */
  background-image:
    linear-gradient(90deg,
      rgba(255, 255, 255, 0.95) 0,
      rgba(255, 255, 255, 0.95) calc(var(--gouttiere) + var(--cadre)),
      rgba(255, 255, 255, 0.45) calc(var(--gouttiere) + var(--cadre) + 12rem),
      rgba(255, 255, 255, 0.25) 100%),
    url("/public/images/background_site.jpeg");
  background-size: cover;
  /* center top, et non center : l'image est en 3:4 portrait, donc sur un
     écran paysage « cover » n'en montre que ~45 % de la hauteur. En haut, ce
     qui reste visible est le plafond et les bandeaux LED — la zone vide de
     sujet, et la plus claire : le texte s'y pose sur le fond le moins
     contrasté de toute la photo, et personne n'a la tête d'un participant
     coupée par le bord de l'écran. Sur un écran portrait, l'image entière
     tient dans la hauteur et ce réglage ne joue plus. */
  background-position: center center;
  background-repeat: no-repeat;
  filter: blur(2px);
}

/* Écrans étroits : le voile est uniforme et ramené à 0.85 (voir le bloc
   ci-dessus pour les mesures). Sans ce @media, la rampe du desktop
   s'aplatit sur la largeur de l'écran et le plateau à 0.95 — calibré pour
   une colonne de texte de 46rem, large de 736px, qui n'existe plus ici —
   efface la photo des deux tiers de l'écran sans rien gagner : à cette
   largeur, le texte occupe toute la surface et il n'y a plus de marge à
   laisser libre. Ce seuil de 700px est le point où le plateau (46rem +
   gouttiere, soit 794px) sort de l'écran ; tant qu'il y est, la rampe
   remplace le palier et le voile fort reste le bon calcul.

   ⚠️  LE VOILE ICI EST LE MÊME QUE CELUI DU BLOC DU DESSUS, MOINS LE PALIER :
   c'est tout ce que ce @media change, et c'est mesuré — sur 390x844, l'écart
   moyen au blanc passe de 6 niveaux (à 0.95) à 17,6, l'amplitude de 10 à 30,
   et --pierre tient 5.30:1 contre 4.5:1 exigé.

   CE BLOC NE PORTE PLUS QU'UNE DÉCLARATION, celle qui marche partout : le
   jeu image-set() est devenu un @supports en fin de feuille, pour la raison
   mesurée écrite à sa place — un image-set() refusé au CALCUL COMPUTÉ ne
   retombe pas sur le JPEG du dessous, il l'efface. Le voile et la photo
   Mobile sont donc, eux aussi, garantis par la même condition que sur le
   desktop : un seul contrat à vérifier, pas deux. Et comme les deux blocs
   listent les mêmes candidats dans le même ordre, le préchargement écarté
   — voir header.php — n'a toujours qu'une cible unique à viser. */
@media (max-width: 700px) {
  body::after {
    background-image:
      linear-gradient(90deg, rgba(255, 255, 255, 0.85) 0, rgba(255, 255, 255, 0.85) 100%),
      url("/public/images/background_site.jpeg");
  }
}

/* Repli : sans filter, pas de photo. Un JPEG net à 10 % d'opacité sur des
   aplats de 200 à 20 de luminance, c'est exactement le bruit de fond que le
   voile est censé supprimer — mieux vaut une page blanche que cela.

   ⚠️  CE BLOC EST PLACÉ APRÈS LE @media CI-DESSUS, et il ne l'était pas
   avant : il l'était avant lui, et il Perdait donc sur les écrans étroits.
   Il fait la même specificity que body::after, et le @media aussi — rien
   d'autre ne les départage, c'est l'ordre source. Entre les deux, à 700 px
   ou moins, le @media réécrivait background-image par-dessus ce
   background-image: none et la photo revenait : exactement ce que ce repli
   interdit, et sur la largeur où le voile est le plus fort. L'avoir mis
   après rétablit ce qu'il annonce partout. Déplacé, rien d'autre modifié.
   Sans effet sur un navigateur qui sait faire le flou, donc sur la
   totalité des navigateurs réels : la condition n'y est jamais vraie. */
@supports not (filter: blur(1px)) {
  body::after {
    background-image: none;
  }
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: 700;
}

p {
  margin: 0;
}

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

/* L'édition est une information ordinale : elle dit de quelle année de
   Career Launch il s'agit, et elle se lit avant la date, qui est ensuite la
   seule chose en grand. Même rang que la note sous le bouton — petite, grise,
   rien autour, aucun aplat : sur cette page un nombre entouré ferait badge.
   Le padding et non une marge, parce que cette ligne est la première du
   hero : sa marge-bottom sortirait du <section> (le parent n'a ni padding ni
   bordure, rien ne l'arrête) et l'écart se retrouverait au-dessus du hero
   au lieu d'être entre les deux lignes. */
.edition {
  padding-bottom: 1rem;
  color: var(--pierre);
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.01em;
}

.jour {
  font-size: clamp(3.5rem, 15vw, 9rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 0.86;
}

.event-name {
  font-size: clamp(2rem, 6vw, 3.25rem);
  letter-spacing: -0.03em;
  line-height: 1.02;
  margin-top: 0.5em;
}

.section-title {
  font-size: 1.5rem;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.page__title {
  font-size: clamp(2rem, 6vw, 3.25rem);
  letter-spacing: -0.03em;
  line-height: 1.02;
}

/* Le chapeau et les descriptions partagent le MÊME jeton, --mesure, et la
   hiérarchie se fait avec la taille et le poids, jamais en resserrant la
   colonne : deux jetons de mesure pour deux blocs de prose serait un
   défaut, pas un choix.

   ⚠️  MAIS PAS LA MÊME LARGEUR EN PIXELS, et c'est mesuré : 62ch se résout
   sur la taille de police de l'ÉLÉMENT, pas sur celle du corps. Donc
   .pitch et .page__lead (1.125rem) font 620.5px de large, .quand et
   .session__text (1.0625rem) font 586px, .note et .footer__line
   (0.9375rem) font 517.1px — trois largeurs, une seule constante, le NOMBRE
   DE CARACTÈRES, qui est celui qu'un œil compte. C'est le bon invariant : une
   mesure en ch est une mesure en caractères par construction.

   Ce que la feuille refuse, en revanche, c'est un jeton DIFFÉRENT — un 58ch
   pour les descriptions et un 66ch pour le chapeau. Là, deux choix voisins
   et rien pour les départager.

   Mesuré sur le chapeau réel : 594.8px d'encre pour une boîte de 620.5px,
   soit 26px de blanc à droite sur sa seule ligne. Réduire sa boîte à 586px
   pour égaliser les bords de colonne le ferait passer sur DEUX lignes à
   toutes les largeurs de desktop — un changement de hero visible partout
   pour une différence que personne ne voit. Donc : inchangé, et le fait
   consigné ici pour que personne ne le « corrige » plus tard. */
.pitch,
.page__lead {
  max-width: var(--mesure);
  margin-top: 1.5rem;
  font-size: 1.125rem;
  line-height: 1.45;
  /* Rachat de ligne : un chapeau ne doit pas laisser un mot seul en
     dernière ligne. Repli naturel si le navigateur l'ignore. */
  text-wrap: balance;
}

.page__lead {
  color: var(--pierre);
}

.session__name {
  font-size: 1.125rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.3;
}

.session__text {
  max-width: var(--mesure);
  margin-top: 0.5rem;
  color: var(--pierre);
  /* Trois lignes réservées. La description est le seul bloc de hauteur
     variable du programme : sans réserve, un atelier à une ligne tire ses
     voisins de 26px et casse l'égalité des nœuds sur l'axe. 3lh est une
     longueur typographique, donc elle suit la taille du texte si celle-ci
     change. Une description tient sur une, deux ou trois lignes, les
     quatre étapes occupent la même hauteur. Au-delà de trois lignes le
     bloc grandit — on ne tronque pas du texte, on perd la régularité. */
  min-height: 3lh;
}



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

/* Le header est pleine largeur et son aplat blanc recouvre l'axe : le
   trait commence au contenu — à l'édition et à la date — pas au-dessus du
   header. */
.header {
  position: relative;
  z-index: 1;
  background: var(--papier);
  /* Le header saigne des DEUX côtés : son aplat blanc doit toucher les bords
     de l'écran, sinon il s'arrête net 28px avant le bord droit et la bande de
     photo passe sous lui — invisible tant que le voile de fond était à 0.64,
     visible depuis qu'il est à 0.25. À gauche il y avait déjà la saignée, à
     droite il n'y en avait pas.

     Et le contenu ne doit pas bouger d'un pixel : allonger la boîte de 28px
     vers la droite sans rendre ces 28px au padding, ce qui est exactement ce
     que ferait un margin-right seul, qui décalerait la nav de 28px vers la
     droite.
     D'où le padding droit à 2 × --axe : la boîte de contenu du header reste
     [58, W-56], c'est-à-dire le bord gauche à une gouttière de l'axe et le
     bord droit à une gouttière du viewport. Les 2px de différence entre les
     deux côtés sont ceux de l'axe, qui n'a pas de symétrique à droite. */
  margin-left: calc(-1 * var(--gouttiere));
  margin-right: calc(-1 * var(--axe));
  /* Le padding vertical vaut l'écart entre les deux rangées du header
     (.header__inner { gap: 0.75rem }), pas 1.5rem : le header est alors une
     seule unité de 12px — au-dessus de la marque, entre les rangées sur
     écran étroit, en dessous de la nav. Avec 1.5rem il y avait trois
     vitesses d'espacement dans le même bloc, dont deux qui ne
     servaient à rien. Les cibles tactiles ne bougent pas : .brand et
     .nav__link portent min-height: 44px, le padding du header n'entre pas
     dans leur boîte. */
  padding: 0.75rem calc(2 * var(--axe)) 0.75rem var(--gouttiere);
}

/* --- Les `gap` en flex, et pourquoi il n'y a pas de repli --------------------

   Six conteneurs de la feuille comptent sur `gap` en flex : `.header__inner`,
   `.nav`, `.brand`, `.choix`, `.page__actions`, `.footer__links`. Sur un moteur
   qui l'ignore, les liens de la nav se touchent, la case à cocher colle à son
   libellé, les icônes du pied de page se touchent.

   Il n'y a pas de repli, et c'est un choix, pas un oubli. Un `gap` de flex
   arrive à Chrome 84, Firefox 63 et Safari 14.1 : il précède le plancher du
   site (Chrome 86, Firefox 85, Safari 15.4) sur les trois moteurs. La panne
   visée n'existe donc pas dans le périmètre que ce site s'est donné.

   Et surtout, la garde qui estoppel l'écrire est FAUSSE, ce qui vaut la peine
   d'être écrit pour qu'on ne la réessaie pas. `@supports not (gap: 1px)`
   teste la PROPRIÉTÉ `gap`, dont la validité ne dépend pas du mode de mise en
   page de l'élément : un moteur qui connaît le gap de grille — Safari 11.1 à
   14 — répond `true` alors que son gap de flex ne fait rien. La porte serait
   donc fermée précisément sur le moteur qu'elle doit ouvrir, et le repli ne
   s'appliquerait jamais. C'est le même piège que le voile : savoir qu'une
   déclaration est valide ne dit rien de ce qu'un moteur en fera sur CET
   élément.

   Reste que des `margin` en repli s'empileraient avec `gap` au-dessus du
   plancher et casseraient l'espacement actuel — un défaut visible pour
   réparer une panne invisible. On garde `gap` seul. Si le plancher du site
   descend un jour sous Safari 14.1, c'est ici qu'il faudra revenir, et
   l'abordage sera alors de ne pas se fier à `CSS.supports`.
   --------------------------------------------------------------------- */

.header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  max-width: 72rem;
}

/* Le nom reste le nom : le monogramme vient à côté, pas à sa place.
   Le gap fait l'alignement, aucune marge en dur. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 44px;
  color: var(--encre);
  font-weight: 500;
  font-size: 0.9375rem;
  letter-spacing: -0.01em;
  text-decoration: none;
}

/* 40px : la taille pour laquelle JC_logo_nav.png a été exporté (2×).
   flex: 0 0 auto pour que le monogramme ne se fasse pas écraser
   quand l'en-tête se replie à 360. */
.brand__mark {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
}

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

/* La navigation est de la navigation : encre, jamais bleu, jamais gras.
   Le bleu de la page est alors dépensé trois fois seulement — l'axe, le
   focus, et le bouton. Un lien de nav qui paraît visuellement égal au
   bouton de la page donnait deux actions principales au lieu d'une. */
.nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--encre);
  font-size: 0.9375rem;
  text-decoration: none;
}

/* La page courante se signale par le poids, pas par une couleur : même
   canal que les cases cochées, donc rien à réapprendre. */
.nav__link.is-active {
  /* 700, comme la case cochée : dans cette page un poids ne veut dire
     qu'une chose — « c'est celui-ci ». Deux intensités pour le même état
     l'auraient affaibli. 400 -> 500 à 0.9375rem, c'est un trait à peine
     plus épais que le 400 et ça ne se voit pas ; 400 -> 700, c'est un
     trait 1.75 fois plus épais, on le voit sans le chercher. Le « jamais
     gras » d'au-dessus vaut pour le lien au repos : ici le poids est une
     information d'état, pas une emphase, et il est le canal que la page a
     déjà choisi. Le bouton garde la primacy : il reste le seul aplat --bleu
     du contenu, et il est en 1.0625rem. */
  font-weight: 700;
  /* Le repère qui suit est sorti du flux : il ne pousse donc rien, et la
     nav ne se décale pas d'une page à l'autre. */
  position: relative;
}

/* Le carré : le nœud des ateliers et celui de la confirmation, à la même
   échelle relative (10px sous un titre de 1.125rem, 8px sous 0.9375rem).
   Il est dans la gouttière, à gauche du lien, jamais sur l'axe — l'axe est
   déjà bleu et le bleu est dépensé. --encre, la même encre que le lien : la
   marque n'ajoute aucune couleur. Étant en position absolue, il est hors de
   la boîte du lien, donc l'anneau de focus se referme sur le texte seul.
   Le nom du site est déjà une marque : celle-ci marque une position, pas
   une identité, et c'est le carré de l'axe qui fait cette lecture. */
.nav__link.is-active::before {
  content: "";
  position: absolute;
  left: -1rem;
  top: calc(50% - 4px);
  width: 8px;
  height: 8px;
  background: var(--encre);
}

/* Le trait de survol reste, y compris sur le lien courant : il ne répond pas
   à la même question. Le trait dit « c'est cliquable, là, maintenant », il
   est passager et il vaut pour tous les liens ; le carré dit « tu es ici »,
   il est permanent. Retirer le trait au lien courant lui ôterait son seul
   retour de clic — le carré n'en est pas un — et casserait une règle stable
   au profit d'une exception. Un état passager et un état durable ne se
   confondent pas parce qu'ils partagent une forme : ils se distinguent
   parce que l'un des deux ne bouge jamais. */
.nav__link:hover,
.nav__link:focus-visible {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
}

/* --- Le lien d'évitement -------------------------------------------------- */

.skip-link {
  position: absolute;
  top: -200px;
  left: var(--gouttiere);
  z-index: 2;
  padding: 0.75rem 1rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  background: var(--bleu);
  color: var(--papier);
  font-size: 0.9375rem;
  text-decoration: none;
}

.skip-link:focus {
  top: 0.5rem;
}

/* --- Contenu -------------------------------------------------------------- */

.main {
  position: relative;
  z-index: 0;
  padding: 1.5rem 0 4rem;
  max-width: 72rem;
}

.quand {
  margin-top: 2.5rem;
  max-width: var(--mesure);
  text-wrap: balance;
}

.cta {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 1.75rem;
  padding: 0.75rem 1.5rem;
  background: var(--bleu);
  color: var(--papier);
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  border-radius: var(--rayon);
  text-decoration: none;
}

.cta:hover {
  background: var(--encre);
}

/* La note qualifie le bouton : elle est dessous, petite, et rien d'autre.
   Ni badge, ni aplat, ni contour. */
.note {
  margin-top: 0.875rem;
  max-width: var(--mesure);
  color: var(--pierre);
  font-size: 0.9375rem;
  /* Le texte vient de phrase_jours_restants() : on ne le réécrit pas, on
     équilibre ses lignes. Sinon « jours » se retrouve seul en bas à 360. */
  text-wrap: balance;
}

.programme {
  margin-top: 4.5rem;
  max-width: 72rem;
}

.programme__list {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
}

.session {
  position: relative;
  padding: 1.75rem 0;
}

/* Le nœud : un petit carré bleu posé sur l'axe, à hauteur du nom de
   l'atelier. L'ordre est porté par la position, pas par un numéro. */
.session__name::before {
  content: "";
  position: absolute;
  left: calc(-1 * (var(--ecart) + 6px));
  top: 0.3em;
  width: 10px;
  height: 10px;
  background: var(--bleu);
}

/* --- Formulaire ----------------------------------------------------------- */

.page {
  max-width: 46rem;
}

.form {
  margin-top: 2.5rem;
  max-width: 34rem;
}

.form__champ {
  margin-top: 1.75rem;
}

/* Le piège à robots du formulaire (inscription.view.php) : un <input
   type="text"> ordinaire, déplacé hors du viewport — 1x1 px, débordement
   masqué, et top:auto pour qu'il garde sa place dans le flux au lieu d'être
   remonté en haut du formulaire. Ces déclarations étaient un attribut
   style="" en ligne ; elles sont ici pour que la CSP puisse interdire
   'unsafe-inline' sur style-src, ce qu'elle fait maintenant.

   Déplacé ET NON display:none, pour une raison qui tient au piège lui-même :
   un bot qui inspecte le style avant de remplir saute les champs qu'il juge
   invisibles. Hors du viewport, le champ reste un champ texte parfaitement
   ordinaire pour un parseur naïf, tout en étant hors d'atteinte à l'écran.
   Et il n'est pas dans la feuille « au cas où la feuille ne chargerait
   pas » : ce fichier EST la feuille du site, il est servi par le même
   mécanisme que le reste de la mise en page, donc si la feuille manque le
   site n'a plus de mise en page du tout — pas seulement ce champ de plus. */
.form__piege {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__label {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1.5;
}

.form__aide {
  margin-bottom: 0.5rem;
  color: var(--pierre);
  font-size: 0.9375rem;
}

.form__input {
  display: block;
  width: 100%;
  min-height: 48px;
  padding: 0.75rem 1rem;
  background: var(--brume);
  color: var(--encre);
  font-family: inherit;
  font-size: 1.0625rem;
  line-height: 1.4;
  border: 0;
  border-radius: var(--rayon);
  -webkit-appearance: none;
  appearance: none;
  /* Un nom d'université sans espace ne doit pas élargir la page. */
  overflow-wrap: break-word;
}

.form__input::placeholder {
  color: var(--pierre);
}

.form__activites {
  margin: 2.25rem 0 0;
  padding: 0;
  border: 0;
}

.choix {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  min-height: 48px;
  margin-top: 0.5rem;
  padding: 0.75rem 1rem;
  background: var(--brume);
  border-radius: var(--rayon);
  cursor: pointer;
}

.choix__case {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--bleu);
}

/* Cochée : la coche elle-même, plus le poids du texte. Jamais la
   couleur seule. */
.choix__case:checked+.choix__texte {
  font-weight: 700;
}

.bouton {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  margin-top: 2rem;
  padding: 0.875rem 1.75rem;
  background: var(--bleu);
  color: var(--papier);
  font-family: inherit;
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  border: 0;
  border-radius: var(--rayon);
  cursor: pointer;
}

.bouton:hover {
  background: var(--encre);
}

/* Pendant l'envoi, le script pose [disabled] et remplace le libellé. Le
   libellé porte le sens — « Envoi en cours… » ; le style, lui, doit dire que
   l'action n'est plus offerte. On sort donc du bleu : le bouton cesse d'être
   le seul aplat --bleu du formulaire et retombe dans la famille des
   surfaces immobiles du formulaire, celles de --brume. Le poids passe à 400,
   même code que « ce n'est pas celui-ci ». --encre sur --brume : 16.22:1,
   bien au-delà des 4.5:1. Un aplat --papier aurait été plus discret encore,
   mais un bouton sans remplissage sur fond --papier n'est plus un bouton :
   on perdrait la cible sans gagner le sens. Le sélecteur double neutralise
   .bouton:hover, de même spécificité mais déclaré plus bas ; l'écrire
   ainsi le rend indépendant de l'ordre des règles. */
.bouton[disabled],
.bouton[disabled]:hover {
  background: var(--brume);
  color: var(--encre);
  font-weight: 400;
  cursor: not-allowed;
}

.lien {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--bleu);
  text-underline-offset: 0.25em;
}

.lien:hover {
  color: var(--encre);
}

.page__actions {
  margin-top: 2rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.5rem;
}

/* --- Erreurs et confirmation ---------------------------------------------- */

/* Pas d'aplat ici : la confirmation en a un, l'erreur se distingue
   donc par ce qu'elle n'a pas, et par son titre en gras. Le rouge ne
   double que ce gras et la bordure du champ : il n'est jamais seul. */
.erreurs {
  margin-top: 2rem;
  max-width: var(--mesure);
}

.erreurs__titre {
  font-weight: 700;
  color: var(--rouge);
}

/* Le résumé prend le focus au chargement d'un POST en échec : tabindex="-1"
   + autofocus, et le document étant neuf, le focus part sans une ligne de JS.
   Le navigateur entourerait alors tout le bloc de son anneau par défaut — un
   anneau autour d'un conteneur, forme qu'aucun élément de la page n'a.

   On le supprime, et c'est la seule suppression de la feuille ; elle est
   correcte ici, pour une raison précise. WCAG 2.4.7 (Focus Visible) demande un
   indicateur parce qu'après avoir tabulé, il faut savoir où l'on est ; sa
   logique présuppose donc un focus que l'utilisateur a déplacé lui-même. Or
   tabindex="-1" rend cet élément impossible à atteindre au Tab : .erreurs:focus
   ne peut être vrai que pour un focus programmatique, et personne n'a rien
   demandé. On supprime un anneau, pas l'indicateur — le titre le porte, juste
   en dessous. Le crochet est :focus et non :focus-visible, parce que les
   navigateurs n'appliquent pas tous :focus-visible à un conteneur focusé par
   script ; en :focus, le comportement est le même partout. */
.erreurs:focus {
  outline: none;
}

/* Le titre prend le relais : un trait rouge de 2px, le double du 1px des liens
   du résumé. L'épaisseur fait le travail — même canal, occurrence plus forte,
   donc « tu es ici » ne se confond pas avec « ceci se suit ». Le trait se
   retire dès que le focus repart, et il n'encadre rien : c'est le point
   d'entrée du bloc qui est marqué, pas sa boîte. */
.erreurs:focus .erreurs__titre {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.25em;
}

.erreurs__liste {
  margin: 0.5rem 0 0;
  padding-left: 1.125rem;
}

.erreurs__liste li {
  color: var(--rouge);
}

/* Chaque message du résumé est un lien vers le champ qu'il concerne. Le lien
   doit se lire comme tel sans crier plus fort que l'erreur qu'il porte :
   donc le rouge du bloc — 7.26:1 sur --papier, celui du texte voisin — le
   même poids, et pour seul différenciateur le trait fin de la feuille, 1px à
   0.25em, le même que sur .nav__link et .lien. Aucune couleur propre au
   lien, aucun aplat : dans le rouge de l'erreur, un lien ne peut pas crier
   plus fort que le texte qu'il contient. Pas de display inline-flex,
   contrairement à .lien : un message se replie sur deux lignes et une boîte
   flex le casserait. Le survol ne change qu'une couleur, comme partout
   ailleurs dans la feuille. */
.erreurs__lien {
  color: var(--rouge);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
}

.erreurs__lien:hover {
  color: var(--encre);
}

.confirmation {
  position: relative;
  margin-top: 2rem;
  padding: 1.5rem 1.5rem 1.5rem;
  max-width: 34rem;
  background: var(--brume);
  border-radius: var(--rayon);
}

.confirmation__title {
  font-size: 1.5rem;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.confirmation__text {
  margin-top: 0.75rem;
  max-width: var(--mesure);
}

/* Le seul or de la feuille : le nœud de la confirmation. C'est le même
   carré que les ateliers, posé sur l'axe, mais en or — la journée est
   arrivée à son bout. */
.confirmation::before {
  content: "";
  position: absolute;
  top: 1.5rem;
  left: calc(-1 * (var(--ecart) + 8px));
  width: 14px;
  height: 14px;
  background: var(--or);
}

.confirmation .lien {
  margin-top: 1rem;
}

/* --- Le tableau des inscriptions ------------------------------------------- */

/* Le tableau est la seule vue de données du site, et la seule fois où une
   colonne est une colonne. Il ne reçoit pas de cadre : la règle qui gouverne
   la page est qu'une seule ligne la traverse, l'axe, et un encadrement ici en
   ajouterait une deuxième — puis chaque cellule se demanderait si la sienne ne
   serait pas plus claire, et c'est ainsi qu'une page de données finit par être
   une page d'interface. La structure reste dans le HTML, que les lecteurs
   d'écran savent lire ; le rendu ne sera que du rythme. */
.inscriptions {
  width: 100%;
  margin-top: 2.5rem;
  /* collapse et non separate : le navigateur espace les cellules de 2px, un
     vide qui ne se voit nulle part mais qui glisse une gouttière fantôme
     entre chaque ligne. Sans trait chez les voisins, il n'y a rien à fusionner
     — le separate n'apporterait donc rien non plus. */
  border-collapse: collapse;
  border: 0;
  /* 0.9375rem : un contenu secondaire, le format de .note, de .form__label et
     de .footer__line — sauf que ces blocs n'ont pas de colonnes et qu'ici il
     y en a six. À 1.0625rem le tableau déborde avant que la ligne ait eu le
     temps de se former. */
  font-size: 0.9375rem;
  line-height: 1.5;
}

/* Les lignes se séparent par la respiration, pas par un trait : 1.125rem,
   l'interligne de .notice > p. Six lignes de texte se touchent ; six blocs de
   2.25rem se détachent. C'est le principe de .session, qui ne vit que de son
   padding. Ni filet, ni aplat, ni ombre : rien ne catégorise une ligne, on voit
   seulement où elle commence. vertical-align top parce qu'une université
   longue sur trois lignes ne doit pas non plus étirer à vide les deux
   cellules à sa droite. */
.inscriptions td {
  padding: 1.125rem 0;
  border: 0;
  vertical-align: top;
  /* Même raison, même déclaration que .form__input : un nom d'université sans
     espace ne doit pas élargir la page. Le tableau peut déborder de la
     gouttière, pas du document. */
  overflow-wrap: break-word;
}

/* L'en-tête se distingue par le poids et par l'espace, jamais par la seule
   couleur (WCAG 1.4.1) : 700, le code déjà employé pour « la page courante »
   et pour la coche cochée — dans cette feuille un poids ne veut dire qu'une
   chose, « c'est celui-ci ». Puis 0.75rem dessous, avant les 1.125rem de la
   première ligne : le tableau s'ouvre sans trait. Et --pierre, parce que les
   colonnes sont la structure, pas le contenu ; le contenu, ce sont les
   personnes. 7.27:1 sur --papier. text-align est déclaré ici et non sur le
   tableau : le navigateur centre n'importe quel th, et une valeur héritée
   perd toujours contre une règle qui vise l'élément. letter-spacing en
   revanche ne descend pas — une colonne est un mot, pas une enseigne. */
.inscriptions th {
  padding: 0 0 0.75rem;
  border: 0;
  color: var(--pierre);
  font-weight: 700;
  text-align: left;
  vertical-align: bottom;
}

/* Le <caption> est le nom accessible du tableau et son seul titre visible : il
   est donc une étiquette, pas un titre. --pierre, 0.9375rem, le format des
   libellés de la page (.form__label). Un titre en corps de .page__title
   donnerait deux titres à deux rangs l'un de l'autre, et le chapeau juste
   au-dessus répète déjà le décompte. Le padding et non une marge : sur une
   boîte table-caption, une marge n'est pas fiable d'un moteur à l'autre. */
.inscriptions caption {
  padding: 0 0 1.5rem;
  color: var(--pierre);
  text-align: left;
}

/* Le numéro et la date sont des chiffres, pas des phrases : à chasse fixe, les
   identifiants s'alignent en colonne et deux dates de même longueur se
   superposent au lieu de se chevaucher d'un cheveu à chaque ligne. C'est la
   seule concession de la feuille à une colonne numérique, et elle ne coûte
   aucun jeton. Les deux extrêmes, dans l'ordre où le tableau les déclare : la
   première colonne est le n°, la dernière la date. Pas de nowrap sur la date —
   à 360, elle se replie en deux lignes, ce qui vaut mieux qu'une page qui
   déborde. */
.inscriptions td:first-child,
.inscriptions td:last-child {
  font-variant-numeric: tabular-nums;
}

/* --- Pied de page --------------------------------------------------------- */

/* Pas d'aplat ici : l'axe doit se poursuivre jusqu'au pied de la page. */
.footer {
  padding: 2rem 0 3rem;
  max-width: 72rem;
}

.footer__line {
  color: var(--pierre);
  font-size: 0.9375rem;
  line-height: 1.5;
  max-width: var(--mesure);
}

.footer__line strong {
  font-weight: 500;
  color: var(--encre);
}

/* Les deux liens externes du pied de page. Le conteneur est un flex pour que
   les deux cibles se touchent sans espace parasite ; .footer__line garde le
   reste de ses règles, ce qui évite une classe de plus. */
.footer__links {
  display: flex;
  gap: 0.5rem;
}

/* 44px de cible, comme .brand et .nav__link : le glyphe fait 20px, la boîte
   cliquable fait le double, et elle est le triple du minimum WCAG 2.5.8
   (24x24). Une icône dessinée à 20px sur une cible de 20px est injouable au
   doigt. La couleur est celle du texte — --pierre au repos, --encre au survol,
   comme .footer__line — donc les icônes ne s'imposent pas et la feuille ne
   gagne aucune couleur. */
.footer__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  color: var(--pierre);
  text-decoration: none;
}

.footer__icon:hover {
  color: var(--encre);
}

.website__icon {
  padding-top: 3px;
}

/* --- Focus ---------------------------------------------------------------- */

/* L'anneau, en deux règles et une condition.

   La première est le REPLI : `:focus`, que tous les moteurs de la feuille
   comprennent, et que rien n'a jamais rendu illisible. Elle PEINT l'anneau.

   La deuxième, celle d'avant, `:focus-visible`, est conservée telle quelle et
   n'a plus rien à faire depuis que le repli existe : sur un moteur qui connaît
   `:focus-visible`, elle redéclare exactement ce que le repli a déjà peint, au
   même élément, donc elle ne change rien. Elle reste parce qu'elle nomme la
   règle du site — l'anneau, c'est `:focus-visible` — et parce que la retirer
   ferait dépendre le comportement d'une ligne écrite pour un seul moteur.

   LA CONDITION, et c'est le point mesuré. Le retrait pour la souris s'écrit
   nécessairement `a:focus:not(:focus-visible)`, et l'idée naturelle est qu'un
   moteur ignorant `:focus-visible` jette cette règle à l'analyse. IL NE LE FAIT
   PAS. `:not()` se lit en mode INDULGENT : un pseudo-classe inconnu qui s'y
   trouve est ignoré, pas invalide, et `:not()` se retrouve vide — donc
   équivalent à ne rien exclure. Mesuré sur ce build, Gecko 157 ET Chromium 153,
   douze variantes : `a:not(pseudo-inconnu)` s'applique (9px) tandis que
   `a:pseudo-inconnu` est bien jeté (1px). Le sélecteur inconnu tombe, la règle
   reste.

   Conséquence, et elle est grave : sur un moteur qui a `:not()` mais pas
   `:focus-visible` — Firefox 84, Safari 9 à 15.3, la seule population que la
   condition distingue de celle d'aujourd'hui — le retrait s'appliquerait à TOUT
   focus, souris comprise, et surtout il l'appliquerait AU CLAVIER : pas d'anneau
   du tout. C'est-à-dire l'inverse exact du repli qu'il est censé servir, et un
   échec de WCAG 2.4.7 fabriqué par le remède.

   D'où le `@supports`, qui est la seule porte qui dise la bonne chose. La
   question à poser n'est pas « la déclaration est-elle valide ? » — la validité
   d'une déclaration ne dit rien de ce qu'un moteur en fera sur un élément
   donné, et c'est exactement le piège du voile plus bas — mais « ce moteur
   connaît-il CE PSEUDO-CLASSE ? ». `selector()` teste cela pour de vrai, et il
   teste un sélecteur, pas une valeur : mesuré, `selector(:focus-visible)` vaut
   `true` sur Gecko 157 comme sur Chromium 153, et
   `selector(a:pseudo-qui-nexiste-pas)` vaut `false`. Au-dessus du plancher la
   condition est donc toujours vraie, les trois règles s'appliquent, et le
   comportement est celui d'avant. En dessous, le retrait n'existe pas et
   l'anneau du repli reste. La porte elle-même est mesurée : un `@supports`
   faux jette bien son bloc entier, declaration comprise.

   LE DÉTAIL QUI FAIT QUE C'EST LE BON TRI : aucune dépendance à l'ordre des
   sources. `:focus-visible` et `:focus:not(:focus-visible)` ne peuvent pas être
   vrais en même temps — l'un pose ce que l'autre exclut, par définition — donc
   les deux règles ne se disputent JAMAIS le même élément. Et là où elles se
   rencontrent, c'est-à-dire au pointeur où seul le retrait s'applique, il les
   départage par SPÉCIFICITÉ et non par ordre : `a:focus` vaut (0,1,1),
   `a:focus:not(:focus-visible)` vaut (0,2,1), parce que `:not()` ne compte pas
   lui-même mais que son argument, `:focus-visible`, compte. Vérifié en inversant
   l'ordre des deux déclarations dans un gabarit de même forme : la même
   déclaration gagne dans les deux sens, donc c'est la spécificité qui décide.
   Mesuré au clavier et à la souris, dans les deux moteurs, sur les trois pages :
   l'anneau est là au Tab et absent au clic, partout. */

a:focus,
button:focus,
input:focus,
summary:focus {
  outline: 2px solid var(--bleu);
  outline-offset: 2px;
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--bleu);
  outline-offset: 2px;
}

@supports selector(:focus-visible) {

  a:focus:not(:focus-visible),
  button:focus:not(:focus-visible),
  input:focus:not(:focus-visible),
  summary:focus:not(:focus-visible) {
    outline: none;
  }
}

/* Le lien d'évitement porte le SEUL anneau blanc du site : il est posé sur son
   propre aplat bleu, donc un anneau bleu par-dessus serait invisible. Même
   repli en deux temps — `:focus` pour tout moteur, `:focus-visible` pour
   l'intention du site.

   `.skip-link:focus` l'emporte sur `a:focus` par SPÉCIFICITÉ, et c'est tout :
   (0,2,0) contre (0,1,1), la classe pèse plus que le type, donc l'ordre des
   sources n'a rien à y faire. Il se trouve ici par ordre de lecture, pas par
   nécessité.

   Le retrait pour la souris, lui, s'applique aussi à ce lien — et il DOIT :
   `a:focus:not(:focus-visible)` vaut (0,2,1), donc il bat `.skip-link:focus`
   (0,2,0), et le clic de souris sur le lien d'évitement n'affiche rien non
   plus. C'est le comportement voulu pour tous les liens. Au clavier en revanche
   les deux sélecteurs ne peuvent pas être vrais ensemble, et c'est
   `.skip-link:focus-visible` qui dessine le blanc. Mesuré dans les deux
   moteurs, sur les trois pages : anneau blanc au premier Tab, aucun anneau au
   clic. */
.skip-link:focus {
  outline: 2px solid var(--papier);
}

.skip-link:focus-visible {
  outline: 2px solid var(--papier);
}

/* Aucun mouvement, donc rien à réduire : pas de transition, pas d'animation.
   Le seul changement à l'usage est le survol, qui ne fait que changer
   une couleur. */

/* --- Champ invalide -------------------------------------------------------- */

/* Sous le champ, à côté d'une entrée posée, un texte en 400 se lit comme
   une phrase de plus. 400 -> 600 le rend trouvable sans la couleur. */
.champ__erreur {
  margin-top: 0.375rem;
  color: var(--rouge);
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.5;
}

/* Le signal le plus fort est sur le champ : les entrées n'ont aucune
   bordure, donc celle-ci est un trait nouveau. Fond --brume inchangé. */
.form__input[aria-invalid="true"] {
  border: 2px solid var(--rouge);
}

/* Le groupe d'activités est un conteneur, pas une entrée : l'entourer
   d'un trait casserait la règle de la page et en ferait une carte. On
   passe par le texte : la légende nomme la question restée sans réponse. */
.form__activites[aria-invalid="true"] .form__label {
  color: var(--rouge);
}

/* Focus ET invalide : l'anneau se dessine hors de la bordure, 2px de
   papier entre les deux, donc rien n'est masqué. Le bleu reste prioritaire. */
.form__input[aria-invalid="true"]:focus-visible {
  outline: 2px solid var(--bleu);
  outline-offset: 2px;
}

/* --- Page « Confidentialité » ---------------------------------------------- */

/* Uniquement de l'espacement. La page est du texte calme : elle reprend la
   mesure du corps (--mesure), la couleur de .page__lead et la taille de
   .section-title — aucune classe neuve, aucun jeton de couleur de plus. Ce
   qui manquait à la feuille, c'était le rythme entre des sections de prose :
   .programme reçoit son margin-top de son propre bloc, une page en texte
   n'a pas de bloc parent pour le lui donner. */
.notice>p,
.notice>ul {
  max-width: var(--mesure);
  margin-top: 1.25rem;
}

/* Même retrait que .erreurs__liste : sur ce site, une liste commence à la
   même profondeur partout. */
.notice>ul {
  padding-left: 1.125rem;
}

/* Un titre de section ne colle jamais au texte qui le précède : 3rem, l'écart
   du programme (.programme). Pas plus — une page de texte n'a pas besoin
   d'être plus aérée qu'une page d'atelier, et le premier titre garde la
   proximité du chapeau qu'il suit. */
.notice>.section-title {
  margin-top: 3rem;
}

/* --- Les deux états qui ne sont pas l'état ouvert -------------------------
   Ce bloc est ajouté À LA FIN de la feuille, et c'est le bon endroit : les
   règles qu'il complète (body::after, le @media 700px, le @supports not) ne se
   départagent que par l'ordre de source et perdent toute valeur si l'on glisse
   quelque chose entre elles. Rien plus haut n'est touché.
   ------------------------------------------------------------------------- */

/* La page de remerciement (merci.view.php) est une page de texte calme, donc
   elle prend la structure de confidentialite.view.php : `.page` pour la
   mesure, `.notice` pour le rythme. Aucune classe neuve pour la structure.

   Cette seule exception réinitialise la ligne d'édition. `.notice > p` pose
   1.25rem de marge au-dessus de TOUT <p> direct, `.edition` compris — or
   `.edition` est la première ligne du hero et se sert de son padding-bottom
   pour l'écart vers le titre : une marge de 1.25rem la descendrait d'un cran
   et l'alignerait plus bas que sur la page d'accueil, pour rien. On rend donc
   la ligne à sa place. (0,2,0) contre (0,1,1) : la règle gagne sans dépendre de
   l'ordre d'écriture. */
.notice>.edition {
  margin-top: 0;
}

/* La seule emphase de la page de remerciement : la phrase qui regarde vers la
   suite, celle qu'un visiteur revient chercher. Le poids, parce que c'est le
   canal que la page a déjà choisi — 500 est celui des libellés de formulaire
   (.form__label) et des noms d'atelier (.session__name), et 700 est « c'est
   celui-ci ». Aucune couleur : --encre est déjà celle du texte, donc la
   hiérarchie ici ne repose sur rien d'autre que le trait, comme partout.

   PAS de transition, PAS d'animation : la feuille n'en a nulle part (voir la
   note « Aucun mouvement » plus haut), et rien ici ne gagnerait à en avoir une.
   Il n'y a donc rien à réduire dans prefers-reduced-motion — on ne
   rajoute pas une @media qui réduise une transition qui n'existe pas. */
.merci__suite {
  font-weight: 500;
  letter-spacing: -0.01em;
}

/* --- La page d'accueil, inscriptions closes -------------------------------
   Ce que rend le hero à la place du bouton : une phrase d'état en gras, puis
   la date — celle de $quand, déjà mise en phrase plus haut par phrase_lieu() et
   phrase_horaires(). Aucune date n'est écrite dans le gabarit, donc aucune ne
   peut s'en désynchroniser quand la config change.

   Le bleu est VOLONTAIREMENT absent. Sur ce site le bleu est l'action : l'axe,
   le focus, et le bouton — trois fois, pas une de plus (cf. .nav__link). Un
   encadré ou un aplat bleu à la place du bouton se lirait encore comme un
   bouton, et l'absence de ce que la page rend cliquable est justement ce qui
   distingue l'état. Le sens passe donc par deux canaux qui ne sont pas la
   couleur : le <strong> qui ouvre la phrase, et les mots eux-mêmes. C'est la
   lecture de l'état, pas une décoration pour le marquer.

   AUCUN jeton de couleur nouveau non plus : --encre par héritage, donc 16.4:1
   sous le voile à 0.95 mesuré plus haut pour cette couleur (et 7.27:1 sur
   --papier), bien au-delà des plafonds que ce fichier note pour --pierre. Pas
   de question de se poser sous le voile à 0.85 des écrans étroits : la couleur
   est celle du texte courant, celle que le voile a été calibré pour.

   PAS de carré sur l'axe, contrairement aux ateliers et à la confirmation :
   le carré marque un moment du programme, et le hero n'en a dans aucun des deux
   états. En ajouter un ici pour marquer l'état produirait un élément que rien
   ne justifie dans une grille qui n'admet qu'une seule ligne et aucun
   encadrement — il signalerait un point de programme qui n'existe pas.

   AUCUN media query : la mesure est celle du corps (--mesure) et le texte fait
   1.0625rem comme le reste de la page, donc la phrase se replie exactement
   comme le chapeau du hero au-dessus d'elle, sur la même colonne et au même
   corps — il n'y a rien à rediriger à 700px. */
.ferme {
  margin-top: 2.5rem;
  max-width: var(--mesure);
  text-wrap: balance;
}

/* ==========================================================================
   Le fond photographique, là où le navigateur sait choisir le format.
   Ce bloc est AJOUTÉ À LA FIN de la feuille, et c'est le bon endroit : il
   complète body::after et son @media 700px, qui ne se départagent que par
   l'ordre des sources. Rien plus haut n'est touché.

   La CONDITION est le cœur du bloc. Elle demande exactement ce que la
   déclaration doit satisfaire pour être acceptée : est-ce que ce navigateur
   comprend un image-set() à descripteur de LARGEUR (`1280w`) ? Le test est
   écrit avec la syntaxe d'une déclaration, donc le navigateur y fait le même
   travail de validation que pour `background-image` — un `w` refusé y fait
   échouer la condition. Condition fausse, bloc ignoré, et le JPEG en url() du
   bloc de base reste en place.

   C'est la seule façon d'avoir les deux : négociation de format là où le
   descripteur de largeur est compris, JPEG partout ailleurs, et dans aucun cas
   une valeur invalide qui efface le voile avec la photo.

   ⚠️  QUI OBTIENT L'AVIF, AUJOURD'HUI : personne, sur les moteurs vérifiés.
   Le descripteur de largeur `w` n'est compris par AUCUN des deux moteurs
   réellement testés ici — Gecko 157 et Chromium 153. Douze variantes sondées
   dans Gecko 157, avec l'URL exacte du site, relatives ou absolues, avec et
   sans `type()`, avec `1280w`, `960w`, `9999w` ou une largeur sans unité :
   `CSS.supports` répond `false` ET la valeur calculée vaut `none` — donc ce
   n'est pas une bizarrerie de `CSS.supports`, c'est un refus réel, au même
   endroit que celui décrit dans body::after. Le descripteur `x` en revanche
   (`2x`) passe, avec ou sans `type()`. Ce bloc est donc, en l'état, une
   déclaration qui ne s'exécute nulle part : les deux navigateurs de contrôle
   prennent le JPEG de 118 Ko, ce qu'ils prenaient déjà. C'est un fait
   ennuyeux et c'est le bon, parce que le JPEG est le repli CORRECT dans tous
   les cas. Ne pas réécrire l'architecture `@supports`/`url()` pour le
   rattraper : le repli est juste, la cascade autour est porteuse, et un
   descripteur de largeur refusé au calcul effacerait le voile avec la photo.
   La négociation de format reprendra son effet le jour où un moteur
   comprendra `w` — et ce jour-là, ce bloc sera déjà en place, sans reprise.

   Le `and (filter: blur(1px))` n'est pas décoratif. Ce bloc est placé APRÈS
   le `@supports not (filter: blur(1px))` qui interdit la photo sur un moteur
   sans flou ; sans cette seconde condition, un moteur qui comprendrait le
   descripteur de largeur mais pas le flou ressusciterait le fond que le repli
   interdit. Elle reste donc écrite même si la première condition est fausse
   partout aujourd'hui : le jour où un moteur comprendra `w`, les deux
   conditions devront être vraies ensemble, et un moteur qui n'aurait qu'une des
   deux est le seul cas que la seconde existe pour couvrir.

   ⚠️  NE PAS factoriser la liste image-set() dans une variable pour
   supprimer la duplication ci-dessous. Un var() rend la déclaration
   analysable, donc une valeur invalide à la valeur CALCULÉE ne retombe pas
   sur une déclaration précédente : elle vaut « unset », donc initial pour
   background-image, donc none — on perdrait le voile, exactement ce que la
   duplication existe pour empêcher. Voir la note dans body::after.
   ------------------------------------------------------------------------- */

/* Cinq candidats, deux largeurs, et le format laissé au navigateur.
   CE QUI EST CHOISI ICI — deux paliers, pas trois :

     960w  en dessous, 1280w au-dessus. 1600w n'est PAS proposé.

   Pourquoi pas 1600 : bg-1600.* est un agrandissement de la source, qui
   n'a aucun détail au-delà de 1280 px. Ses 1600 px sont donc 1280 px de
   réel étalés sur 1600 — le même nombre de détails que le JPEG 1280, pour
   59 Ko de plus que lui. Annoncer une largeur qu'on n'a pas est le pire
   des deux mondes ici : le navigateur paie pour des pixels qui n'existent
   pas. La borne haute est donc 1280, ce que la source permet réellement.

   Pourquoi deux paliers alors que le JPEG sert les deux : parce que sur un
   téléphone, le palier unique coûtait 118 Ko pour afficher ~770 px
   d'appareil. Mesuré sur 390x844 en DPR 3, « cover » sur une image en
   3:4 demande max(390/960, 844/1280) = 0.659 de facteur, donc
   771x1669 px PHYSIQUES — et 960 de large couvre déjà 1.24 fois ce besoin.
   Un DPR 2 sur 360 de large ne demande que 360x640 px : 960 y est 2.7 fois
   suréchantillonné. En dessous de 1280 de largeur CSS, bg-960.avif (14,6 Ko)
   rend donc la même chose que le JPEG pour un huitième du poids.
   Au-dessus, « cover » décide du facteur par la largeur et le JPEG 1280
   est le dernier palier possible : sur un 1440x900 en DPR 1 il est
   étiré à 1.27, sous le flou de 2 px et le voile à 0.25, invisible.

   ⚠️  On ne peut pas TRICHER le navigateur avec sizes pour qu'il rende
   compte de l'agrandissement de « cover » : le facteur dépend de la
   hauteur, pas seulement de la largeur, il n'est donc pas exprimable en
   vw. En l'absence de sizes, la sélection par largeur pour un
   background-image suppose 100vw — c'est l'hypothèse honnête, et le
   palier 1280 en est de toute façon le plafond.

   type() explicite sur chaque candidat : c'est lui qui fait que le
   navigateur saute les formats qu'il ne décode pas. Sans lui, la
   sélection se fait à l'aveugle sur une extension. */
@supports (background-image: image-set(url("/public/images/bg-960.avif") 960w type("image/avif"))) and (filter: blur(1px)) {

  /* La rampe du desktop : plateau à 0.95 jusqu'à --cadre + gouttiere (1082px),
     0.45 à 12rem plus loin, 0.25 au bord droit. Valeurs et mesures dans le
     bloc body::after, plus haut. */
  @media (min-width: 701px) {
    body::after {
      background-image:
        linear-gradient(90deg,
          rgba(255, 255, 255, 0.95) 0,
          rgba(255, 255, 255, 0.95) calc(var(--gouttiere) + var(--cadre)),
          rgba(255, 255, 255, 0.45) calc(var(--gouttiere) + var(--cadre) + 12rem),
          rgba(255, 255, 255, 0.25) 100%),
        image-set(url("/public/images/bg-960.avif") 960w type("image/avif"),
          url("/public/images/bg-960.webp") 960w type("image/webp"),
          url("/public/images/bg-1280.avif") 1280w type("image/avif"),
          url("/public/images/bg-1280.webp") 1280w type("image/webp"),
          url("/public/images/background_site.jpeg") 1280w type("image/jpeg"));
    }
  }

  /* Le voile uniforme à 0.85 des écrans étroits, avec le MÊME jeu d'images.
     Séparé du @media 700px plus haut, et non fusionné avec lui : la condition
     @supports est ici, et elle ne peut pas être atteinte ailleurs que là où
     le voile est le bon — c'est-à-dire aux deux seules largeurs que ces deux
     @media couvrent, et nulle part entre les deux. */
  @media (max-width: 700px) {
    body::after {
      background-image:
        linear-gradient(90deg, rgba(255, 255, 255, 0.85) 0, rgba(255, 255, 255, 0.85) 100%),
        image-set(url("/public/images/bg-960.avif") 960w type("image/avif"),
          url("/public/images/bg-960.webp") 960w type("image/webp"),
          url("/public/images/bg-1280.avif") 1280w type("image/avif"),
          url("/public/images/bg-1280.webp") 1280w type("image/webp"),
          url("/public/images/background_site.jpeg") 1280w type("image/jpeg"));
    }
  }
}


/* ==========================================================================
   Desktop : 701px et plus.
   Ce bloc est volontairement fermé sur lui-même. La feuille a une seule règle
   qui s'applique partout et une seule qui redirige les écrans étroits
   (max-width: 700px) : tout ce qui vit ici s'ajoute PAR-DESSUS la base, et
   ne peut donc pas revenir la modifier. Un 701px aujourd'hui ne peut pas
   faire de dégât à un 390px demain, pas même par refonte : c'est la propriété
   qui rend cette feuille réversible, et elle vaut d'être écrite.

   Ne pas fusionner ce bloc dans la base « pour simplifier ». La base EST
   l'écran étroit, lui compris : c'est elle qui rend le 390px exactement
   comme il est aujourd'hui.

   LE CONTENU DE CE BLOC, et pourquoi il est en deux morceaux :
   701px est le seuil historique du site, mais deux colonnes séparées par 96px
   ne tiennent que si le viewport offre 1024 + 86 = 1110px. En deçà, à 701px,
   chaque colonne ferait 259px : le chapeau du hero y tiendrait sur trois lignes
   de 29 caractères, et la date prendrait la moitié de la colonne. Il y a donc
   deux largeurs, et elles ne font pas la même chose : dès 701px ce qui ne
   dépend que du cadre et du rythme (le cadre, la date, les rangs du programme,
   le pied de page), et dès 1050px ce qui exige la place pour deux colonnes. Les
   deux restent du desktop, donc aucun 390px ne peut en être atteint.

   LE PROBLÈME QUE CES DEUX BLOCS RÉSOLVENT, mesuré avant refonte sur
   1440x900 : .main, .programme et .footer portaient max-width: 72rem (1152px)
   alors que leur texte s'arrêtait à 586-620px — 620px d'encre sur 1440, et sur
   1920. Trois échecs distincts, trois corrections distinctes :

     1. tout le contenu était collé à gauche, et le tiers droit de l'écran
        n'était qu'une photo sans rien dessus ;
     2. les boîtes annonçaient 1152px sans en utiliser que la moitié, et un
        correctif antérieur avait même ramené .header__inner à var(--mesure)
        (586px) — la marque et la nav se tassaient à x=644 sous 800 à 1300px
        de blanc, pendant que .main gardait 1152px ;
     3. la colonne de gauche était lâche en même temps qu'elle était étroite :
        quatre ateliers espacés de 169px pour 82px de contenu, parce que
        .session__text réservait 3lh pour des descriptions qui en tiennent deux.

   LA COMPOSITION, en une phrase : un cadre de 1024px à deux colonnes de 464px,
   séparées par 96px, où la colonne de gauche porte l'identité et les noms des
   ateliers — ce qui était sur l'axe, et qui y reste, carrés bleus compris —
   et la colonne de droite porte le détail : quand, où, le bouton, la note, et
   la description de chaque atelier. Trois bords verticaux tiennent sur toute
   la page — x=58, x=618, x=1082 — et la photo commence au troisième.

   Le cadre --cadre répond aux trois échecs : il donne à la page une largeur qui
   existe, il aligne l'en-tête et le pied de page dessus, et il garantit que la
   grille ne s'arrête jamais en cours de route. La composition elle-même, ses
   deux colonnes et ce qui les sépare, sont dans le bloc de 1050px plus bas.
   ========================================================================== */
@media (min-width: 701px) {

  /* LE CADRE, et lui seul. Cinq blocs, une valeur.

     C'est la réponse au deuxième échec : des boîtes de 1152px qui n'utilisent
     que 586px, c'est une grille qui n'en est pas une. 1024px est la largeur du
     PAPIER, pas d'un texte : aucune de ces cinq boîtes n'a de contenu qui la
     remplisse, et c'est voulu — le cadre est une grille, et une grille
     n'a pas à être pleine.

     Elle est aussi la seule garantie structurelle du voile : le plateau de
     body::after finit à --gouttiere + --cadre = 1082px, donc tant que ces cinq
     boîtes s'arrêtent à 1024, AUCUNE encre ne peut se retrouver dans la rampe.
     Ne pas élargir l'une d'elles sans déplacer le plateau avec.

     Sur .page, l'élargissement est un CADRE, pas une mesure : la prose y reste
     collée à --mesure (620px pour .page__lead, 586 pour .notice > p) et le
     formulaire à 34rem. C'est le but — les cinq boîtes partagent le même bord
     gauche et le même bord droit, au lieu de flotter dans des largeurs
     différentes. Ce que font les pages en prose de CE 1024px est décrit dans
     le bloc de 1050px plus bas : `.page.notice` y resserre son cadre à
     --mesure et le centre. `/inscription` y garde les deux colonnes.
     404 et 500, eux, gardent le cadre calé à gauche et 1024px de large, sans
     rien dedans à droite : ce sont deux blocs, pas une grille, et les borner
     ferait passer leur <h1> sur deux lignes pour rien.

     .header__inner est dans le même groupe parce que c'est le même problème,
     et il était le pire : avant, il portait max-width: var(--mesure), soit
     586px, et le flex space-between collait donc la nav à x=644 pendant que
     .main, .programme et .footer annonçaient 1152px. Trois largeurs pour une
     seule page, dont deux ne positionnaient rien : sur 1920, la marque et la
     nav tenaient dans 586px d'un écran de 1920. Maintenant la marque reste à
     gauche, sur l'axe, à x=58, et la nav arrive au bord droit du cadre, x=1082
     — qui est aussi le bord de la rampe et celui de la grille. Trois fins de
     ligne au même pixel, et la seule qui pouvait varier. 1024 - 170 (la marque)
     - 196 (la nav) = 658px de jeu : aucun repli, de 701px à 2560px. */
  .main,
  .programme,
  .footer,
  .header__inner,
  .page {
    max-width: var(--cadre);
  }

  /* LA DATE : 8rem au lieu de 9rem, et c'est une contrainte de GRILLE.

     Mesuré à 9rem sur la police de la pile (Helvetica Neue) : « 22.10.26 »
     fait 509px d'encre, pour 464px de colonne gauche. Elle débordait donc de
     45px — sans jamais toucher la colonne de droite, la gouttière de 96px
     l'en empêchant, mais en cassant le bord de colonne à toutes les largeurs
     de desktop, ce qui est le contraire d'une grille. À 8rem l'encre fait
     452px : 12px de jeu dans la colonne, et 108px avant la colonne de droite.

     Ce 12px n'est pas un hasard : le nombre dépend de la police. Le même
     glyphe sur Inter (chasses de 0.6em) fait 486px à 8rem et déborderait de
     22px. Le débordement reste sans conséquence — la colonne gauche et sa
     gouttière font 464 + 96 = 560px, et le pire cas raisonnable sur la pile
     (6 chiffres à 0.636em + 2 points à 0.318em, tracking -0.045em, soit
     4.09em) fait 524px à 8rem : la date peut déborder, elle ne peut pas
     atteindre la colonne de droite.

     On ne touche pas au clamp() de la base : il sert l'écran étroit, où
     15vw est juste. Ici c'est sa borne haute qui décide, et elle décide
     d'une colonne. */
  .jour {
    font-size: clamp(3.5rem, 15vw, 8rem);
  }

  /* LE PROGRAMME : quatre rangs de hauteur égale, sans place réservée.

     Réponse au troisième échec. .session__text réservait min-height: 3lh parce
     que, dans une liste à une colonne, une description sur deux lignes
     emmène ses voisins et casse l'égalité des nœuds sur l'axe. La réserve
     marchait — et coûtait 27px de vide à chacun des quatre ateliers, 169px de
     pas pour 82px de contenu.

     Elle est REMPLACÉE, pas réduite : .programme__list devient une grille à
     une colonne implicite et `grid-auto-rows: 1fr`, ce qui donne aux quatre
     rangs la hauteur du plus haut sans réserver quoi que ce soit. Sur une
     grille de hauteur indéfinie, une piste en fr se dimensionne sur le
     max-content de toutes les pistes : les quatre rangs font donc exactement
     la hauteur du plus long, et un `min-height: 0` suffit à rendre la place.

     Le pourquoi de la réserve est donc PRESERVÉ par un mécanisme qui ne fait
     pas de trou : l'égalité des nœuds, elle, ne tient plus d'une hauteur
     devinée en lignes mais de la grille elle-même. Mesuré : les quatre rangs
     font exactement la même hauteur à 701, 1050, 1280, 1440 et 1920 — 142px à
     701px (une seule colonne, nom et description empilés) et 110.375px dès
     1050px, contre 169px avant refonte.

     PAS de row-gap, et c'est un choix : à 0 les quatre blocs se touchent
     exactement, et la respiration vient entièrement du padding de .session —
     28px au-dessus et 28px en dessous, contre 27.2px entre deux lignes de
     description. Un intervalle de 4px ne se voyait pas à l'écran et
     n'expliquait rien ; le padding, lui, existait déjà et fait déjà le
     travail. */
  .programme__list {
    display: grid;
    grid-auto-rows: 1fr;
  }

  .session {
    align-items: start;
  }

  /* Sur une seule colonne, .session empile le nom au-dessus de sa description
     et le `margin-top` de 0.5rem est ce qui les sépare : il ne peut pas
     disparaître. Sur deux colonnes, à partir de 1050px, les deux se trouvent
     côte à côte dans la MÊME rangée — et le nom et la description se
     présenteraient alors avec leurs lignes de base décalées de 8px, le carré
     de l'axe avant le texte qu'il annonce. D'où le `margin-top: 0` du bloc
     de 1050px ; cette règle ne fait que remettre le `min-height` à zéro. */
  .session__text {
    min-height: 0;
  }

  /* LE PIED DE PAGE : deux colonnes, sur la même grille que le reste.

     Avant : trois .footer__line empilées à gauche, dans une boîte de 1152px
     dont le texte s'arrêtait à 517px — trois lignes esseulées en bas d'une
     page de 1200px, sans lien avec rien. Le crédit va dans la colonne de
     gauche (le texte), les liens dans celle de droite (les actions), et ils se
     touchent : un pied de page se lit comme une phrase à gauche et une liste
     d'actions à droite. Mesuré : le crédit finit à x=522 et le lien commence à
     x=618, donc les deux colonnes sont celles du hero et du programme, aux
     mêmes trois bords qu'eux.

     La répartition est ÉCRITE, pas déduite, parce que .footer a trois enfants
     aujourd'hui (crédit, lien, icônes) et quatre le jour où `mainteneur_site`
     existera dans la config. Les nth-child ci-dessous placent le premier dans
     la colonne 1 et tous les suivants en haut de la colonne 2 : ajouter une
     ligne de pied de page ne demandera pas de règle nouvelle, et la grille ne
     peut pas les répartir en damier comme le ferait un placement automatique.

     `align-items: start` et non le stretch par défaut : sans lui, la ligne de
     crédit s'étirerait à la hauteur de la colonne de droite et sa dernière
     ligne flotterait au milieu du vide. */
  .footer {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--colonne-ecart);
    align-content: start;
    align-items: start;
  }

  .footer__line:first-child {
    grid-column: 1;
    grid-row: 1;
  }

  .footer__line:nth-child(2) {
    grid-column: 2;
    grid-row: 1;
  }

  .footer__line:nth-child(3) {
    grid-column: 2;
    grid-row: 2;
  }

  .footer__line:nth-child(4) {
    grid-column: 2;
    grid-row: 3;
  }

  /* La marge haute de .quand et de .ferme existait pour espacer le hero de son
     chapeau ; en colonne de droite, elle ne séparerait le bloc de RIEN — elle
     le ferait descendre de 40px sous le haut de la colonne. C'est la marge du
     bloc qui le suit qui porte l'écart, donc celle-ci tombe. .cta (1.75rem) et
     .note (0.875rem) gardent la leur : elles séparent deux blocs d'un même
     groupe, pas deux colonnes. */
  .quand,
  .ferme {
    margin-top: 0;
  }
}

/* ==========================================================================
   Desktop large : 1050px et plus, la composition à deux colonnes.
   Même règle que le bloc précédent : ce @media est frère, jamais imbriqué dans
   la base, et il ne peut donc pas modifier un écran étroit. Il ne touche pas
   au voile, ni à l'en-tête, ni au pied de page — tout ce qui ne dépend que du
   cadre est déjà réglé à 701px. Ici il n'y a que la répartition en deux
   colonnes, et elle est la même pour le hero et pour le programme : deux
   colonnes de 464px séparées par --colonne-ecart. Les deux grilles sont donc
  Alignées colonne sur colonne, et c'est mesuré :
   grid-template-columns vaut "464px 464px" sur .hero comme sur .footer.

   LE HERO, deux colonnes : l'identité à gauche, le pratique à droite.
   `.hero__identite` et `.hero__pratique` sont deux <div> du gabarit
   (index.view.php), et ils ne pourraient pas l'être en CSS seul : sans le
   <div>, les sept éléments frères — cinq à l'état closee — se répartiraient
   ligne par ligne dans la grille, et la hauteur d'une rangée serait le maximum
   des deux colonnes, donc du blanc dans le hero dès qu'un bloc de droite
   dépasse son voisin de gauche. Le regroupement est structurel, et il ne
   change rien au contenu ni à l'ordre : .edition, .jour, .event-name et
   .pitch d'un côté, .quand puis .cta puis .note — ou .ferme seul, à l'état
   closee — de l'autre. Rien n'est écrit en dur : tout vient de $evenement ou
   de phrase_lieu()/phrase_horaires(), comme avant.

   `align-items: end`, et non start : les deux colonnes n'ont pas la même
   hauteur — 305px pour l'identité, 172px pour le bloc pratique, mesuré — et
   les aligner par le bas achève les deux à la même ligne, celle de la fin du
   chapeau. Par le haut, on aurait eu 133px de vide SOUS le bouton, en pleine
   colonne de droite ; par le bas, le vide est de 134px mais il est en haut de
   la colonne, à hauteur de la date, où il se lit comme de l'air et non comme
   un trou. C'est le seul arbitrage que `align-items` tranche ici, et il ne
   coûte rien à l'écran.
   ========================================================================== */
@media (min-width: 1050px) {

  /* Les deux colonnes sont ÉCRITES et non laissées au placement automatique,
     même si l'ordre du gabarit suffirait aujourd'hui : la position d'un bloc
     du hero ne doit pas pouvoir changer parce qu'on a ajouté un <p> avant
     lui. `align-items` s'occupe du haut et du bas, pas de la colonne. */
  .hero {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--colonne-ecart);
    align-items: end;
  }

  .hero__identite {
    grid-column: 1;
  }

  .hero__pratique {
    grid-column: 2;
  }

  /* Chaque atelier devient une LIGNE de deux colonnes : le nom à gauche, sa
     description à droite. C'est la forme d'un programme — on lit les quatre
     noms en diagonale, puis les quatre descriptions en diagonale — et c'est ce
     qui remplit la largeur du cadre au lieu de laisser 438px vides à droite
     d'un texte de 586px.

     La ligne de gauche est celle de l'axe : .session__name est le premier
     enfant, donc la première colonne, donc x=58, donc le carré bleu de
     .session__name::before tombe toujours À CHEVAL sur l'axe (58 - 34 = 24,
     le carré faisant 10px de large). C'est mesuré, pas supposé : c'était la
     question que cette disposition devait trancher, et elle se tranche ici —
     le programme reste sur l'axe, il n'a pas de deuxième registre de carrés.

     `align-items: start` vient du bloc de 701px ci-dessus, et il compte plus
     encore ici : sans lui la description s'étirerait à la hauteur de la ligne
     et le `min-height: 0` des lignes courtes se verrait.

     Le `margin-top: 0.5rem` de la description tombe ici, et la raison est
     dans le bloc de 701px : les deux éléments sont désormais sur la même
     rangée, donc leurs lignes de base ne doivent pas être décalées de 8px.
     Mesuré après : 110.4px de pas entre deux ateliers, contre 118.4px avec
     la marge, et 169px avant refonte. */
  .session {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--colonne-ecart);
  }

  .session__text {
    margin-top: 0;
  }

  /* --- /inscription : identité à gauche, action à droite ------------------
     L'accueil n'était pas la seule page à laisser la moitié droite vide :
     mesuré à 1440px AVANT ce bloc, l'encre de <main> s'arrêtait à 661px sur
     /inscription (45.9 % du viewport), et le formulaire lui-même à 603px — dans
     un cadre qui va jusqu'à 1082. C'est-à-dire la première phrase de la
     demande, mot pour mot, sur la page où l'on clique le bouton.

     La grille est donc la MÊME que celle du hero, et pour la même raison : deux
     colonnes de 464px séparées par 96px demandent 1110px de viewport, donc le
     seuil est 1050px comme partout ailleurs. Un seul seuil pour « la page a
     deux colonnes », plutôt que deux paliers qui divergeraient. En dessous,
     /inscription garde alors sa colonne unique de 34rem, sa forme naturelle.

     `.page.formulaire`, et JAMAIS `.page` seul : `.page` est partagé avec 404
     et 500, dont les <p> directs se répartiraient en damier — le titre en
     colonne 1, le chapeau en colonne 2, la page illisible. `.formulaire` est le
     marqueur que porte inscription.view.php ; les deux <div> qu'il contient
     sont nommés, donc rien n'est laissé au placement automatique.

     `align-items: start`, pas `stretch` : sans lui, la colonne de gauche
     s'étirerait à la hauteur du formulaire et son titre flotterait au milieu du
     vide au lieu de rester en haut de son bloc.

     PAS de `justify-items: start` ici, et c'est une mesure. Sur une grille,
     `start` réduit l'item à sa largeur AJUSTÉE, donc à son min-content : la
     colonne de droite était tombée à 259px, la largeur du plus long mot du
     formulaire, et les champs n'étaient plus à leur taille. Une colonne de
     grille doit remplir sa piste — c'est le comportement par défaut, il n'a
     rien à écrire. */
  .page.formulaire {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--colonne-ecart);
    align-items: start;
  }

  .formulaire__intro {
    grid-column: 1;
  }

  .formulaire__action {
    grid-column: 2;
  }

  /* La marge haute du premier bloc de la colonne de droite tombait : elle
     séparait le hero de son chapeau, et dans une colonne elle ne séparerait le
     bloc de rien — elle le ferait descendre de 32 ou 40px sous le haut de la
     colonne. `margin-top: 0` porte sur le PREMIER enfant uniquement : sans
     résumé d'erreurs, c'est le formulaire qui s'aligne sur le titre ; avec, le
     résumé s'aligne sur le titre et le formulaire garde ses 2.5rem sous lui,
     qui séparent deux blocs d'un même groupe.

     Le résumé d'erreurs reste DANS la colonne de droite et juste au-dessus du
     formulaire : il est `role="alert"` et autofocus, et il doit rester la
     première chose au-dessus du formulaire qu'il décrit. */
  .formulaire__action>.erreurs:first-child,
  .formulaire__action>.form:first-child {
    margin-top: 0;
  }

  /* La confirmation (état `$success`) n'a pas deux colonnes à remplir : c'est
     UN bloc. Elle occupe donc les deux colonnes, ce qui lui rend ses 34rem —
     et, plus important, la laisse à x=58, donc son carré or à x=22, à cheval
     sur l'axe. Dans la colonne de gauche elle serait aussi à 58, mais dans la
     colonne de droite elle serait à 618 et son carré à 582, dans la gouttière,
     sans trait sous lui : le seul or de la feuille serait devenu un carré
     flottant au milieu de la page. */
  .confirmation {
    grid-column: 1 / -1;
  }

  /* --- Les pages en prose : la mesure, centrée dans le cadre ---------------
     /confidentialite et /merci ne se partagent pas en deux colonnes : ce sont
     des pages de lecture, et l'ordre de lecture y compte plus que remplir une
     grille. `columns` est donc écarté — il coupe entre deux blocs sans
     prévenir, et il couperait ici entre le titre d'une section et son premier
     paragraphe.

     Ce qui leur manque n'est pas de la largeur, c'est une PLACE. Avant, la
     prose faisait 586px à l'intérieur d'un cadre de 1024 : 438px de blanc à
     droite, et un bord droit qui ne tombait ni sur la colonne 1 (x=522) ni
     sur le bord du cadre (x=1082) — donc sur rien du tout. C'est cela qui se
     lisait comme un accident.

     La correction tient en DEUX déclarations, sur le `<section>` lui-même :
     borner le cadre de prose à la mesure, puis le centrer dans le cadre du
     site. Le `<section>` est ici le seul élément à borner, et c'est
     indispensable : `--mesure` est en `ch`, donc il se résout sur la taille de
     police de l'ÉLÉMENT — 586px pour .notice (17px), 620px pour .page__lead
     (18px), 827px pour un <h2> (24px), 1792px pour le <h1> (52px). Mis sur
     `> *`, chaque bloc aurait été centré dans SA propre largeur, et la prose
     se serait retrouvée en escalier — mesuré : x=156 pour les <h2>, 260 pour
     le chapeau, 277 pour les paragraphes, 311 pour les notes. Bornée à la seule
     boîte du `<section>`, qui hérite de 17px et vaut donc 586px, elle borne
     tous ses enfants par elle-même, quelle que soit leur taille, et ils
     partagent le même bord gauche. C'est la seule façon d'aligner des blocs de
     tailles différentes, et c'est pourquoi la règle ne porte que sur elle.

     Mesuré à 1440 : le cadre de prose fait 586px et commence à x=277, donc
     219px de blanc de chaque côté du cadre de 1024. L'encre de <main> passe de
     664px (46.1 %) à 855px (59.4 %).

     UN SEUL bloc se réécrit, et c'est `.page__lead` : c'était le seul enfant de
     .notice que `--mesure` laissait passer à 620px (18px de corps), et le
     cadre le borne maintenant à 586px. Il passe de 3 lignes à 4 — 27px de plus,
     et la page de 2050 à 2077px. Les autres ne bougent pas : `.notice > p`
     faisait déjà 586px, `.note` 517px, et les <h2> 390px au plus. C'est la
     preuve que la borne est la bonne : elle ne sert que là où un bloc débordait
     du cadre, ce qui est précisément ce qu'on voulait supprimer.

     404 et 500 gardent le cadre calé à gauche : ils n'ont ni `.notice` ni
     structure à deux blocs, et leur <h1> à 690px ne doit pas être contraint à
     586px — il passerait sur deux lignes pour rien. */
  .page.notice {
    max-width: var(--mesure);
    margin-inline: auto;
  }
}

/* ==========================================================================
   Desktop très large : 2200px et plus.
   Un seul palier, et il ne porte que --cadre.

   À 2560px, le cadre de 1024px ne faisait que 41.8 % de l'écran et le panneau
   photographique 57.7 %. Le cadre est ici ce qui décide de TOUT le reste — les
   colonnes, la nav, la rampe du voile — donc agrandir la photo ne se règle pas
   en changeant sa rampe mais en changeant le cadre.

   80rem, soit 1280px, et pas davantage : au-delà, les colonnes de 592px
   laisseraient la mesure de --mesure (620px) au milieu d'un espace mort dans
   chaque colonne, et ce serait le retour du défaut qu'on vient de corriger.
   À 1280px, (1280 - 96) / 2 = 592 : la prose de 620px remplit la colonne, et
   la date (452px à 8rem) y garde 140px de jeu.

   ⚠️  Le palier se décide ici, dans :root, et NULLE PART ailleurs : les deux
   rampes de body::after écrivent `calc(var(--gouttiere) + var(--cadre))`, donc
   le plateau du voile suit tout seul, sans troisième copie du dégradé — copies
   qui seraient, elles, faciles à mal ordonner et à casser sur un moteur sans
   flou (voir la note ⚠️ du @supports en fin de feuille).

   Mesuré après, à 2560, en comparant la même page avec et sans ce palier :
   l'encre de <main> passe de 1069 à 1308px sur l'accueil (41.8 % → 51.1 %
   du viewport), de 1082 à 1290px sur /inscription (42.3 % → 50.4 %), et de
   855 à 983px sur /confidentialite (33.4 % → 38.4 %). Le panneau photographique
   tombe de 1478 à 1222px, soit de 57.7 % à 47.7 % de l'écran : il domine
   moins, il ne disparaît pas.

   Ce que ce palier NE fait pas, et il faut le dire : il ne remet pas la photo
   à sa juste place. Tant que le cadre est une grille à deux colonnes de prose,
   un écran de 2560px aura toujours un tiers d'écran de photo — c'est le prix
   d'une page qui reste cadrée, et non d'une page qui s'étire. Aller plus loin
   demanderait une vraie échelle de paliers sur la photo elle-même, ce qui
   changerait le sujet du fichier : sa règle est « une ligne, l'axe », pas
   « une grille qui se dilate ». */
@media (min-width: 2200px) {
  :root {
    --cadre: 80rem;
  }
}
