/* Feuille de style minimale. Le design complet, le responsive et l'accessibilité
   sont le lot 8 (§24) : habiller maintenant reviendrait à tout réécrire. Ce qui
   suit ne vise que la lisibilité pendant le développement.

   La COUCHE DE JETONS ci-dessous, elle, n'est pas provisoire. Elle transcrit
   §16.3 et survivra au lot 8, qui consommera ces noms sans les redéfinir. Les
   deux palettes sont conformes AA sur tous les couples texte/fond : les ratios
   sont recalculés par tests/test_theme.py, qui échoue si une valeur dérive.

   Chaque couleur n'est écrite qu'UNE fois, puis affectée. Un thème sombre dont
   les valeurs sont recopiées dans le bloc `@media` et dans le bloc `[data-theme]`
   diverge dès la première retouche, et la divergence ne se voit que sur l'un des
   deux chemins — celui que le développeur n'emprunte pas.

   Reste la LISTE des affectations, elle bien recopiée trois fois. Un jeton oublié
   dans un seul des trois blocs y laisse la valeur du clair, et c'est exactement ce
   qui était arrivé à `--bandeau-provisoire`. Les trois listes sont donc comparées
   entre elles par tests/test_theme.py, qui échoue sur le nom manquant. */

:root {
  /* Thème clair — §16.3 */
  --clair-encre:      #131A19;   /* texte, noir légèrement vert */
  --clair-fond:       #EFF2F1;   /* fond de page, gris froid pâle */
  --clair-surface:    #FFFFFF;   /* blocs de contenu */
  --clair-j1:         #2A4B7C;   /* journée 1 — cours */
  --clair-j2:         #597726;   /* journée 2 — pratique */
  --clair-alerte:     #A32E1E;   /* complet, erreurs */
  --clair-trait:      #CFD6D3;   /* filets décoratifs */
  --clair-trait-fort: #848A88;   /* filets porteurs de structure — 3:1 (§16.12) */
  --clair-sourdine:   #5E6B67;   /* texte secondaire */

  /* Thème sombre — §16.13. Le fond sombre EST l'encre du clair, et l'encre
     sombre EST son fond : le couple est repris littéralement, ce qui garde au
     site la même identité d'un thème à l'autre. */
  --sombre-encre:      #EFF2F1;
  --sombre-fond:       #131A19;
  --sombre-surface:    #222928;
  --sombre-j1:         #849BCE;
  --sombre-j2:         #BFDC89;
  --sombre-alerte:     #F07860;
  --sombre-trait:      #2A2F2D;
  --sombre-trait-fort: #6E7471;
  --sombre-sourdine:   #86948F;

  /* Bandeaux d'état du back-office, dans les deux thèmes. */
  --clair-bandeau-test:        #FFF3CD;
  --clair-bandeau-production:  #D1E7DD;
  --clair-bandeau-inconnu:     #F8D7DA;
  --clair-bandeau-provisoire:  #E4DDF2;   /* contenus pas encore réels (§25) */
  --clair-ligne-alerte:        #FDECEA;
  --clair-succes:              #E7F1E9;
  --sombre-bandeau-test:       #3A3320;
  --sombre-bandeau-production: #1F3327;
  --sombre-bandeau-inconnu:    #3B2320;
  --sombre-bandeau-provisoire: #2C2440;
  --sombre-ligne-alerte:       #33211E;
  --sombre-succes:             #1E2C22;

  /* Thème appliqué. Le clair est le défaut : une feuille dont les valeurs de base
     seraient sombres afficherait une page noire à qui n'a pas de support des
     media queries. */
  color-scheme: light;
  --encre:      var(--clair-encre);
  --fond:       var(--clair-fond);
  --surface:    var(--clair-surface);
  --j1:         var(--clair-j1);
  --j2:         var(--clair-j2);
  --alerte:     var(--clair-alerte);
  --trait:      var(--clair-trait);
  --trait-fort: var(--clair-trait-fort);
  --sourdine:   var(--clair-sourdine);
  --bandeau-test:       var(--clair-bandeau-test);
  --bandeau-production: var(--clair-bandeau-production);
  --bandeau-inconnu:    var(--clair-bandeau-inconnu);
  --bandeau-provisoire: var(--clair-bandeau-provisoire);
  --ligne-alerte:       var(--clair-ligne-alerte);
  --succes:             var(--clair-succes);

  /* ── Typographie (§16.3) ────────────────────────────────────────────
     Deux familles, deux rôles. Bricolage Grotesque a des proportions
     volontairement irrégulières qui évoquent l'atelier plutôt que le logiciel ;
     Literata est dessinée pour la lecture longue et donne au programme un
     caractère de manuel, ce qui est le registre d'un cours.

     Les deux replis sont RÉELS et non décoratifs : les fichiers woff2 ne sont
     pas encore au dépôt (voir `@font-face` plus bas), et la page doit tenir sans
     eux. `system-ui` porte donc l'affiche, Georgia le texte — une grotesque et
     une serif, c'est-à-dire le même contraste de familles, à la personnalité
     près. */
  --titre: "Bricolage Grotesque", system-ui, -apple-system, "Segoe UI", sans-serif;
  --texte: "Literata", Georgia, "Times New Roman", serif;

  --t-xs:  0.8125rem;  /* 13px — mentions légales, méta */
  --t-s:   0.9375rem;  /* 15px — libellés, notes */
  --t-b:   1.125rem;   /* 18px — corps */
  --t-md:  1.375rem;   /* 22px — chapô */
  --t-lg:  1.75rem;    /* 28px — titres de section */
  --t-xl:  2.25rem;    /* 36px — titres de journée */
  --t-2xl: 2.875rem;   /* 46px — titre de page */
  --t-3xl: 3.625rem;   /* 58px — chiffre de places */

  /* Espacement, base 4 (§16.3). Une échelle et non des valeurs au jugé : c'est
     ce qui fait que deux blocs écrits à trois semaines d'écart s'alignent. */
  --e1: 4px;   --e2: 8px;   --e3: 12px;  --e4: 16px;
  --e5: 24px;  --e6: 32px;  --e7: 48px;  --e8: 64px;
  --e9: 96px;  --e10: 128px;

  /* Structure (§16.3) */
  --largeur-max: 1120px;
  --largeur-prose: 62ch;
  --rayon: 3px;          /* uniquement sur les contrôles interactifs */
}

/* Polices auto-hébergées (§16.3) : « pas de CDN de police — c'est un transfert de
   données personnelles vers un tiers, une dépendance externe, et un point de
   lenteur ». La CSP de §17.5 le rend d'ailleurs impossible : `default-src 'self'`
   n'autorise aucune origine tierce pour les fichiers de fonte.

   LES DEUX FICHIERS NE SONT PAS AU DÉPÔT. Bricolage Grotesque et Literata sont
   sous licence SIL OFL et se récupèrent chez leurs auteurs ; les déposer est un
   geste de l'organisateur, pas une décision de code. Ces déclarations les
   attendent, et `font-display: swap` fait que leur absence ne coûte rien : le
   navigateur ne trouve pas le fichier, la règle est ignorée, et le repli de
   `--titre` et `--texte` s'applique. La page est complète sans elles ; elle gagne
   son caractère avec.

   Les deux sont des fontes variables — un seul fichier pour toutes les graisses,
   ce qui tient dans le budget de 150 Ko de §16.12 là où quatre fichiers statiques
   le dépasseraient. */
@font-face {
  font-family: "Bricolage Grotesque";
  src: url("/static/fonts/bricolage-grotesque.woff2") format("woff2");
  font-weight: 200 800;
  font-display: swap;
}
@font-face {
  font-family: "Literata";
  src: url("/static/fonts/literata.woff2") format("woff2");
  font-weight: 200 900;
  font-display: swap;
}

/* Le sombre s'applique par deux chemins qui ne se recouvrent pas.

   1. Le réglage du système, quand le visiteur n'a rien choisi. La garde
      `:not([data-theme="clair"])` est ce qui rend le bouton « Clair » efficace :
      sans elle, choisir le clair sur un système réglé en sombre n'aurait aucun
      effet, la media query l'emportant sur un `:root` nu.
   2. Le choix explicite, posé en `data-theme` par le serveur (§16.13). Il vient
      après, donc il gagne — dans les deux sens. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="clair"]) {
    color-scheme: dark;
    --encre:      var(--sombre-encre);
    --fond:       var(--sombre-fond);
    --surface:    var(--sombre-surface);
    --j1:         var(--sombre-j1);
    --j2:         var(--sombre-j2);
    --alerte:     var(--sombre-alerte);
    --trait:      var(--sombre-trait);
    --trait-fort: var(--sombre-trait-fort);
    --sourdine:   var(--sombre-sourdine);
    --bandeau-test:       var(--sombre-bandeau-test);
    --bandeau-production: var(--sombre-bandeau-production);
    --bandeau-inconnu:    var(--sombre-bandeau-inconnu);
    --bandeau-provisoire: var(--sombre-bandeau-provisoire);
    --ligne-alerte:       var(--sombre-ligne-alerte);
    --succes:             var(--sombre-succes);
  }
}

:root[data-theme="sombre"] {
  color-scheme: dark;
  --encre:      var(--sombre-encre);
  --fond:       var(--sombre-fond);
  --surface:    var(--sombre-surface);
  --j1:         var(--sombre-j1);
  --j2:         var(--sombre-j2);
  --alerte:     var(--sombre-alerte);
  --trait:      var(--sombre-trait);
  --trait-fort: var(--sombre-trait-fort);
  --sourdine:   var(--sombre-sourdine);
  --bandeau-test:       var(--sombre-bandeau-test);
  --bandeau-production: var(--sombre-bandeau-production);
  --bandeau-inconnu:    var(--sombre-bandeau-inconnu);
  --bandeau-provisoire: var(--sombre-bandeau-provisoire);
  --ligne-alerte:       var(--sombre-ligne-alerte);
  --succes:             var(--sombre-succes);
}

/* Le clair explicite redéclare `color-scheme` : sans cette ligne, un système en
   sombre continuerait de rendre les contrôles de formulaire en sombre sur une
   page redevenue claire. */
:root[data-theme="clair"] { color-scheme: light; }

/* ═══════════════════════════════════════════════════════════════════════
   Mise en page (§16.4, §16.5)

   La page d'accueil n'est plus une liste de produits mais l'emploi du temps des
   deux journées : deux colonnes, un bandeau qui les enjambe, et les compteurs de
   places DANS les colonnes. §16.2 : « on ne vend pas une promesse, on montre un
   programme ».
   ═══════════════════════════════════════════════════════════════════════ */

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

body {
  margin: 0;
  /* Le fond est posé explicitement. Un `body` sans fond laisse voir celui du
     navigateur, qui suit le système et non le choix du visiteur : c'est
     exactement la panne que la bascule est censée supprimer (§16.13). */
  color: var(--encre);
  background: var(--fond);
  font-family: var(--texte);
  font-size: var(--t-b);
  /* Interlignage à 1.65 pour la serif, un cran au-dessus de ce qu'on donnerait à
     une sans (§16.3). */
  line-height: 1.65;
  -webkit-text-size-adjust: 100%;
}

h1, h2, h3, .journee__nom, .compteur__nombre, .journee__prix, .bundle__prix {
  font-family: var(--titre);
  line-height: 1.15;
  /* Les titres se coupent en lignes équilibrées plutôt qu'en une veuve d'un mot.
     Ignoré par les navigateurs qui ne le connaissent pas, sans conséquence. */
  text-wrap: balance;
}

h1 { font-size: var(--t-2xl); margin: 0 0 var(--e4); font-weight: 700; }
h2 { font-size: var(--t-lg);  margin: 0 0 var(--e3); font-weight: 600; }
h3 { font-size: var(--t-md);  margin: var(--e6) 0 var(--e2); font-weight: 600; }

p { margin: 0 0 var(--e4); }
a { color: var(--j1); }

/* La prose garde une mesure lisible même dans un bloc large (§16.3). */
main > section > p, .journee__repli, .composer { max-width: var(--largeur-prose); }

/* Lien d'évitement (§16.12). Il n'occupe aucune place tant qu'il n'a pas le focus,
   et il ne se contente pas d'apparaître : il se pose EN HAUT et au-dessus du reste,
   sans quoi il s'ouvrirait derrière la coiffe collante. */
.evitement {
  position: absolute; left: var(--e4); top: -4em; z-index: 10;
  padding: var(--e2) var(--e4); border-radius: var(--rayon);
  background: var(--encre); color: var(--fond); text-decoration: none;
}
.evitement:focus { top: var(--e2); }

/* ── Coiffe et pied ─────────────────────────────────────────────────── */

.coiffe {
  display: flex; flex-wrap: wrap; gap: var(--e4);
  align-items: baseline; justify-content: space-between;
  max-width: var(--largeur-max); margin: 0 auto;
  padding: var(--e5) var(--e4);
  border-bottom: 1px solid var(--trait);
}
.coiffe__nom {
  font-family: var(--titre); font-size: var(--t-md); font-weight: 700;
  color: inherit; text-decoration: none;
}
.coiffe__nav a { font-size: var(--t-s); }

main { max-width: var(--largeur-max); margin: 0 auto; padding: var(--e6) var(--e4) var(--e8); }

.pied {
  max-width: var(--largeur-max); margin: 0 auto;
  padding: var(--e6) var(--e4) var(--e7);
  border-top: 1px solid var(--trait);
  font-size: var(--t-s); color: var(--sourdine);
}
.pied__liens { display: flex; flex-wrap: wrap; gap: var(--e2) var(--e5); }

/* Cibles tactiles de la coiffe et du pied (§16.12). Le corps de page les tenait
   déjà — boutons, sélecteurs et dépliants portent tous `min-height: 44px` — mais la
   navigation était restée à l'écart : mesurés dans un navigateur, 17 px de haut pour
   le lien « Programme » de la coiffe, 36 pour le nom de l'organisme, 25 pour chacun
   des quatre liens du pied. Ce sont des liens de NAVIGATION, pas des liens en ligne
   dans une phrase : l'exemption d'usage sur la taille de cible ne les couvre pas, et
   ce sont précisément ceux qu'on vise au pouce sur un téléphone.

   `inline-flex` pour la coiffe, dont les liens sont en ligne : une hauteur minimale
   n'a aucun effet sur une boîte en ligne. Les liens du pied sont déjà des éléments
   flexibles, donc de niveau bloc, et la hauteur suffit. */
.coiffe__nom,
.coiffe__nav a { display: inline-flex; align-items: center; min-height: 44px; }
.pied__liens a { display: flex; align-items: center; min-height: 44px; }
.pied .mention { margin: var(--e4) 0 0; font-size: var(--t-xs); }

/* ── Chapô (§16.4) ──────────────────────────────────────────────────── */
/* Il répond en dix secondes à « qu'est-ce qu'on y fait, quand, où, combien »
   (§16.1). Les faits sont des `span` séparés par un point médian posé en CSS : le
   séparateur est décoratif, il n'a pas à être lu ni à survivre à un copier-coller. */

.chapo { margin-bottom: var(--e7); }
.chapo__faits {
  display: flex; flex-wrap: wrap; gap: var(--e2) var(--e4);
  max-width: none;
  font-size: var(--t-md); color: var(--sourdine);
}
/* Le séparateur est porté par le fait qui PRÉCÈDE, et non par celui qui suit.
   Avec `span + span::before`, une coupe de ligne laisse un point médian seul en
   tête de ligne, où il se lit comme une puce mal formée — constaté à 1280 px sur
   « · 12 participants au maximum ». Porté par le précédent, il reste en fin de
   ligne, à sa place. Décoratif : il n'est pas dans le texte, donc pas dans un
   copier-coller ni dans la restitution vocale. */
.chapo__faits span:not(:last-child)::after { content: "·"; margin-left: var(--e4); }

/* ── La grille des journées (§16.4) ─────────────────────────────────── */
/* Deux colonnes et un bandeau qui les enjambe. `auto-fit` plutôt qu'un nombre
   fixe : trois journées se rangeraient d'elles-mêmes, et le jour où il n'y en a
   qu'une, elle n'occupe pas une demi-page vide. */

/* Deux journées, deux colonnes, et le bandeau qui les enjambe (§16.4). Écrit, et
   non déduit d'un `repeat(auto-fit, …)` : la souplesse apparente produisait ici
   l'inverse de son intention.

   `auto-fit` ne replie une piste que si elle est VIDE. Or le bandeau du couple
   porte `grid-column: 1 / -1` : il occupe toutes les pistes, donc aucune n'est
   jamais vide, donc aucune ne se replie. Dès que la grille dépassait 1008 px —
   c'est-à-dire à partir d'environ 1056 px de fenêtre, et pour toutes les tailles
   au-dessus puisque `main` plafonne à 1120 px — la règle créait une TROISIÈME
   piste que le bandeau maintenait en vie. Les deux journées se serraient dans les
   deux premières, à 347 px chacune, et 371 px de grille restaient vides à droite,
   sous un bandeau qui allait jusqu'au bord. Le déséquilibre se voyait sur toutes
   les fenêtres de bureau, et sur aucune fenêtre de développement étroite.

   Le seuil est celui de §16.5, en miroir du `max-width: 44.99em` plus bas : au-
   dessus les colonnes cohabitent, en dessous elles s'empilent. */
.grille {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--e5);
}
@media (min-width: 45em) {
  .grille { grid-template-columns: 1fr 1fr; }
}

.journee {
  display: flex; flex-direction: column;
  padding: var(--e5);
  background: var(--surface);
  /* Toute surface élevée porte une bordure en `--trait-fort`, qui tient ses 3:1 et
     qui est, elle, un vrai composant au sens de WCAG 1.4.11. L'élévation elle-même
     ne se mesure pas en rapport de contraste (§16.13). */
  border: 1px solid var(--trait-fort);
}

/* Le filet de couleur : c'est lui qui identifie la journée, et la couleur voyage
   ensuite jusqu'au billet PDF (§16.2). Il est en `<p aria-hidden>` et non en
   `::before` pour une raison précise : en mode contraste forcé, les couleurs de
   fond des pseudo-éléments sont supprimées et le filet disparaîtrait entièrement.
   Un élément réel garde une bordure, que le mode forcé conserve. */
.journee__filet, .bundle__filet {
  height: 6px; margin: 0 0 var(--e4); border: 0;
}
.journee--j1 .journee__filet { background: var(--j1); }
.journee--j2 .journee__filet { background: var(--j2); }
/* Le bandeau porte les deux couleurs l'une après l'autre : la forme dit ce que le
   produit est (§16.4). Un dégradé à arrêt net, donc deux aplats et une frontière. */
.bundle__filet {
  background: linear-gradient(to right, var(--j1) 0 50%, var(--j2) 50% 100%);
}
@media (forced-colors: active) {
  /* Les aplats tombent ; la structure reste par une bordure, que le mode conserve. */
  .journee__filet, .bundle__filet { border-top: 6px solid; height: 0; }
}

.journee__nom  { font-size: var(--t-xl); margin: 0; }
.journee__quoi { margin: 0 0 var(--e4); color: var(--sourdine); font-size: var(--t-md); }
.journee__quoi::first-letter { text-transform: uppercase; }
.journee__prix { font-size: var(--t-lg); margin: 0 0 var(--e4); font-weight: 600; }
.journee__repli, .journee__sans-grille { font-size: var(--t-s); color: var(--sourdine); }
/* La ligne « quand et où » de /programme. Le lieu suit la date sur la même ligne
   quand la place le permet, et passe dessous sinon — d'où le `display: block` plutôt
   qu'un séparateur, qui pendrait en fin de ligne à la coupe. */
.journee__quand { font-size: var(--t-s); color: var(--sourdine); }
.journee__lieu { display: block; }

/* « Complet » ne passe pas par l'opacité de 55 % de §16.7 : à 55 %, le contraste
   réel du texte tombe sous le seuil de §16.12, et l'exception de WCAG 1.4.3 sur les
   composants inactifs couvre un contrôle désactivé, pas une information. La mise en
   retrait passe donc par une couleur calculée et une bordure discontinue, qui redit
   l'état sans la couleur (§16.6). */
.journee--complet { color: var(--sourdine); border-style: dashed; }
/* Le nom garde son encre pleine : la mise en retrait dit que le produit n'est plus
   achetable, pas qu'on ne sait plus lequel c'est. */
.journee--complet .journee__nom { color: var(--encre); }
.journee--complet .alerte { color: var(--alerte); }

/* ── La grille horaire ──────────────────────────────────────────────── */

.journee__programme { margin: 0 0 var(--e5); }

/* La commande de pliage est habillée POUR TOUTES LES LARGEURS, et masquée par la
   seule règle ci-dessous. L'inverse — masquée par défaut, rendue sous le seuil —
   liait sa présence à la largeur alors qu'elle dépend de l'état du bloc : un
   `<details>` refermé par le script sur téléphone puis vu sur grand écran perdait
   sa seule commande, et le programme avec elle (§16.2, §16.12).

   `list-item` et non `block` : `display: block` sur un `<summary>` SUPPRIME le
   triangle d'ouverture dans les navigateurs WebKit. Le contrôle perd alors le seul
   signe qui dit qu'il s'ouvre, et « Voir le programme » se lit comme un titre —
   constaté à 375 px. §16.5 dessine d'ailleurs ce triangle.

   C'est une commande : elle atteint la cible tactile de 44 px comme les autres, et
   se lit comme un contrôle et non comme un titre. */
.journee__programme > summary {
  display: list-item;
  min-height: 44px; padding: var(--e2) 0;
  font-family: var(--titre); font-weight: 600;
  color: var(--j1); cursor: pointer;
}
.journee--j2 .journee__programme > summary { color: var(--j2); }

/* Au-dessus de 720 px il n'y a rien à replier : la colonne est déjà courte, et un
   contrôle qui ne sert à rien encombre le parcours clavier. Le `<details>` reste
   `open` dans le HTML, donc le contenu est là même sans script (§16.12).

   `[open]` est ce qui distingue « inutile » de « nécessaire ». Sur un bloc ouvert,
   la commande n'a rien à offrir et sort du parcours clavier. Sur un bloc fermé,
   elle est le seul moyen de revoir le programme : la masquer l'enfermerait. */
@media (min-width: 45em) {
  .journee__programme[open] > summary { display: none; }
}

.horaire { width: 100%; border-collapse: collapse; font-size: var(--t-s); }
.horaire tr + tr th, .horaire tr + tr td { border-top: 1px solid var(--trait); }
.horaire th, .horaire td { padding: var(--e2) 0; text-align: left; vertical-align: baseline; }
.horaire__heure {
  width: 4.5em; padding-right: var(--e3); font-weight: 600;
  font-family: var(--titre);
  /* Les heures s'alignent en colonne : sans chiffres tabulaires, « 09:00 » et
     « 14:00 » n'ont pas la même largeur et la colonne ondule. */
  font-variant-numeric: tabular-nums;
  color: var(--sourdine);
}
.horaire__quoi { font-family: var(--texte); }

/* ── Le compteur de places (§16.6) ──────────────────────────────────── */
/* Le seul endroit de la page où l'œil doit s'arrêter net, donc le seul alignement
   centré (§16.4). Il vient après le programme et avant le prix : on lit ce qu'on
   achète, puis ce qu'il en reste, puis ce que ça coûte. */

.compteur { margin: auto 0 var(--e4); text-align: center; }
.compteur__nombre {
  font-size: var(--t-3xl); font-weight: 700; margin: 0;
  font-variant-numeric: tabular-nums;
  /* Le nombre change au rafraîchissement (§16.8) ; sans hauteur stable, la colonne
     saute d'une ligne quand on passe de 10 à 9. */
  line-height: 1;
}
/* « Nombre en graisse supérieure » pour l'état de tension (§16.7). Le commentaire
   décrivait déjà cette règle, la règle ne posait qu'une couleur : 800 est une graisse
   réelle de « Bricolage Grotesque », déclarée variable de 200 à 800 plus haut.
   La graisse n'est de toute façon pas seule à le dire, et c'est l'essentiel — elle se
   perd sur la police de repli, et §16.6 règle 3 interdit que la forme porte seule
   l'information. Le libellé change avec elle : « Plus que 3 places » au lieu de
   « 3 places restantes ». */
.compteur__avant {
  margin: 0; font-family: var(--titre); font-size: var(--t-s); font-weight: 600;
  color: var(--alerte);
}
.compteur__nombre--tension { color: var(--alerte); font-weight: 800; }
.compteur__mot--tension { color: var(--alerte); }
.compteur__mot { margin: var(--e1) 0 0; font-size: var(--t-s); color: var(--sourdine); }
.compteur__mot.alerte { font-size: var(--t-lg); font-family: var(--titre); font-weight: 700; }

/* La jauge, un segment par place. Pas de largeur calculée : `style-src 'self'`
   (§17.5) interdit l'attribut `style`, donc tout pourcentage posé à l'affichage.
   Les segments s'en passent et sont exacts — une place vendue, un segment qui
   change — là où un pourcentage arrondi ferait mentir la dernière place. */
.jauge {
  display: flex; flex-wrap: wrap; gap: 2px;
  justify-content: center; margin: var(--e3) 0 0;
}
.jauge__pris, .jauge__libre { width: 10px; height: 10px; }
.jauge__libre { background: var(--trait); }
/* Le repli d'abord : sans couleur de journée — un troisième produit, une autre page
   — le segment pris doit rester visible, il ne doit jamais se confondre avec un
   segment libre. Les deux règles suivantes le spécialisent. */
.jauge__pris { background: var(--trait-fort); }
.journee--j1 .jauge__pris { background: var(--j1); }
.journee--j2 .jauge__pris { background: var(--j2); }
@media (forced-colors: active) {
  /* Les aplats tombent : on redit l'état par la forme, pleine contre creuse. */
  .jauge__pris  { border: 5px solid; }
  .jauge__libre { border: 1px solid; }
}

/* Le seul moment orchestré de la page (§16.9) : les deux jauges se dessinent depuis
   zéro en 600 ms, décalées de 80 ms l'une de l'autre.

   Un masque, et non une largeur animée. La jauge est faite de segments, un par
   place (§16.6) : animer sa largeur les redistribuerait à chaque image, et la barre
   se recomposerait au lieu de se dessiner. `clip-path` découvre une barre déjà
   composée, ce qui est exactement ce que §16.9 demande sans toucher à ce qui est
   dessiné.

   L'animation porte sur `.jauge`, jamais sur `.jauge__pris` ni `.jauge__libre`, et
   ce n'est pas un détail de sélecteur. `achat.js` vide le conteneur et le repeuple
   toutes les trente secondes (§16.8) : le conteneur survit, les segments non. Une
   animation posée sur les segments repartirait à chaque rafraîchissement, et la page
   se remettrait à bouger toute seule — ce que la première ligne de §16.9 interdit.

   `backwards` : sans lui, la jauge de J2 s'afficherait entière pendant ses 80 ms de
   décalage, puis disparaîtrait pour se redessiner. */
@keyframes jauge-se-dessine {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
.jauge { animation: jauge-se-dessine 600ms ease-out backwards; }
.journee--j2 .jauge { animation-delay: 80ms; }

/* ── Le bandeau du couple (§16.4) ───────────────────────────────────── */
/* Il enjambe les deux colonnes. `1 / -1` et non `span 2` : avec `auto-fit`, le
   nombre de colonnes dépend de la largeur, et `span 2` déborderait sur une seule. */

.bundle {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: var(--e5);
  align-items: flex-end; justify-content: space-between;
  padding: var(--e5);
  background: var(--surface); border: 1px solid var(--trait-fort);
}
.bundle__filet { flex-basis: 100%; }
.bundle__texte { flex: 1 1 24rem; max-width: var(--largeur-prose); }
.bundle__texte h2 { font-size: var(--t-xl); }
.bundle__texte p:last-child { margin-bottom: 0; }
.bundle__achat { display: flex; flex-direction: column; align-items: flex-start; gap: var(--e3); }
.bundle__prix { font-size: var(--t-xl); font-weight: 700; margin: 0; }

/* Le bandeau complet reprend la mise en retrait de la colonne de journée (§16.7) :
   même grisé, même bordure discontinue. Les trois règles sont écrites ICI et non à
   côté de celles des journées, et ce n'est pas un rangement — c'est la cascade.
   `.bundle` pose `border: 1px solid` et `.bundle__texte h2` sa couleur, tous deux
   plus bas dans la feuille que le bloc des journées : à spécificité égale, la
   dernière règle gagne. Rangées avec les autres, celles-ci seraient écrasées sans
   que rien ne le signale — le bandeau garderait sa bordure pleine et son titre plein.
   `tests/test_programme.py` tient cet ordre. */
.bundle--complet { color: var(--sourdine); border-style: dashed; }
.bundle--complet .bundle__texte h2 { color: var(--encre); }
.bundle--complet .alerte { color: var(--alerte); }

/* ── Boutons et liens d'action (§16.11, §16.12) ─────────────────────── */
/* Cibles de 44 px minimum. `inline-flex` plutôt qu'un `padding` calculé : la
   hauteur est alors garantie quel que soit le texte, et le libellé reste centré. */

.bouton, button {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: var(--e2) var(--e5);
  font: inherit; font-family: var(--titre); font-weight: 600;
  border: 1px solid var(--encre); border-radius: var(--rayon);
  background: var(--surface); color: var(--encre);
  text-decoration: none; cursor: pointer;
}
.bouton--principal { background: var(--encre); color: var(--fond); }
/* Un bouton désactivé relève de l'exception de 1.4.3, mais une opacité le rend
   illisible sans rien apprendre à personne. Il garde donc un contraste réel : ce
   sont l'attribut et le curseur qui disent l'état. */
button[disabled] {
  background: var(--surface); color: var(--sourdine);
  border-color: var(--trait-fort); cursor: not-allowed;
}

/* Les deux lignes discrètes qui ferment la page (§16.4) : « Composer ma commande »
   et le renvoi vers le parcours entreprise. Même registre, donc même règle — les
   séparer inviterait à ne styler que l'une le jour où l'autre change. Entre elles
   prendront place les cinq sections de prose que §16.4 attend de l'organisateur. */
.composer,
.renvoi-entreprise { margin-top: var(--e6); font-size: var(--t-s); color: var(--sourdine); }
.composer a,
.renvoi-entreprise a { font-family: var(--titre); font-weight: 600; }

/* ── Formulaires (le tunnel d'achat) ────────────────────────────────── */

fieldset { border: 1px solid var(--trait-fort); margin: var(--e5) 0; padding: var(--e4); }
legend { font-family: var(--titre); font-weight: 600; }
label { display: block; margin: var(--e3) 0; }
input, select, textarea {
  font: inherit; padding: var(--e2); border-radius: var(--rayon);
  color: var(--encre); background: var(--surface); border: 1px solid var(--trait-fort);
}
/* §10.2 : une place unique s'inscrit au nom de l'acheteur, et son bloc participant
   se replie derrière un `<summary>`. Celui-ci se lit comme le lien qu'il est ; rendu
   avec le style de `button`, il passerait pour l'envoi du formulaire. */
.mention-acheteur { margin: var(--e3) 0 0; color: var(--sourdine); }
/* Le plancher de 44 px est DÉCLARÉ et non obtenu par accident. Ce résumé mesurait
   déjà 45,7 px — huit pixels de remplissage de part et d'autre d'une ligne de 29,7 —
   mais rien ne le disait : réduire `--e2` d'un cran, ou changer la famille de texte,
   le faisait passer sous le seuil sans que personne le voie. §16.12 range le
   dépliant parmi les commandes, et `test_accessibilite.py` ne peut garder que ce qui
   est écrit.

   `min-height` seul, sans `display: flex` : la disposition flexible SUPPRIME le
   triangle d'ouverture du `<summary>`, comme le dit déjà la règle du programme. */
.pli-acheteur > summary { cursor: pointer; color: var(--j1); width: fit-content;
                          min-height: 44px; padding: var(--e2) 0; }
.produits { list-style: none; padding: 0; }
.produit { border: 1px solid var(--trait-fort); padding: var(--e4);
           margin-bottom: var(--e4); background: var(--surface); }
.alerte, .erreurs { color: var(--alerte); }
/* Les trois encarts d'état du tunnel — le bandeau d'un produit bloqué, la note d'un
   recalcul, l'attente d'un paiement. Ce sont des SURFACES ÉLEVÉES, et §16.13 dit ce
   que cela coûte : « L'élévation ne se mesure pas en rapport de contraste. » En thème
   sombre, exiger 3:1 entre `--surface` et `--fond` est mathématiquement impossible,
   l'intervalle est vide ; la conformité est donc portée par la bordure — « toute
   surface élevée porte une bordure en `--trait-fort`, qui tient ses 3:1 et qui est,
   elle, un vrai composant ».

   Elle était en `--trait`, que §16.12 sépare précisément pour ce motif et que
   `test_theme.py` écarte de ses couples comme décoratif. Le contour de ces encarts
   plafonnait donc à 1,30:1 en sombre, l'élévation à 1,19:1, et aucun des deux ne
   dessinait plus le bloc. Ce sont pourtant ceux qui portent `role="alert"` et
   `role="status"` — le billet valide ou invalide, l'attente de paiement, la note du
   tunnel : des objets graphiques nécessaires à la compréhension au sens de
   WCAG 2.2 SC 1.4.11. */
.bandeau, .note, .attente {
  background: var(--surface); border: 1px solid var(--trait-fort);
  padding: var(--e3);
}

/* ── Composition mobile (§16.5) ─────────────────────────────────────── */
/* Sous 720 px les colonnes s'empilent — la grille le fait d'elle-même — et la
   grille horaire passe derrière un `<details>` replié, « pour que les deux
   compteurs de places et les deux boutons restent atteignables sans défilement
   interminable ». Le repli est posé par le script ; sans lui, tout reste déplié,
   ce qui est le bon sens de la panne. La commande de pliage, elle, est habillée
   plus haut, pour toutes les largeurs : sa présence suit l'état du bloc et non la
   largeur de la fenêtre. */

@media (max-width: 44.99em) {
  h1 { font-size: var(--t-xl); }
  .journee__nom, .bundle__texte h2 { font-size: var(--t-lg); }
  .compteur__nombre { font-size: var(--t-2xl); }
  .chapo__faits { font-size: var(--t-b); }
  main { padding-top: var(--e5); }

  /* Empilés, les faits du chapô n'ont plus de séparateur à porter : un point médian
     en fin de chaque ligne pend dans le vide. C'est l'espacement qui sépare quand la
     ligne se rompt. */
  .chapo__faits { flex-direction: column; gap: var(--e1); }
  .chapo__faits span:not(:last-child)::after { content: none; }
}

/* Focus visible (§16.12) — 2 px décalés de 2 px, jamais `outline: none` sans
   remplacement. L'anneau est en `--encre` : c'est le seul jeton dont le contraste
   au fond est garanti dans les deux thèmes (15,67:1 de part et d'autre), donc le
   seul qui n'oblige pas à définir une couleur d'anneau par thème. */
:focus-visible { outline: 2px solid var(--encre); outline-offset: 2px; }

/* Bascule de thème (§16.13). */
.bascule { margin: 1rem 0 0; }
.bascule fieldset { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
                    border: 0; margin: 0; padding: 0; }
.bascule legend { float: left; margin-right: .5rem; padding: 0;
                  font-size: .9rem; font-weight: 600; }
.bascule button { padding: .3rem .7rem; font-size: .9rem;
                  background: var(--surface); color: var(--encre);
                  border: 1px solid var(--trait-fort); }
/* L'état retenu ne se signale pas par la seule couleur (§16.12) : le bouton porte
   `aria-pressed`, que le lecteur d'écran annonce, et le soulignement le redit à
   l'œil pour qui ne distingue pas les deux fonds. */
.bascule button[aria-pressed="true"] { background: var(--encre); color: var(--fond);
                                       border-color: var(--encre);
                                       text-decoration: underline; }

/* `[hidden]` doit gagner. La feuille du navigateur pose bien `[hidden]{display:none}`,
   mais toute règle d'auteur définissant `display` la neutralise : `button` plus haut
   est déclaré `inline-block`, si bien qu'un `bouton.hidden = true` ne masquait rien.
   Le défaut est silencieux — l'élément reste visible ET reste dans le parcours
   clavier, sans que rien ne signale que le script a pourtant fait son travail. */
[hidden] { display: none !important; }

/* Texte réservé au lecteur d'écran. Ni `display: none` ni `visibility: hidden`,
   qui le retireraient aussi de la restitution vocale : le texte reste dans le flux,
   réduit à un pixel découpé. Il porte le nom de la ligne sur les boutons « Retirer »
   du récapitulatif, où « Retirer » répété cinq fois ne dirait rien (§16.12). */
.lecteur-seul { position: absolute; width: 1px; height: 1px; overflow: hidden;
                clip-path: inset(50%); white-space: nowrap; }

/* Le champ leurre de §17.3. À ne pas confondre avec `.lecteur-seul` juste au-dessus,
   qui cache à l'œil en laissant au lecteur d'écran : ici on cache aux deux, parce
   qu'un visiteur qui ne verrait pas le champ mais l'entendrait le remplirait — et
   serait refusé.

   La règle vit ici et non dans un `style="display:none"` : §17.5 pose
   `style-src 'self'`, et l'attribut serait ignoré EN SILENCE. Le leurre deviendrait
   visible, tout le monde le remplirait, et toutes les demandes légitimes seraient
   refusées sans que rien côté serveur ne le dise. */
.leurre { display: none; }

/* ═══════════════════════════════════════════════════════════════════════
   Le tunnel d'achat (§16.10)

   « Une seule colonne, largeur 480 px maximum, libellés au-dessus des champs. Le
   récapitulatif de commande reste visible en permanence : collé en haut sur mobile,
   en colonne latérale sur grand écran. »

   Le récapitulatif est DANS le formulaire, entre les places et l'acheteur : c'est ce
   qui permet à ses commandes — quantité, retrait — d'être postées sans JavaScript
   (§16.10). Le sortir du formulaire pour le poser à côté en ferait un bloc mort sur
   un navigateur sans script. C'est donc la grille qui le déplace, pas le balisage :
   il reste au même endroit du document, et change seulement de colonne.
   ═══════════════════════════════════════════════════════════════════════ */

.tunnel { max-width: var(--largeur-max); }

/* Une colonne, 480 px. Les champs prennent toute la largeur : un `input` à sa
   largeur naturelle donne une colonne de champs de longueurs inégales, qui se lit
   comme un formulaire mal aligné. */
.tunnel fieldset { max-width: 480px; }
.tunnel label { max-width: 480px; }
.tunnel input:not([type="checkbox"]), .tunnel textarea, .tunnel select { width: 100%; }
.tunnel label > small { display: block; margin-top: var(--e1);
                        font-size: var(--t-s); color: var(--sourdine); }
/* La case à cocher reste à côté de son texte : elle n'est pas un champ de saisie
   mais une réponse par oui ou non, et l'aligner en bloc au-dessus de son libellé la
   séparerait de ce qu'elle coche. */
.tunnel input[type="checkbox"] { width: auto; margin-right: var(--e2); }

/* La case elle-même fait 13 px et rien ne l'agrandit sans déformer son glyphe : la
   cible, c'est le LIBELLÉ, et c'est lui qui doit atteindre 44 px (§16.12). Il les
   atteint par un remplissage vertical et non par un `display: flex` : le libellé des
   CGV est une phrase qui contient un lien, et la disposition flexible en ferait
   trois éléments distincts qui cesseraient de se replier comme un texte. */
.consentement, .choix { display: block; min-height: 44px; padding-block: var(--e2); }

/* Les blocs participants sont indentés SOUS leur ligne et portent le filet de la ou
   des journées concernées (§16.10 point 3). « Le rattachement est visuel autant que
   technique » : c'est la forme du formulaire qui garantit qu'un participant sait à
   quelle ligne il appartient, et donc quels billets lui revenir. */
.participants {
  margin: var(--e3) 0 var(--e5) var(--e3);
  padding-left: var(--e4);
  border-left: 3px solid var(--trait-fort);
}
/* Le formulaire entreprise range ses blocs participants derrière un dépliant, qui
   partage cette classe sans être le même élément — un `<details>` là, un `<div>` ici.
   Son résumé n'avait aucune règle, donc aucun plancher : 29,7 px, la hauteur d'une
   ligne. Il ne s'en tire aujourd'hui que parce que son libellé passe à la ligne —
   « Nommer les participants de « Journée 1 — cours » (facultatif) » tient sur deux
   lignes dans une colonne de 415 px, ce qui le porte à 59,4. C'est le libellé
   PROVISOIRE du référentiel (§25, décision 2) : le jour où l'organisateur en donne un
   court, la cible retombe à un tiers sous le seuil. */
details.participants > summary {
  min-height: 44px; padding: var(--e2) 0; cursor: pointer;
}
.participants--j1 { border-left-color: var(--j1); }
.participants--j2 { border-left-color: var(--j2); }
/* Le couple porte les deux couleurs, comme le bandeau de la page d'accueil. Le
   `border-image` s'applique aux quatre côtés : les trois autres sont à zéro, seul
   le gauche a une épaisseur. */
.participants--j1-j2 {
  border-image: linear-gradient(to bottom, var(--j1) 0 50%, var(--j2) 50% 100%) 1;
}
@media (forced-colors: active) {
  /* Les couleurs tombent ; le filet reste, et avec lui le rattachement visuel. */
  .participants { border-left: 3px solid; border-image: none; }
}

.participant { margin: var(--e4) 0; }
.ligne { margin-bottom: var(--e4); }
.ligne label { font-family: var(--titre); font-weight: 600; }
/* « Mettre à jour » est le chemin SANS JavaScript (§16.10) : il poste le formulaire
   et le serveur renvoie la page recalculée. Il reste donc un vrai bouton, discret
   mais lisible, à côté de son sélecteur — et non caché par le script, qui pourrait
   ne pas s'exécuter. */
.ligne .maj { margin-left: var(--e2); padding: var(--e2) var(--e3); font-size: var(--t-s); }
.ligne .tension { display: block; margin-top: var(--e1);
                  font-size: var(--t-s); color: var(--alerte); }

/* Le récapitulatif. `--surface` et une bordure porteuse : c'est une surface élevée,
   et §16.13 veut qu'elle se distingue par un écart perceptif plus une bordure en
   `--trait-fort`, qui tient ses 3:1 là où l'élévation seule ne le peut pas. */
.recapitulatif {
  padding: var(--e4);
  background: var(--surface);
  border: 1px solid var(--trait-fort);
}
.recapitulatif h2 { margin-top: 0; font-size: var(--t-md); }
/* Cinq colonnes — offre, quantité, prix unitaire, total, retrait — dans une
   colonne latérale, c'est étroit. Le corps descend d'un cran et l'espacement se
   resserre : à `--t-b`, « Journée 1 — cours » passait sur trois lignes et poussait
   les montants hors du cadre. */
.recapitulatif table { width: 100%; border-collapse: collapse; font-size: var(--t-s); }
.recapitulatif th, .recapitulatif td {
  padding: var(--e2) var(--e1); text-align: left;
  border-bottom: 1px solid var(--trait);
  vertical-align: middle;
}
/* La colonne « Offre » prend ce qui reste ; les quatre autres se serrent sur leur
   contenu, faute de quoi le libellé, qui est le plus long, se fait comprimer par
   des colonnes de chiffres qui n'en ont pas besoin. */
.recapitulatif td:first-child, .recapitulatif th:first-child { width: 40%; }
.recapitulatif thead th { font-size: var(--t-xs); color: var(--sourdine); font-weight: 600; }
/* §16.7 : « une ligne déjà présente est signalée en `--alerte` et empêche le paiement
   tant qu'elle n'est pas retirée. » Le fond redouble la couleur du texte, qui ne porte
   de toute façon pas seule l'information (§16.6, §16.12) : la ligne du produit écrit
   « Complet », dit ce qu'il faut faire, ses deux commandes de quantité sont
   désactivées, et `valider` refuse le POST. */
.recapitulatif tr.alerte th, .recapitulatif tr.alerte td { background: var(--ligne-alerte); }
/* La suggestion du couple (§16.10). Registre de la note et non de l'alerte : c'est
   une information, pas un état de la commande — d'où `--sourdine` et la taille des
   mentions, comme les deux lignes discrètes qui ferment la page d'accueil. */
.suggestion { font-size: var(--t-s); color: var(--sourdine); margin: var(--e3) 0 0; }
.recapitulatif tfoot th, .recapitulatif tfoot td { font-weight: 700; border-bottom: 0; }
/* Les montants s'alignent sur leur virgule, ce que seuls des chiffres de largeur
   fixe permettent : sans cela, un total à quatre chiffres décale la colonne. */
.recapitulatif td:nth-child(3), .recapitulatif td:nth-child(4),
.recapitulatif tfoot td { text-align: right; font-variant-numeric: tabular-nums; }
.recapitulatif button {
  padding: var(--e1) var(--e2); font-size: var(--t-s);
  background: var(--surface); color: var(--encre); border-color: var(--trait-fort);
}

/* La cellule de quantité porte trois choses sur une ligne : « − », le nombre, « + ».
   Le nombre est un `span` et non un champ — la commande est le bouton, le nombre
   n'est qu'un état affiché. */
.recapitulatif .quantite { white-space: nowrap; }
.recapitulatif .quantite span[data-role] {
  display: inline-block; min-width: 2rem; text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Cibles tactiles de 44 px (§16.12), sur toutes les commandes du panier. Les deux
   boutons de quantité tomberaient à la largeur d'un caractère sans `min-width` :
   c'est la dimension horizontale qui manque à un « + », pas la verticale. */
.recapitulatif button, .ligne .maj { min-height: 44px; min-width: 44px; }
.payer, .ligne select { min-height: 44px; }

/* Le bouton de paiement porte le montant (§16.10, §18.2) ; il reste lisible quand
   le script le désactive au clic — c'est `button[disabled]` qui s'en charge, sans
   opacité. Il ferme la colonne, en pleine largeur : c'est l'action de la page. */
.payer {
  width: 100%; max-width: 480px;
  margin-top: var(--e5);
  background: var(--encre); color: var(--fond);
  font-size: var(--t-b);
}

/* ── Le récapitulatif toujours visible (§16.10) ─────────────────────── */

/* Grand écran : colonne latérale, collée. `align-self: start` est ce qui rend le
   `sticky` opérant — un élément étiré sur toute la hauteur de sa zone n'a nulle part
   où coller. */
@media (min-width: 62em) {
  .tunnel {
    display: grid;
    grid-template-columns: minmax(0, 480px) minmax(320px, 28rem);
    gap: var(--e5) var(--e7);
    align-items: start;
  }
  .tunnel > .recapitulatif {
    grid-column: 2; grid-row: 1 / -1;
    align-self: start; position: sticky; top: var(--e5);
  }
  .tunnel > *:not(.recapitulatif) { grid-column: 1; }
}

/* Sous 32 em, cinq colonnes ne tiennent pas. Le calcul est sans appel : les deux
   boutons de quantité et le bouton « Retirer » doivent mesurer 44 px (§16.12), la
   colonne de quantité en réclame donc 132 et le retrait 71 ; sur les 341 px utiles
   d'un téléphone de 375, il reste 138 px pour le libellé et DEUX colonnes de
   montants. C'est impossible, et ce qui se fait couper est le TOTAL — le seul chiffre
   que §16.10 dit ne jamais pouvoir être périmé.

   La colonne « Prix unitaire » cède donc la place. Elle est la seule redondante : le
   prix unitaire est écrit juste au-dessus, sur le sélecteur du produit, et le total
   de ligne divisé par la quantité le redonne. Aucune information ne quitte la page,
   seulement le tableau, et seulement sur un écran où elle ne rentrait pas. */
@media (max-width: 32em) {
  .recapitulatif th:nth-child(3), .recapitulatif td:nth-child(3) { display: none; }
  /* Les gouttières cèdent avant les cibles tactiles : c'est l'ordre des priorités de
     §16.12, où les 44 px ne sont pas négociables et l'espacement l'est. Quatre
     colonnes restantes à deux pixels de marge de part et d'autre rendent les dix-huit
     pixels qui manquaient encore. */
  .recapitulatif { padding: var(--e3); }
  .recapitulatif th, .recapitulatif td { padding: var(--e2) 2px; }
  .recapitulatif .quantite span[data-role] { min-width: 1.5rem; }
}

/* Petit écran : collé en haut. La hauteur est bornée et le débordement défile, sans
   quoi un panier de cinq lignes occuperait tout l'écran d'un téléphone et cacherait
   précisément les champs qu'on est en train de remplir — le récapitulatif doit rester
   visible, pas devenir la page. */
@media (max-width: 61.99em) {
  /* « Une seule colonne, largeur 480 px maximum » (§16.10). Sous le seuil de la
     grille, le plafond ne peut plus venir des pistes : il doit être écrit. Il porte
     sur TOUS les enfants directs, et pas seulement sur ceux qu'on a écrits
     aujourd'hui — le récapitulatif s'étalait jusqu'au `--largeur-max` du tunnel
     entre 512 et 992 px pendant que les fieldsets, les libellés et le bouton de
     paiement restaient à 480, et le paragraphe qui ferme la page faisait de même. La
     colonne n'avait plus une largeur, elle en avait deux. Un plafond énuméré aurait
     rouvert la même brèche au premier bloc ajouté.

     Écrit ici et pas sur `.recapitulatif` en général : la piste latérale de la
     grille vaut `minmax(320px, 28rem)`, et 28rem dépasse 480 px dès que la taille de
     police racine franchit environ 17 px — c'est-à-dire précisément l'agrandissement
     de texte que demande WCAG 2.2 SC 1.4.4. Un plafond global ferait alors flotter le
     récapitulatif dans une piste trop large, et il faudrait le rattraper dans le bloc
     grille par un `max-width: none` qui n'existerait que pour réparer le premier. */
  .tunnel > * { max-width: 480px; }
  .recapitulatif {
    position: sticky; top: 0; z-index: 2;
    max-height: 45vh; overflow-y: auto;
  }
}

/* Le bloc d'erreurs ouvre la page, avant le formulaire (§16.10). `role="alert"` le
   fait annoncer ; la bordure et la couleur le disent à l'œil, et le fond le sépare
   du reste sans dépendre de la seule couleur du texte. */
.erreurs {
  max-width: 480px; margin-bottom: var(--e5);
  border: 1px solid var(--alerte); border-left-width: 4px;
  padding: var(--e4);
  background: var(--surface);
}
.erreurs p:first-child { font-family: var(--titre); font-weight: 600; margin-top: 0; }
/* Le bloc reçoit le focus après un envoi refusé ; son anneau ne doit pas se confondre
   avec sa propre bordure d'alerte, d'où le décalage habituel de 2 px que
   `:focus-visible` pose déjà. Rien à ajouter, mais il faut que le conteneur soit
   focalisable sans devenir un arrêt de tabulation : c'est `tabindex="-1"` dans le
   gabarit, pas une règle de style. */

/* L'erreur d'un champ, sous le champ (§16.10). Elle ne s'appuie pas sur la seule
   couleur : le texte dit quoi faire, `aria-invalid` porte l'état pour les
   technologies d'assistance, et la barre à gauche le redit en mode contraste forcé,
   où les couleurs du site sont remplacées. */
.erreur-champ {
  margin: var(--e1) 0 0;
  padding-left: var(--e2);
  border-left: 3px solid var(--alerte);
  font-size: var(--t-s);
  color: var(--alerte);
}
/* Le champ fautif se signale aussi lui-même. `aria-invalid` plutôt qu'une classe :
   l'attribut est déjà là pour le lecteur d'écran, et le styler évite d'entretenir un
   second marqueur qui finirait par ne plus correspondre. */
.tunnel [aria-invalid="true"] { border-color: var(--alerte); border-width: 2px; }
.erreurs ul { margin: 0 0 var(--e3); padding-left: var(--e5); }
.erreurs p:last-child { margin-bottom: 0; }

/* §16.9. Le mouvement répond aux actions, jamais tout seul — et il se coupe
   entièrement pour qui l'a demandé au système. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    /* Les délais aussi, et c'est le décalage de 80 ms de §16.9 qui l'impose. Une
       durée ramenée à 0,01 ms avec un délai laissé intact tiendrait la jauge de J2
       masquée par `backwards` pendant ses 80 ms, puis la ferait apparaître d'un
       coup : couper la durée sans couper le délai ne supprime pas le mouvement, il
       le remplace par un clignotement — précisément ce dont se protège celui qui a
       demandé moins d'animation. */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }
}
