/* =========================================================================
   Atlas d'étude — Feuille de style partagée par tous les guides
   Thème sombre, mobile-first. Aucune dépendance externe.
   ========================================================================= */

:root {
  /* Palette sombre (cohérente avec l'app Halterofit, dark mode only) */
  --bg:            #0d0f13;   /* fond global */
  --surface:       #14171d;   /* cartes, barre latérale */
  --elevated:      #1b1f27;   /* encadrés, blocs de code */
  --border:        #272d38;   /* lignes de séparation */
  --border-soft:   #1f2530;
  --text:          #e8ebf1;   /* texte principal */
  --muted:         #9aa4b5;   /* texte secondaire */
  --faint:         #6b7385;   /* texte tertiaire / légendes */

  /* Accents */
  --primary:       #5b9dff;   /* bleu — liens, titres de section */
  --primary-soft:  rgba(91, 157, 255, 0.12);
  --green:         #51d88a;   /* à retenir */
  --green-soft:    rgba(81, 216, 138, 0.10);
  --amber:         #f5b14c;   /* exercices */
  --amber-soft:    rgba(245, 177, 76, 0.10);
  --violet:        #b08cff;   /* quiz */
  --violet-soft:   rgba(176, 140, 255, 0.10);
  --red:           #ff6b6b;   /* pièges */
  --red-soft:      rgba(255, 107, 107, 0.10);
  --cyan:          #5ad1ff;   /* ponts entre concepts */
  --cyan-soft:     rgba(90, 209, 255, 0.10);
  --gold:          #ffd479;   /* la formule (patron canonique) */
  --gold-soft:     rgba(255, 212, 121, 0.07);
  --pink:          #ff86b8;   /* questions d'entrevue */
  --pink-soft:     rgba(255, 134, 184, 0.10);

  /* Version OPAQUE de trois accents. Les `*-soft` sont en rgba à ~10 % : sur un
     empilement, la carte du dessous transparaît et l'ordre z cesse de se voir —
     or c'est exactement ce qu'une figure d'empilement doit montrer. */
  --primary-plein: #1d2a44;
  --green-plein:   #14342a;
  --pink-plein:    #392531;
  --red-plein:     #3a2023;

  --radius:        12px;
  --sidebar-w:     290px;
  --content-max:   760px;
  --appbar-h:      52px;      /* l'en-tête global, présent sur toutes les pages */

  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
               Arial, sans-serif;
  --font-mono: "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas,
               "Liberation Mono", monospace;
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ===================== En-tête global =====================
   Présent sur TOUTES les pages du site, accueil compris. Il est injecté par
   `assets/guide.js` : sur une page de guide il réutilise le `<header
   id="topbar">` du squelette et le remonte en premier enfant de <body>, pour
   courir sur toute la largeur au-dessus de la barre latérale. */

.appbar {
  position: sticky;
  top: 0;
  z-index: 50;               /* au-dessus du tiroir : le ☰ reste atteignable */
  background: rgba(13, 15, 19, 0.88);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
.appbar__inner {
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--appbar-h);
  padding: 0 16px;
}
.appbar__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-shrink: 0;
  color: var(--text);
  text-decoration: none;
}
.appbar__brand:hover { text-decoration: none; }
.appbar__logo {
  width: 27px; height: 27px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--primary), var(--violet));
  display: grid; place-items: center;
  font-weight: 800; font-size: 15px; color: #fff;
}
.appbar__name { font-weight: 700; font-size: 15px; letter-spacing: -0.01em; }

/* Le module courant, sur téléphone : c'est ce qui situe le lecteur quand la
   barre latérale est repliée. Sur ordinateur, la barre latérale le dit déjà. */
.appbar__here {
  display: none;
  min-width: 0;
  padding-left: 11px;
  border-left: 1px solid var(--border);
  font-size: 13.5px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.appbar__nav { margin-left: auto; display: flex; align-items: center; gap: 4px; flex-shrink: 0; }

.appbar__menu {
  display: none;
  appearance: none;
  background: var(--elevated);
  border: 1px solid var(--border);
  color: var(--text);
  width: 36px; height: 36px;
  border-radius: 9px;
  font-size: 16px;
  cursor: pointer;
  place-items: center;
  flex-shrink: 0;
}

/* Le fil de progression dans la page — deux pixels, mais sur un module de
   1 500 lignes lu au pouce c'est la seule idée de « combien il reste ». */
.appbar__progress { height: 2px; }
.appbar__progress i {
  display: block;
  height: 2px;
  width: 0;
  background: linear-gradient(90deg, var(--primary), var(--violet));
}

/* ---- Le sélecteur de territoire ----
   Un `<details>` : il s'ouvre et se ferme sans une ligne de JavaScript. */
.switch { position: relative; }
.switch__btn {
  display: flex; align-items: center; gap: 6px;
  padding: 7px 10px;
  border-radius: 8px;
  font-size: 13.5px;
  color: var(--muted);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.switch__btn::-webkit-details-marker { display: none; }
/* La pastille du territoire courant : elle dit où l'on est autant qu'elle
   ouvre la liste — et, sur téléphone, elle tient lieu de libellé. */
.switch__pastille {
  width: 21px; height: 21px;
  border-radius: 6px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800; color: #fff;
  background: var(--elevated);
}
.switch__btn::after { content: "▾"; font-size: 10px; opacity: 0.8; }
.switch__btn:hover, .switch[open] .switch__btn { background: var(--elevated); color: var(--text); }
.switch__menu {
  position: absolute;
  right: 0;
  top: calc(100% + 9px);
  width: min(300px, calc(100vw - 32px));
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 13px;
  box-shadow: 0 20px 44px rgba(0, 0, 0, 0.55);
}
.switch__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 9px;
  font-size: 14px;
  color: var(--text);
  text-decoration: none;
}
a.switch__item:hover { background: var(--elevated); text-decoration: none; }
.switch__logo {
  width: 26px; height: 26px;
  border-radius: 7px;
  display: grid; place-items: center;
  font-weight: 800; font-size: 13px; color: #fff;
  background: var(--elevated);
  flex-shrink: 0;
}
.switch__label { flex: 1; min-width: 0; }
.switch__here { font-size: 11px; font-weight: 700; color: var(--primary); letter-spacing: 0.06em; text-transform: uppercase; }
.switch__group {
  margin: 6px 10px 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}
.switch__item--all {
  justify-content: center;
  margin-top: 4px;
  padding-top: 11px;
  border-top: 1px solid var(--border-soft);
  border-radius: 0 0 9px 9px;
  font-size: 13px;
  color: var(--primary);
}

/* ===================== Mise en page générale ===================== */

.layout { display: flex; min-height: calc(100dvh - var(--appbar-h)); }

/* ---- Barre latérale ---- */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 18px 0 28px;
  position: sticky;
  top: var(--appbar-h);
  height: calc(100dvh - var(--appbar-h));
  overflow-y: auto;
}

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 22px 14px;
  color: var(--text);
  text-decoration: none;
}
.sidebar__brand:hover { text-decoration: none; }
.sidebar__brand:hover .sidebar__title { color: var(--primary); }
.sidebar__ident { min-width: 0; }
.sidebar__logo {
  width: 34px; height: 34px;
  border-radius: 9px;
  background: linear-gradient(135deg, var(--primary), var(--violet));
  display: grid; place-items: center;
  font-weight: 800; font-size: 18px; color: #fff;
  flex-shrink: 0;
}
.sidebar__title { font-weight: 700; font-size: 16px; line-height: 1.25; }
.sidebar__subtitle { font-size: 12px; color: var(--faint); }

/* Combien de modules lus jusqu'au bout — la seule mesure honnête de l'avancée */
.sidebar__progress {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 22px 14px;
}
.jauge {
  flex: 1;
  height: 4px;
  border-radius: 3px;
  background: var(--elevated);
  overflow: hidden;
}
.jauge i {
  display: block;
  height: 4px;
  background: linear-gradient(90deg, var(--green), var(--primary));
  transition: width 0.25s ease;
}
.jauge__texte {
  font-size: 11.5px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.sidebar__filtre { padding: 0 22px 10px; border-bottom: 1px solid var(--border-soft); }
.filtre__champ {
  width: 100%;
  appearance: none;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 8px 11px;
  color: var(--text);
  font-family: inherit;
  font-size: 13.5px;
}
.filtre__champ::placeholder { color: var(--faint); }
.filtre__champ:focus { outline: none; border-color: var(--primary); background: var(--elevated); }

.nav__group-label {
  padding: 16px 22px 6px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 700;
}
.nav__group-label[hidden] { display: none; }
.nav a {
  display: flex;
  align-items: baseline;
  gap: 2px;
  padding: 8px 18px 8px 22px;
  color: var(--muted);
  text-decoration: none;
  font-size: 14.5px;
  border-left: 3px solid transparent;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
/* `display: flex` l'emporterait sur l'attribut `hidden` posé par le filtre. */
.nav a[hidden] { display: none; }
.nav a:hover { background: var(--elevated); color: var(--text); text-decoration: none; }
.nav a.active {
  color: var(--text);
  background: var(--primary-soft);
  border-left-color: var(--primary);
  font-weight: 600;
}
.nav a .num {
  width: 1.7em;
  flex-shrink: 0;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.nav a.active .num { color: var(--primary); }
.nav a .lib { flex: 1; min-width: 0; }
.nav a .coche { width: 1.1em; flex-shrink: 0; text-align: right; color: var(--green); font-size: 12px; }
/* Un module COMMENCÉ : ni neuf, ni terminé. Un point ambre plutôt qu'un
   pourcentage — la barre latérale est déjà dense sur 375 px. */
.nav a.encours .coche { color: var(--amber); font-size: 15px; line-height: 1; }
/* La durée estimée : présente, jamais bruyante. Elle sert à décider d'ouvrir
   un module, pas à être lue à chaque ligne. */
.nav a .duree {
  flex-shrink: 0;
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}
.nav a:hover .duree, .nav a.active .duree { opacity: 1; }

.nav__vide { padding: 14px 22px; font-size: 13.5px; color: var(--faint); }
.nav__vide[hidden] { display: none; }

/* ---- Contenu ---- */
.content { flex: 1; min-width: 0; }
.content__inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 48px 32px 96px;
}

.backdrop {
  display: none;
  position: fixed;
  inset: var(--appbar-h) 0 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 35;
}

/* ===================== Typographie ===================== */

h1 {
  font-size: 30px;
  line-height: 1.2;
  margin: 0 0 8px;
  letter-spacing: -0.02em;
}
.eyebrow {
  display: inline-block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--primary);
  margin-bottom: 14px;
}
h2 {
  font-size: 23px;
  margin: 44px 0 14px;
  padding-top: 8px;
  letter-spacing: -0.01em;
  border-top: 1px solid var(--border-soft);
  /* Sans ça, un lien du sommaire ferait atterrir le titre SOUS l'en-tête
     collant, et le lecteur croirait avoir sauté une section. */
  scroll-margin-top: calc(var(--appbar-h) + 14px);
}
h3, .fiche { scroll-margin-top: calc(var(--appbar-h) + 14px); }
h2:first-of-type { border-top: none; }
h3 { font-size: 18.5px; margin: 30px 0 10px; }
p { margin: 0 0 16px; }
a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }
strong { color: #fff; font-weight: 650; }
ul, ol { margin: 0 0 16px; padding-left: 22px; }
li { margin: 6px 0; }
hr { border: none; border-top: 1px solid var(--border); margin: 36px 0; }
.lead { font-size: 18.5px; color: var(--muted); margin-bottom: 28px; }

/* Code en ligne */
:not(pre) > code {
  font-family: var(--font-mono);
  font-size: 0.86em;
  background: var(--elevated);
  border: 1px solid var(--border-soft);
  padding: 1px 6px;
  border-radius: 6px;
  color: #f2c4a0;
  /* `pre-wrap` garde les espaces du code ; `anywhere` autorise la coupure d'un
     identifiant trop long UNIQUEMENT quand il n'y a pas d'autre solution.
     Sans ça, un extrait comme `{ ...values, [e.target.name]: e.target.value }`
     élargit toute la page sur un écran de téléphone. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ===================== Blocs de code ===================== */

pre[class*="language-"] {
  margin: 0 0 8px;
  padding: 16px 18px;
  background: #0f1218 !important;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow-x: auto;
  font-size: 13.5px;
  line-height: 1.65;
  -webkit-overflow-scrolling: touch;
  tab-size: 2;
}
pre[class*="language-"] code {
  font-family: var(--font-mono);
  white-space: pre;
}

/* Étiquette de fichier au-dessus d'un bloc de code */
.code-file {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--faint);
  background: var(--elevated);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 8px 8px 0 0;
  padding: 6px 12px;
  margin-top: 8px;
  /* Un chemin de fichier long ne doit pas élargir la page sur un téléphone. */
  max-width: 100%;
  overflow-wrap: anywhere;
}
.code-file + pre[class*="language-"] { border-radius: 0 0 var(--radius) var(--radius); margin-top: 0; }
.code-file .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); }

/* La coloration Prism : commentaires plus visibles (vert doux) */
.token.comment { color: #7d8aa0 !important; font-style: italic; }

/* Légende sous un bloc de code */
.code-caption {
  font-size: 13.5px;
  color: var(--faint);
  margin: -2px 0 18px;
  padding-left: 2px;
}

/* ===================== Encadrés pédagogiques ===================== */

.box {
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius);
  padding: 16px 18px;
  margin: 22px 0;
  background: var(--surface);
}
.box > :last-child { margin-bottom: 0; }
.box__title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 15px;
  margin-bottom: 10px;
  letter-spacing: 0.01em;
}
.box__title .ico { font-size: 17px; line-height: 1; }

.box--concept   { border-left-color: var(--primary); background: linear-gradient(180deg, var(--primary-soft), transparent 60%); }
.box--concept .box__title { color: var(--primary); }

.box--halterofit { border-left-color: var(--violet); }
.box--halterofit .box__title { color: var(--violet); }

.box--retenir   { border-left-color: var(--green); background: var(--green-soft); }
.box--retenir .box__title { color: var(--green); }

.box--piege     { border-left-color: var(--red); background: var(--red-soft); }
.box--piege .box__title { color: var(--red); }

.box--note      { border-left-color: var(--faint); }
.box--note .box__title { color: var(--muted); }

/* ---- ✏️ Affûté : ce qui a changé depuis la dernière lecture ----
   Un module de cours est corrigé chaque semaine contre ce que le professeur a
   RÉELLEMENT enseigné. Quelqu'un qui l'a déjà lu doit voir ce qui a bougé sans
   relire : cet encadré replié en tête, dont chaque ligne est un lien vers la
   section touchée. Ce qui vient du cours se lit, lui, dans le corps — la
   signature et la pastille, plus bas.

   Pas de teinte neuve, et c'est délibéré : la palette se paie une variable par
   couleur, et celle-ci ne parle pas de la MATIÈRE — elle parle du document. Le
   gris du .box--note avec une bordure TIRETÉE la sépare des huit encadrés de
   contenu sans en inventer un neuvième ton. */
.box--affute    { border-left-style: dashed; border-left-color: var(--muted); }
.box--affute .box__title { color: var(--muted); }
.box--affute details { margin: 4px 0 0; }
.box--affute summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 14px;
  font-weight: 600;
  padding: 6px 0;
  list-style: none;
}
.box--affute summary::-webkit-details-marker { display: none; }
.box--affute summary::before { content: "Voir ce qui a changé  ▸"; }
.box--affute details[open] summary::before { content: "Ce qui a changé  ▾"; }
.box--affute details[open] summary { color: var(--text); }
.box--affute ul { margin: 4px 0 2px; padding-left: 20px; }
.box--affute li { margin: 5px 0; font-size: 15px; }
/* UN encadré par module ; chaque passe y entre sous son sous-titre, la plus
   récente en premier. Deux boîtes empilées se lisaient comme une redondance
   (Patrick, 2026-09-05) : la provenance est déjà dans le .src de chaque ligne. */
.box--affute .passe {
  margin: 12px 0 2px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.box--affute .passe:first-child { margin-top: 6px; }
/* La source de chaque ligne — vocabulaire FERMÉ, voir SPEC § 5 ter. */
.box--affute .src { color: var(--faint); font-size: 13px; }

/* ---- Ce qui vient du cours (SPEC § 5 ter) ----
   Le rose est celui du 🎓 : dans un module de cours, c'est déjà la couleur de
   la voix du professeur. Les étiquettes sont fabriquées par guide.js.

   La SIGNATURE d'un passage : un filet le long du passage pour le pouce qui
   défile, et une étiquette qui dit d'où il vient et quand — au-dessus du
   passage sur un écran étroit, dans la marge quand elle a la place. Elle
   remplace le crayon .ajout, qui disait « regarde » sans dire d'où, et qu'on
   retrouvait empilé cinq fois de suite. */
.du-cours {
  position: relative;
  border-left: 2px solid var(--pink);
  padding-left: 12px;
  margin: 0 0 16px;
}
.du-cours > :last-child { margin-bottom: 0; }
/* Un encadré a déjà sa bordure : enveloppé seul, il ne garde que l'étiquette —
   deux filets parallèles se lisaient comme un défaut. */
.du-cours:not(:has(> :not(.du-cours__src, .box, .quiz, .exercice, .formule, .figure))) {
  border-left: 0;
  padding-left: 0;
}
/* L'étiquette en petites capitales — celle d'un passage, et celle de chaque
   séance dans une pastille dépliée. */
.du-cours__src,
.pastille__de {
  display: block;
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pink);
}
a.du-cours__src:hover { text-decoration: underline; }
/* La marge droite de la colonne n'a la place d'une note qu'au-delà de 1300 px :
   barre latérale (290) + colonne (760) + note (24 + 112) et son jeu. En deçà,
   la note déborderait et la page défilerait de côté. */
@media (min-width: 1300px) {
  .du-cours > .du-cours__src {
    position: absolute;
    top: 3px;
    left: calc(100% + 24px);
    width: 112px;
    margin: 0;
    font-size: 12px;
    line-height: 1.45;
    letter-spacing: 0.02em;
    text-transform: none;
  }
}

/* La ligne sous un titre de section : la PASTILLE du fil (ce que le professeur a
   traité à fond, ou seulement effleuré) et la signature d'une section tirée du
   cours pour l'essentiel. La pastille se déplie sur ce que le fil en dit. */
.provenance {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 6px 8px;
  margin: -4px 0 16px;
}
/* La pilule : la section signée, la pastille et sa ligne de résumé — ou la pastille
   seule quand elle n'a rien à déplier, et c'est alors un lien vers la séance. Son
   triangle ▸ / ▾ est celui des replis, plus bas. */
.provenance .du-cours__src,
.pastille > summary,
a.pastille {
  display: inline-block;
  margin: 0;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.02em;
  text-transform: none;
}
.provenance .du-cours__src { border: 1px solid var(--pink); }
.pastille > summary { cursor: pointer; list-style: none; }
.pastille > summary::after { opacity: 0.7; }
.pastille[open] { flex-basis: 100%; }
.pastille--fond > summary, a.pastille--fond { color: var(--bg); background: var(--pink); }
.pastille--effleure > summary, a.pastille--effleure { color: var(--muted); border: 1px solid var(--border); }
a.pastille { text-decoration: none; }
a.pastille:hover { text-decoration: underline; }
.pastille__quoi {
  margin: 8px 0 4px;
  padding: 10px 12px;
  border-left: 3px solid var(--pink);
  border-radius: 0 8px 8px 0;
  background: var(--pink-soft);
  font-size: 14.5px;
  line-height: 1.55;
}
.pastille--effleure .pastille__quoi { border-left-color: var(--border); background: var(--elevated); }
.pastille__de { margin-bottom: 4px; }
.pastille--effleure .pastille__de { color: var(--muted); }
.sommaire__fond {
  display: inline-block;
  margin-left: 4px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--pink);
  color: var(--bg);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  vertical-align: 1px;
}

/* ---- Le seul message de la synchronisation : le jumelage d'un appareil ---- */
.annonce {
  position: fixed;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 100;
  max-width: calc(100vw - 32px);
  padding: 10px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--elevated);
  color: var(--text);
  font-size: 14px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}

/* ---- Une section que le cours n'a pas encore traitée ----
   Elle ne se COUPE pas — le professeur peut la donner la semaine suivante —
   mais elle ne doit pas coûter de temps d'étude aujourd'hui. Le <h2> et son
   ancre restent en place (liens.js contrôle le sommaire) ; seul le corps se
   replie, et se rouvre quand le fil des séances passe la section à « traité ».
   Même geste pour une SÉANCE ancienne du fil (SPEC § 6), à bordure PLEINE :
   elle est passée, pas à venir. */
.attente,
.seance-repliee {
  border-left: 3px dashed var(--border-soft);
  padding-left: 14px;
  margin: 10px 0 18px;
}
.seance-repliee { border-left-style: solid; }
.attente > summary,
.seance-repliee > summary {
  cursor: pointer;
  color: var(--faint);
  font-size: 14px;
  font-style: italic;
  padding: 4px 0;
  list-style: none;
}
.attente > summary::-webkit-details-marker,
.seance-repliee > summary::-webkit-details-marker,
.pastille > summary::-webkit-details-marker { display: none; }
.attente > summary::after,
.seance-repliee > summary::after,
.pastille > summary::after { content: "  ▸"; }
.attente[open] > summary::after,
.seance-repliee[open] > summary::after,
.pastille[open] > summary::after { content: "  ▾"; }

/* Pont entre deux concepts (mobile ↔ web, module ↔ module, détail ↔ principe) */
.box--pont      { border-left-color: var(--cyan); background: var(--cyan-soft); }
.box--pont .box__title { color: var(--cyan); }

/* Questions d'entrevue rattachées au module — et son jumeau des modules de
   COURS, « 🎓 À l'examen » (SPEC § 3 sexies).

   Les deux partagent TOUT : une question, une réponse repliée, la même teinte.
   C'est délibéré, et ce n'est pas une économie de paresse — `modules.js` refuse
   qu'un même module porte les deux, si bien que la couleur n'a personne à
   distinguer. Ce qui distingue, c'est le pictogramme et le titre, là où le
   lecteur regarde. Deux teintes auraient coûté une variable de palette pour
   séparer des choses qui ne se croisent jamais. */
.box--entrevue,
.box--examen    { border-left-color: var(--pink); background: var(--pink-soft); }
.box--entrevue .box__title,
.box--examen .box__title { color: var(--pink); }
.box--entrevue details,
.box--examen details {
  background: var(--elevated);
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  padding: 0 12px;
  margin: 6px 0 12px;
}
.box--entrevue summary,
.box--examen summary {
  cursor: pointer;
  color: var(--pink);
  font-size: 14px;
  font-weight: 600;
  padding: 9px 0;
  list-style: none;
}
.box--entrevue summary::-webkit-details-marker,
.box--examen summary::-webkit-details-marker { display: none; }
.box--entrevue summary::before { content: "Voir une bonne réponse  ▸"; }
.box--entrevue details[open] summary::before { content: "Réponse  ▾"; }
/* Le seul écart : à l'examen, il n'y a pas « une bonne » réponse parmi
   d'autres — il y a ce que le correcteur attend. */
.box--examen summary::before { content: "Voir la réponse attendue  ▸"; }
.box--examen details[open] summary::before { content: "Réponse attendue  ▾"; }
.box--entrevue details[open] summary,
.box--examen details[open] summary { color: var(--text); }
.box--entrevue details > :not(summary),
.box--examen details > :not(summary) { margin: 0 0 12px; font-size: 15px; }
.box--entrevue .q,
.box--examen .q { font-weight: 600; margin: 14px 0 4px; }
.box--entrevue .q:first-of-type,
.box--examen .q:first-of-type { margin-top: 6px; }

/* ---- La formule : le patron de code canonique du module ----
   Traitement volontairement différent des autres encadrés (bordure complète,
   fond assombri, titre en capitales) pour qu'on la RECONNAISSE d'un coup d'œil
   en feuilletant, et qu'elle s'ancre à force d'être revue. */
.formule {
  border: 1px solid var(--gold);
  border-radius: var(--radius);
  padding: 16px 18px 4px;
  margin: 26px 0;
  background: var(--gold-soft);
}
.formule > :last-child { margin-bottom: 0; }
.formule__title {
  display: flex; align-items: center; gap: 8px;
  color: var(--gold);
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  margin-bottom: 12px;
}
.formule__title .ico { font-size: 16px; line-height: 1; text-transform: none; letter-spacing: 0; }
.formule__name {
  font-weight: 700;
  font-size: 16.5px;
  color: var(--text);
  margin: 0 0 10px;
}
.formule pre[class*="language-"] { border-color: rgba(255, 212, 121, 0.28); }

/* ---- L'aide-mémoire : ce qui se mémorise littéralement ----
   La cousine de `.formule`, et le même parti pris : bordure COMPLÈTE, pour se
   reconnaître d'un coup d'œil en feuilletant. Ce qui les sépare est ce qu'elles
   portent — un patron de code là, une table de faits ici.

   AUCUNE couleur d'accent, et c'est délibéré. Les huit accents de la palette
   sont pris, et chacun porte un TON : vert on récapitule, rouge attention,
   ambre à toi de lire. Un aide-mémoire n'a pas de ton — il n'argumente pas, il
   énonce. Le neutre est donc sa couleur juste, et il reste le seul bloc gris à
   bordure pleine du site : ça suffit à le repérer.

   Il se démarque aussi par sa DENSITÉ : interligne resserré, `th` discrets,
   `code` sans retour à la ligne (règle globale sur `td code`). On y revient
   pour vérifier un détail, pas pour lire. */
.aide-memoire {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin: 26px 0;
  background: var(--surface);
}
.aide-memoire > :last-child { margin-bottom: 0; }
.aide-memoire__title {
  display: flex; align-items: center; gap: 8px;
  color: var(--text);
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  margin-bottom: 12px;
}
.aide-memoire__title .ico { font-size: 16px; line-height: 1; text-transform: none; letter-spacing: 0; }
/* Une LISTE DE DÉFINITIONS, pas un tableau. La rubrique est née table de valeurs
   courtes (`flex-direction` → `row`) ; elle porte aujourd'hui des paires
   question → explication, et le contenant a fini par mentir sur le contenu.
   Sur 375 px, deux colonnes laissaient 106 px à l'amorce — qui se repliait sur
   quatre lignes — et 250 px à la réponse, soit une trentaine de caractères par
   ligne, très en dessous du confort de lecture. Une paire pleine largeur ne
   déborde jamais et lit deux fois plus large.

   Effet de bord recherché : `td code { white-space: nowrap }` ne s'applique plus,
   faute de `td`. C'est cette règle qui imposait jusqu'à 338 px de défilement
   latéral DANS l'encadré — invisible pour `mobile.js`, que `.table-wrap`
   rassurait. */
.aide-memoire dl { margin: 0; font-size: 14px; }
/* Le `dt` AMORCE, le `dd` répond exactement (SPEC § 3 quinquies). L'amorce se lit
   comme une entrée d'index, d'où le poids — mais elle reste une question posée,
   jamais une simple étiquette de sujet. */
.aide-memoire dt { color: var(--text); font-weight: 600; line-height: 1.5; }
.aide-memoire dd { margin: 3px 0 14px; color: var(--muted); line-height: 1.6; }
.aide-memoire dd:last-child { margin-bottom: 0; }

/* ---- Exercice de lecture ---- */
.exercice {
  border: 1px solid var(--border);
  border-left: 4px solid var(--amber);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin: 24px 0;
  background: var(--amber-soft);
}
.exercice__title {
  display: flex; align-items: center; gap: 8px;
  color: var(--amber);
  font-weight: 700;
  font-size: 15px;
  margin-bottom: 10px;
}
.exercice details {
  margin-top: 12px;
  border-top: 1px dashed var(--border);
  padding-top: 6px;
}
.exercice summary {
  cursor: pointer;
  color: var(--amber);
  font-weight: 600;
  font-size: 14.5px;
  padding: 8px 0;
  list-style: none;
  user-select: none;
}
.exercice summary::-webkit-details-marker { display: none; }
.exercice summary::before { content: "▸ "; display: inline-block; transition: transform 0.15s; }
.exercice details[open] summary::before { content: "▾ "; }
.exercice details > :not(summary) { animation: fade 0.2s ease; }

@keyframes fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; } }

/* ---- Quiz ---- */
.quiz {
  border: 1px solid var(--border);
  border-left: 4px solid var(--violet);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin: 24px 0;
  background: var(--violet-soft);
}
.quiz__title {
  display: flex; align-items: center; gap: 8px;
  color: var(--violet);
  font-weight: 700; font-size: 15px;
  margin-bottom: 6px;
}
.quiz__q { font-weight: 600; margin: 16px 0 6px; }
.quiz__q:first-of-type { margin-top: 8px; }
.quiz details {
  background: var(--elevated);
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  padding: 0 12px;
  margin-top: 4px;
}
.quiz summary {
  cursor: pointer;
  color: var(--violet);
  font-size: 14px;
  font-weight: 600;
  padding: 9px 0;
  list-style: none;
}
.quiz summary::-webkit-details-marker { display: none; }
.quiz summary::before { content: "Voir la réponse  ▸"; }
.quiz details[open] summary::before { content: "Réponse  ▾"; }
.quiz details[open] summary { color: var(--text); }
.quiz details > p { margin: 0 0 12px; font-size: 15px; }

/* Un quiz peut maintenant montrer du code — c'est même l'essentiel : sans
   extrait, une question ne peut demander qu'une définition.
   Même taille que les blocs de code du corps du module : un extrait plus gros
   déborderait davantage sur un écran de téléphone. */
.quiz pre[class*="language-"] { font-size: 12px; margin: 10px 0 4px; }
.quiz details pre[class*="language-"] { margin: 4px 0 12px; }
.quiz .code-file { margin-top: 10px; }

/* ===================== Visuels =====================
   Les guides expliquaient la géométrie du CSS en prose : le modèle de boîte par
   une analogie de tableau encadré, `justify-content` par un tableau de cinq
   lignes. Or `space-between` et `space-around` ne se distinguent pas en mots —
   un tableau qui décrit une disposition est l'aveu qu'il manque un visuel.

   LE PARTI PRIS. Quand le navigateur sait déjà dessiner la chose, on ne la
   dessine pas : on la MONTRE. Deux `<div>` portant réellement `width`,
   `padding` et `border`, ne différant que par `box-sizing`, produisent la
   démonstration par le moteur de rendu lui-même. C'est exact par construction —
   un schéma peut se tromper, ou dériver le jour où la prose change ; un élément
   réellement stylé ne le peut pas. Et il se recolore seul avec la palette.

   LE GARDE-FOU. `.figure__cadre` porte `overflow-x: auto`, comme `.table-wrap`
   le fait pour les tableaux : même si une démo se trompe, le débordement reste
   CONFINÉ au visuel et la page ne s'élargit pas. C'est un verrou structurel, pas
   une discipline à tenir. Corollaire : aucune largeur de ce bloc ne dépasse
   300 px, le lecteur n'en ayant que ~317 à l'intérieur d'une figure.

   LE PIÈGE À CONNAÎTRE, valable pour toute la section. Un enfant de flex ou de
   grille refuse par défaut de descendre sous la largeur de son contenu
   (`min-width: auto`). Sans `min-width: 0` ET `overflow-wrap: anywhere`, un
   libellé un peu long — `Content-Type: application/json` dans une barre,
   `<span class="badge">` au cinquième niveau d'un arbre — élargit la figure au
   lieu de se couper. Les deux propriétés vont ensemble, et elles ne se posent
   que sur ce qui PORTE du texte : sur un conteneur, `overflow-wrap` s'hériterait
   et couperait la prose au milieu des mots.

   LA GRAMMAIRE DES COULEURS. Trois modificateurs traversent les familles et ont
   un sens fixe : `--frontiere` (violet) est le nœud OÙ commence un autre régime,
   `--cible` (rouge) est ce qui est fautif ou entraîné malgré soi, `--detache`
   (pointillé) est présent ici mais absent de l'arbre qui compte. Chacun est
   commenté à sa définition ; le SPEC § 5 bis en donne le tableau, pour qu'on les
   RETROUVE au lieu d'en inventer un quatrième.

   Le gabarit d'une figure est au SPEC § 5 bis. */

/* Le défaut du navigateur pour <figure> est `margin: 1em 40px` : 80 px mangés
   sur les 347 d'un téléphone, sans que rien ne le signale. */
figure { margin: 0; }

.figure {
  margin: 24px 0;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.figure__titre {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
}
.figure__cadre { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* Même rendu que `.code-caption`, mais classe distincte : `.code-caption` porte
   un `margin-top` NÉGATIF, calibré pour coller sous un <pre> ; une légende de
   figure suit un cadre et doit au contraire respirer. */
.figure__legende {
  margin-top: 12px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--faint);
}

/* --- Vocabulaire commun aux démos --- */
.demo__etiquette {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--primary);
  display: block;
  margin-bottom: 5px;
}
.demo + .demo { margin-top: 16px; }

/* --- Une formule isolée ---
   MathML n'est pas décoré ici, et ça reste la règle (SPEC § 5 bis) : ni couleur,
   ni taille, ni classe. Une seule chose lui manque à l'affichage, et elle est
   structurelle — le navigateur ne donne AUCUNE marge verticale à un
   `display="block"`, si bien que la formule se colle au paragraphe du dessus
   et à celui du dessous. Une formule isolée est un paragraphe : elle en prend
   la respiration, et rien de plus. Vu en regardant la première page du dépôt
   à en contenir. */
math[display="block"] { margin: 16px 0; }

/* --- Les quatre couches d'une boîte, réellement emboîtées ---
   Chaque couche est un vrai élément dont le padding EST la couche montrée :
   l'épaisseur qu'on voit est celle que le navigateur applique. */
.demo__couche {
  padding: 22px 12px 12px;
  border-radius: 8px;
  position: relative;
}
/* La couleur est portée par la couche elle-même : son étiquette l'hérite. Trois
   règles à combinateur disaient la même chose plus loin, et séparaient le fond,
   la bordure et le libellé d'une couche en deux endroits du fichier. */
.demo__couche--marge   { background: var(--amber-soft); border: 1px dashed var(--amber); color: var(--amber); }
.demo__couche--bordure { background: var(--red-soft);   border: 1px solid  var(--red);   color: var(--red); }
.demo__couche--padding { background: var(--green-soft); border: 1px dashed var(--green); color: var(--green); }
.demo__couche--contenu {
  background: var(--primary-soft);
  border: 1px solid var(--primary);
  padding: 16px 12px;
  text-align: center;
  font-size: 13.5px;
  color: var(--text);
}
.demo__nom {
  position: absolute;
  top: 5px; left: 10px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
}

/* --- content-box vs border-box ---
   Les deux boîtes reçoivent les MÊMES déclarations ; seul `box-sizing` change.
   L'enveloppe est en `inline-block` : elle se rétracte sur la largeur réellement
   occupée par la boîte, donc la règle en dessous mesure le rendu au lieu de
   répéter un nombre écrit à la main — elle ne peut pas mentir. */
.demo__mesure { display: inline-block; max-width: 100%; }
.demo__boite {
  width: 180px;
  padding: 18px;
  border: 3px solid var(--primary);
  background: var(--primary-soft);
  font-size: 13px;
  text-align: center;
  border-radius: 6px;
}
.demo__boite--content { box-sizing: content-box; }
.demo__boite--border  { box-sizing: border-box; }
/* LA MESURE : le nombre obtenu, posé contre le rendu qui le produit — un
   désaccord entre les deux se verrait, et c'est tout l'intérêt de l'écrire.
   Trois classes le faisaient chacune de leur côté, avec trois écarts fortuits
   (11 / 11,5 px, 4 / 5 px) qu'aucune décision ne justifiait. */
.demo__regle, .demo__ecart, .chrono__total {
  display: block;
  margin-top: 5px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--amber);
  text-align: center;
}
/* Celle-ci TRACE en plus la largeur qu'elle mesure. */
.demo__regle {
  width: 100%;
  border-top: 2px solid var(--amber);
  padding-top: 3px;
}

/* --- Marges qui fusionnent ---
   Le fond de la pile rend l'espace vide visible : c'est LUI le sujet.
   Les deux cas vont CÔTE À CÔTE, jamais l'un sous l'autre : 32 px et 48 px
   séparés par une hauteur d'écran ne se comparent pas. Alignés au même départ,
   la différence saute aux yeux — c'est toute la raison d'être de la figure. */
.demo__duo { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: start; }
.demo__duo > .demo { min-width: 0; }
.demo__duo .demo__etiquette { font-size: 10.5px; }
/* Les deux colonnes doivent partir de la MÊME ligne : l'espacement vertical
   entre démos successives décalerait la seconde et ruinerait la comparaison. */
.demo__duo .demo + .demo { margin-top: 0; }
/* LA SCÈNE — le cadre en pointillé dans lequel se joue une démo. Les quatre
   variantes ne diffèrent que par leur disposition ; leur peau s'écrit ici une
   fois, sinon la retoucher demanderait quatre corrections cohérentes et rien
   ne signalerait qu'elles vont ensemble. */
.demo__pile, .demo__scene, .demo__ecran, .demo__ancre {
  background: var(--bg);
  border: 1px dashed var(--border);
  border-radius: 8px;
}

.demo__pile {
  padding: 0 10px;
  overflow: hidden;      /* sans ça, la marge de l'enfant s'échapperait de la pile */
}
.demo__pile--flex { display: flex; flex-direction: column; }
/* Un SPÉCIMEN étiqueté en monospace — le bloc d'une pile, le plot d'une ancre.
   Peau commune, puis ce qui les distingue vraiment. */
.demo__bloc, .demo__plot {
  background: var(--elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-family: var(--font-mono);
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Uniquement les blocs empilés DIRECTEMENT dans une démo — l'anatomie d'une
   requête. Écrite sans le `.demo >`, cette règle gagnait sur `.demo__bloc--bas`
   par sa spécificité (0,2,0 contre 0,1,0) et ramenait sa marge de 16 px à 6 :
   la figure des marges qui fusionnent annonçait 48 px et en affichait 38. Une
   peau générique ne doit jamais écraser le paramètre d'une figure — c'est la
   seule façon dont une démo vive peut se mettre à mentir. */
.demo > .demo__bloc + .demo__bloc { margin-top: 6px; }
.demo__bloc {
  padding: 9px 11px;
  font-size: 11.5px;
}
/* Le nom d'une partie, au-dessus de son contenu littéral : la figure de
   l'anatomie d'une requête montre du texte HTTP réel, et il faut dire de quelle
   des trois parties il s'agit sans le mélanger au texte lui-même. */
/* ⚠️ Elle est en CAPITALES : n'y mets qu'un RÔLE, jamais une donnée littérale.
   Une adresse « 0x1A0 » s'y afficherait « 0X1A0 », à trois lignes du même nombre
   écrit en clair dans le corps du bloc — deux graphies pour une seule valeur, dans
   la figure censée fixer la notion. Le HTML étant juste, aucun vérificateur ne le
   dit : ça se voit aux captures de rendu.js. SPEC § 5 bis. */
.demo__part {
  display: block;
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 3px;
}
.demo__bloc--haut { margin: 16px 0 32px; }
.demo__bloc--bas  { margin: 16px 0; }

/* --- display : block / inline / inline-block --- */
.demo__texte {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.9;
  color: var(--muted);
}
.demo__jeton {
  background: var(--violet-soft);
  border: 1px solid var(--violet);
  color: var(--text);
  border-radius: 5px;
  padding: 2px 8px;
  font-size: 12.5px;
  text-align: center;
  width: 130px;
  height: 38px;
}
/* La largeur et la hauteur sont écrites UNE fois, sur la base : le CSS dit
   alors littéralement ce que la légende promet — seul `display` change, et
   c'est lui qui décide si elles sont honorées ou ignorées. */
.demo__jeton--inline       { display: inline; }
.demo__jeton--inline-block { display: inline-block; }
.demo__jeton--block        { display: block; }

/* --- Scènes flex : justify-content et align-items ---
   Une suite de scènes qui ne diffèrent que par une valeur se lit comme un
   tableau : on les resserre pour que l'œil COMPARE au lieu de parcourir. */
.demo--serre + .demo--serre { margin-top: 10px; }
.demo__scene {
  display: flex;
  padding: 6px;
  gap: 4px;
}
.demo__scene--start   { justify-content: flex-start; }
.demo__scene--center  { justify-content: center; }
.demo__scene--end     { justify-content: flex-end; }
.demo__scene--between { justify-content: space-between; }
.demo__scene--around  { justify-content: space-around; }
.demo__scene--evenly  { justify-content: space-evenly; }
/* `.demo__scene` ne déclare aucune direction : elle vaut donc `row`, le défaut
   du web — et c'est ce défaut qu'une figure oppose à celui de React Native.
   Elle sert aussi de pile verticale ESPACÉE, ce que `.demo__pile--flex` ne sait
   pas faire : celle-là n'a pas de `gap`, ses blocs se touchent. */
.demo__scene--colonne { flex-direction: column; }
/* L'axe croisé ne se voit que si la scène est plus haute que ses enfants. */
.demo__scene--croise    { height: 72px; gap: 6px; }
.demo__scene--stretch   { align-items: stretch; }
.demo__scene--haut       { align-items: flex-start; }
.demo__scene--milieu    { align-items: center; }
.demo__scene--bas    { align-items: flex-end; }
/* Une TUILE : l'enfant d'une scène flex (`item`) ou la case d'une grille
   (`case`). Deux rôles distincts à enseigner, une seule peau à entretenir. */
.demo__item, .demo__case {
  background: var(--primary-soft);
  border: 1px solid var(--primary);
  border-radius: 5px;
  color: var(--text);
  display: grid;
  place-items: center;
  overflow-wrap: anywhere;
}
.demo__item {
  width: 38px;
  font-size: 12px;
  padding: 7px 0;
}
/* Hauteurs inégales : sans elles, `align-items` n'a rien à aligner. Elles
   restent nettement en dessous de la hauteur de la scène (72 px) — un enfant
   qui la remplit ne laisse plus de jeu vertical, et l'alignement cesse de se
   voir alors même qu'il s'applique. */
.demo__item--court { padding: 3px 0;  font-size: 11px; }
.demo__item--long  { padding: 12px 0; font-size: 15px; }

/* --- Grilles --- */
.demo__grille { display: grid; gap: 5px; }
.demo__grille--trois { grid-template-columns: repeat(3, 1fr); }
.demo__grille--asym  { grid-template-columns: 2fr 1fr; }
.demo__grille--fixe  { grid-template-columns: 96px 1fr; }
.demo__grille--auto  { grid-template-columns: repeat(auto-fit, minmax(68px, 1fr)); }
/* Six colonnes égales, pour une figure qui aligne deux rangées de nombres et
   leur somme colonne par colonne : l'égalité doit se VOIR, donc les cellules
   doivent tomber les unes sous les autres. `--auto` ne convient pas — il
   replie la rangée dès que la place manque, et l'alignement disparaît avec
   la démonstration. */
.demo__grille--six   { grid-template-columns: repeat(6, 1fr); }
.demo__case {
  padding: 10px 4px;
  font-size: 11.5px;
}
/* Trois largeurs de conteneur pour la MÊME règle de grille : c'est ainsi qu'on
   montre `auto-fit` sans redimensionner quoi que ce soit. */
.demo__ecran {
  padding: 6px;
  max-width: 100%;
}
/* Ces largeurs tiennent SANS être comprimées jusqu'à 320 px de fenêtre, où le
   lecteur ne dispose que de 262 px dans une figure. Une valeur plus grande
   serait rabotée par `max-width: 100%` sur les petits écrans, le conteneur
   perdrait une colonne, et l'étiquette qui annonce sa largeur deviendrait
   fausse là précisément où on la lit. */
.demo__ecran--large  { width: 258px; }
.demo__ecran--moyen  { width: 160px; }
.demo__ecran--etroit { width: 86px; }

/* --- Unités : la même déclaration, cinq référentiels --- */
.demo__barre {
  background: var(--cyan-soft);
  border: 1px solid var(--cyan);
  border-radius: 5px;
  padding: 6px 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
}
.demo__barre + .demo__barre { margin-top: 6px; }
.demo__barre--px  { width: 200px; }
.demo__barre--pourcent  { width: 60%; }
.demo__barre--rem { width: 12rem; }
/* `em` se mesure sur la fonte de l'élément : 12em à 12px valent 144px, là où
   12rem en valent 192. C'est toute la différence, et elle se voit. */
.demo__barre--em  { font-size: 12px; width: 12em; }
/* `vw` ignore son conteneur et ne regarde que la fenêtre : c'est justement ce
   qu'on montre. 25 % gardent la barre à l'intérieur du cadre jusqu'aux très
   grands écrans, sans quoi la démonstration se paierait d'une barre de
   défilement à chaque ouverture. */
.demo__barre--vw  { width: 25vw; }

/* --- position et empilement --- */
.demo__ancre {
  position: relative;
  padding: 10px;
  min-height: 96px;
}
.demo__plot {
  padding: 7px 9px;
  font-size: 11px;
  color: var(--muted);
  width: 150px;
}
.demo__plot + .demo__plot { margin-top: 6px; }
.demo__plot--decale {
  position: relative;
  left: 26px;
  border-color: var(--amber);
  color: var(--amber);
}
/* `absolute` sort du flux : il se place par rapport à l'ancre positionnée,
   et les autres se referment sur la place qu'il occupait. */
.demo__plot--sorti {
  position: absolute;
  top: 10px; right: 10px;
  width: auto;
  border-color: var(--violet);
  color: var(--violet);
}

.demo__empilement { position: relative; height: 104px; }
.demo__carte {
  position: absolute;
  width: 108px;
  padding: 9px 8px;
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  text-align: center;
}
/* Les fonds sont OPAQUES (`--*-plein`, déclarés avec la palette) : avec les
   accents `-soft`, qui sont en rgba, la carte du dessous transparaîtrait et
   l'ordre d'empilement — le sujet même de la figure — cesserait de se voir. */
.demo__carte--a { left: 0;    top: 0;    z-index: 1; background: var(--primary-plein); border: 1px solid var(--primary); color: var(--primary); }
.demo__carte--c { left: 84px; top: 48px; z-index: 3; background: var(--pink-plein);    border: 1px solid var(--pink);    color: var(--pink); }
/* La carte verte paraît dans les DEUX figures d'empilement : une seule
   identité, deux positions. Elle était décrite deux fois, à dix-sept lignes
   d'écart, sans que rien ne signale la seconde. */
.demo__carte--b, .demo__carte--dessus {
  z-index: 2;
  background: var(--green-plein);
  border: 1px solid var(--green);
  color: var(--green);
}
.demo__carte--b      { left: 42px; top: 24px; }
.demo__carte--dessus { left: 56px; top: 40px; }
/* Le piège : `opacity` inférieure à 1 crée un contexte d'empilement. Le 9999
   qu'il enferme ne se compare plus qu'à ses frères DANS ce contexte. */
.demo__contexte {
  position: absolute;
  left: 0; top: 8px;
  z-index: 1;
  opacity: 0.99;
}
.demo__carte--enferme {
  position: static;
  z-index: 9999;
  background: var(--red-plein);
  border: 1px solid var(--red);
  color: var(--red);
}

/* --- Chronologies : montrer du TEMPS ---
   Une grille de douze colonnes égales = douze unités de temps. Une barre déclare
   la colonne où elle commence et le nombre qu'elle occupe ; sa longueur à
   l'écran est donc PROPORTIONNELLE PAR CONSTRUCTION, sans qu'aucune largeur en
   pixels soit écrite nulle part. C'est le même pari que les démos de mise en
   page : le moteur produit la démonstration, on ne la dessine pas.

   Chaque figure dit dans sa légende ce que vaut une colonne. C'est le seul
   chiffre à ne pas se tromper, et il est écrit à côté du dessin.

   À NE PAS CONFONDRE avec `.demo__barre`, plus haut : celle-là enseigne les
   UNITÉS CSS, sa largeur est écrite (`200px`, `12rem`) parce que c'est son
   sujet. Une barre de temps n'en porte aucune. Deux mécanismes, deux noms. */
.chrono { display: grid; gap: 4px; }
.chrono__groupe + .chrono__groupe { margin-top: 14px; }
/* LA GRILLE, écrite une seule fois. Les pistes et la graduation doivent porter
   exactement les mêmes colonnes : si elles divergent d'une seule, chaque
   graduation désigne le mauvais instant, sur toutes les chronologies à la fois,
   et rien ne s'affiche de travers. C'était écrit deux fois, à 82 lignes d'écart. */
.chrono__piste,
.chrono__axe {
  display: grid;
  grid-template-columns: 76px repeat(12, 1fr);
}
.chrono__piste {
  align-items: center;
  min-height: 24px;
}
.chrono__nom {
  /* La RANGÉE autant que la colonne. Sans `grid-row`, le nom n'a qu'une
     position partielle : il est placé APRÈS les barres verrouillées en
     rangée 1, si bien qu'une barre auto-placée s'installe dans sa colonne et
     déborde de deux colonnes. Invisible tant que toutes les barres déclarent
     leur départ — c'est la première piste séquentielle qui l'a révélé. */
  grid-column: 1;
  grid-row: 1;
  font-family: var(--font-mono);
  /* 9,5 px et 4 px de gouttière : un nom de douze caractères — `appointments`,
     tel que l'onglet Network l'affiche — tient alors sur une ligne dans les
     76 px de la colonne. Élargir la colonne aurait été l'autre solution, mais
     elle vole sa place aux barres, et c'est la barre qui porte le sens. */
  font-size: 9.5px;
  color: var(--muted);
  padding-right: 4px;
  overflow-wrap: anywhere;
}
.chrono__barre {
  grid-row: 1;
  background: var(--primary-soft);
  border: 1px solid var(--primary);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text);
  text-align: center;
  padding: 5px 2px;
  /* Jamais de `white-space: nowrap` ici : il rendrait le libellé plus fort que
     la grille, et c'est la grille qui doit décider de la largeur. */
  /* ET UNE BARRE D'UNE SEULE COLONNE SE LAISSE VIDE. Elle fait ~18 px : le
     `overflow-wrap` ci-dessous y coupe un mot en fragments de deux lettres
     empilés sur six lignes. Le HTML est juste, rien ne déborde, et ni
     `mobile.js` ni `rendu.js` ne le voient — c'est la capture qui l'a dit. Ce
     qu'une barre d'une colonne porterait va dans `.chrono__nom` ou la légende. */
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Une attente SUBIE — page blanche, squelette : c'est du temps qui passe sans
   que le lecteur obtienne quoi que ce soit. Le pointillé le dit. */
.chrono__barre--attente {
  background: transparent;
  border-style: dashed;
  border-color: var(--faint);
  color: var(--faint);
}
.chrono__barre--gain {
  background: var(--green-soft);
  border-color: var(--green);
  color: var(--green);
}
/* Une requête qui a échoué — la « ligne rouge » que la prose du module nomme
   comme l'une des trois signatures à reconnaître dans l'onglet Network. */
.chrono__barre--echec {
  background: var(--red-soft);
  border-color: var(--red);
  color: var(--red);
}

/* Placement : `a{N}` = colonne de départ, `l{N}` = nombre de colonnes occupées.
   Le décalage de 1 vient de la première colonne, qui porte le nom de la piste.

   LES DEUX SÉRIES SONT COMPLÈTES, de 1 à 12, et c'est délibéré. Elles ne
   l'étaient pas : on écrivait la classe le jour où une figure la réclamait, si
   bien que poser une chronologie s'interrompait pour aller ajouter une ligne
   ici. Le pire des deux mondes — le vocabulaire grandissait quand même, mais
   par à-coups, au milieu d'un travail de rédaction.

   Ça ne contredit PAS la leçon du vocabulaire SVG posé d'avance puis retiré
   (voir plus haut dans cette section). Ces douze classes-là étaient
   spéculatives : on ignorait quelles formes on dessinerait un jour, et on a
   deviné faux. Une grille de douze colonnes a exactement douze départs et
   douze longueurs possibles — l'ensemble est FINI et déterminé par la
   géométrie, pas imaginé. Une série complète se déclare d'un bloc ; un
   vocabulaire de formes s'écrit pour la figure qu'on a sous les yeux. */
.chrono__barre--a1  { grid-column-start: 2; }
.chrono__barre--a2  { grid-column-start: 3; }
.chrono__barre--a3  { grid-column-start: 4; }
.chrono__barre--a4  { grid-column-start: 5; }
.chrono__barre--a5  { grid-column-start: 6; }
.chrono__barre--a6  { grid-column-start: 7; }
.chrono__barre--a7  { grid-column-start: 8; }
.chrono__barre--a8  { grid-column-start: 9; }
.chrono__barre--a9  { grid-column-start: 10; }
.chrono__barre--a10 { grid-column-start: 11; }
.chrono__barre--a11 { grid-column-start: 12; }
.chrono__barre--a12 { grid-column-start: 13; }
/* Sans classe `a{N}`, une barre se place TOUTE SEULE à la suite de la
   précédente : la grille enchaîne. C'est ce qu'il faut pour une piste
   séquentielle — sinon il faudrait une classe de départ par segment. */
.chrono__barre--l1  { grid-column-end: span 1; }
.chrono__barre--l2  { grid-column-end: span 2; }
.chrono__barre--l3  { grid-column-end: span 3; }
.chrono__barre--l4  { grid-column-end: span 4; }
.chrono__barre--l5  { grid-column-end: span 5; }
.chrono__barre--l6  { grid-column-end: span 6; }
.chrono__barre--l7  { grid-column-end: span 7; }
.chrono__barre--l8  { grid-column-end: span 8; }
.chrono__barre--l9  { grid-column-end: span 9; }
.chrono__barre--l10 { grid-column-end: span 10; }
.chrono__barre--l11 { grid-column-end: span 11; }
.chrono__barre--l12 { grid-column-end: span 12; }

/* La graduation, sous les pistes : elle porte l'échelle, donc la véracité. */
.chrono__axe {
  margin-top: 2px;
  border-top: 1px solid var(--border);
  padding-top: 3px;
}
/* Chaque graduation occupe QUATRE colonnes : un libellé de zone (« chez le
   client ») a besoin de plus de place qu'une colonne n'en offre. */
.chrono__tic {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--faint);
  line-height: 1.35;
  min-width: 0;
  overflow-wrap: anywhere;
}
.chrono__tic--a1  { grid-column: 2 / span 4; }
.chrono__tic--a5  { grid-column: 6 / span 4; }
.chrono__tic--a9  { grid-column: 10 / span 4; }
/* Le total, aligné à droite comme la règle ambre des démos de boîte. */
/* Le total d'une chronologie se lit en bout de piste. */
.chrono__total { text-align: right; }
.chrono__total--gain { color: var(--green); }

/* --- Arbres, frises, et la glissière qui les annote ---
   POURQUOI PAS DU SVG, ici. Un `<text>` SVG ne se recompose pas : il n'a pas de
   retour à la ligne. Les arbres de `_meta/intake/` s'en sortent parce qu'ils ne
   contiennent que des NOMBRES dans des cercles ; les nôtres portent des
   identifiants de code — `<section id="liste-services">`, vingt-neuf caractères,
   soit 183 des 317 pixels disponibles. En HTML ils se coupent sur deux lignes ;
   en SVG ils sortiraient du cadre. Le texte reste par ailleurs sélectionnable et
   lu comme une STRUCTURE par un lecteur d'écran, là où un SVG sous `role="img"`
   se réduit à une seule phrase.

   LA GLISSIÈRE (`.rail`) est ce qui rend le SVG inutile ici. Le seul avantage
   qu'il aurait — tracer une flèche — ne vaut que pour des points arbitraires.
   Nos deux flèches suivent l'axe vertical : une colonne de flex étirée par
   `align-items: stretch` couvre exactement la hauteur qu'elle annote, sans un
   seul positionnement absolu. Rien à recalculer quand le texte se recompose. */
.rails { display: flex; align-items: stretch; gap: 8px; }
.rails__corps { flex: 1; min-width: 0; }

.rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
  color: var(--cyan);
}
.rail__titre {
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.04em;
}
.rail__trait { flex: 1; border-left: 2px solid currentColor; min-height: 16px; }
/* La pointe est un CARACTÈRE, pas un triangle en bordures : elle s'échelonne
   avec la police au lieu d'être figée en pixels. Le dépôt fait déjà ça pour ses
   marqueurs de `<summary>`. */
.rail::after { content: "▼"; font-size: 10px; line-height: 1; }
/* `column-reverse` suffit à retourner la glissière : la pointe passe en haut,
   l'étiquette en bas. Aucune duplication de règle. */
.rail--remonte { flex-direction: column-reverse; color: var(--amber); }
.rail--remonte::after { content: "▲"; }

/* --- L'arbre --- */
.arbre { font-family: var(--font-mono); font-size: 10.5px; }
/* Une ligne par nœud : le nœud et son annotation restent côte à côte, et
   l'annotation qui se recompose reste dans SA colonne au lieu de repartir à la
   marge gauche. Sans ça il fallait des <div> vides pour couper les lignes. */
/* `flex-wrap` fait passer l'annotation SOUS le nœud quand la place manque —
   aux niveaux profonds, il ne restait que vingt-quatre pixels pour un mot qui
   en réclame trente-cinq. L'alternative aurait été de couper les mots au
   milieu ; se replier d'une ligne est plus lisible et ne coûte rien. */
.arbre__ligne { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; }
.arbre__noeud {
  display: inline-block;
  background: var(--elevated);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 7px;
  margin: 2px 0;
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Le spécimen FAUTIF d'une comparaison, quelle que soit sa famille : le nœud
   qu'on pointe dans un arbre, la ligne qui a hérité d'un état qui n'est pas le
   sien dans une pile. Deux familles, une seule teinte à entretenir — la même
   raison qui a réuni `.demo__regle, .demo__ecart, .chrono__total` plus haut,
   après qu'une duplication eut vécu dix-sept lignes sans que rien la signale. */
.arbre__noeud--cible, .demo__bloc--alerte { border-color: var(--red); color: var(--red); }
/* PRÉSENT ICI, ABSENT DE L'ARBRE QUI COMPTE. Le nœud est dessiné à sa place,
   mais il ne figure pas dans l'arbre dont la figure parle : un élément créé en
   mémoire et pas encore branché dans le DOM, une image décorative élaguée de
   l'arbre d'accessibilité. Le commentaire nommait autrefois le premier cas seul
   — donc l'emploi, pas la classe —, et le lecteur suivant en aurait conclu que
   sa figure n'y entrait pas, puis aurait inventé `--elague`. C'est le scénario
   que le SPEC § 5 bis raconte des douze classes SVG posées d'avance.
   Elle ne se propage PAS vers le bas : contrairement à `--frontiere`, elle n'a
   délibérément aucune variante `.arbre__branche--…`, un nœud absent n'emportant
   pas ses enfants avec lui. */
.arbre__noeud--detache { border-style: dashed; border-color: var(--faint); color: var(--faint); }
/* LE NŒUD OÙ COMMENCE UN AUTRE RÉGIME — à partir d'ici, les règles changent.
   Nommé par niveau et non par domaine : rien dans la classe ne dit « client »,
   parce que ce n'est pas de ça qu'il s'agit sur le dessin. */
.arbre__noeud--frontiere { border-color: var(--violet); color: var(--violet); }
.arbre__noeud--texte { border-style: dashed; color: var(--muted); }
/* L'indentation ET le trait de filiation, dans la même règle : un niveau de
   profondeur est un décalage plus une ligne, pas deux notions distinctes. */
.arbre__branche {
  margin-left: 5px;
  padding-left: 12px;
  border-left: 1px solid var(--border);
}
/* Le SOUS-ARBRE passé dans l'autre régime. C'est la branche qui se colore, pas
   chaque nœud : le régime se propage vers le bas, donc il appartient au
   sous-arbre — le peindre nœud par nœud répéterait N fois ce que la ligne de
   filiation dit une seule, et effacerait la frontière qu'on veut montrer. Ce
   qu'on lit alors, c'est la HAUTEUR du trait, ce qui est exactement le propos. */
.arbre__branche--frontiere { border-left-color: var(--violet); }
.arbre__role {
  font-family: var(--font-body);
  font-size: 10px;
  color: var(--faint);
  /* Une base RÉELLE, pas zéro : avec `flex: 1` (base 0) l'annotation « tient »
     toujours et ne se replie jamais, quelle que soit la place restante. Avec
     110 px, elle passe à la ligne dès qu'on descend dans l'arbre. */
  flex: 1 1 110px;
  min-width: 0;
}

/* --- La frise : une séquence, numérotée par le CSS --- */
.frise { list-style: none; margin: 0; padding: 0; counter-reset: etape; }
/* Une frise qui REPREND la précédente, sans repartir de 1. Née d'une contrainte
   de la glissière : `.rail` s'étire sur toute la hauteur de `.rails__corps`, donc
   sa pointe désigne la PREMIÈRE étape qui s'y trouve. Quand la flèche de retour
   ne vise pas le début de la séquence, il faut sortir les étapes d'avant du
   `.rails` — et la suite doit alors continuer le compte. */
.frise--suite { counter-reset: etape 1; }
.frise__etape {
  counter-increment: etape;
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 8px;
  padding: 6px 0;
  align-items: baseline;
}
.frise__etape + .frise__etape { border-top: 1px solid var(--border-soft); }
/* Le numéro vient d'un compteur : réordonner les étapes ne demande pas de
   renuméroter le HTML — la même raison qui interdit les titres numérotés. */
.frise__etape::before {
  content: counter(etape);
  grid-column: 1;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--primary);
  border: 1px solid var(--primary);
  border-radius: 50%;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
}
.frise__corps { grid-column: 2; min-width: 0; }
.frise__nom { font-weight: 650; font-size: 13px; color: var(--text); }
.frise__quoi { display: block; font-weight: 400; font-size: 12px; color: var(--muted); margin-top: 1px; }
/* UNE PASTILLE : deux ou trois mots en monospace, dans un coin d'étape. Les
   deux qui suivent ne diffèrent que par ce qu'elles annoncent — une gravité
   (teinte, sans contour) ou un lieu (contour neutre, poussé à droite). */
.frise__marque, .frise__zone {
  font-family: var(--font-mono);
  font-size: 9.5px;
  border-radius: 4px;
  padding: 1px 5px;
}
.frise__marque {
  display: inline-block;
  margin-top: 3px;
}
/* Nommées par NIVEAU, pas par domaine. La première version s'appelait
   `--reflow` / `--repaint` : des noms qui n'avaient de sens que dans la figure
   du pipeline, donc inutilisables ailleurs alors que la teinte, elle, resservait
   telle quelle. Un composant générique ne porte pas le vocabulaire d'une figure. */
.frise__marque--alerte    { background: var(--red-soft);   color: var(--red); }
.frise__marque--attention { background: var(--amber-soft); color: var(--amber); }
.frise__marque--ok        { background: var(--green-soft); color: var(--green); }
/* Où l'étape se déroule — la question que la figure CORS doit trancher. */
.frise__zone {
  float: right;
  color: var(--faint);
  border: 1px solid var(--border);
  margin-left: 6px;
}

/* --- La notation UML : les pièces communes, la boîte de classe, le lien ---
   Dans les guides de territoire, une figure ILLUSTRE le propos. Dans un cours
   de modélisation, le diagramme EST le propos — c'est l'équivalent d'un bloc de
   code, et on apprend à le lire en le lisant. Mais ça ne change pas la règle du
   § 5 bis : une figure se mérite quand elle montre plusieurs états de la MÊME
   chose. C'est mesuré sur ce dépôt — les onze figures des deux modules les plus
   visuels sont toutes des comparaisons (« le même badge, trois valeurs de
   display »), et le ratio n'y dépasse jamais 0,87 figure pour 1 000 mots.

   LE TRACÉ DE LA COPIE, PAS UNE TRANSCRIPTION. Jusqu'au 2026-09-23, ces figures
   écrivaient chaque diagramme en PILE, une ligne par message ou par
   association, au motif qu'une grille de boîtes reliées donnerait des boîtes de
   ~60 px sur un écran de 375. La mesure visait le mauvais objet : la copie
   exige des lignes de vie, des ovales dans un rectangle, des boîtes reliées, et
   une pile n'apprenait à tracer rien de tout ça. Six lignes de vie tiennent
   pourtant à 320 px (famille `.seq`, plus bas). Chaque sorte de diagramme a donc
   sa famille, et celle-ci ne garde que ce qu'elles partagent — l'acteur,
   l'ovale, le paquetage, le numéro, le volet, la lecture — plus la boîte de
   classe.

   TOUJOURS PAS DE SVG : un nœud y tient 8 à 9 caractères dans un `viewBox` de
   320, quand `LigneCommande` en fait 13 et `livre = trouver(codeBarre)` 26. */

/* La boîte de classe : trois compartiments, et c'est la superposition qui EST
   la notation. En modèle du domaine le troisième est absent — on ne l'écrit
   simplement pas, plutôt que de le masquer : la figure doit montrer ce qu'elle
   affirme. */
.uml {
  border: 1px solid var(--primary);
  border-radius: 6px;
  overflow: hidden;
  background: var(--elevated);
  font-family: var(--font-body);
  font-size: 11px;
  max-width: 260px;
}
.uml__nom {
  background: var(--primary-soft);
  color: var(--primary);
  font-weight: 700;
  text-align: center;
  padding: 6px 8px;
  font-size: 11.5px;
}
.uml__compart {
  border-top: 1px solid var(--border);
  padding: 6px 8px;
  color: var(--text);
  line-height: 1.7;
  overflow-wrap: anywhere;
}
/* Un compartiment VIDE se voit — c'est ainsi qu'on montre qu'un modèle du
   domaine n'a pas d'opérations sans avoir à l'écrire en toutes lettres. */
.uml__compart--vide {
  color: var(--faint);
  font-style: italic;
  font-family: var(--font-body);
  font-size: 10.5px;
}

/* L'ovale du cas d'utilisation, celui de la copie : une ellipse qui épouse son
   texte (`border-radius: 50%`), le padding latéral gardant les lettres loin des
   bords, là où elle se rétrécit. Commun au diagramme `.cas` et aux liens
   `.lien`, qui le posent chacun à leur façon. */
.uml__cu {
  z-index: 2;
  padding: 7px 12px;
  border: 1px solid var(--violet);
  border-radius: 50%;
  background: var(--elevated);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.3;
  text-align: center;
}

/* La lecture en toutes lettres, sous le tracé qu'elle commente : c'est elle
   qui porte l'information quand le visuel ne s'affiche pas (§ 5 bis, règle 1).
   Elle appartient au tracé du DESSUS — d'où les écarts `.uml__lecture + …` de
   chaque famille : sans eux, chaque phrase paraissait annoncer le tracé
   suivant au lieu de commenter le précédent. Vu sur une capture, pas en
   relisant le CSS. */
.uml__lecture {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--faint);
  text-align: center;
  margin-top: 4px;
  line-height: 1.5;
}

/* --- Ce que les diagrammes d'INTERACTION partagent ---------------------------
   Deux pièces, communes à la séquence, à la communication et aux figures qui
   comparent deux notations : elles vivent ici plutôt que dans l'une des deux
   familles, qui les emploient toutes deux. */

/* Le NUMÉRO d'un message. En communication, sans axe du temps, il porte seul
   l'ordre ; en séquence, il le double. Il doit donc se repérer avant le reste,
   exactement comme la multiplicité dans un lien d'association. */
.uml__num {
  color: var(--violet);
  font-weight: 700;
  font-size: 11.5px;
}

/* L'intertitre d'un VOLET, quand une figure montre la même chose sous deux
   notations. Il nomme la moitié qu'on regarde ; il ne porte pas l'information,
   c'est le travail de la légende (§ 5 bis, règle 1). */
.uml__volet {
  font-family: var(--font-body);
  font-size: 11.5px;
  font-weight: 700;
  color: var(--muted);
  text-align: center;
  margin-bottom: 10px;
}
.uml__lecture + .uml__volet { margin-top: 20px; }

/* L'ACTEUR, le bonhomme de la copie — en séquence, en communication, en cas
   d'utilisation. Sans image ni SVG : une tête (::before), puis un corps en
   quatre dégradés (::after) — le tronc, les bras, et deux jambes tracées en
   diagonale de coin à coin de leur boîte. Son nom s'écrit dessous, dans le
   texte de l'élément qui le porte. */
.uml__acteur {
  display: block;
  position: relative;
  width: 14px;
  height: 26px;
  margin: 0 auto 2px;
}
.uml__acteur::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 0;
  width: 7px;
  height: 7px;
  border: 1px solid var(--muted);
  border-radius: 50%;
}
.uml__acteur::after {
  content: "";
  position: absolute;
  inset: 9px 0 0 0;
  background:
    linear-gradient(var(--muted), var(--muted)) 50% 0 / 1px 9px no-repeat,
    linear-gradient(var(--muted), var(--muted)) 50% 3px / 14px 1px no-repeat,
    linear-gradient(to top left, transparent calc(50% - 0.6px), var(--muted) 50%, transparent calc(50% + 0.6px)) 0 100% / 7px 8px no-repeat,
    linear-gradient(to top right, transparent calc(50% - 0.6px), var(--muted) 50%, transparent calc(50% + 0.6px)) 100% 100% / 7px 8px no-repeat;
}

/* --- Le diagramme d'ÉTATS : des états arrondis, des transitions étiquetées ---
   PAS DE SVG, alors que le SPEC § 5 bis réservait NOMMÉMENT ce cas au schéma —
   « une machine à états dont les transitions bouclent et se croisent ». La
   mesure l'a refusé, et ce n'est pas celle qu'on attendait : un nœud SVG tient
   huit à neuf caractères, ce qui laisserait passer `Payée` et `Perdu`, mais
   l'ARC porte l'étiquette `événement [garde] / action` — `payer(montant)
   [solde >= montant] / debiter()` en fait quarante-trois, et un `<text>` SVG ne
   se coupe pas. Le graphe de `cours/a26/ift-2008` a mérité le SVG parce que ses
   arcs ne portaient RIEN ; ici l'arc est le propos.

   EN HAUTEUR QUAND IL LE FAUT. À 320 px, `at(dateRetourPrevue)` interdit de
   poser une chaîne d'états à l'horizontale : une chaîne se lit donc de haut en
   bas, l'étiquette à droite de sa flèche (`--colonne`). Plusieurs états qui
   mènent au MÊME état tiennent, eux, à l'horizontale (`--vers`) : une colonne
   d'états, une flèche par rangée, et l'état visé étiré sur toute la hauteur,
   pour que chaque flèche le touche.

   Les pseudo-états initial et final restent des CARACTÈRES — ● et ◉ — dans les
   `<pre>` de la prose, où rien ne leur manque : aucune figure ne les dessine
   encore, et la classe qui les habillerait s'écrira avec elle. */
.etats {
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--text);
}
.etats__etat {
  width: max-content;
  padding: 4px 9px;
  border: 1px solid var(--primary);
  border-radius: 9px;
  background: var(--elevated);
  text-align: center;
  font-weight: 600;
}
.etats__evt { padding: 0 4px 1px; white-space: nowrap; }

.etats--vers {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  align-items: center;
  max-width: 260px;
  margin: 0 auto;
  padding-top: 3px;
}
.etats--vers > .etats__etat { grid-column: 1; justify-self: stretch; width: auto; margin: 11px 0; }
/* L'état visé : étiré sur toutes les rangées — les rangées en trop n'existent
   pas et mesurent zéro —, ses flèches le touchent toutes. */
.etats--vers > .etats__cible {
  grid-column: 3;
  grid-row: 1 / span 20;
  align-self: stretch;
  display: flex;
  align-items: center;
  margin: 11px 0;
}
/* La flèche d'une rangée : le trait au milieu de la rangée, l'étiquette posée
   dessus. */
.etats__fleche { grid-column: 2; position: relative; min-width: 0; }
.etats__fleche > .etats__evt { position: absolute; left: 0; right: 0; bottom: 2px; text-align: center; }
.etats__trait { display: block; position: relative; border-top: 1px solid var(--muted); }
.etats__trait::after {
  content: "";
  position: absolute;
  right: 0;
  top: -4px;
  width: 7px;
  height: 7px;
  border-top: 1px solid var(--muted);
  border-right: 1px solid var(--muted);
  transform: rotate(45deg);
}

.etats--colonne { display: flex; flex-direction: column; align-items: center; }
/* L'état COMPOSITE. Ce qu'il doit rendre littéral, c'est qu'une transition
   partant de LUI vaut pour tout ce qu'il contient : une vraie bordure autour
   de vrais sous-états, et la transition de groupe qui part de son BORD. La
   figure se lit alors comme la règle s'énonce. Son nom appartient au cadre,
   en haut à gauche, et non aux états qu'il contient. */
.etats__cadre {
  width: max-content;
  padding: 6px 12px 12px;
  border: 1px solid var(--primary);
  border-radius: 12px;
}
.etats__nom { display: block; margin-bottom: 6px; color: var(--primary); font-weight: 700; }
/* La transition qui DESCEND d'un état au suivant : le trait au centre, sa
   pointe en bas, l'étiquette à droite. */
.etats__descend {
  display: grid;
  grid-template-columns: 1fr max-content 1fr;
  align-items: center;
  width: 100%;
  min-height: 34px;
}
.etats__descend::before {
  content: "";
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  align-self: stretch;
  border-left: 1px solid var(--muted);
}
.etats__descend::after {
  content: "";
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  align-self: end;
  width: 7px;
  height: 7px;
  margin-bottom: 1px;
  border-right: 1px solid var(--muted);
  border-bottom: 1px solid var(--muted);
  transform: rotate(45deg);
}
.etats__descend > .etats__evt { grid-column: 3; grid-row: 1; justify-self: start; padding-left: 6px; }

/* --- Le diagramme de SÉQUENCE, DSS compris : des lignes de vie en colonnes ---
   UNE GRILLE, ET LES FLÈCHES S'Y ACCROCHENT. Le participant k occupe deux
   demi-colonnes : sa ligne de vie est donc une LIGNE DE GRILLE, la 2k. Un
   message de i vers j s'étend de la ligne 2i à la ligne 2j — ses deux bouts
   tombent sur les lignes de vie par construction, sans une coordonnée écrite.
   La grille inverse d'elle-même deux lignes données à l'envers : la même paire
   de classes (`--de3 --a1`) sert dans les deux sens, et le sens vers la gauche
   se DÉDUIT de la paire au lieu de se déclarer à côté.
   Tout se place par des SÉRIES de classes — nombre de participants,
   participant, départ, arrivée, rangée, fin de rangée —, jamais par `style=` :
   ce sont des échelles finies, que `visuels.js` compte comme telles.

   CE QUI TIENT À 320 PX, mesuré le 2026-09-23 sur la figure à six objets de
   « Les diagrammes d'interaction », en police large pour simuler un téléphone :
   six participants, à trois conditions. Les têtes ALTERNENT sur deux étages —
   une tête peut alors déborder de ses deux demi-colonnes sans toucher la ligne
   de vie voisine, qui ne commence qu'en dessous. Une tête trop longue se coupe
   par un <wbr> à la jointure de ses mots. Et le texte prend la police du corps :
   la mono coûtait un quart de largeur de plus, et les solutionnaires n'en
   emploient pas.

   UN LIBELLÉ VERS LA DROITE part de son émetteur et peut déborder au-delà de la
   flèche ; son fond masque les lignes de vie qu'il croise. VERS LA GAUCHE, il se
   replie entre les deux lignes de vie : débordant, il passerait sur la barre de
   l'émetteur — et un débordement à gauche ne fait défiler aucun cadre, donc
   aucun contrôle ne le verrait. */
.seq {
  display: grid;
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--text);
  padding: 0 8px 14px;
}
.uml__lecture + .seq { margin-top: 18px; }
.seq--n2 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.seq--n3 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.seq--n4 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.seq--n5 { grid-template-columns: repeat(10, minmax(0, 1fr)); }
.seq--n6 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
.seq--p1 { grid-column: 1 / 3; }
.seq--p2 { grid-column: 3 / 5; }
.seq--p3 { grid-column: 5 / 7; }
.seq--p4 { grid-column: 7 / 9; }
.seq--p5 { grid-column: 9 / 11; }
.seq--p6 { grid-column: 11 / 13; }
.seq--de1 { grid-column-start: 2; }
.seq--de2 { grid-column-start: 4; }
.seq--de3 { grid-column-start: 6; }
.seq--de4 { grid-column-start: 8; }
.seq--de5 { grid-column-start: 10; }
.seq--de6 { grid-column-start: 12; }
.seq--a1 { grid-column-end: 2; }
.seq--a2 { grid-column-end: 4; }
.seq--a3 { grid-column-end: 6; }
.seq--a4 { grid-column-end: 8; }
.seq--a5 { grid-column-end: 10; }
.seq--a6 { grid-column-end: 12; }
.seq--r1 { grid-row-start: 1; }
.seq--r2 { grid-row-start: 2; }
.seq--r3 { grid-row-start: 3; }
.seq--r4 { grid-row-start: 4; }
.seq--r5 { grid-row-start: 5; }
.seq--r6 { grid-row-start: 6; }
.seq--r7 { grid-row-start: 7; }
.seq--r8 { grid-row-start: 8; }
.seq--r9 { grid-row-start: 9; }
.seq--r10 { grid-row-start: 10; }
.seq--r11 { grid-row-start: 11; }
.seq--r12 { grid-row-start: 12; }
/* La fin d'une barre ou d'un cadre : la première rangée qu'il ne couvre plus. */
.seq--f3 { grid-row-end: 3; }
.seq--f4 { grid-row-end: 4; }
.seq--f5 { grid-row-end: 5; }
.seq--f6 { grid-row-end: 6; }
.seq--f7 { grid-row-end: 7; }
.seq--f8 { grid-row-end: 8; }
.seq--f9 { grid-row-end: 9; }
.seq--f10 { grid-row-end: 10; }
.seq--f11 { grid-row-end: 11; }
.seq--f12 { grid-row-end: 12; }
.seq--f13 { grid-row-end: 13; }

/* La tête : centrée sur sa ligne de vie, aussi large que son plus long
   morceau (`min-content`), si bien qu'un <wbr> la coupe sans l'élargir. */
.seq__tete {
  position: relative;
  z-index: 2;
  justify-self: center;
  align-self: end;
  width: min-content;
  margin-top: 4px;
  padding: 2px 5px;
  border: 1px solid var(--primary);
  border-radius: 3px;
  background: var(--elevated);
  text-align: center;
  font-weight: 600;
}
.seq__tete--acteur { border: none; background: var(--surface); padding: 0; width: max-content; }
/* L'objet CRÉÉ : sa tête descend à la rangée de son `create`, et c'est elle qui
   porte la pointe du message — le trait file dessous jusqu'à la ligne de vie,
   si bien qu'il s'arrête à son bord sans qu'on en connaisse la largeur. */
.seq__tete--cree::before {
  content: "";
  position: absolute;
  left: -9px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -4px;
  border-top: 1px solid var(--muted);
  border-right: 1px solid var(--muted);
  transform: rotate(45deg);
}

/* La ligne de vie : un trait pointillé centré sur ses deux demi-colonnes. Elle
   s'étend sur trente rangées ; celles qui n'existent pas mesurent zéro, et la
   marge négative la prolonge dans le pied de la figure. */
.seq__vie {
  justify-self: center;
  width: 0;
  grid-row-end: span 30;
  margin-bottom: -14px;
  border-left: 1px dashed var(--faint);
}
/* La barre d'activation longue, celle d'un objet qui travaille sur plusieurs
   messages : de la rangée qui SUIT le message qui l'ouvre (la marge négative la
   remonte jusqu'à sa flèche) à la flèche qui la ferme. Posée au-dessus des
   messages, elle cache la pointe qui l'atteint : la flèche s'arrête à son bord,
   comme sur la copie. */
.seq__barre {
  justify-self: center;
  z-index: 4;
  width: 7px;
  margin: -5px 0 4px;
  border: 1px solid var(--primary);
  background: var(--primary-plein);
}

/* Le message : son libellé, puis le trait au bas de sa rangée. */
.seq__msg {
  z-index: 3;
  align-self: end;
  display: flex;
  flex-direction: column;
  padding-top: 10px;
}
.seq__lib {
  align-self: flex-start;
  margin-left: 4px;
  padding: 0 3px 1px;
  white-space: nowrap;
  background: var(--surface);
}
.seq__trait {
  position: relative;
  height: 0;
  margin-bottom: 4px;
  border-top: 1px solid var(--muted);
}
/* Pointe pleine : le message SYNCHRONE, celui qu'on trace presque toujours. */
.seq__trait::after {
  content: "";
  position: absolute;
  right: -1px;
  top: -4px;
  border: 4px solid transparent;
  border-left: 7px solid var(--muted);
  border-right: 0;
}
/* Tout appel active son destinataire : une amorce de barre sous la pointe, posée
   seule. Une barre longue qui commence là la recouvre. */
.seq__msg:not(.seq__msg--retour):not(.seq__msg--create):not(.seq__msg--soi) .seq__trait::before {
  content: "";
  position: absolute;
  right: -4px;
  top: 0;
  width: 7px;
  height: 12px;
  box-sizing: border-box;
  border: 1px solid var(--primary);
  background: var(--primary-plein);
}
/* Pointillé pour le retour et la création ; pointe fine pour eux et pour
   l'asynchrone. C'est tout ce qui distingue les quatre, et ça se voit. */
.seq__msg--retour .seq__trait,
.seq__msg--create .seq__trait { border-top-style: dashed; }
.seq__msg--retour .seq__trait::after,
.seq__msg--create .seq__trait::after,
.seq__msg--async .seq__trait::after {
  right: 0;
  top: -4px;
  width: 7px;
  height: 7px;
  border: none;
  border-top: 1px solid var(--muted);
  border-right: 1px solid var(--muted);
  transform: rotate(45deg);
}
.seq__msg--create { z-index: 1; margin-bottom: 9px; }
.seq__msg--create .seq__lib { margin-bottom: 12px; }
/* VERS LA GAUCHE — la paire départ-arrivée le dit, aucune classe de sens. */
:is(.seq--de2.seq--a1, .seq--de3.seq--a1, .seq--de3.seq--a2, .seq--de4.seq--a1, .seq--de4.seq--a2,
    .seq--de4.seq--a3, .seq--de5.seq--a1, .seq--de5.seq--a2, .seq--de5.seq--a3, .seq--de5.seq--a4,
    .seq--de6.seq--a1, .seq--de6.seq--a2, .seq--de6.seq--a3, .seq--de6.seq--a4, .seq--de6.seq--a5)
  .seq__lib { align-self: stretch; margin: 0 6px; white-space: normal; text-align: center; }
:is(.seq--de2.seq--a1, .seq--de3.seq--a1, .seq--de3.seq--a2, .seq--de4.seq--a1, .seq--de4.seq--a2,
    .seq--de4.seq--a3, .seq--de5.seq--a1, .seq--de5.seq--a2, .seq--de5.seq--a3, .seq--de5.seq--a4,
    .seq--de6.seq--a1, .seq--de6.seq--a2, .seq--de6.seq--a3, .seq--de6.seq--a4, .seq--de6.seq--a5)
  .seq__trait::after { right: auto; left: -1px; transform: scaleX(-1); }
:is(.seq--de2.seq--a1, .seq--de3.seq--a1, .seq--de3.seq--a2, .seq--de4.seq--a1, .seq--de4.seq--a2,
    .seq--de4.seq--a3, .seq--de5.seq--a1, .seq--de5.seq--a2, .seq--de5.seq--a3, .seq--de5.seq--a4,
    .seq--de6.seq--a1, .seq--de6.seq--a2, .seq--de6.seq--a3, .seq--de6.seq--a4, .seq--de6.seq--a5):is(.seq__msg--retour, .seq__msg--async)
  .seq__trait::after { left: 0; transform: rotate(-135deg); }
:is(.seq--de2.seq--a1, .seq--de3.seq--a1, .seq--de3.seq--a2, .seq--de4.seq--a1, .seq--de4.seq--a2,
    .seq--de4.seq--a3, .seq--de5.seq--a1, .seq--de5.seq--a2, .seq--de5.seq--a3, .seq--de5.seq--a4,
    .seq--de6.seq--a1, .seq--de6.seq--a2, .seq--de6.seq--a3, .seq--de6.seq--a4, .seq--de6.seq--a5)
  .seq__trait::before { right: auto; left: -4px; }

/* Le message À SOI-MÊME : une boucle d'une demi-colonne à droite de la ligne de
   vie, qui revient sur elle un peu plus bas ; le libellé suit la boucle. */
.seq__msg--soi { grid-column-end: span 1; flex-direction: row; align-items: flex-start; padding-top: 8px; }
.seq__msg--soi .seq__trait {
  order: 1;
  flex: none;
  width: 16px;
  height: 12px;
  margin: 0;
  border: 1px solid var(--muted);
  border-left: none;
}
.seq__msg--soi .seq__trait::after {
  top: auto;
  bottom: -4.5px;
  right: auto;
  left: 0;
  border: 4px solid transparent;
  border-right: 7px solid var(--muted);
  border-left: 0;
}
.seq__msg--soi .seq__lib {
  order: 2;
  flex: none;
  width: max-content;
  max-width: 104px;
  margin: 0;
  padding: 0 0 0 4px;
  white-space: normal;
  background: none;
}

/* Le FRAGMENT combiné : un cadre posé sur les rangées qu'il couvre, les
   messages inchangés dedans. Ce qui fait la notation est l'opérateur ACCROCHÉ
   au coin, pas sa forme de pentagone. */
.seq__cadre {
  grid-column: 1 / -1;
  margin: 4px -4px -2px;
  border: 1px solid var(--muted);
  border-radius: 2px;
}
/* La rangée de garde : l'opérateur à la première, la condition entre crochets.
   La garde est au fragment ce que la multiplicité est à l'association — la
   chose qu'on oublie, et celle qui change tout le sens : même couleur. */
.seq__garde {
  grid-column: 1 / -1;
  z-index: 3;
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 4px -4px 0;
  padding-left: 1px;
  color: var(--amber);
}
.seq__garde > span { padding: 1px 3px; background: var(--surface); }
/* L'onglet se pose SUR le coin du cadre : sans fond, pour que les deux bords du
   cadre restent son haut et sa gauche. */
.seq__garde > .seq__op {
  margin-left: -1px;
  background: none;
  padding: 1px 9px 1px 6px;
  border-right: 1px solid var(--muted);
  border-bottom: 1px solid var(--muted);
  color: var(--violet);
  font-weight: 700;
  clip-path: polygon(0 0, 100% 0, 100% 60%, calc(100% - 5px) 100%, 0 100%);
}
/* Le séparateur d'opérandes d'un `alt` : un trait POINTILLÉ, qui dit « ou
   bien ». Un `opt` n'en a jamais — toute la différence entre les deux. */
.seq__garde--sep { margin-top: 8px; border-top: 1px dashed var(--muted); padding-top: 3px; }

/* --- Le diagramme de COMMUNICATION : les mêmes objets, reliés en arbre ---
   À l'examen, les boîtes se posent où l'on veut et les liens se tirent entre
   elles ; sur un téléphone, « où l'on veut » veut dire EN HAUTEUR. Un lien part
   de l'objet qui envoie et descend vers celui qui reçoit, et ses messages
   s'écrivent le long du trait, chacun avec son numéro : sans axe du temps,
   c'est lui seul qui dit l'ordre. Un tronc sert plusieurs liens quand un objet
   en envoie plusieurs ; chacun garde sa dent jusqu'à sa boîte, et le dernier
   arrête le tronc. */
.com {
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.35;
  color: var(--text);
}
.uml__volet + .com { margin-top: 4px; }
.com__objet {
  display: inline-block;
  position: relative;
  padding: 3px 6px;
  border: 1px solid var(--primary);
  border-radius: 3px;
  background: var(--elevated);
  font-weight: 600;
}
.com__objet--acteur { padding: 0; border: none; background: none; text-align: center; }
.com__lien { position: relative; margin-left: 14px; padding: 6px 0 2px 18px; }
.com__lien:not(:last-child) { padding-left: 17px; border-left: 1px solid var(--muted); }
.com__lien > .com__objet::before {
  content: "";
  position: absolute;
  left: -19px;
  top: 50%;
  width: 18px;
  border-top: 1px solid var(--muted);
}
.com__lien:not(:last-child) > .com__objet::before { left: -18px; width: 17px; }
.com__msg { position: relative; margin-bottom: 3px; }
.com__lien:last-child > .com__msg::before {
  content: "";
  position: absolute;
  left: -19px;
  top: -6px;
  bottom: -14px;
  border-left: 1px solid var(--muted);
}
/* Un lien sans message : son tronc s'arrête à la dent de sa boîte. */
.com__msg:empty { margin-bottom: 0; }
.com__lien:last-child > .com__msg:empty::before { bottom: -11px; }
/* Des liens ORIENTÉS — qui connaît qui — quand la figure compare des
   dépendances plutôt que des messages : une pointe ouverte à l'arrivée de
   chaque dent, contre la boîte. */
.com--dirige .com__lien > .com__objet::after {
  content: "";
  position: absolute;
  left: -8px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -4px;
  border-top: 1px solid var(--muted);
  border-right: 1px solid var(--muted);
  transform: rotate(45deg);
}

/* --- Le diagramme de CAS D'UTILISATION : l'acteur, le rectangle nommé ---
   Le tracé des solutionnaires : l'acteur à gauche, hors du rectangle ; le
   rectangle, qui porte le nom du système ; les ovales dedans ; un trait par cas
   que l'acteur déclenche lui-même, tiré en OBLIQUE jusqu'à lui.
   L'OBLIQUE SANS SVG. Un trait est un élément de grille tendu du centre de
   l'acteur au centre de l'ovale, et sa diagonale est un dégradé tracé de coin à
   coin. L'ovale, posé dessus, en masque la fin : le trait s'arrête à son bord
   sans qu'on en connaisse la largeur. Les deux centres, eux, sont des MARGES —
   la demi-hauteur d'un ovale d'une ligne, celle de l'acteur —, justes tant
   qu'un ovale tient sur une ligne : c'est le seul point du tracé qui ne soit pas
   exact par construction, et `rendu.js` le montre s'il casse. */
.cas {
  display: grid;
  grid-template-columns: 58px 16px 12px minmax(0, 1fr) minmax(0, 1fr) 12px;
  max-width: 290px;
  margin: 0 auto;
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--text);
  padding-bottom: 8px;
}
.cas--r2 { grid-row-start: 2; }
.cas--r3 { grid-row-start: 3; }
.cas--r4 { grid-row-start: 4; }
.cas--r5 { grid-row-start: 5; }
.cas--r6 { grid-row-start: 6; }
.cas--f3 { grid-row-end: 3; }
.cas--f4 { grid-row-end: 4; }
.cas--f5 { grid-row-end: 5; }
.cas--f6 { grid-row-end: 6; }
.cas--f7 { grid-row-end: 7; }
.cas__systeme {
  grid-column: 3 / 7;
  grid-row: 1 / span 20;
  margin-bottom: -8px;
  border: 1px solid var(--primary);
  border-radius: 3px;
}
.cas__nom { grid-column: 3 / 7; grid-row: 1; padding: 5px 0 6px; color: var(--primary); text-align: center; font-weight: 700; }
.cas__acteur { grid-column: 1; align-self: center; text-align: center; font-weight: 600; }
.cas > .uml__cu { grid-column: 4 / 6; justify-self: center; align-self: center; margin: 4px 0; }
.cas__trait { grid-column: 1 / 5; z-index: 1; margin-left: 38px; }
.cas__trait--monte {
  margin-top: 18px;
  margin-bottom: 22px;
  background: linear-gradient(to top left, transparent calc(50% - 0.6px), var(--muted) 50%, transparent calc(50% + 0.6px));
}
.cas__trait--descend {
  margin-top: 22px;
  margin-bottom: 18px;
  background: linear-gradient(to top right, transparent calc(50% - 0.6px), var(--muted) 50%, transparent calc(50% + 0.6px));
}
/* La relation entre deux cas empilés : un trait pointillé au centre, la pointe
   vers le cas du dessus, le stéréotype à droite. */
.cas__rel {
  grid-column: 4 / 6;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  min-height: 30px;
}
.cas__rel::before { content: ""; grid-column: 1; grid-row: 1; justify-self: end; align-self: stretch; border-right: 1px dashed var(--muted); }
.cas__rel::after {
  content: "";
  grid-column: 1;
  grid-row: 1;
  justify-self: end;
  align-self: start;
  width: 7px;
  height: 7px;
  margin: 0 -4px 0 0;
  border-top: 1px solid var(--muted);
  border-left: 1px solid var(--muted);
  transform: rotate(45deg);
}
.cas__rel > span { grid-column: 2; grid-row: 1; padding-left: 7px; color: var(--muted); }

/* --- Le LIEN à vrai trait : deux bouts, et ce qui passe entre eux ---
   La paire que la copie dessine le plus souvent : deux cas reliés par un
   «include», deux classes par une association. Un trait réel, ses terminaisons
   (pointe, triangle) dessinées à son bout, son nom au-dessus. Le trait occupe la
   rangée du milieu d'une grille dont les deux autres rangées sont des `1fr` :
   elles prennent la même hauteur, si bien que le trait tombe au milieu des
   bouts, qu'il y ait un nom au-dessus ou non. */
.lien {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--text);
}
.lien + .lien, .uml__lecture + .lien { margin-top: 16px; }
.lien > .uml__cu { flex: none; max-width: 100px; }
/* Un ovale seul n'a pas de trait à laisser passer. */
.lien > .uml__cu:only-child { max-width: 200px; }
.lien__trait {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  grid-template-rows: 1fr auto 1fr;
  flex: 1 1 auto;
  min-width: 64px;
  max-width: 150px;
}
.lien__nom { grid-column: 1 / -1; grid-row: 1; padding: 0 8px 2px; color: var(--muted); text-align: center; white-space: nowrap; }
.lien__ligne { grid-column: 1 / -1; grid-row: 2; position: relative; height: 0; border-top: 1px solid var(--muted); }
.lien--pointille .lien__ligne { border-top-style: dashed; }
.lien--fleche-d .lien__ligne::after {
  content: "";
  position: absolute;
  right: 0;
  top: -4px;
  width: 7px;
  height: 7px;
  border-top: 1px solid var(--muted);
  border-right: 1px solid var(--muted);
  transform: rotate(45deg);
}
/* Le triangle VIDE de la généralisation : un triangle plein, et un plus petit
   à la couleur du fond posé dedans. */
.lien--triangle-d .lien__ligne::after {
  content: "";
  position: absolute;
  right: 0;
  top: -6px;
  border: 6px solid transparent;
  border-left: 10px solid var(--muted);
  border-right: 0;
}
.lien--triangle-d .lien__ligne::before {
  content: "";
  position: absolute;
  right: 1.5px;
  top: -4px;
  z-index: 1;
  border: 4px solid transparent;
  border-left: 6.5px solid var(--surface);
  border-right: 0;
}
/* La boîte de classe comme BOUT : compacte, son nom et un compartiment vide,
   comme sur la copie. */
.lien > .uml { flex: none; }
.lien > .uml > .uml__nom { padding: 3px 8px; }
.uml__compart:empty { height: 9px; padding: 0; }
/* La multiplicité, au bout du trait, du côté de la classe qu'elle compte —
   l'erreur n° 1 à l'examen, d'où la seule couleur du lien : elle se repère
   avant le reste. Écartée du bout, qu'elle partage souvent avec une pointe. */
.lien__mult { grid-row: 3; padding: 4px 9px 0; color: var(--amber); font-weight: 700; }
.lien__mult--g { grid-column: 1; }
.lien__mult--d { grid-column: 3; }
/* Le losange, contre le TOUT : vide pour l'agrégation, plein pour la
   composition. */
.lien--losange-g .lien__ligne::before,
.lien--losange-plein-g .lien__ligne::before {
  content: "";
  position: absolute;
  left: 2px;
  top: -5px;
  width: 8px;
  height: 8px;
  border: 1px solid var(--muted);
  background: var(--surface);
  transform: rotate(45deg);
}
.lien--losange-plein-g .lien__ligne::before { background: var(--muted); }
/* La navigabilité vers la GAUCHE : la pointe ouverte à l'autre bout. */
.lien--fleche-g .lien__ligne::before {
  content: "";
  position: absolute;
  left: 0;
  top: -4px;
  width: 7px;
  height: 7px;
  border-top: 1px solid var(--muted);
  border-right: 1px solid var(--muted);
  transform: rotate(-135deg);
}
/* Le VERDICT d'un lien — permis, interdit —, sous le trait, là où une
   association porterait ses multiplicités. Un verdict fautif prend la marque
   `--cible` de la grammaire des couleurs (SPEC § 5 bis). */
.lien__verdict { grid-column: 1 / -1; grid-row: 3; padding-top: 2px; color: var(--muted); text-align: center; font-weight: 700; letter-spacing: 0.04em; }
.lien__verdict--cible { color: var(--red); }
/* Le PAQUETAGE : un rectangle et son onglet en haut à gauche, comme une
   chemise cartonnée — c'est ainsi que le module le décrit, et que les
   solutionnaires dessinent une couche. */
.uml__paquet {
  flex: none;
  position: relative;
  margin-top: 8px;
  padding: 6px 10px;
  border: 1px solid var(--primary);
  border-radius: 0 3px 3px 3px;
  background: var(--elevated);
  font-weight: 650;
}
.uml__paquet::before {
  content: "";
  position: absolute;
  left: -1px;
  top: -8px;
  width: 22px;
  height: 7px;
  border: 1px solid var(--primary);
  border-bottom: none;
  border-radius: 2px 2px 0 0;
  background: var(--elevated);
}

/* --- Le SCHÉMA SVG : une topologie que rien d'autre ne sait rendre ---
   LE COMPTEUR EST PARTI DE ZÉRO ICI, et il a fallu un graphe pour ça. Douze
   classes `.fig__*` avaient été posées d'avance, en prévision du premier
   schéma ; elles ont traversé quatre lots de figures sans servir et ont été
   retirées. Tout s'était fait en démos vives, en chronologies, en arbres et en
   frises — y compris le candidat qu'on croyait décisif, la machine à états de
   « Formulaires 2 », faite en frise plus glissière.

   CE QUI A CÉDÉ. Un graphe est une topologie ARBITRAIRE : ses arcs sautent des
   niveaux, reviennent en arrière, se croisent. Aucune des familles HTML ne sait
   rendre ça — un `.arbre` suppose une filiation, une `.frise` suppose une
   séquence. Et la contrainte qui avait tué tous les candidats précédents tombe
   ici : un nœud SVG tient huit à neuf caractères, et un sommet de graphe
   s'appelle « A ». C'est le seul cas que le SPEC § 5 bis réservait au SVG, et
   c'est celui-là qui s'est présenté.

   ÉCRITES POUR UNE FIGURE, PAS D'AVANCE. Six classes, exactement ce que la
   figure des deux ordres de visite emploie. C'est la leçon des douze : le jour
   où un second schéma arrive, ses classes s'écriront pour lui.

   AUCUNE COULEUR NE PEUT VIVRE DANS LE DESSIN. Les attributs de présentation
   SVG n'honorent pas `var()` de façon fiable sur WebKit : un `fill` en dur
   rendrait le tracé noir sur fond noir — invisible, sans erreur. Tout passe donc
   par ces classes, et `visuels.js` refuse tout `fill=`/`stroke=` en attribut.
   Il refuse aussi les `id`, donc les `<marker>` : les têtes de flèche sont des
   `<path>` dessinés à la main. */

/* Pas de `width`/`height` en attribut sur le <svg> (§ 5 bis, règle 11) : la
   mise à l'échelle est ici. Le viewBox fait 320 unités de large et le lecteur
   en a ~317 dans une figure, donc une unité vaut à peu près un pixel — c'est
   ce rapport de 1 qui rend les libellés lisibles, et c'est lui qu'on perdrait
   avec le viewBox de 720 des schémas de `_meta/intake/`. */
.graphe {
  display: block;
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 auto;
}
/* Le sommet. Fond OPAQUE, comme les cartes d'empilement plus haut : un arc qui
   passe derrière un cercle transparent se verrait à travers, et la topologie —
   le sujet même de la figure — cesserait de se lire. */
.graphe__sommet {
  fill: var(--elevated);
  stroke: var(--primary);
  stroke-width: 2;
}
/* Le nom du sommet, au centre de son cercle. Monospace comme tout identifiant
   du dépôt, et `text-anchor` plutôt qu'un x calculé : le centrage reste juste
   si le nom passe un jour de « A » à « A1 ». */
.graphe__nom {
  fill: var(--text);
  font-family: var(--font-mono);
  font-size: 13px;
  text-anchor: middle;
}
/* L'arc. `fill: none` est le seul `fill` que `visuels.js` laisse écrire en
   attribut, et il est ici pour la même raison qu'ailleurs : sans lui, un <path>
   ouvert serait rempli entre ses extrémités. */
.graphe__arc {
  stroke: var(--violet);
  stroke-width: 1.5;
  fill: none;
}
/* La tête de flèche, en <path> plein — pas un <marker>, qui exigerait un id
   que `liens.js` refuserait en double dès la deuxième figure. */
.graphe__pointe { fill: var(--violet); }
/* LE RANG DE VISITE — le seul élément qui change d'un panneau à l'autre, donc
   le seul que la figure demande de comparer. Il est en ambre parce qu'il ne
   fait pas partie du graphe : le graphe est identique dans les deux panneaux,
   c'est l'ordre qui diffère. Un chiffre à côté d'une lettre ne se confond avec
   rien, et c'est pourquoi les sommets sont nommés A..F et non 1..6. */
.graphe__rang {
  fill: var(--amber);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  text-anchor: middle;
}
/* PRÉSENT ICI, ABSENT DE L'ARBRE QUI COMPTE — le même sens que
   `.arbre__noeud--detache`, dans la famille SVG. Le § 5 bis pose les trois
   modificateurs de la grammaire commune comme « valables dans toutes les
   familles » : celui-ci n'est donc pas une invention, c'est la même marque
   portée par un `<circle>` au lieu d'un `<div>`. Les sommets `Ā` d'un arbre de
   probabilités existent bel et bien dans l'expérience, mais aucun chemin ne
   passe par eux pour atteindre A, donc aucun n'entre dans le calcul.
   Le tiret est écrit en unités du viewBox, pas en pixels : le SVG est mis à
   l'échelle par le CSS, et un `stroke-dasharray` en pixels se déformerait
   avec lui. */
.graphe__sommet--detache {
  stroke: var(--faint);
  stroke-dasharray: 4 3;
}
/* LE POIDS D'UN ARC — la probabilité qu'on paie en descendant cette branche.
   Écrite pour l'arbre de Bayes de STT-1000, qui est un graphe orienté comme un
   autre et réemploie donc sommets et arcs tels quels. Elle se distingue de
   `--rang` par ce qu'elle annote : le rang appartient au SOMMET et dit quand on
   l'a visité, le poids appartient à l'ARC et dit ce qu'il coûte. Deux teintes
   parce que deux objets — les confondre ferait lire un ordre de visite là où il
   y a une probabilité. `text-anchor: middle` : l'étiquette se pose au milieu de
   son arc, et y reste si l'arc se redresse. */
.graphe__poids {
  fill: var(--muted);
  font-family: var(--font-mono);
  font-size: 9px;
  text-anchor: middle;
}
/* LE SOMMET RECTANGULAIRE — un maillon de liste chaînée. Même famille que le
   sommet rond, et c'est voulu : une liste chaînée EST un graphe orienté, elle
   réemploie donc arcs, pointes et libellés tels quels. Ce qui change est la
   forme, parce qu'un maillon a deux compartiments — ce qu'il vaut, et où mène
   la suite — là où un sommet de graphe n'en a qu'un.
   Fond OPAQUE pour la même raison que `.graphe__sommet` : un arc qui passe
   derrière une boîte transparente se verrait à travers, et le chaînage — le
   sujet même de la figure — cesserait de se lire. */
.graphe__boite {
  fill: var(--elevated);
  stroke: var(--primary);
  stroke-width: 1.5;
}
/* La cloison entre les deux compartiments du maillon. En couleur de bordure :
   elle sépare, elle ne désigne rien. */
.graphe__sep {
  fill: none;
  stroke: var(--border);
  stroke-width: 1;
}

/* --- Les ensembles dessinés : Venn, et l'univers partitionné --- */
/* CE QUI A ÉTÉ PAYÉ POUR ARRIVER ICI. La première version était en HTML : une
   grille de cases portant `A ∩ B₁`, `Ā ∩ B₁`… Tous les vérificateurs au vert,
   et une figure qui n'apprenait RIEN — elle ÉCRIVAIT le nom de chaque région
   au lieu de la MONTRER, toutes les cases à la même taille, donc aucune
   géométrie à lire. Publiée, puis retirée le jour même. C'est le défaut exact
   que la règle 2 du § 5 bis nomme « figure de complaisance », atteint en
   croyant l'éviter : une grille d'étiquettes est un tableau, et le module en
   avait déjà un.

   ET C'EST CE QUI LÈVE L'INTERDIT DU SVG. Le § 5 bis exige DEUX conditions
   réunies, et un diagramme d'ensembles les réunit : aucune famille HTML ne rend
   deux disques sécants — l'échec ci-dessus n'est plus une supposition, il est
   mesuré —, et la contrainte des huit à neuf caractères par nœud tombe, les
   libellés étant « A », « B₁ », « A ∩ B ». Les deux, comme le SPEC l'exige,
   jamais l'une.

   L'INTERSECTION EST UNE LENTILLE CALCULÉE, PAS UN DÉCOUPAGE. Elle se trace en
   `<path>`, par les deux arcs qui la bornent — donc exacte par construction,
   puisque ses arcs SONT ceux des disques. Pas de `<clipPath>` : il exigerait un
   `id`, que `liens.js` refuse en double dès la deuxième figure.

   UNE SEULE CLASSE POUR LE DISQUE ET POUR LA BANDE. `.venn__ens` ne fait que
   remplir et contourner ; elle habille aussi bien un `<circle>` qu'un `<rect>`,
   et c'est ce qui permet à la partition — un rectangle coupé en bandes, traversé
   par A — d'employer le même vocabulaire que le Venn sans une classe de plus.
   Le trait de séparation, lui, est le SEUL élément propre à la partition. */

/* Pas de `width`/`height` en attribut sur le <svg> (§ 5 bis, règle 11), et le
   même viewBox de 320 unités que `.graphe` : le lecteur en a ~317 dans une
   figure, donc une unité vaut à peu près un pixel. */
.venn {
  display: block;
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 auto;
}
/* Le cadre de l'univers. `fill: none` vit ici plutôt qu'en attribut, où
   `visuels.js` le tolérerait : une figure dont toute la couleur passe par le
   CSS se recolore d'un bloc avec la palette. */
.venn__cadre {
  fill: none;
  stroke: var(--border);
  stroke-width: 1.5;
}
/* Un ensemble : disque du Venn, ou bande qui traverse une partition. */
.venn__ens {
  fill: var(--primary-soft);
  stroke: var(--primary);
  stroke-width: 1.5;
}
/* Le SECOND ensemble d'un Venn. Une teinte différente, sans quoi « A » et « B »
   ne se distinguent que par leur position — et la légende devrait alors les
   désigner par la place, ce que le § 5 bis lui interdit hors des structures à
   colonnes déclarées. */
.venn__ens--b {
  fill: var(--violet-soft);
  stroke: var(--violet);
}
/* LA ZONE COMMUNE, par-dessus les deux disques.
   SA TEINTE S'EST CHOISIE SUR CAPTURE, PAS AU JUGÉ. La première version
   employait `--primary-plein` (#1d2a44), la seule variable dense de la palette :
   à l'écran elle tombait à deux points du disque bleu qui la borde, et la région
   que toute la figure existe pour montrer se distinguait à peine de son voisin.
   Le bleu de la palette à 26 % s'AJOUTE au contraire à ce qui est dessous, si
   bien que la zone commune se lit comme « deux fois dedans » — ce qu'elle est.
   Ne la repasse pas en couleur pleine : aucun vérificateur ne signale ce défaut,
   il faut regarder la capture de `rendu.js`. */
.venn__inter { fill: rgba(91, 157, 255, 0.26); }
/* Le trait qui sépare deux morceaux d'une partition. Plein, pas pointillé : la
   frontière entre B₁ et B₂ est aussi réelle que le bord de l'univers.
   EN `--muted`, PAS EN `--border` comme le cadre, et c'est la capture qui l'a
   tranché : la cloison doit rester lisible LÀ OÙ ELLE TRAVERSE la forme A,
   par-dessus un fond déjà teinté. En couleur de bordure elle s'y effaçait — et
   c'est précisément à cet endroit qu'elle porte le propos, puisque c'est elle
   qui coupe A en `A ∩ B₁`, `A ∩ B₂`, `A ∩ B₃`. Une figure dont la démonstration
   s'efface au seul endroit qui compte est une figure fausse. */
.venn__cloison {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.5;
}
/* Le nom d'un ensemble, au cœur de sa forme. */
.venn__nom {
  fill: var(--text);
  font-family: var(--font-mono);
  font-size: 13px;
  text-anchor: middle;
}
/* Le nom d'une RÉGION — « A ∩ B », « Ā ∩ B̄ ». Plus petit et en texte
   secondaire que le nom d'ensemble : la région se lit sur le dessin, son
   libellé ne fait que la confirmer. */
.venn__zone {
  fill: var(--muted);
  font-family: var(--font-mono);
  font-size: 10px;
  text-anchor: middle;
}

/* --- Le nuage de points : une association qu'on regarde --- */
/* POURQUOI CETTE FAMILLE. « Un lien en forme de cloche monte puis redescend, et
   les deux moitiés s'annulent » — la prose de `lois-conjointes` décrivait un
   dessin en toutes lettres, ce que le § 5 bis nomme comme le signe qu'il en
   manque un. Et c'est le contre-exemple le plus cher du chapitre : une
   covariance nulle sans indépendance ne se démontre pas en mots, elle se voit.

   EXACTE PAR SYMÉTRIE. Les points se posent en miroir autour de la moyenne de X,
   deux à deux à la même hauteur. Chaque produit (x − μx)(y − μy) trouve donc son
   opposé exact, et la covariance est nulle PAR CONSTRUCTION, pas parce qu'on
   l'affirme sous le dessin. Déplacer un point sans déplacer son miroir rendrait
   la figure fausse en silence — c'est la seule chose à ne pas faire ici. */
.nuage {
  display: block;
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 auto;
}
.nuage__axe {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.5;
}
/* Les DEUX moyennes, en pointillé : elles ne font pas partie des données, elles
   découpent le plan en quadrants où le produit change de signe. Le pointillé les
   distingue des axes, qui sont, eux, le repère. */
.nuage__moyenne {
  fill: none;
  stroke: var(--faint);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}
.nuage__point {
  fill: var(--primary);
  stroke: none;
}
.nuage__nom {
  fill: var(--muted);
  font-family: var(--font-mono);
  font-size: 10px;
  text-anchor: middle;
}

/* ===================== Tableaux ===================== */
.table-wrap { overflow-x: auto; margin: 18px 0; -webkit-overflow-scrolling: touch; }
table { border-collapse: collapse; width: 100%; font-size: 14.5px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--muted); font-weight: 600; background: var(--surface); }
td code { white-space: nowrap; }

/* ===================== Navigation bas de page ===================== */
.pager {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 12px;
  margin-top: 56px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}

/* La commande « lu / non lu ». Elle occupe sa propre ligne, en tête du bloc de
   fin de module : c'est là qu'on se trouve quand on descend chercher le lien
   « Suivant » — et donc là qu'on découvre que le module vient d'être marqué lu
   au passage. Pleine largeur pour rester atteignable au pouce sur 375 px. */
.finmodule {
  flex-basis: 100%;
  display: flex;
  justify-content: center;
}
.finmodule__btn {
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  padding: 9px 16px;
  min-height: 40px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.finmodule__btn:hover { color: var(--text); border-color: var(--primary); background: var(--elevated); }
.finmodule__btn.est-lu { color: var(--green); border-color: transparent; background: var(--green-soft); }
.pager a {
  flex: 1;
  display: block;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  text-decoration: none;
  transition: border-color 0.12s, background 0.12s;
}
.pager a:hover { border-color: var(--primary); background: var(--elevated); }
.pager .dir { font-size: 12px; color: var(--faint); }
.pager .label { color: var(--text); font-weight: 600; font-size: 14.5px; }
.pager .next { text-align: right; }
.pager .ghost { visibility: hidden; }

/* ===================== Accueil ===================== */
.hero { margin-bottom: 36px; }
.cards { display: grid; gap: 14px; grid-template-columns: 1fr; margin: 22px 0; }
.card {
  display: block;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 18px;
  background: var(--surface);
  text-decoration: none;
  transition: border-color 0.12s, transform 0.12s, background 0.12s;
}
.card:hover { border-color: var(--primary); background: var(--elevated); transform: translateY(-1px); text-decoration: none; }
.card__num {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--primary);
  font-weight: 700;
}
.card__title { color: var(--text); font-weight: 700; font-size: 16.5px; margin: 4px 0 6px; }
.card__desc { color: var(--muted); font-size: 14px; line-height: 1.55; }

/* L'étiquette du hero d'un guide (« UML », « GRASP »).
   `:not(.token)` N'EST PAS UNE PRÉCAUTION DÉCORATIVE — sans lui, cette règle
   repeignait TOUTES LES BALISES DE TOUS LES BLOCS DE CODE du dépôt. Prism
   marque chaque balise `<span class="token tag">` : la classe `tag` y est bien
   présente, donc une classe NUE la capture. Chaque `&lt;html&gt;`, chaque
   `&lt;/body&gt;` recevait le fond, la bordure, le `border-radius: 20px` — et
   surtout le `margin` du bas, qui écartait les lignes du bloc les unes des
   autres. Le code devenait franchement plus dur à lire, et c'est le lecteur
   qui l'a signalé.
   Aucun vérificateur ne pouvait le voir : le HTML est juste, la faute n'existe
   qu'après application du CSS — et `rendu.js` n'inspecte que les figures et les
   largeurs, pas les blocs de code. La leçon générale est au JOURNAL.
   LA MORALE, pour la prochaine classe qu'on écrira ici : le dépôt partage sa
   feuille de style avec Prism, donc une classe nue doit être confrontée au
   vocabulaire des tokens (`tag`, `string`, `property`, `important`, `url`,
   `entity`, `selector`, `function`…) avant d'être posée. `.tag` était la seule
   collision du dépôt ; ce n'est pas une raison pour que la prochaine passe. */
.tag:not(.token) {
  display: inline-block;
  font-size: 12px;
  font-family: var(--font-mono);
  color: var(--muted);
  background: var(--elevated);
  border: 1px solid var(--border);
  padding: 3px 9px;
  border-radius: 20px;
  margin: 0 6px 6px 0;
}

/* ===================== Accueil de l'Atlas (le hub) =====================
   Tout ce bloc est rendu par `assets/hub.js` à partir de `assets/atlas.js` et
   des tables `pages` des guides. L'`index.html` ne contient que des conteneurs
   vides : ajouter un guide n'y touche pas une ligne. */

/* Le hub n'a pas de barre latérale : le contenu est simplement centré. */
.hub { max-width: 900px; margin: 0 auto; padding: 48px 24px 96px; }

.hub__hero { margin-bottom: 34px; }
.hub__hero h1 { font-size: 34px; margin-bottom: 12px; }
.hub__hero .lead { margin-bottom: 0; }

.hub__section {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 46px 0 4px;
}
.hub__section .note { text-transform: none; letter-spacing: 0; font-weight: 400; font-size: 12.5px; }

/* ---- Reprendre la lecture : le premier geste de qui revient ---- */
.reprendre {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  margin: 26px 0 0;
  border: 1px solid var(--border);
  border-left: 4px solid var(--primary);
  border-radius: 14px;
  background: linear-gradient(180deg, var(--primary-soft), transparent 70%), var(--surface);
  text-decoration: none;
  transition: border-color 0.14s, background 0.14s;
}
.reprendre:hover { border-color: var(--primary); text-decoration: none; }
.reprendre__corps { flex: 1; min-width: 0; }
.reprendre__quoi {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--primary);
}
.reprendre__titre { color: var(--text); font-weight: 700; font-size: 16.5px; margin: 3px 0 2px; }
.reprendre__ou { font-size: 13px; color: var(--faint); }
.reprendre__fleche { color: var(--primary); font-size: 20px; flex-shrink: 0; }

/* ---- Les cartes de guides ---- */
.guides { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; margin: 16px 0 0; }

.guide {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 20px;
  background: var(--surface);
  text-decoration: none;
  transition: border-color 0.14s, transform 0.14s, background 0.14s;
}
a.guide:hover {
  border-color: var(--primary);
  background: var(--elevated);
  transform: translateY(-2px);
  text-decoration: none;
}

.guide__head { display: flex; align-items: center; gap: 11px; margin-bottom: 12px; }
.guide__logo {
  width: 36px; height: 36px;
  border-radius: 10px;
  display: grid; place-items: center;
  font-weight: 800; font-size: 18px; color: #fff;
  flex-shrink: 0;
}
.guide__title { color: var(--text); font-weight: 700; font-size: 17.5px; flex: 1; min-width: 0; }
.guide__desc { color: var(--muted); font-size: 14px; line-height: 1.6; flex: 1; margin: 0 0 14px; }
.guide__meta {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 12px;
  color: var(--faint);
  margin-bottom: 12px;
}
.guide__meta .jauge { max-width: 110px; }
.guide__tags { margin-top: auto; }

/* Les guides à venir ne méritent pas une carte pleine : une ligne suffit. */
.avenir {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  font-size: 13.5px;
  color: var(--faint);
}
.avenir__item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 6px;
  border: 1px dashed var(--border);
  border-radius: 20px;
}
.avenir__logo {
  width: 20px; height: 20px;
  border-radius: 6px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800; color: #fff;
  background: var(--elevated);
  opacity: 0.75;
}

/* ---- Les notes de cours, groupées par trimestre ---- */
.trimestre { margin-top: 22px; }
.trimestre__titre {
  font-size: 13px;
  font-weight: 700;
  color: var(--muted);
  margin-bottom: 10px;
}
.cours { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; }
.cours__carte {
  display: block;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px 16px;
  background: var(--surface);
  text-decoration: none;
  transition: border-color 0.14s, background 0.14s;
}
.cours__carte:hover { border-color: var(--primary); background: var(--elevated); text-decoration: none; }
.cours__sigle { font-family: var(--font-mono); font-size: 12px; color: var(--primary); font-weight: 700; }
.cours__titre { color: var(--text); font-weight: 650; font-size: 15px; margin: 3px 0 4px; }
.cours__desc { color: var(--muted); font-size: 13.5px; line-height: 1.55; }

/* Bouton discret « repartir de zéro », dans le pied de page */
.hub__reset {
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  color: var(--faint);
  font: inherit;
  font-size: 13px;
  text-decoration: underline;
  cursor: pointer;
}
.hub__reset:hover { color: var(--red); }

/* Sommaire cliquable en tête de page */
.sommaire {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--elevated);
  padding: 14px 18px;
  margin: 22px 0 8px;
  columns: 2;
  column-gap: 28px;
}
.sommaire a { display: block; font-size: 14px; padding: 3px 0; break-inside: avoid; }
@media (max-width: 700px) { .sommaire { columns: 1; } }

/* Intitulé du sommaire en tête de module. `column-span` le fait courir sur les
   deux colonnes. */
.sommaire__title {
  column-span: all;
  font-size: 11.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 8px;
}

.hub__footer {
  margin-top: 56px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
  font-size: 13.5px;
  color: var(--faint);
}
.hub__synchro { margin: 10px 0 0; }

/* ===================== Responsive (mobile-first switch) ===================== */
@media (max-width: 880px) {
  body { font-size: 16px; }

  .appbar__inner { padding: 0 12px; gap: 8px; }
  .appbar__menu { display: grid; }
  .appbar__here { display: block; flex: 1; }
  /* Le nom « Atlas » cède la place au titre du module — mais seulement là où
     il y a un titre à loger : l'accueil le garde. */
  .appbar--guide .appbar__name { display: none; }
  .switch__btn { padding: 7px 9px; gap: 4px; }
  /* Sur une page de guide seulement, les libellés cèdent la place au titre du
     module : savoir quel module on lit vaut plus que relire « Territoires ».
     L'accueil, lui, n'a pas de titre à loger — il garde ses mots. */
  .appbar--guide .appbar__mot { display: none; }

  /* Le tiroir s'ouvre SOUS l'en-tête, qui reste visible : le même ☰ referme. */
  .sidebar {
    position: fixed;
    top: var(--appbar-h); left: 0;
    z-index: 40;
    height: calc(100dvh - var(--appbar-h));
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    box-shadow: 0 0 40px rgba(0,0,0,0.5);
  }
  .sidebar.open { transform: translateX(0); }
  body.menu-open { overflow: hidden; }
  body.menu-open .backdrop { display: block; }

  .content__inner { padding: 24px 18px 80px; }

  h1 { font-size: 25px; }
  h2 { font-size: 20px; margin-top: 36px; }
  h3 { font-size: 17px; }
  .lead { font-size: 16.5px; }

  pre[class*="language-"] { font-size: 12.5px; padding: 13px 14px; }

  .pager { flex-direction: column; }
  .pager .ghost { display: none; }

  .hub { padding: 28px 18px 80px; }
  .hub__hero h1 { font-size: 27px; }
  .guides, .cours { grid-template-columns: 1fr; }
}

/* Petits écrans : un peu plus serré */
@media (max-width: 480px) {
  .content__inner { padding: 20px 14px 72px; }
  pre[class*="language-"] { font-size: 12px; }
  .switch__btn { padding: 7px 8px; }
}
