/* ===========================================================================
   Portfolio de Sébastien Oger — feuille de style unique des neuf pages.

   Une seule feuille pour tout le site : neuf pages qui embarqueraient chacune
   leur CSS feraient neuf endroits à corriger au premier changement de palette.

   La palette est héritée telle quelle de montreal-housing-site, elle-même
   issue du thème Power BI « Reference Dark ». Ses contrastes sont mesurés au
   ΔE CIEDE2000 sur fond sombre, simulation des trois dichromaties comprise.
   Elle ne se réinvente pas : la refaire, c'est perdre ce travail.

   SOMMAIRE
     1.  Jetons
     2.  Bascule de langue
     3.  Base, et les surfaces que le navigateur dessine
     4.  En-tête et pied de page
     5.  Ouverture : titre, vignette, bandeau de chiffres
     6.  Le plan de travail : cadre, quadrillage, barre
     7.  Le graphe : grille, voies, nœuds, axe de temps
     8.  Animation
     9.  Mouvement réduit
    10.  Les pages projets
   =========================================================================== */

/* ============================== 1. JETONS ================================ */

:root {
  /* Palette héritée. Ne pas modifier sans refaire les mesures de contraste. */
  --canvas:      #0E1A25;
  --surface:     #192938;
  --surface-2:   #1F3346;
  --line:        #2B4760;

  --ink:         #EEF4F9;
  --ink-soft:    #B4C8DA;
  --ink-mute:    #8FA3B5;

  --accent:      #85C9F4;
  --accent-deep: #4E9FD8;
  --good:        #B8E0C5;
  --warn:        #FA584C;

  --radius:      14px;
  --radius-sm:   9px;
  --radius-box:  8px;      /* le rayon d'un nœud n8n, plus serré qu'une carte */
  --wrap:        1120px;
  --wrap-wide:   1600px;   /* le plan de travail : c'est lui l'artefact */

  /* Ombre portée : un décalage ET un flou. Un halo coloré sans décalage
     n'est pas de la profondeur, c'est de la décoration.

     Deux profondeurs, et une seule par élément. Une bordure d'un pixel
     doublée d'une ombre très diffuse est une signature d'interface générée :
     il faut choisir entre une arête définie et une élévation douce. Les
     cartes prennent l'arête, les nœuds une élévation serrée comme n8n. */
  --shadow:     0 1px 2px rgb(0 0 0 / .32), 0 12px 32px rgb(0 0 0 / .28);
  --shadow-box: 0 1px 2px rgb(0 0 0 / .40), 0 3px 8px rgb(0 0 0 / .28);

  /* Échelle typographique fluide. Chaque pas est à au moins 1,25× du
     précédent : une échelle plate se lit comme une absence de décision. */
  --step--2: clamp(.72rem, .70rem + .10vw, .78rem);
  --step--1: clamp(.82rem, .79rem + .14vw, .90rem);
  --step-0:  clamp(1rem, .96rem + .2vw, 1.09rem);
  --step-1:  clamp(1.2rem, 1.12rem + .4vw, 1.45rem);
  --step-2:  clamp(1.55rem, 1.38rem + .84vw, 2.1rem);
  --step-3:  clamp(1.9rem, 1.5rem + 1.9vw, 3rem);

  /* Courbe d'accélération : sortie exponentielle, sans rebond ni élastique. */
  --ease-out: cubic-bezier(.22, 1, .36, 1);

  /* Horloge de l'animation du graphe. Deux valeurs pour tout ralentir. */
  --beat: 5.5s;
  --stagger: .2s;
}

/* ========================= 2. BASCULE DE LANGUE ==========================

   Tout le bilinguisme tient dans ces deux règles. Le JavaScript ne fait que
   changer l'attribut lang du document ; c'est le CSS qui masque la langue
   inactive. Conséquence directe : la page reste lisible et indexable sans
   JavaScript.

   Le piège, et la raison d'être de tools/check_page.py : une phrase oubliée
   dans une langue ne produit AUCUNE erreur, elle disparaît simplement.
   -------------------------------------------------------------------------- */

html[lang="fr"] [data-lang="en"] { display: none; }
html[lang="en"] [data-lang="fr"] { display: none; }

/* ============================ 3. BASE =================================== */

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Aucune page ne doit pouvoir défiler horizontalement. clip et non hidden :
     hidden transformerait le body en conteneur de défilement, ce qui casse
     position:sticky et rogne les éléments positionnés qui doivent en sortir. */
  overflow-x: clip;
}

h1, h2, h3 {
  line-height: 1.18;
  letter-spacing: -.018em;
  margin: 0;
  text-wrap: balance;
}
p { margin: 0; text-wrap: pretty; }
a { color: var(--accent); text-underline-offset: 3px; }

.wrap { width: min(100% - 32px, var(--wrap)); margin-inline: auto; }
.wrap-wide { width: min(100% - 32px, var(--wrap-wide)); margin-inline: auto; }

/* --- Les surfaces dessinées par le navigateur -----------------------------
   Sélection, curseur, barre de défilement et anneau de focus arrivent avec
   des valeurs par défaut qui n'appartiennent à aucune direction artistique.
   Les habiller coûte dix lignes et c'est le signal le moins cher qu'une page
   a été construite plutôt qu'assemblée. */

::selection { background: var(--accent-deep); color: #08131D; }
body { caret-color: var(--accent); }

html {
  scrollbar-color: var(--line) var(--canvas);   /* Firefox */
  scrollbar-width: thin;
}
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--canvas); }
::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 999px;
  border: 3px solid var(--canvas);
}
::-webkit-scrollbar-thumb:hover { background: var(--surface-2); }

:where(a, button, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 6px;
}

/* Sprite d'icônes. On le sort du flux plutôt que de le masquer avec
   display:none, qui empêche certains navigateurs de résoudre les <use>. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* Le saut d'accès : premier élément focalisable de chaque page, invisible
   tant qu'il n'a pas le focus. Sans lui, un visiteur au clavier traverse les
   dix-huit nœuds du graphe avant d'atteindre le pied de page. */
.skip {
  position: absolute;
  left: 16px;
  top: -60px;
  z-index: 100;
  padding: 10px 18px;
  background: var(--accent);
  color: #0A1622;
  font-size: var(--step--1);
  font-weight: 600;
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: top .18s var(--ease-out);
}
.skip:focus-visible { top: 16px; }

/* Texte réservé aux lecteurs d'écran. Il complète une information que l'œil
   reçoit par un signe graphique — ici la flèche de départ vers un autre site,
   qui ne se lit pas à voix haute. Retiré du flux visuel sans display:none, qui
   le retirerait aussi de l'arbre d'accessibilité. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* La flèche de départ. Posée contre le libellé d'un lien qui quitte le
   portfolio : la note du plan de travail promet qu'une bordure bleue ouvre une
   page, pas qu'on reste sur le site. Dimensionnée en em, donc accordée à la
   taille du texte qu'elle suit, et jamais à la place du texte. */
.out-mark {
  width: .78em;
  height: .78em;
  /* L'écart vient de l'espace insécable écrite dans le HTML, pas d'une marge :
     c'est elle qui empêche la flèche de partir seule à la ligne quand le
     libellé atteint la largeur de sa colonne. */
  margin-left: .04em;
  vertical-align: -.05em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .6;
  transition: opacity .16s var(--ease-out);
}
a:hover .out-mark,
a:focus-visible .out-mark { opacity: 1; }

/* Le dernier mot et sa flèche, en un seul bloc. Un navigateur coupe volontiers
   AVANT une boîte remplacée — un SVG en est une — que l'espace qui la précède
   soit insécable ou non. Sans ce groupe, la flèche se retrouve seule et centrée
   sur sa propre ligne dès que le libellé frôle la largeur de sa colonne. */
.no-break { white-space: nowrap; }

/* ==================== 4. EN-TÊTE ET PIED DE PAGE ========================= */

/* ============ L'EN-TÊTE EST VERROUILLÉE, ET ELLE L'EST PARTOUT ============

   Ces règles sont reprises TELLES QUELLES de montreal-housing-site, qui est
   en ligne, validé et intouchable. Elles sont identiques sur les neuf pages
   du portfolio : même hauteur, mêmes classes, mêmes valeurs. C'est ce qui
   fait qu'un visiteur qui passe d'une page à l'autre ne voit jamais la barre
   bouger.

   Décidé par Sébastien le 20 septembre 2026. Ne pas diverger : si un jour
   l'en-tête doit changer, elle change sur TOUTES les pages à la fois, et
   montreal-housing-site reste l'original.

   CE QU'ELLE CONTIENT, ET RIEN D'AUTRE :
     - la marque, qui ramène au HAUT DE LA PAGE COURANTE (#top) ;
     - quelques onglets vers des sections de la page courante ;
     - la bascule de langue.
   Aucun lien vers une autre page : le graphe de l'accueil EST la navigation,
   et le fil d'Ariane d'une page profonde dit comment on remonte.

   « top » est un fragment spécial de la spécification HTML : sans élément
   portant cet identifiant, le navigateur remonte au début du document. Rien à
   ancrer, rien à recharger, et le défilement doux s'applique.
   -------------------------------------------------------------------------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--canvas) 86%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.topbar-inner {
  display: flex;
  align-items: center;
  gap: 20px;
  min-height: 62px;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  margin-right: auto;
}
.brand-mark {
  width: 11px; height: 22px;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--accent), var(--accent-deep));
  flex: none;
}
.brand-text { font-size: var(--step--1); letter-spacing: -.01em; }

.topnav { display: flex; gap: 22px; }
.topnav a {
  color: var(--ink-soft);
  text-decoration: none;
  font-size: var(--step--1);
  font-weight: 500;
  padding: 6px 0;
  border-bottom: 2px solid transparent;
  transition: color .15s, border-color .15s;
}
.topnav a:hover { color: var(--ink); border-bottom-color: var(--accent); }

.lang-toggle {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 6px 11px;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--ink-mute);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  flex: none;
}
.lang-toggle:hover { border-color: var(--accent); }
.lang-sep { opacity: .4; }
html[lang="fr"] .lang-opt[data-for="fr"],
html[lang="en"] .lang-opt[data-for="en"] { color: var(--accent); }

/* Sous 640 px les onglets disparaissent : la marque et la bascule suffisent,
   et le contenu de la page est de toute façon en une seule colonne. */
@media (max-width: 640px) {
  .topnav { display: none; }
}

/* ---------------------------- LE PIED DE PAGE ---------------------------- */
.footer {
  margin-top: clamp(40px, 6vw, 72px);
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--surface) 55%, var(--canvas));
  padding: clamp(28px, 4vw, 44px) 0 36px;
}
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}
.footer-name { font-size: var(--step-1); font-weight: 700; letter-spacing: -.02em; }
.footer-role { font-size: var(--step--1); color: var(--ink-mute); }
.footer-links { display: flex; flex-wrap: wrap; gap: 10px; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  font-size: var(--step--1);
  font-weight: 600;
  text-decoration: none;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  transition: border-color .16s var(--ease-out),
              background .16s var(--ease-out),
              transform .16s var(--ease-out);
}
.btn:hover { border-color: var(--accent); background: var(--surface-2); }
.btn:active { transform: translateY(1px); }
.btn-icon { width: 1.15em; height: 1.15em; flex: none; fill: currentColor; }

.footer-legal {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-size: var(--step--2);
  color: var(--ink-mute);
  max-width: 74ch;
}

/* ============================== 5. OUVERTURE =============================

   Tout ici est compressé volontairement. Le plan de travail est le sujet de
   la page ; ce qui le précède doit tenir en un regard et libérer l'écran.
   -------------------------------------------------------------------------- */

.hero { padding: clamp(14px, 2.5vw, 26px) 0 clamp(16px, 2.5vw, 24px); }

.hero-identity {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2.5vw, 22px);
  flex-wrap: wrap;
}
.hero-identity h1 {
  font-size: var(--step-3);
  font-weight: 700;
  letter-spacing: -.035em;   /* plancher de -.04em : au-delà les lettres se touchent */
  flex: 1 1 240px;
}
.avatar {
  width: clamp(60px, 8vw, 84px);
  height: clamp(60px, 8vw, 84px);
  flex: none;
  border-radius: 50%;        /* le PNG est recadré pile sur le disque */
  border: 2px solid var(--line);
  background: var(--surface);
  display: block;
}
.lede {
  margin-top: 12px;
  max-width: 64ch;           /* mesure de lecture : jamais au-delà de 75ch */
  font-size: var(--step-0);
  color: var(--ink-soft);
  line-height: 1.6;
}
.lede strong { color: var(--ink); font-weight: 600; }

/* --- Bandeau de chiffres -------------------------------------------------
   Une bande fine de quatre cellules séparées par un filet, pas quatre cartes
   à ombre. Le motif vient du bandeau de la référence ; ce qui le sépare du
   gabarit « grand chiffre, petite étiquette » est la ligne de réserve, qui
   nomme la source de chaque nombre.

   KPI PROVISOIRES, à revoir quand les neuf pages seront écrites. */
.kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  margin: clamp(16px, 2.5vw, 22px) 0 0;
}
.kpi {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px 14px 13px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.kpi-label {
  font-size: var(--step--2);
  font-weight: 500;
  color: var(--ink-mute);
}
.card-figure {
  font-size: var(--step-2);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.035em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.card-caveat {
  margin-top: 5px;
  font-size: var(--step--2);
  line-height: 1.4;
  color: var(--ink-mute);
  text-wrap: pretty;
}

/* ====================== 6. LE PLAN DE TRAVAIL ============================

   Le cadre qui fait « éditeur de workflow » : une barre d'outils sobre, un
   fond quadrillé pointillé, et une arête nette. Rien de tout cela n'est
   interactif, et rien ne fait semblant de l'être : pas de bouton de zoom
   factice, pas de contrôle qui ne contrôle rien.
   -------------------------------------------------------------------------- */

.board {
  margin-top: clamp(16px, 2.5vw, 24px);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.board-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 14px;
  background: color-mix(in srgb, var(--surface) 70%, var(--canvas));
  border-bottom: 1px solid var(--line);
  font-size: var(--step--2);
}
.board-crumb { color: var(--ink-mute); font-weight: 400; font-size: inherit; }
.board-crumb strong { color: var(--ink-soft); font-weight: 600; }
.board-sep { color: var(--line); }

/* Le cadre du canvas : un simple ancrage pour les contrôles flottants. Il ne
   porte aucun défilement, à la différence du quadrillage qu'il enveloppe, donc
   ce qui est posé dessus ne part pas avec le graphe. */
.board-canvas { position: relative; }

/* Les deux profils posés SUR le plan de travail, comme les contrôles flottants
   d'un éditeur n8n. Reliés à rien, et rien dans leur forme ne suggère le
   contraire : tuile plus petite que celle d'un nœud, pas de câble, pas de
   libellé dessous. Le nom accessible tient dans aria-label — « LinkedIn » et
   « GitHub » sont des noms propres, ils ne se traduisent pas. */
.board-tools {
  position: absolute;
  z-index: 2;
  top: clamp(14px, 2vw, 22px);
  right: clamp(14px, 2vw, 22px);
  display: flex;
  gap: 8px;
}
.tool {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-box);
  background: color-mix(in srgb, var(--surface) 86%, var(--canvas));
  border: 1px solid var(--line);
  box-shadow: var(--shadow-box);
  color: var(--ink-mute);
  text-decoration: none;     /* la tuile ISO porte du texte, pas une icône */
  transition: border-color .18s var(--ease-out),
              background .18s var(--ease-out),
              color .18s var(--ease-out);
}
.tool:hover,
.tool:focus-visible {
  border-color: var(--accent-deep);
  background: var(--surface-2);
  color: var(--ink);
}
.tool-icon { width: 17px; height: 17px; fill: currentColor; }


/* Le raccourci qui reste sur le site prend la bordure des portes du graphe.
   Trois lettres plutôt qu'une icône : « ISO » se lit d'un coup d'œil là où un
   pictogramme de norme demanderait d'être deviné, et le sigle est aussi
   universel que les deux logos qui l'accompagnent. */
.tool--gate {
  border-color: var(--accent-deep);
  color: var(--accent);
}
.tool--gate:hover,
.tool--gate:focus-visible {
  border-color: var(--accent);
  color: var(--ink);
}
.tool-text {
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .03em;
}

.board-grid {
  /* Le quadrillage pointillé de l'éditeur n8n. */
  background-color: color-mix(in srgb, var(--canvas) 84%, #000);
  background-image:
    radial-gradient(circle at 1px 1px, rgb(180 200 218 / .13) 1px, transparent 0),
    linear-gradient(to right,  rgb(180 200 218 / .055) 1px, transparent 1px),
    linear-gradient(to bottom, rgb(180 200 218 / .055) 1px, transparent 1px);
  background-size: 20px 20px, 100px 100px, 100px 100px;
  padding: clamp(20px, 3vw, 36px) clamp(12px, 2vw, 26px);
}

@media (min-width: 1000px) {
  .board-grid { overflow-x: auto; overscroll-behavior-x: contain; }
  .board-grid .flow { min-width: 1360px; }
}

/* ========================== 7. LE GRAPHE ================================

   TROIS VOIES ET UN AXE DE TEMPS.
     voie 1, en haut    : les certifications
     voie 2, au milieu  : les projets
     voie 3, en bas     : le parcours professionnel, sur lequel se branchent
                          le déclencheur, la fusion et le contact
     l'axe              : les repères d'années, sous les trois voies

   L'AXE EST ORDINAL, PAS LINÉAIRE. 2017, 2021, 2024, 2025 et 2026 sont à
   intervalles réguliers à l'écran alors que les écarts réels valent 4, 3, 1
   et 1 an. C'est ce qui rend le graphe possible : un axe linéaire écraserait
   les huit nœuds de 2024-2026 sur un dixième de la largeur. La conséquence
   voulue est visible : clairsemé à gauche, dense à droite.

   UNE SEULE GRILLE, PLATE. Aucun conteneur de voie : chaque élément déclare
   sa rangée et sa colonne en ligne dans le HTML via --row et --col. C'est
   verbeux mais entièrement déterministe, et surtout cela libère l'ordre du
   DOM, qui peut alors être purement chronologique — c'est cet ordre-là que
   suit l'affichage sur téléphone.
   -------------------------------------------------------------------------- */

.flow {
  --gutter: 44px;
  /* Hauteur d'une voie. Relevée de 164 px à 182 px avec la place rendue par le
     repli de la note : c'est la seule dimension du graphe qui profite d'une
     hauteur en plus, puisque c'est elle qui donne aux courbes de liaison leur
     amplitude. Toute la géométrie des montées en dépend par calc(). */
  --lane-h: 182px;
  --tile: 52px;
  --lane-gap: 28px;
  /* L'écart entre deux colonnes de la grille. Nommé ici parce que les câbles
     doivent le franchir : voir --col-gap dans .lane-line et .riser. */
  --col-gap: 6px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* --- Sur téléphone : une colonne, un rail vertical ------------------------
   Les trois voies n'existent pas à 390 px : deux colonnes de boîtes sur
   358 px utiles ne sont pas serrées, elles sont impossibles. Le graphe se
   déroule donc verticalement dans l'ordre chronologique du DOM, le long d'un
   rail unique qui est l'axe de temps lui-même. */
.rail {
  position: absolute;
  left: calc(var(--gutter) / 2 - 1px);
  top: 4px;
  bottom: 4px;
  width: 2px;
  background: linear-gradient(180deg,
              transparent, var(--line) 3%, var(--line) 97%, transparent);
  pointer-events: none;
}

/* Les lignes de voie et les montées entre voies ne servent qu'à partir du
   moment où les voies existent, donc uniquement sur grand écran. */
.lane-line, .riser, .axis { display: none; }

/* Les courbes de liaison sont des SVG : voir la section 8 pour le mecanisme. */

/* --- Un nœud -------------------------------------------------------------
   Le texte est DANS la boîte, comme sur la maquette. Une boîte fluide se
   redimensionne proprement d'une colonne de 115 px à une pleine largeur de
   téléphone, là où une tuile de taille fixe accompagnée d'un libellé extérieur
   impose deux mises en page différentes. La ligne de voie passe derrière la
   boîte, que son fond opaque masque. */
.node {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: var(--gutter);
  text-decoration: none;
  color: inherit;
}

/* La TUILE porte toute la boite. Le libelle vit DEHORS, sous elle sur grand
   ecran, a cote sur telephone : c'est le vocabulaire exact d'un noeud n8n, et
   c'est aussi ce qui rend le composant reutilisable aux deux largeurs, puisque
   la tuile est un carre qui ne se recompose jamais. */
.node-tile {
  position: relative;
  width: var(--tile);
  height: var(--tile);
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--radius-box);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-box);
  transition: border-color .18s var(--ease-out),
              background .18s var(--ease-out),
              transform .18s var(--ease-out);
}

/* Le câble de greffe, du rail jusqu'au bord gauche de la boîte. En position
   absolue DANS le nœud, donc mesuré contre le nœud et jamais contre la page :
   rien ne se décale quand un libellé passe à la ligne. */
.node::before {
  content: "";
  position: absolute;
  right: 100%;
  top: 50%;
  width: calc(var(--gutter) / 2);
  height: 2px;
  margin-top: -1px;
  background: var(--line);
}

.node-icon {
  width: 45%;
  height: 45%;
  flex: none;
  fill: none;
  stroke: var(--ink-mute);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke .18s var(--ease-out);
}
.node-body { min-width: 0; }
.node-label {
  display: block;
  font-size: var(--step--2);
  font-weight: 600;
  color: var(--ink);
  line-height: 1.3;
}
.node-sub {
  display: block;
  font-size: var(--step--2);
  color: var(--ink-mute);
  line-height: 1.35;
}
.node-date {
  display: block;
  margin-top: 2px;
  font-size: var(--step--2);
  font-weight: 700;
  color: var(--accent-deep);
  font-variant-numeric: tabular-nums;
}

/* --- Porte et non-porte --------------------------------------------------
   Toutes les boîtes ne sont pas des liens. La différence se voit sans être
   expliquée, et elle ne tient jamais à la seule couleur : la bordure change,
   l'icône change, et le survol comme le focus la confirment. */
.node--gate .node-tile { border-color: var(--accent-deep); }
.node--gate .node-icon { stroke: var(--accent); }
.node--gate:hover .node-tile,
.node--gate:focus-visible .node-tile {
  border-color: var(--accent);
  background: var(--surface-2);
  transform: translateY(-2px);
}
.node--gate:hover .node-label { text-decoration: underline; }

.node--plain .node-tile { border-style: dashed; }
.node--plain .node-label { color: var(--ink-soft); }

/* Le déclencheur : bord gauche arrondi, la forme que n8n donne à tous ses
   nœuds déclencheurs. */
.node--trigger .node-tile {
  border-radius: 999px var(--radius-box) var(--radius-box) 999px;
  border-color: var(--accent);
}
.node--trigger::before { display: none; }

/* --- Un repère d'année ---------------------------------------------------
   Sur téléphone, les repères ponctuent la colonne ; ce sont eux qui font de
   la descente une chronologie plutôt qu'une liste. */
.tick {
  position: relative;
  margin-left: var(--gutter);
  padding-top: 6px;
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .06em;
  color: var(--accent-deep);
  font-variant-numeric: tabular-nums;
}
.tick::before {
  content: "";
  position: absolute;
  right: calc(100% + var(--gutter) / 2 - 5px);
  top: 50%;
  margin-top: -4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--canvas);
  border: 2px solid var(--accent-deep);
}

/* --- Les pastilles de certification --------------------------------------
   Une certification n'est pas une etape de workflow : c'est un evenement date.
   Sa place est donc SUR l'axe du temps, pas dans une voie. Cinq pastilles
   serrees entre avril et aout 2026 montrent la cadence mieux que cinq tuiles,
   et elles rendent au graphe la legerete que cinq tuiles lui prenaient. */
.badges {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-left: var(--gutter);
}
.badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 8px;
  border-radius: 999px;
  border: 1px solid var(--accent-deep);
  background: color-mix(in srgb, var(--accent) 8%, var(--canvas));
  color: var(--ink);
  text-decoration: none;
  font-size: var(--step--2);
  font-weight: 600;
  white-space: nowrap;
  transition: border-color .16s var(--ease-out), background .16s var(--ease-out);
}
.badge:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, var(--canvas));
}
.badge-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
.badge-date { color: var(--ink-mute); font-weight: 500; font-variant-numeric: tabular-nums; }
/* Le filet pointillé ne sert que lorsque l'axe existe, donc pas en colonne. */
.badge-stem { display: none; }

/* --- L'étiquette de voie -------------------------------------------------
   Sur téléphone elle n'a pas lieu d'être : il n'y a qu'une colonne. */
.lane-name { display: none; }

/* --- La note autocollante ------------------------------------------------
   n8n en pose sur ses plans de travail pour commenter un workflow. Celle-ci
   porte les deux choses que le graphe ne peut pas dire seul : quelles boîtes
   s'ouvrent, et ce que l'axe signifie exactement. */
.sticky {
  margin: clamp(20px, 3vw, 30px) 0 0;
  max-width: 64ch;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--good) 10%, var(--canvas));
  border: 1px solid color-mix(in srgb, var(--good) 30%, transparent);
  font-size: var(--step--2);
  line-height: 1.55;
  color: var(--ink-soft);
}
.sticky strong { color: var(--ink); font-weight: 600; }
.sticky p + p { margin-top: 5px; }


/* ======================= LE GRAPHE SUR GRAND ÉCRAN =======================

   La grille prend ses six rangées :
     1 · voie des certifications      hauteur --lane-h
     2 · intervalle                   hauteur --lane-gap, réservé aux montées
     3 · voie des projets             hauteur --lane-h
     4 · intervalle                   hauteur --lane-gap, réservé aux montées
     5 · voie du parcours             hauteur --lane-h
     6 · axe de temps                 hauteur automatique

   Les rangées 1, 3 et 5 ayant une hauteur FIXE, la distance entre deux
   lignes de voie est connue d'avance. Une montée peut donc relier deux voies
   au pixel près sans qu'aucune coordonnée ne soit mesurée : elle occupe une
   rangée d'intervalle et déborde d'une demi-hauteur de voie de chaque côté.
   -------------------------------------------------------------------------- */

@media (min-width: 1000px) {
  .flow {
    display: grid;
    grid-template-columns: repeat(var(--cols, 12), 1fr);
    /* 1 · branche des certifications   2 · intervalle des liaisons
       3 · voie des projets              4 · intervalle des liaisons
       5 · voie du parcours              6 · axe et graduations
       7 · pastilles de certification */
    grid-template-rows:
      var(--lane-h) var(--lane-gap)
      var(--lane-h) var(--lane-gap)
      var(--lane-h) auto auto;
    column-gap: var(--col-gap);
    row-gap: 0;
    --tile: 64px;
  }

  .rail { display: none; }

  /* La ligne d'une voie. Elle est déclarée AVANT les nœuds dans le HTML, donc
     peinte avant eux : le fond opaque de chaque boîte la recouvre, et elle ne
     reste visible que dans les intervalles. C'est exactement le rendu d'un
     câble qui passe derrière un nœud. */
  .lane-line {
    display: block;
    grid-row: var(--row);
    grid-column: var(--from) / var(--to);
    align-self: start;
    margin-top: calc(var(--tile) / 2 - 1px);
    height: 2px;
    background: var(--line);
    /* FRANCHIR LA GOUTTIÈRE. Une piste de grille s'arrête au début de son
       column-gap ; deux câbles logés dans des pistes voisines se terminent
       donc à 6 px l'un de l'autre, et le graphe se lit comme une suite de
       traits interrompus. Chaque câble déborde d'un demi-gap de chaque côté :
       ils se rejoignent alors exactement au milieu de la gouttière, sans que
       personne ait à mesurer quoi que ce soit. */
    margin-inline: calc(var(--col-gap) / -2);
  }

  /* Une liaison entre deux voies. C'est un SVG etire sur sa cellule, qui
     couvre la rangee d'intervalle et deborde en haut et en bas jusqu'au CENTRE
     DES TUILES des deux voies qu'il relie. Les rangees ayant une hauteur fixe,
     cette distance est connue d'avance : aucune coordonnee n'est mesuree. */
  .riser {
    display: block;
    grid-row: var(--row);
    grid-column: var(--col) / span var(--rspan, 1);
    align-self: start;
    /* Même demi-gap de débord que .lane-line, pour la même raison. Un SVG est
       un élément remplacé : justify-self: stretch ne l'étire pas, sa largeur
       reste donc écrite, gouttière comprise. */
    width: calc(100% + var(--col-gap));
    margin-inline: calc(var(--col-gap) / -2);
    height: calc(var(--lane-h) + var(--lane-gap));
    margin-top: calc(var(--tile) / 2 - var(--lane-h));
    overflow: visible;
    pointer-events: none;
  }

  .node {
    grid-row: var(--row);
    grid-column: var(--col) / span var(--span, 1);
    align-self: start;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 8px;
    margin-left: 0;
  }
  .node::before { display: none; }

  .tick {
    grid-row: 6;
    grid-column: var(--col);
    justify-self: center;
    align-self: start;
    margin-left: 0;
    padding-top: 16px;
    text-align: center;
  }
  /* Le trait de graduation, du haut de la rangée jusqu'au chiffre. */
  .tick::before {
    right: auto;
    left: 50%;
    top: 0;
    margin: 0 0 0 -1px;
    width: 2px;
    height: 11px;
    border: 0;
    border-radius: 0;
    background: var(--accent-deep);
  }

  /* La ligne de l'axe, distincte des lignes de voie : c'est elle qui porte
     les graduations, et elle traverse toute la largeur du graphe. */
  .axis {
    display: block;
    grid-row: 6;
    grid-column: 1 / -1;
    align-self: start;
    height: 2px;
    background: linear-gradient(90deg,
                transparent, var(--line) 3%, var(--line) 97%, transparent);
  }

  /* L'étiquette de voie, posée à gauche au-dessus de sa ligne. */
  .badges {
    grid-row: 6 / 8;
    grid-column: var(--from) / var(--to);
    align-self: start;
    margin-left: 0;
    justify-content: space-between;
    align-items: start;
    gap: 4px;
  }
  .badge {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0;
    padding: 0;
    border: 0;
    background: none;
    border-radius: 0;
    white-space: normal;
  }
  .badge:hover { background: none; }
  .badge-dot {
    width: 9px;
    height: 9px;
    background: var(--canvas);
    border: 2px solid var(--accent);
    margin-top: -4px;          /* la pastille se centre sur le trait de l'axe */
  }
  /* Le filet enjambe les graduations d'annees : 44 px suffisent a passer
     sous leur etiquette sans la toucher. */
  .badge-stem {
    display: block;
    width: 0;
    height: 44px;
    border-left: 1px dashed color-mix(in srgb, var(--accent-deep) 60%, transparent);
  }
  .badge-text { color: var(--ink-soft); }
  .badge-date { display: block; }
  .badge:hover .badge-text { color: var(--ink); text-decoration: underline; }
  .badge:hover .badge-dot { background: var(--accent); }

  .lane-name {
    display: block;
    grid-row: var(--row);
    grid-column: 1 / span 4;
    align-self: start;
    padding-top: calc(var(--tile) / 2 - .7em);
    font-size: var(--step--2);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;    /* étiquette courte : l'usage admis des capitales */
    color: var(--ink-mute);
  }
}

/* ============================ 8. ANIMATION ===============================

   Un seul moment animé sur toute la page, et c'est le bon : la donnée qui
   traverse le workflow, comme lors d'une exécution n8n. Rien d'autre n'entre
   en scène, aucune section n'apparaît au défilement.

   L'animation AJOUTE à une page déjà complète. Sans CSS animé et sans
   JavaScript, les dix-huit nœuds sont visibles, lisibles et cliquables : on
   n'anime jamais l'apparition d'un contenu, seulement son accentuation.

   L'impulsion est un dégradé qui parcourt la ligne, et non un tracé SVG :
   toutes les liaisons de ce graphe sont droites, donc un pseudo-élément
   suffit et rien n'a besoin d'être remesuré quand la fenêtre change.
   -------------------------------------------------------------------------- */

/* L'impulsion parcourt la voie sur le premier tiers du cycle, puis le plan de
   travail se repose. Une animation continue sur une page lue deux minutes
   devient un parasite ; une exécution qui repasse de temps en temps dit
   simplement que le workflow est vivant. */
.lane-line, .rail {
  position: relative;
  overflow: hidden;
}
.lane-line::after, .rail::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 250% 100%;
  background-repeat: no-repeat;
  opacity: 0;
  animation: flow-h var(--beat) linear infinite;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}
.rail::after {
  background: linear-gradient(180deg, transparent, var(--accent), transparent);
  background-size: 100% 250%;
  animation-name: flow-v;
}

@keyframes flow-h {
  0%   { background-position: -150% 0; opacity: 0; }
  4%   { opacity: .95; }
  34%  { background-position: 150% 0; opacity: .95; }
  38%  { opacity: 0; }
  100% { background-position: 150% 0; opacity: 0; }
}
@keyframes flow-v {
  0%   { background-position: 0 -150%; opacity: 0; }
  4%   { opacity: .95; }
  34%  { background-position: 0 150%; opacity: .95; }
  38%  { opacity: 0; }
  100% { background-position: 0 150%; opacity: 0; }
}

/* --- Les courbes de liaison ----------------------------------------------
   n8n ne relie jamais deux noeuds a angle droit : ses cables sont des courbes
   de Bezier qui sortent d'un bord et entrent dans l'autre. Chaque liaison est
   donc un SVG dont le viewBox vaut toujours 0 0 100 100, etire sur sa cellule
   par preserveAspectRatio="none".

   Deux attributs font tout le travail :

   vector-effect="non-scaling-stroke" garde le trait a son epaisseur reelle
   malgre l'etirement non uniforme. Sans lui, une cellule large et basse
   donnerait un cable epais a l'horizontale et fin a la verticale.

   pathLength="100" renormalise la longueur du trace a 100, quelle que soit sa
   taille a l'ecran. On peut donc ecrire une fois stroke-dasharray: 8 92 et
   obtenir un segment lumineux long de 8 % du cable, identique quelle que soit
   la largeur de la fenetre. Sans cela, il faudrait mesurer chaque courbe en
   JavaScript apres chaque redimensionnement. */

.riser path { fill: none; }
.wire {
  stroke: var(--line);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.wire-pulse {
  stroke: var(--accent);
  stroke-width: 2.5;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 8 92;
  stroke-dashoffset: 100;
  opacity: 0;
  animation: wire-travel var(--beat) linear infinite;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}
@keyframes wire-travel {
  0%   { stroke-dashoffset: 100; opacity: 0; }
  4%   { opacity: .95; }
  34%  { stroke-dashoffset: 0; opacity: .95; }
  38%  { stroke-dashoffset: 0; opacity: 0; }
  100% { stroke-dashoffset: 0; opacity: 0; }
}

/* La tuile s'allume au passage de l'impulsion, puis retombe. */
.node--gate .node-tile {
  animation: node-wake var(--beat) var(--ease-out) infinite;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}
@keyframes node-wake {
  0%, 24%   { border-color: var(--accent-deep); }
  30%       { border-color: var(--accent); }
  44%, 100% { border-color: var(--accent-deep); }
}

/* --- La DEL du bouton de contact -----------------------------------------

   L'appel a l'action est le seul geste attendu du visiteur, et il se trouve au
   bout du parcours, la ou le regard arrive en dernier. Il s'annonce par un
   point lumineux qui fait le tour de la tuile en continu, comme une DEL sur
   une carte electronique. Cela s'AJOUTE a la signalisation ordinaire d'une
   porte : bordure bleue au repos, reveil au passage de l'impulsion, relief au
   survol. Rien n'est remplace.

   DEUX COUCHES, ET RIEN D'AUTRE.
     ::before  un degrade conique pose 3 px au-dela du bord de la tuile. Le
               degrade est transparent sur les deux tiers de son tour ; le
               tiers restant est la comete, sa traine puis sa tete.
     ::after   un cache opaque de la couleur du fond, pose exactement sur la
               zone interieure. Il recouvre le centre du degrade et ne laisse
               vivre que l'anneau de 3 px.

   POURQUOI PAS UN mask-composite. La technique du masque en anneau decoupe un
   RECTANGLE au centre, sans rayon : sur une tuile a coins arrondis, le filet
   lumineux s'amincit dans les quatre angles. Un cache qui reprend le rayon de
   la tuile donne un anneau d'epaisseur constante sur tout le tour.

   POURQUOI PAS UN transform: rotate. Faire tourner le degrade en tournant son
   pseudo-element ferait tourner AUSSI son cadre : les coins du carre sortiraient
   de la tuile et ses aretes y rentreraient, l'anneau se briserait quatre fois
   par tour. C'est l'angle du degrade qui doit tourner, pas l'element ; et un
   angle ne s'anime que si le navigateur en connait le type, d'ou @property.
   -------------------------------------------------------------------------- */

@property --led-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.node--cta .node-tile::before {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: calc(var(--radius-box) + 2px);
  /* La valeur est ecrite ici en plus de @property : sur un navigateur qui
     ignore la regle, la variable reste definie et l'anneau s'affiche fige au
     lieu de faire disparaitre tout le fond. */
  --led-angle: 0deg;
  background: conic-gradient(from var(--led-angle),
              transparent 0 60%,
              color-mix(in srgb, var(--accent) 22%, transparent) 74%,
              color-mix(in srgb, var(--accent) 85%, transparent) 86%,
              var(--accent) 90%,
              var(--ink) 93%,
              transparent 96% 100%);
  /* Le halo. Sans lui la traîne se confond avec la bordure bleue de la tuile,
     qui est de la même couleur : c'est la diffusion, pas la teinte, qui fait
     lire une source lumineuse plutôt qu'un liseré. Il ne déborde que vers
     l'extérieur, le cache recouvrant tout ce qui rentre. */
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent) 65%, transparent));
  pointer-events: none;
  animation: cta-led 2.8s linear infinite;
}

.node--cta .node-tile::after {
  content: "";
  position: absolute;
  inset: 0;                      /* la zone interieure, bordure exclue */
  border-radius: calc(var(--radius-box) - 1px);
  background: var(--surface);
  pointer-events: none;
}
.node--cta:hover .node-tile::after,
.node--cta:focus-visible .node-tile::after { background: var(--surface-2); }

/* Les deux pseudo-elements sont positionnes, donc peints apres le contenu en
   flux : sans cette remontee, le cache passerait par-dessus l'icone. */
.node--cta .node-icon { position: relative; z-index: 1; }

@keyframes cta-led {
  to { --led-angle: 360deg; }
}

/* ========================= 9. MOUVEMENT RÉDUIT ===========================

   prefers-reduced-motion SUPPRIME le mouvement, il ne l'accélère pas. Une
   animation ramenée à .01ms reste un clignotement pour qui l'a désactivée
   parce qu'elle lui donne la nausée.

   Ce qui reste : le graphe immobile, les voies dessinées, les boîtes à leur
   couleur de repos. C'est une image finie, pas une page amputée.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  .lane-line::after,
  .rail::after,
  .wire-pulse,
  .node--gate .node-tile { animation: none; }

  /* La DEL ne ralentit pas, elle s'éteint : un point lumineux arrêté sur un
     bord serait un défaut d'affichage, pas une image au repos. Le cache, lui,
     reste — il ne fait que reproduire le fond de la tuile. */
  .node--cta .node-tile::before { animation: none; background: none; }

  .lane-line::after, .rail::after, .wire-pulse { opacity: 0; }
  .node--gate .node-tile { border-color: var(--accent-deep); }

  *, *::before, *::after {
    transition-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ====================== 10. LES PAGES PROJETS ============================

   Tout ce qui suit ne sert QU'AUX pages projets. L'accueil n'en charge pas
   une seule règle : aucune de ces classes n'apparaît dans index.html.

   Rien n'est inventé ici. Le vocabulaire — section, carte, étape, puce de
   stack, figure légendée — est repris de montreal-housing-site, où il a déjà
   servi une page entière validée. Deux ajouts seulement, que la référence
   n'avait pas : le fil d'Ariane, parce qu'une page profonde sans menu doit
   dire comment on remonte, et l'encart vidéo, parce qu'aucune page du site
   n'avait encore de vidéo à montrer.
   -------------------------------------------------------------------------- */

/* --- Fil d'Ariane --------------------------------------------------------
   Le site n'a pas de menu : le graphe de l'accueil EST la navigation. Sur une
   page projet, la marque collée en haut ramène à l'accueil, mais elle ne dit
   pas d'où l'on vient. Cette ligne le dit, en toutes lettres. */
.crumbs {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 18px;
  font-size: var(--step--2);
  color: var(--ink-mute);
}
.crumbs a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-soft);
  text-decoration: none;
  font-weight: 500;
}
.crumbs a:hover { color: var(--accent); }
.crumbs-icon {
  width: 1em;
  height: 1em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.crumbs-sep { opacity: .45; }

/* --- Ouverture d'une page projet -----------------------------------------
   Deux colonnes sur grand écran : le texte porte l'argument, l'image porte la
   preuve. En dessous de 900 px l'image passe SOUS le texte — jamais au-dessus,
   sinon un visiteur sur téléphone voit une capture d'écran illisible avant
   d'avoir lu la première phrase. */
.p-hero { padding: clamp(26px, 4vw, 48px) 0 clamp(30px, 4vw, 52px); }
.p-hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(26px, 4vw, 46px);
  align-items: center;
}
@media (min-width: 900px) {
  .p-hero-grid { grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr); }
}

.eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 16px;
}
.eyebrow-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-deep);
  flex: none;
}

.p-hero h1 {
  font-size: var(--step-3);
  font-weight: 700;
  letter-spacing: -.035em;
  max-width: 17ch;
}
.p-lede {
  margin-top: 20px;
  max-width: 58ch;
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--ink-soft);
}
.p-lede strong { color: var(--ink); font-weight: 600; }

/* --- Boutons d'appel -----------------------------------------------------
   .btn est déjà défini en section 4 pour le pied de page, et la section 4 est
   close. Ces deux règles ne font que l'habiller : elles n'en redéfinissent ni
   la géométrie ni l'espacement. */
.cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }
.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #0A1622;
}
.btn--primary:hover { background: #A3D7F8; border-color: #A3D7F8; }

/* --- Une figure ----------------------------------------------------------
   L'image porte une arête nette et une élévation, comme une carte : ce sont
   des captures d'un outil, pas des illustrations, et l'encadrement dit qu'on
   regarde un écran. */
.shot { margin: 0; }
.shot img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--canvas);
  box-shadow: var(--shadow);
}
.shot figcaption {
  margin-top: 12px;
  font-size: var(--step--2);
  color: var(--ink-mute);
  text-wrap: pretty;
}
.shot figcaption strong { color: var(--ink-soft); font-weight: 600; }

/* --- Sections ------------------------------------------------------------ */
.section { padding: clamp(46px, 7vw, 92px) 0; }
.section-alt {
  background: linear-gradient(180deg,
              transparent,
              color-mix(in srgb, var(--surface) 55%, transparent) 12% 88%,
              transparent);
  border-block: 1px solid var(--line);
}
.section h2 {
  font-size: var(--step-2);
  font-weight: 700;
  max-width: 22ch;
}
.section-note {
  margin-top: 16px;
  max-width: 70ch;
  color: var(--ink-soft);
  font-size: var(--step-0);
}
.section-note strong { color: var(--ink); font-weight: 600; }
.section-note em { color: var(--good); font-style: normal; font-weight: 600; }

/* --- Cartes de contenu ---------------------------------------------------
   .card-figure et .card-caveat existent déjà pour le bandeau de chiffres de
   l'accueil, à une taille de bandeau. Dans une carte, le chiffre doit peser
   plus lourd : on ne redéfinit la taille QUE sous .card. */
/* Deux colonnes, jamais trois. auto-fit donnerait trois pistes a 1120 px et
   laisserait la quatrieme carte seule sur sa rangee ; et ces cartes portent un
   paragraphe, pas une ligne : a 265 px la mesure de lecture tombe sous 30
   caracteres. Le passage a une colonne se fait donc en un seul palier. */
.cards {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  margin-top: 36px;
}
@media (min-width: 720px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px 24px 22px;
  display: flex;
  flex-direction: column;
  gap: 11px;
}
.card h3 { font-size: var(--step-1); font-weight: 600; }
.card p { font-size: var(--step--1); color: var(--ink-soft); }
.card p strong { color: var(--ink); font-weight: 600; }
.card .card-figure { font-size: var(--step-2); }
.card .card-caveat {
  margin-top: auto;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

/* La pastille d'icône d'une carte : même géométrie que le numéro d'étape,
   pour que les deux motifs se lisent comme une seule famille. */
.card-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-box);
  background: var(--surface-2);
  border: 1px solid var(--line);
  flex: none;
}
.card-icon svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Deux colonnes texte / image ----------------------------------------- */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(24px, 4vw, 44px);
  align-items: center;
  margin-top: 36px;
}
@media (min-width: 860px) {
  .split { grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); }
  /* L'image à gauche, une rangée sur deux : sans cette alternance, la page se
     lit comme une colonne de texte flanquée d'une colonne d'images. */
  .split--mirror { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); }
  .split--mirror .split-text { order: 2; }
}
.split-text h3 { font-size: var(--step-1); font-weight: 600; margin-bottom: 12px; }
.split-text p { color: var(--ink-soft); font-size: var(--step-0); }
.split-text p + p { margin-top: 12px; }
.split-text strong { color: var(--ink); font-weight: 600; }

/* Une liste de points, sans puce de navigateur : le filet coloré à gauche
   tient le rôle de la puce et rattache la liste à la palette. */
.beats { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 11px; }
.beats li {
  padding-left: 15px;
  border-left: 2px solid var(--accent-deep);
  font-size: var(--step--1);
  color: var(--ink-soft);
}
.beats strong { color: var(--ink); font-weight: 600; }

/* --- La citation ---------------------------------------------------------
   Une seule par page. Deux citations dans une même page, et aucune ne compte. */
.pull {
  margin: clamp(28px, 4vw, 40px) 0 0;
  padding: 20px 0 20px 26px;
  border-left: 3px solid var(--accent);
  font-size: var(--step-1);
  line-height: 1.45;
  color: var(--ink);
  max-width: 46ch;
  text-wrap: balance;
}
.pull-by {
  display: block;
  margin-top: 12px;
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* --- Les encarts vidéo ---------------------------------------------------

   Cinq lecteurs YouTube intégrés, en mode sans cookie. Le cadre ne fait que
   deux choses : imposer le 16/9 et habiller le lecteur de la même arête que
   les figures de la page, pour que la vidéo se lise comme une capture d'écran
   de plus et non comme une pièce rapportée.

   Les rangées alternent gauche / droite pour que cinq encarts ne se lisent
   pas comme une liste. */
.reel {
  list-style: none;
  margin: 40px 0 0;
  padding: 0;
  display: grid;
  gap: clamp(34px, 5vw, 56px);
}
.reel-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(18px, 3vw, 32px);
  align-items: center;
}
@media (min-width: 880px) {
  .reel-item { grid-template-columns: minmax(0, 1.25fr) minmax(0, .75fr); }
  .reel-item:nth-child(even) { grid-template-columns: minmax(0, .75fr) minmax(0, 1.25fr); }
  .reel-item:nth-child(even) .video-meta { order: -1; }
}

/* overflow: hidden est ce qui fait que le lecteur, rectangulaire, prend les
   coins arrondis du cadre. Sans lui, l'arrondi de la bordure ne rogne rien. */
.video-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--canvas);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.video-frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.video-meta h3 { font-size: var(--step-1); font-weight: 600; margin-bottom: 10px; }
.video-meta p { font-size: var(--step--1); color: var(--ink-soft); }
.video-meta strong { color: var(--ink); font-weight: 600; }
.video-num {
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .12em;
  color: var(--accent-deep);
  font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
}

/* --- Le chemin d'une transaction ----------------------------------------- */
.pipeline {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 16px;
  margin-top: 36px;
}
.step {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
}
.step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-box);
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--accent);
  font-size: var(--step--1);
  font-weight: 700;
  margin-bottom: 12px;
}
.step h3 { font-size: var(--step-0); font-weight: 600; margin-bottom: 7px; }
.step p { font-size: var(--step--1); color: var(--ink-soft); }
.step strong { color: var(--ink); font-weight: 600; }

/* --- Puces de stack ------------------------------------------------------ */
.stack { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 28px; }
.chip {
  padding: 6px 13px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: var(--step--1);
  font-weight: 500;
}

/* --- Le bloc de clôture --------------------------------------------------
   La dernière chose lue avant le pied de page. Il ne résume pas : il propose
   le geste suivant. */
.closing {
  margin-top: 36px;
  padding: clamp(24px, 4vw, 36px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}
.closing p { color: var(--ink-soft); font-size: var(--step-0); max-width: 66ch; }
.closing p + p { margin-top: 12px; }
.closing strong { color: var(--ink); font-weight: 600; }

/* --- La note de sources --------------------------------------------------
   Même rôle que la ligne de réserve des cartes de l'accueil : dire d'où vient
   chaque chiffre de la page. Sur une vitrine lue par un recruteur, un chiffre
   sans source se retourne en entretien. */
.page-note {
  padding: 26px 0 0;
  border-top: 1px solid var(--line);
  margin-top: clamp(40px, 6vw, 64px);
}
.page-note h2 {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 14px;
  max-width: none;
}
.page-note ul { margin: 0; padding-left: 20px; display: grid; gap: 8px; }
.page-note li { font-size: var(--step--2); color: var(--ink-mute); }
.page-note li strong { color: var(--ink-soft); font-weight: 600; }
.page-note code {
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--surface);
  border: 1px solid var(--line);
  font-family: ui-monospace, "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
  font-size: .92em;
  color: var(--ink-soft);
}


/* ================== 11. L'APPLICATION EMBARQUÉE ==========================

   Ajoutée le 20 septembre 2026, pour la page Quality Control by Data, qui
   n'est pas une page projet ordinaire : elle EST l'application.

   Cette section n'altère aucune règle de la section 10, qui est verrouillée
   avec le gabarit des pages projets. Elle ajoute, elle ne retouche pas — la
   même logique qui avait fait naître la section 10 à côté de l'accueil.

   Deux classes seulement, plus un lien d'évitement. L'apparence de l'outil
   lui-même ne vit pas ici : elle est dans app/quality-control-by-data/src/
   style.css, dont le bloc :root recopie les jetons ci-dessus.
   -------------------------------------------------------------------------- */

/* --- Le cadre ------------------------------------------------------------
   Une arête nette et une élévation, comme une figure de la section 10 : on
   regarde un écran, et le cadre le dit. Le fond est celui du canvas, donc
   identique à celui que l'application se donne — la jointure ne se voit pas
   pendant que le cadre se charge. */
.app-frame {
  margin-top: 30px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--canvas);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.app-frame iframe {
  display: block;
  width: 100%;
  border: 0;
  /* La hauteur réelle est posée par assets/embed.js, à partir de celle que
     l'application mesure. L'attribut height du HTML sert de repli. */
}

/* --- La ligne sous le cadre ---------------------------------------------- */
.app-aside {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 18px;
  font-size: var(--step--2);
  color: var(--ink-mute);
}

/* --- Sauter l'application au clavier -------------------------------------
   .skip, en section 3, est ancrée en haut du document : elle ne peut pas
   servir ici. Celle-ci se cale sur son conteneur, et suit le même principe —
   hors de vue tant qu'elle n'a pas le focus, puis franchement visible.

   Sans elle, tabuler depuis le haut de la page traverserait les quelques
   centaines d'éléments interactifs de l'outil avant d'atteindre la suite. */
.skip-app {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.skip-app:focus-visible {
  position: static;
  display: inline-block;
  width: auto;
  height: auto;
  clip-path: none;
  margin-top: 20px;
  padding: 10px 18px;
  background: var(--accent);
  color: #0A1622;
  font-size: var(--step--1);
  font-weight: 600;
  border-radius: var(--radius-sm);
  text-decoration: none;
}

/* --- Imprimer ------------------------------------------------------------
   Deux impressions très différentes partent de cette page.

   1. L'utilisateur fait Ctrl+P : il veut la page. Le cadre n'a alors rien à
      y faire — une application interactive ne s'imprime pas, et son @media
      print interne sortirait le rapport de l'outil au milieu du texte, en
      palette claire sur une page sombre. Il est donc masqué, et remplacé par
      une ligne qui dit où le retrouver.

   2. L'utilisateur clique « Imprimer / PDF » DANS l'outil : il veut le
      rapport. L'application prévient alors assets/embed.js, qui pose
      .printing-app sur <html> — et là c'est l'inverse, la page s'efface et
      le cadre reste seul. Le @media print de l'application s'applique à son
      contenu : palette claire, rapport seul. Vérifié au rendu PDF. */
@media print {
  .app-frame { display: none; }
  .app-frame + .app-aside::after {
    content: "L'outil de cette page est interactif : il ne s'imprime pas. Son rapport de contrôle, lui, s'imprime depuis son propre bouton « Imprimer / PDF ».";
    display: block;
    margin-top: 6px;
  }
  html[lang="en"] .app-frame + .app-aside::after {
    content: "The tool on this page is interactive and does not print. Its control report does, from its own \201CPrint / PDF\201D button.";
  }

  /* Cas 2 : l'application a demandé l'impression de son rapport. */
  .printing-app body > *:not(main),
  .printing-app main > section:not(#outil),
  .printing-app #outil > .wrap > *:not(.app-frame) { display: none !important; }
  .printing-app .app-frame {
    display: block;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: #fff;
    margin: 0;
  }
  .printing-app .section { padding: 0; border: none; background: none; }
  .printing-app .wrap { width: 100%; }
}


/* ==================== 12. LA PAGE CERTIFICATIONS =========================

   Tout ce qui suit ne sert QU'À `certifications.html`. Aucune autre page ne
   charge une seule de ces règles.

   Pourquoi une section de plus, et pas des ajouts dans la section 10 : la
   section 10 est close avec le gabarit des pages projets, et cette page n'est
   pas une page projet. C'est la même logique qui a fait naître la section 11
   à côté de l'application embarquée.

   Trois motifs seulement sont neufs ici. Tout le reste — .card, .chip,
   .section, .kpis, .page-note — est repris tel quel :

     1. La carte d'attestation, parce qu'un badge officiel Microsoft est une
        image claire qu'on ne peut pas poser nue sur un fond sombre.
     2. L'accordéon, parce que dix-huit cours déroulés d'un bloc font un mur
        de texte que personne ne lit.
     3. L'encadré de cours, la brique que l'accordéon contient.
     4. Un titre au-dessus du bandeau de chiffres, parce que cette page est la
        seule où il en porte un.
   -------------------------------------------------------------------------- */


/* --- Le titre du bandeau de chiffres -------------------------------------
   Partout ailleurs, le bandeau .kpis suit immédiatement un texte d'ouverture
   et n'a pas besoin d'être annoncé. Ici il ouvre la page à lui seul : la
   carte qui le précédait a été supprimée le 21 septembre 2026, et son titre
   est resté.

   Un cran SOUS le h1, jamais à la taille d'un h2 de section : deux titres de
   même poids à trente pixels l'un de l'autre se disputeraient l'entrée de la
   page. */
.kpis-title {
  margin-top: clamp(30px, 4vw, 46px);
  font-size: var(--step-1);
  font-weight: 600;
  color: var(--ink);
  max-width: none;
}

/* --- Les cartes d'attestation Microsoft ----------------------------------
   Trois pistes, là où .cards en impose deux : ces cartes portent un badge et
   trois lignes, pas un paragraphe. La mesure de lecture n'est donc pas le
   facteur limitant, et trois côte à côte se lisent comme une collection —
   ce qu'elles sont. */
.creds {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  margin-top: 36px;
}
@media (min-width: 680px) {
  .creds { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 980px) {
  .creds { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Le badge officiel est un SVG à fond transparent dont le texte est gris
   foncé : posé directement sur --surface, la moitié de ce qu'il dit
   disparaît. La pastille claire lui rend le fond pour lequel il a été
   dessiné, et se lit comme un badge posé sur la carte. On ne retouche jamais
   le fichier du badge : c'est une pièce officielle. */
.cred-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 104px;
  height: 104px;
  border-radius: var(--radius);
  background: #F4F8FB;
  border: 1px solid var(--line);
  padding: 8px;
  flex: none;
}
.cred-badge img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Le code d'examen. Puce d'accent plutôt que titre : c'est une référence,
   pas un nom — et c'est par ce code qu'un recruteur reconnaît la
   certification avant d'en avoir lu l'intitulé. */
.cred-code {
  align-self: flex-start;
  padding: 4px 11px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 42%, transparent);
  color: var(--accent);
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .07em;
}

/* L'intitulé officiel ne se traduit pas : il est déposé en anglais, et c'est
   sous cette forme qu'il se vérifie. Il ne porte donc aucun bloc data-lang. */
.cred-name {
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}
.cred-date {
  font-size: var(--step--2);
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}


/* --- L'accordéon ---------------------------------------------------------

   <details> et <summary> natifs : ouverture et fermeture, focus clavier et
   restitution aux lecteurs d'écran sont donnés par le navigateur. Aucune
   ligne de JavaScript, et la page reste utilisable si le script du site ne
   s'exécute pas — ce qui vaut mieux quand le contenu masqué est l'essentiel
   de la page.

   Conséquence de conception assumée : le <summary> ne contient AUCUN lien.
   Un lien dans un summary se dispute le clic avec l'ouverture du bloc, et se
   perd dans la restitution vocale. Le lien vers l'attestation du certificat
   vit donc en tête du contenu déplié. */
.acc-list {
  display: grid;
  gap: 14px;
  margin-top: 36px;
}

/* overflow: hidden fait que le contenu déplié prend les coins du cadre. */
.acc {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.acc[open] { border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); }

/* list-style et ::-webkit-details-marker retirent le triangle natif, que le
   chevron remplace. Les deux déclarations sont nécessaires : la première pour
   Firefox et Chrome récents, la seconde pour Safari. */
.acc > summary {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 17px 20px;
  cursor: pointer;
  list-style: none;
  -webkit-tap-highlight-color: transparent;
}
.acc > summary::-webkit-details-marker { display: none; }
.acc > summary:hover { background: var(--surface-2); }
.acc > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Même géométrie que .step-num et .card-icon : les trois motifs de pastille
   du site se lisent comme une seule famille. */
.acc-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-box);
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--accent);
  font-size: var(--step--1);
  font-weight: 700;
  flex: none;
}
.acc[open] .acc-num { background: var(--accent); border-color: var(--accent); color: #0A1622; }

/* min-width: 0 est ce qui autorise le titre à se couper : sans lui, un
   élément de grille refuse de passer sous la largeur de son contenu, et la
   ligne déborde du cadre sur téléphone. */
.acc-head { flex: 1; min-width: 0; }
.acc-title {
  display: block;
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink);
  text-wrap: pretty;
}
.acc-meta {
  display: block;
  margin-top: 3px;
  font-size: var(--step--2);
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}

/* Le chevron. Il pointe à droite quand le bloc est fermé, vers le bas quand
   il est ouvert : c'est la seule chose qui dit à un lecteur qu'il y a
   quelque chose à ouvrir. */
.acc-chev {
  width: 15px;
  height: 15px;
  flex: none;
  fill: none;
  stroke: var(--ink-mute);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform .18s ease, stroke .18s ease;
}
.acc > summary:hover .acc-chev { stroke: var(--accent); }
.acc[open] .acc-chev { transform: rotate(90deg); stroke: var(--accent); }

.acc-body {
  padding: 4px 20px 20px;
  border-top: 1px solid var(--line);
  margin-top: -1px;
}
.acc-intro {
  margin: 16px 0 0;
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: 68ch;
}
.acc-intro strong { color: var(--ink); font-weight: 600; }


/* --- L'encadré d'un cours ------------------------------------------------
   Posé sur --surface-2, un cran plus clair que l'accordéon qui le contient :
   il faut que dix-huit encadrés se détachent de leur fond sans qu'on ait
   ajouté une ombre de plus. */
.courses {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: grid;
  gap: 12px;
}
.course {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-box);
  padding: 16px 18px;
}
.course-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
}
.course-num {
  font-size: var(--step--2);
  font-weight: 700;
  color: var(--accent-deep);
  font-variant-numeric: tabular-nums;
  flex: none;
}

/* Le titre du cours EST le lien vers son attestation. L'encadré entier ne
   l'est pas : un lien qui enveloppe un paragraphe et cinq puces se fait lire
   en entier par un lecteur d'écran avant qu'on sache où il mène. */
.course-link {
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  text-wrap: pretty;
}
.course-link:hover { color: var(--accent); text-decoration: underline; }

.course-meta {
  margin-top: 7px;
  font-size: var(--step--2);
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}
.course p {
  margin-top: 9px;
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: 72ch;
}
.course p strong { color: var(--ink); font-weight: 600; }

/* .stack et .chip viennent de la section 10, où ils habillent une pile
   technique sous un titre de section. Ici ils tiennent dans un encadré : on
   resserre, et on recule le fond d'un cran pour que la puce reste lisible sur
   --surface-2. Ni .stack ni .chip ne sont redéfinis ailleurs. */
.course .stack { margin-top: 13px; gap: 7px; }
.course .chip {
  background: var(--canvas);
  font-size: var(--step--2);
  padding: 4px 10px;
}

/* Le total, sous la liste. Il répond à la question que dix-huit encadrés
   posent : combien, en tout. */
.tally {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 16px;
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}
.tally-figure {
  font-size: var(--step-2);
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.tally-label {
  font-size: var(--step--1);
  color: var(--ink-mute);
  max-width: 62ch;
}


/* --- Mouvement réduit ----------------------------------------------------
   La section 9 coupe les animations du graphe de l'accueil, qui n'existent
   pas ici. La rotation du chevron, elle, est propre à cette page : elle se
   coupe donc ici. L'état reste lu — le chevron pointe toujours vers le bas
   quand le bloc est ouvert, il y arrive simplement sans transition. */
@media (prefers-reduced-motion: reduce) {
  .acc-chev { transition: none; }
}


/* ============= 13. LA PAGE MATERIALS EXPLORER ============================

   Une seule règle, et elle ne sert qu'à l'impression.

   La section 11 est écrite pour la première page qui embarquait une
   application. Elle y masque le cadre à l'impression — une application
   interactive ne s'imprime pas — et le remplace par une ligne qui dit où
   retrouver le rapport, depuis le bouton « Imprimer / PDF » de l'outil.

   Materials Explorer ne produit pas de rapport et n'a pas ce bouton. Le texte
   de la section 11 y serait donc faux, et il renverrait le lecteur vers une
   commande qui n'existe pas. Cette page pose `.app-aside--noreport`, et la
   règle ci-dessous y substitue la phrase juste.

   Pourquoi une section de plus, et pas une correction de la section 11 : la
   section 11 est verrouillée avec la page Quality Control by Data depuis le
   20 septembre 2026. Elle ajoute, elle ne se retouche pas — la même logique
   qui avait fait naître la section 10 à côté de l'accueil, et la 11 à côté
   de la 10.
   -------------------------------------------------------------------------- */

@media print {
  .app-frame + .app-aside--noreport::after {
    content: "L'outil de cette page est interactif : il ne s'imprime pas. Il s'ouvre à l'adresse de cette page, dans un navigateur.";
  }
  html[lang="en"] .app-frame + .app-aside--noreport::after {
    content: "The tool on this page is interactive and does not print. Open this page in a browser to use it.";
  }
}

/* ================= 14. DEUX CORRECTIFS DE SECTIONS CLOSES ================

   Deux défauts relevés le 22 septembre 2026 en alignant la page Montreal
   Housing sur le reste du site, et corrigés à la demande de Sébastien. Ils
   vivent ici, et non dans les sections 4 et 5, parce que celles-ci sont
   closes : elles s'ajoutent, elles ne se retouchent pas. Même logique que la
   naissance des sections 10, 11 et 13.

   Ces deux règles repeignent des pages verrouillées — l'accueil et les quatre
   pages projets. Cette réouverture est la troisième depuis le 20 septembre,
   et elle est aussi explicite que les deux premières.
   -------------------------------------------------------------------------- */

/* --- 14.1 Le décalage du chiffre, déclaré cette fois ----------------------
   Toute carte KPI du site est bâtie ainsi : un <dt> pour l'étiquette, deux
   <dd> pour le chiffre et sa réserve. Or le navigateur applique d'office
   `margin-inline-start: 40px` à un <dd>, et aucune règle du site ne la
   remettait à zéro : les DEUX <dd> partaient 40 px à droite de l'étiquette.

   Sébastien a tranché le 22 septembre 2026, et sa lecture est juste : ce
   décalage aère la carte. Mais il n'aérait que le chiffre — sur la réserve,
   il coûtait de la mesure de lecture là où il y en a le moins. Une carte
   tombe à 173 px de large sur un écran de 390 px, et 40 px y pèsent 28 %
   de la largeur utile : la réserve passait de 23 à 17 caractères par ligne.

   D'où la répartition retenue : le chiffre garde ses 40 px, la réserve
   reprend toute la largeur. Et la valeur est ÉCRITE — ce n'est plus une
   marge de navigateur héritée par accident, que la moindre remise à zéro
   future ferait disparaître sans prévenir. */
.kpi dd { margin: 0; }
.kpi .card-figure { margin-inline-start: 40px; }

/* --- 14.2 Les icônes dessinées au trait dans un bouton --------------------
   `.btn-icon` pose `fill: currentColor`. C'est une propriété héritée, qui
   traverse le <use> et atteint le contenu cloné du <symbol>. Les deux
   marques, LinkedIn et GitHub, sont des silhouettes pleines : elles s'en
   accommodent. `#icon-mail`, lui, est dessiné au trait — un <rect> et une
   polyligne, sans attribut `fill` — et se remplissait donc en bloc plein.
   Le bouton « Me contacter » affichait un rectangle arrondi clair, pas une
   enveloppe.

   `.crumbs-icon` de la section 10 pose déjà `fill: none` et
   `stroke: currentColor` : la feuille connaissait le motif, `.btn-icon` ne
   l'appliquait pas.

   `:has()` évite d'ajouter une classe dans le HTML de quatre pages
   verrouillées. Baseline « largement disponible » depuis décembre 2025,
   trente mois après Firefox 121 — aucune réserve en 2026. */
.btn-icon:has(use[href="#icon-mail"]) {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ========================= 15. LA PAGE 404 ==============================

   Quatre règles, pour une seule page, et elles ne touchent à rien d'autre :
   `404.html` est la seule page du site à porter `class="page-404"` sur son
   <body>. Toutes sont préfixées par cette classe, qui n'apparaît nulle
   part ailleurs : aucune section close n'est touchée — même réflexe qu'aux
   sections 11, 13 et 14.

   --- 15.1 Le message tient au centre de l'écran ---------------------------

   C'est la seule page du site SANS PIED DE PAGE. Décision de Sébastien : ses
   trois boutons ouvraient trois sorties concurrentes de celle qu'on veut
   faire prendre, et une page d'erreur n'a qu'un travail — ramener à l'accueil,
   vite.

   Ce qui reste tient en trois éléments. Dans un <body> ordinaire, ils se
   posent sous l'en-tête et laissent le reste de la fenêtre en fond nu : le
   message a l'air tombé en haut d'une page vide, sur celle du site qui a le
   moins le droit de ressembler à quelque chose de cassé. Le <main> prend donc
   toute la hauteur disponible et centre ce qu'il contient.

   `svh` et non `vh` : sur mobile, `vh` se cale sur la fenêtre barre d'adresse
   rétractée, donc 100vh dépasse toujours l'écran visible au chargement et
   ajoute une barre de défilement à une page qui n'a rien à faire défiler.
   `svh` prend la plus petite des deux hauteurs. Baseline « largement
   disponible » depuis 2023.

   `grid` plutôt que `flex` sur le <main> : un enfant de grille s'étire en
   largeur par défaut, ce qui laisse `.wrap` calculer sa largeur et se centrer
   comme sur toutes les autres pages. En flex, il aurait fallu le lui
   redemander. */
.page-404 {
  display: flex;
  flex-direction: column;
  min-height: 100svh;
}
.page-404 main {
  flex: 1 0 auto;
  display: grid;
  align-content: center;
}

/* --- 15.2 Le constat en grand --------------------------------------------
   `--step-3` plafonne à 3rem : c'est la taille d'un titre de page projet, qui
   porte une promesse en une phrase. Ici le titre porte deux mots et un
   nombre, et il doit se lire avant tout le reste — c'est lui qui dit au
   visiteur, en une fraction de seconde, que le site n'est pas cassé mais
   qu'il s'est trompé de porte.

   Le rapport à `--step-3` est de 1,53× au plafond, au-dessus du pas de 1,25×
   de l'échelle : un cran de plus, pas une rupture. La graisse, le crénage et
   la couleur restent ceux de `.p-hero h1`, en section 10 — seule la taille
   change, et la chasse des chiffres d'Inter fait le reste. */
.page-404 .p-hero h1 {
  font-size: clamp(2.6rem, 1.9rem + 3.4vw, 4.6rem);
}

/* --- 15.3 Tout est centré -------------------------------------------------
   Demande de Sébastien. Le reste du site aligne à gauche, et il a raison de
   le faire : une page qui se lit ligne après ligne a besoin d'un bord de
   départ stable. Celle-ci ne se lit pas, elle se constate — trois éléments,
   aucun paragraphe, et le regard tombe au milieu de l'écran puisque le
   <main> y centre déjà son contenu en hauteur. Un bloc centré verticalement
   mais aligné à gauche se lit comme un défaut de mise en page.

   `text-align` ne suffit pas, et c'est le piège de ce genre de correction :
   `.p-hero h1` et `.p-lede` portent une largeur maximale, en section 10. Leur
   BOÎTE reste donc collée à gauche du conteneur, et seul le texte se centre à
   l'intérieur — un titre de 17ch centré dans ses 17ch, posé à gauche d'un
   conteneur de 1120px. `margin-inline: auto` centre la boîte elle-même.

   `.cta-row` est une rangée flex : elle se centre par `justify-content`, pas
   par `text-align`. */
.page-404 .p-hero { text-align: center; }
.page-404 .p-hero h1,
.page-404 .p-lede { margin-inline: auto; }
.page-404 .cta-row { justify-content: center; }

/* ====================== 16. LA PAGE CONTACT ==============================

   Réservée à `contact.html`. Aucune règle ne touche une section close : tout
   est préfixé par une classe qui n'existe que sur cette page — même réflexe
   qu'aux sections 11, 13, 14 et 15.

   C'est la seule page du site qui demande quelque chose au visiteur. Les neuf
   autres se lisent ; celle-ci se remplit, et un champ mal dessiné coûte un
   message perdu là où un paragraphe mal dessiné ne coûte qu'un peu de confort.

   --- 16.1 La colonne du formulaire ---------------------------------------

   UNE SEULE COLONNE, et bornée. La page a d'abord porté une colonne de
   raccourcis à droite ; elle a été retirée le 23 septembre 2026, ses deux
   boutons faisant doublon avec ceux du pied de page.

   `65ch` n'est pas une largeur de lecture, c'est une largeur de SAISIE. Un
   champ qui traverse 1120 px oblige l'œil à repartir de très loin à chaque
   ligne, et surtout il ment sur ce qu'on attend : un champ large appelle une
   réponse longue. Le formulaire reste calé à gauche, sous le titre de sa
   section. */
.form-col { max-width: 65ch; }

/* --- 16.1b Le portrait à côté du titre -----------------------------------

   Demande de Sébastien : un visage donne envie d'écrire là où un formulaire
   seul n'engage personne.

   `.avatar` EST REPRISE DE LA SECTION 5, QUI EST CLOSE, et elle n'y est pas
   touchée : le disque, la bordure et le fond viennent de là, et c'est ce qui
   garantit que le portrait a exactement la même facture qu'à l'accueil. Seule
   la taille est redéfinie ici, sous `.h2-avatar .avatar` — deux classes contre
   une, donc la surcharge l'emporte sans qu'un `!important` soit nécessaire.

   Pourquoi la redéfinir : à l'accueil l'avatar accompagne un `h1` de
   `--step-3`, jusqu'à 3rem, et il monte donc à 84 px. Ici il accompagne un
   `h2` de `--step-2`, qui plafonne à 2,1rem. Le garder à 84 px ferait un
   portrait plus haut que le titre qu'il accompagne — la vignette prendrait le
   pas sur la phrase, et c'est la phrase qui porte l'action.

   `flex-wrap` n'est pas posé, volontairement : le titre fait deux mots, et
   même à 390 px le couple tient sur une ligne. Le faire passer dessous
   laisserait un portrait seul au-dessus d'un mot seul. */
.h2-avatar {
  display: flex;
  align-items: center;
  gap: clamp(12px, 2vw, 16px);
}
.h2-avatar .avatar {
  width: clamp(56px, 6vw, 70px);
  height: clamp(56px, 6vw, 70px);
}

/* --- 16.1c Le creux avant le formulaire ----------------------------------

   `.section` réserve jusqu'à 92 px en haut, et `.p-hero` jusqu'à 52 px en
   bas : près de 150 px séparaient la rangée de boutons du titre du
   formulaire. Les deux valeurs sont justes ailleurs, où les sections sont
   longues et denses et ont besoin de cette respiration entre elles. Ici la
   page n'a qu'une section, et l'ouverture qui la précède a perdu son
   paragraphe d'accroche le 23 septembre 2026 : le creux ne séparait plus
   rien, il se lisait comme un blanc oublié.

   Le sélecteur est l'IDENTIFIANT de la section, unique à cette page : la
   section 10 n'est pas touchée, et sa spécificité l'emporte sans artifice.
   Seul le haut change — le bas continue de séparer le formulaire du pied de
   page, ce qui reste nécessaire. */
#ecrire { padding-top: clamp(28px, 4vw, 52px); }

/* --- 16.2 Les champs -----------------------------------------------------

   L'étiquette est AU-DESSUS du champ, jamais dedans. Une étiquette flottante
   disparaît à la saisie : le visiteur qui relit son formulaire avant d'envoyer
   ne sait plus ce que contient chaque case, et celui qui revient après une
   erreur encore moins.

   `font-size: max(16px, …)` sur les champs, et c'est le piège mobile à ne
   jamais rouvrir : sous 16 px, iOS zoome automatiquement au premier appui
   dans un champ, la page part de travers et ne revient pas seule. La valeur
   est un plancher, pas une taille — l'échelle du site reprend la main dès
   qu'elle passe au-dessus. */
.field { display: grid; gap: 7px; margin-bottom: 18px; }
.field-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0 18px;
}
@media (min-width: 620px) {
  .field-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.field > label {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--ink-soft);
}
.field-hint {
  font-size: var(--step--2);
  color: var(--ink-mute);
  font-weight: 400;
}

/* L'asterisque des champs obligatoires. À L'ACCENT, PAS AU ROUGE : sur ce
   site `--warn` signale une anomalie, et un champ obligatoire n'en est pas
   une. La couleur ne porte de toute facon aucune information seule -- la
   legende en tete de formulaire dit ce que le signe veut dire, et
   l'attribut `required` le dit aux lecteurs d'ecran. */
.req {
  color: var(--accent);
  font-weight: 700;
  margin-left: .25em;
}
.form-legend {
  margin-bottom: 20px;
  font-size: var(--step--2);
  color: var(--ink-mute);
}
.form-legend .req { margin: 0 .3em 0 0; }

.field input,
.field textarea {
  width: 100%;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--canvas);
  color: var(--ink);
  font-family: inherit;
  font-size: max(16px, var(--step--1));
  line-height: 1.45;
  transition: border-color .16s var(--ease-out), background .16s var(--ease-out);
}
.field textarea { resize: vertical; min-height: 168px; }
.field input:hover,
.field textarea:hover { border-color: var(--accent-deep); }
.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--surface);
}
.field input::placeholder,
.field textarea::placeholder { color: var(--ink-mute); opacity: .7; }

/* Le fond plus sombre que la surface environnante est ce qui dit « on écrit
   ici ». L'inverse — un champ plus clair que son conteneur — se lit comme un
   bloc de texte déjà rempli. */

/* --- 16.3 Le champ que personne ne voit ----------------------------------

   Il est sorti de l'écran plutôt que masqué par `display: none` : un champ
   sans boîte est facile à détecter pour un automate, et c'est précisément lui
   qu'on cherche à laisser remplir.

   `aria-hidden` et `tabindex="-1"` sont posés dans le HTML : sans eux, un
   lecteur d'écran annoncerait un champ invisible et une tabulation y
   tomberait. Le piège ne doit gêner personne.

   Son nom n'apparaît nulle part dans le texte de la page — ni dans la section
   qui raconte le trajet du message. Le citer reviendrait à publier la seule
   information qui le désamorce. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- 16.4 Le bouton d'envoi et le défi, sur une rangée -------------------

   Demande de Sébastien du 23 septembre 2026 : les deux côte à côte, alignés
   sur le même axe. Ce n'est pas qu'une question de place. Empilé au-dessus du
   bouton, le badge se lit comme une étape à franchir avant d'envoyer ; posé à
   côté, il redevient ce qu'il est — une mention. Et il ne l'est pas par
   euphémisme : Turnstile résout son épreuve seul dès le chargement de la page,
   le bouton n'attend rien de lui.

   `align-items: center` est ce qui les met sur l'axe demandé : les deux boîtes
   n'ont pas la même hauteur — 44 px environ pour le bouton, 65 pour le widget
   — et sans cette ligne elles s'aligneraient par le haut, ce qui se lirait
   comme un défaut de calage.

   `flex-wrap` plutôt qu'une requête de média : à 390 px, le bouton et le
   widget demandent ensemble plus de 500 px. La rangée se replie alors d'elle
   même, sans qu'aucun seuil n'ait à être deviné — et le badge reprend sa place
   sous le bouton, ce qui est la bonne disposition à cette largeur.

   L'ORDRE DU DOM COMPTE, et il est le bon : le bouton vient en premier, donc
   il est atteint en premier à la tabulation comme à la lecture d'écran. Ne
   pas l'inverser pour obtenir le badge à gauche — cela se réglerait par
   `order`, jamais en déplaçant la balise. */
.form-send {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
}

/* Hauteur réservée d'avance : le widget se charge après la page. La largeur,
   elle, ne l'est PAS — et c'est délibéré. Un slot vide de 300 px de large
   pousserait la rangée à se replier avant même que le widget existe, et le
   bouton descendrait d'un cran au chargement, au moment précis où le visiteur
   vise. En le laissant à zéro, le bouton est posé à gauche dès le départ et
   ne bouge plus : c'est le widget qui vient se ranger à côté. */
.turnstile-slot { min-height: 68px; }

/* --- 16.5 Le bouton pendant l'envoi --------------------------------------
   Deux libellés dans le même bouton, chacun dans ses deux langues, et c'est
   le CSS qui choisit — comme pour tout le reste du site. Le JavaScript ne fait
   que poser une classe.

   Le bouton reste désactivé tant que `contact.js` ne s'est pas exécuté : un
   bouton qui ne répond pas au clic est pire qu'un bouton visiblement éteint. */
.form .btn-busy { display: none; }
.form.is-sending .btn-idle { display: none; }
.form.is-sending .btn-busy { display: inline; }
.form button[type="submit"][disabled] { opacity: .55; cursor: not-allowed; }
.form button[type="submit"][disabled]:hover {
  border-color: var(--accent);
  background: var(--accent);
}

/* Envoi réussi : le formulaire s'efface au profit du message. Le garder à
   l'écran, vidé, donne l'impression que rien n'est parti ; le garder rempli
   invite à cliquer une seconde fois. */
.form.is-done { display: none; }

/* --- 16.6 Les messages d'état --------------------------------------------
   Tous les cas vivent dans le HTML, tous masqués, et le script en révèle un.
   Ajouter un cas d'erreur est donc une addition dans la page, pas dans le
   script — et `tools/check_page.py` vérifie que chacun porte bien ses deux
   langues.

   Le conteneur n'a ni bordure ni fond tant qu'aucun message n'est affiché :
   un cadre vide en permanence sous un formulaire se lit comme une erreur
   qu'on n'arrive pas à lire. */
.form-status { display: none; }
.form-status.is-active {
  display: block;
  margin-bottom: 20px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--ink-soft);
}
.form-msg { display: none; }
.form-msg.is-shown { display: block; }
.form-msg strong { color: var(--ink); font-weight: 600; }

/* La couleur du filet gauche porte l'issue. Elle ne porte JAMAIS l'information
   à elle seule : chaque message commence par un mot en gras qui dit ce qui
   s'est passé, et reste donc lisible en noir et blanc comme en dichromatie. */
.form-status.is-active:has(.form-msg[data-code="ok"].is-shown) {
  border-left-color: var(--good);
}
.form-status.is-active:has(.form-msg.is-shown:not([data-code="ok"])) {
  border-left-color: var(--warn);
}

/* --- 16.7 Les liens au fil du texte -------------------------------------
   Dans un message d'état ou dans le bloc sans JavaScript. Ils mènent tous à
   LinkedIn : depuis le 23 septembre 2026, aucune adresse de courriel n'est
   écrite sur cette page, et LinkedIn est la seule voie de secours si le
   formulaire tombe. Un lien de secours doit se voir sans être un bouton. */
.form-msg a,
.noscript-note a {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in oklab, var(--accent) 45%, transparent);
}
.form-msg a:hover,
.noscript-note a:hover { border-bottom-color: var(--accent); }

/* --- 16.8 Sans JavaScript ------------------------------------------------
   Le formulaire ne peut pas fonctionner : le défi anti-robot est lui-même du
   JavaScript. Plutôt que d'afficher des champs qui ne répondront pas, on
   affiche le bloc ci-dessous — et le formulaire reste, mais son bouton est
   désactivé et le dit.

   Toutes les autres pages du site restent entières sans JavaScript. Celle-ci
   est la seule exception, et c'est pour cela que l'adresse figure déjà dans
   l'ouverture de la page. */
.noscript-note {
  margin-bottom: 22px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--ink-soft);
}

/* ========================= 17. LA PAGE CV ================================

   Un diagramme en arbre posé sur le plan de travail de l'accueil. Tout ce qui
   suit ne sert qu'à `cv.html` : chaque règle passe par une classe qui
   n'existe que dans cette page (.tree, .tree-*, .board-action,
   .board-crumb-mid, .board-crumb-gap), et aucune section close n'est
   retouchée — même réflexe qu'aux sections 11, 13, 14 et 16. Les classes
   reprises d'ailleurs (.board, .node, .node--trigger, .node--cta) le sont
   TELLES QUELLES ; ce qui doit s'en écarter est ajusté ici, sous .tree,
   jamais à la source.

   DEUX GÉOMÉTRIES POUR UN SEUL HTML.
     sous 1000 px  l'arbre pousse VERS LE BAS : un tronc sous la tuile, un
                   coude arrondi vers chaque enfant. Les coudes sont des
                   bordures CSS, donc ils suivent le texte quand il passe à
                   la ligne : rien n'est mesuré.
     dès 1000 px   l'arbre pousse VERS LA DROITE, en courbes de Bézier comme
                   les câbles de l'accueil. La géométrie y est FIXE : chaque
                   poste occupe un créneau de hauteur constante, déplié ou
                   non, ce qui permet d'écrire les courbes une fois pour
                   toutes dans le HTML, et évite que l'arbre ne saute quand
                   une branche s'ouvre. Le seuil est celui du plan de travail
                   de l'accueil.
   -------------------------------------------------------------------------- */

/* --- 17.1 La barre : le chemin du sous-workflow, et un seul contrôle ------ */

/* Sous 640 px, le segment du milieu cède sa place à des points de
   suspension, comme n8n abrège un fil trop long : la barre garde le départ
   (qui) et l'arrivée (où l'on est). */
.board-crumb-gap { display: none; }
@media (max-width: 639px) {
  .board-crumb-mid { display: none; }
  .board-crumb-gap { display: inline; color: var(--ink-mute); }
}

.board-action {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: 5px 12px 5px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 86%, var(--canvas));
  color: var(--ink-soft);
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  transition: border-color .16s var(--ease-out),
              background .16s var(--ease-out),
              color .16s var(--ease-out);
}
.board-action:hover,
.board-action:focus-visible {
  border-color: var(--accent-deep);
  background: var(--surface-2);
  color: var(--ink);
}
.board-action-state { display: inline-flex; align-items: center; gap: 6px; }
.board-action-state--open,
.board-action[data-state="open"] .board-action-state--closed { display: none; }
.board-action[data-state="open"] .board-action-state--open { display: inline-flex; }
.board-action-icon {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Les deux ancres de la page s'arrêtent sous la barre collée. Le plan de
   travail n'a pas de creux en tête, à la différence des sections des autres
   pages : sans cette marge, l'onglet « Les CV » — et l'arrivée sur la page,
   voir assets/cv-arrivee.js — le feraient glisser sous la barre. */
#cv,
#profil { scroll-margin-top: 78px; }

/* --- 17.2 La charpente --------------------------------------------------- */

.tree {
  --tile: 52px;               /* la tuile de l'accueil, section 7 */
  --branch: var(--accent-deep);
  --elbow-r: 12px;
  list-style: none;
  margin: 0;
  padding: 0;
}
/* :where() ramène la remise à zéro à une spécificité nulle : sans lui,
   `.tree ul` l'emporterait sur les marges de .tree-postes et .tree-cv, qui
   portent toute la géométrie des branches. */
:where(.tree ul) { list-style: none; margin: 0; padding: 0; }

/* Les nœuds sont des <button> : on retire au navigateur tout ce qu'il y
   ajoute, pour que la forme vienne du plan de travail et de lui seul. */
.tree-node {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* --- 17.3 Les nœuds ------------------------------------------------------ */

/* La racine reprend le nœud de l'accueil. Trois ajustements : pas de retrait
   de gouttière, puisqu'il n'y a pas de rail ici ; un libellé à la taille du
   texte courant, puisqu'il porte l'appel à l'action ; et la DEL taillée à la
   forme du déclencheur, bord gauche arrondi. Elle passe au-dessus du tronc
   (z-index), qui démarre ainsi sous la tuile et non à côté. */
.tree .node {
  position: relative;
  z-index: 1;
  width: 100%;
  margin-left: 0;
  /* Dès 1000 px, la section 7 passe les nœuds du graphe en colonne,
     libellé sous la tuile. Ici le libellé reste À CÔTÉ à toutes les
     largeurs : c'est de son bord droit que partent les branches. */
  flex-direction: row;
  align-items: center;
  align-self: center;         /* la section 7 pose align-self: start */
  gap: 12px;
  text-align: left;
}
.tree .node .node-label {
  font-size: var(--step-0);
  line-height: 1.3;
  text-wrap: balance;
}
.tree .node .node-sub { margin-top: 3px; }
.tree .node--cta .node-tile::before {
  border-radius: 999px calc(var(--radius-box) + 2px) calc(var(--radius-box) + 2px) 999px;
}
.tree .node--cta .node-tile::after {
  border-radius: 999px calc(var(--radius-box) - 1px) calc(var(--radius-box) - 1px) 999px;
}
.tree .node:hover .node-label,
.tree .node:focus-visible .node-label {
  text-decoration: underline;
  text-decoration-color: var(--accent-deep);
  text-underline-offset: 4px;
}
/* Le geste est fait : la DEL s'éteint dès que l'arbre est déplié. */
.tree .node[aria-expanded="true"] .node-tile::before { animation: none; background: none; }

/* Le fil qui mène au compteur, puis le compteur lui-même : le vocabulaire
   d'une carte heuristique. Replié, il annonce combien de CV la branche
   porte. Déplié, il devient le nœud d'où partent les branches — un anneau
   creux, à leur couleur. Le chiffre n'a plus rien à annoncer : les enfants
   sont là. Le passage ne tient pas à la couleur seule : plein contre creux,
   22 px contre 12.

   La pastille se contracte par transform, pas par width : sa boîte garde ses
   22 px, donc son centre ne bouge pas — c'est de lui que partent les courbes
   — et rien n'est recalculé dans la mise en page pendant l'animation.
   L'anneau passe à 3,6 px de bord pour en garder 2 à l'échelle 0,55, la
   même épaisseur que les branches. */
.tree-stub {
  flex: 1 1 16px;
  min-width: 16px;
  height: 2px;
  border-radius: 1px;
  background: var(--branch);
}
/* Le fil TOUCHE sa pastille : il rattrape l'écart du conteneur (12 px sur la
   racine, 10 sur un poste) plus une demi-pastille, et court dessous jusqu'à
   son centre. Pleine, la pastille le couvre ; contractée en anneau, elle
   laisse voir le fil arriver jusqu'à son bord. */
.tree .node .tree-stub { margin-right: -23px; }
.tree-role .tree-stub { margin-right: -21px; }
.tree-count {
  position: relative;
  z-index: 1;
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border: 2px solid var(--accent);
  border-radius: 999px;
  background: var(--accent);
  color: #0A1622;
  font-size: .72rem;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  transition: transform .24s var(--ease-out),
              background-color .24s var(--ease-out),
              border-color .24s var(--ease-out);
}
.tree-node[aria-expanded="true"] > .tree-count {
  transform: scale(.55);
  border-width: 3.6px;
  border-color: var(--branch);
  background: var(--canvas);
  color: transparent;
  font-size: 0;
}

/* Un poste : l'intitulé du CV, puis son sous-titre, mot pour mot. */
.tree-role {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 0;
}
.tree-body { min-width: 0; }
.tree-label {
  display: block;
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  transition: color .16s var(--ease-out);
}
.tree-sub {
  display: block;
  margin-top: 2px;
  font-size: var(--step--1);
  line-height: 1.35;
  color: var(--ink-mute);
}
.tree-role:hover .tree-label,
.tree-role:focus-visible .tree-label {
  text-decoration: underline;
  text-decoration-color: var(--accent-deep);
  text-underline-offset: 4px;
}
/* Le chemin pris s'allume : le poste déplié passe à l'accent. */
.tree-role[aria-expanded="true"] .tree-label { color: var(--accent); }

/* Une feuille : le téléchargement. C'est la seule boîte de l'arbre, parce
   que c'est le seul endroit où cliquer produit quelque chose.

   AU SURVOL, ELLE REMONTE, comme les portes du graphe de l'accueil : même
   course de 2 px, au survol comme au focus clavier. Demandé par Sébastien le
   27 septembre 2026, pour que la feuille se lise comme une chose qu'on
   saisit. Posée à plat au repos, elle prend en montant l'ombre des tuiles
   de l'accueil (--shadow-box) : c'est l'ombre qui fait voir qu'elle se
   détache, 2 px seuls se liraient comme un tressaillement. Au clic, elle
   redescend d'un pixel sous sa place de repos : l'appui.
   Sous prefers-reduced-motion, la section 9 ramène la course à un saut
   instantané : la feuille change d'état sans glisser. */
.tree-leaf {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 6px 16px 6px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface) 60%, var(--canvas));
  color: var(--ink);
  text-decoration: none;
  transition: border-color .16s var(--ease-out),
              background .16s var(--ease-out),
              box-shadow .18s var(--ease-out),
              transform .18s var(--ease-out);
}
.tree-leaf:hover,
.tree-leaf:focus-visible {
  border-color: var(--accent);
  background: var(--surface-2);
  box-shadow: var(--shadow-box);
  transform: translateY(-2px);
}
.tree-leaf:active {
  box-shadow: none;
  transform: translateY(1px);
}
.tree-leaf-icon {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: var(--ink-mute);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke .16s var(--ease-out);
}
.tree-leaf:hover .tree-leaf-icon,
.tree-leaf:focus-visible .tree-leaf-icon { stroke: var(--accent); }
.tree-leaf-body { display: flex; flex-direction: column; line-height: 1.25; }
.tree-leaf-lang { font-size: var(--step--1); font-weight: 600; }
.tree-leaf-meta {
  font-size: var(--step--2);
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}

/* La feuille dans la langue du site est mise en avant, sans un clic de plus
   et sans une ligne de JavaScript : c'est l'attribut lang du document qui
   décide, comme pour tout le bilinguisme du site. Fond plein contre fond nu,
   donc lisible sans la couleur. */
html[lang="fr"] .tree-leaf[hreflang="fr"],
html[lang="en"] .tree-leaf[hreflang="en"] {
  border-color: var(--accent-deep);
  background: var(--surface-2);
}
html[lang="fr"] .tree-leaf[hreflang="fr"] .tree-leaf-icon,
html[lang="en"] .tree-leaf[hreflang="en"] .tree-leaf-icon { stroke: var(--accent); }

/* --- 17.4 Sur téléphone : l'arbre pousse vers le bas ----------------------

   Le tronc part du centre de la tuile racine. Chaque enfant reçoit un coude
   — bordure gauche, bordure basse, angle arrondi — et le tronc continue vers
   le frère suivant par un second trait. C'est la hauteur de chaque <li> qui
   porte le tronc : les espaces entre frères sont donc des marges INTERNES
   (padding), jamais des gap, sans quoi le tronc s'interromprait entre deux
   enfants. */
.tree-wires { display: none; }

.tree-postes {
  --elbow-y: 20px;            /* milieu de la 1re ligne : 8 px de marge + ~11 */
  margin: 6px 0 0 calc(var(--tile) / 2 - 1px);
  padding-left: 22px;
}
.tree-cv {
  --elbow-y: 24px;            /* milieu d'une feuille de 46 px */
  margin-left: 8px;
  padding: 2px 0 10px 22px;
}
.tree-cv > li:not(:last-child) { padding-bottom: 8px; }

.tree-branch > li { position: relative; }
.tree-branch > li::before {
  content: "";
  position: absolute;
  left: -22px;
  top: 0;
  width: 16px;
  height: var(--elbow-y);
  border-left: 2px solid var(--branch);
  border-bottom: 2px solid var(--branch);
  border-bottom-left-radius: var(--elbow-r);
}
.tree-branch > li:not(:last-child)::after {
  content: "";
  position: absolute;
  left: -22px;
  top: 0;
  bottom: 0;
  border-left: 2px solid var(--branch);
}

/* Le premier trait de chaque niveau remonte jusqu'à son parent. Sous la
   racine, il passe sous la tuile, qui le couvre ; sous un poste, il remonte
   dans la marge basse du bouton, là où il n'y a pas de texte. */
.tree-postes > li:first-child::before { top: -30px; height: calc(var(--elbow-y) + 30px); }
.tree-postes > li:first-child::after  { top: -30px; }
.tree-cv > li:first-child::before { top: -10px; height: calc(var(--elbow-y) + 10px); }
.tree-cv > li:first-child::after  { top: -10px; }

/* Le compteur SUIT son libellé, sur la même ligne, comme sur la maquette de
   Sébastien. À cette largeur les enfants descendent du tronc de GAUCHE :
   un fil étiré jusqu'au bord droit ferait croire que c'est de là que partent
   les branches. Déplié, le fil et l'anneau disparaissent donc — ici, rien
   n'en part ; c'est la couleur du libellé qui dit que la branche est ouverte.

   Le fil mesure 24 px pour en montrer 13 : ses 11 derniers passent sous la
   pastille, qui le recouvre (voir la marge négative de .tree-stub). */
@media (max-width: 999px) {
  .tree-stub { flex: 0 0 24px; min-width: 24px; }
  .tree-node[aria-expanded="true"] > .tree-stub,
  .tree-node[aria-expanded="true"] > .tree-count { display: none; }

  /* LA RACINE, ELLE, POSE SA PASTILLE SUR LE BORD BAS DE LA TUILE : c'est de
     là que le tronc descend à cette largeur. Repliée, la pastille annonce les
     six CV au point exact d'où ils vont sortir ; dépliée, elle reste, en
     anneau, et le tronc en part — la même logique qu'en grand écran, où le
     moyeu est à l'origine des courbes. Hors du flux, elle ne prend aucune
     place au libellé : celui-ci garde la même largeur replié ou déplié, et
     l'arbre ne saute pas au premier clic. */
  .tree-root > .tree-node > .tree-stub { display: none; }
  .tree-root > .tree-node > .tree-count {
    position: absolute;
    left: calc(var(--tile) / 2 - 11px);
    top: calc(50% + var(--tile) / 2 - 11px);
  }
  .tree-root > .tree-node[aria-expanded="true"] > .tree-count { display: grid; }
}
@media (scripting: none) and (max-width: 999px) {
  .tree-stub,
  .tree-count { display: none; }
}

/* --- 17.5 Dès 1000 px : l'arbre pousse vers la droite ---------------------

   GÉOMÉTRIE FIXE, ET RÉPÉTÉE DANS LE HTML. Les courbes de cv.html sont
   écrites pour ces valeurs exactes : créneau de 116 px, écart de 14 px,
   arbre de 376 px ; colonnes de liaison de 72 et 52 px ; feuilles de 46 px
   espacées de 10, donc centrées à ±28 px du milieu de leur créneau. Changer
   une valeur ici sans réécrire les tracés décroche les courbes de leurs
   nœuds.

   Le compteur est à cheval sur la frontière de colonne, par une marge
   négative d'une demi-largeur : c'est de son centre que partent les
   courbes. */
@media (min-width: 1000px) {
  .tree {
    --slot: 116px;
    --slot-gap: 14px;
    --tree-h: calc(3 * var(--slot) + 2 * var(--slot-gap));
    --root-w: 216px;
    --gap-1: 72px;
    --role-w: 360px;
    --gap-2: 52px;
    --leaf-w: 152px;
    --sprout-x: -10px;
    --sprout-y: 0px;
    display: flex;
    align-items: center;
    width: calc(var(--root-w) + var(--gap-1) + var(--role-w) + var(--gap-2) + var(--leaf-w));
    height: var(--tree-h);
    margin-inline: auto;
  }

  .tree-root { position: relative; display: flex; align-items: center; }
  .tree-root > .tree-node { flex: none; width: var(--root-w); }

  /* La racine prend ici la disposition des nœuds de l'accueil à cette
     largeur : la tuile, puis son libellé DESSOUS. Le libellé sort du flux
     pour que le bouton ne mesure que la hauteur de la tuile — c'est le
     centre de la TUILE, et non celui du bloc tuile + libellé, qui doit
     tomber sur le milieu de l'arbre, là où partent les courbes. Le fil part
     alors du bord de la tuile, et non d'un bloc de texte dont la boîte
     déborde toujours la dernière ligne écrite. */
  .tree-root > .tree-node .node-body {
    position: absolute;
    top: calc(100% + 10px);
    left: 0;
    width: var(--root-w);
  }

  .tree-postes {
    display: flex;
    flex-direction: column;
    gap: var(--slot-gap);
    margin: 0 0 0 var(--gap-1);
    padding: 0;
  }
  .tree-poste {
    display: flex;
    align-items: center;
    height: var(--slot);
  }
  .tree-role { flex: none; width: var(--role-w); padding-block: 6px; }

  .tree-cv {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0 0 0 var(--gap-2);
    padding: 0;
  }
  .tree-cv > li:not(:last-child) { padding-bottom: 0; }
  .tree-leaf { height: 46px; min-height: 0; padding-block: 0; }

  /* Les coudes du téléphone cèdent la place aux courbes. */
  .tree-branch > li::before,
  .tree-branch > li::after { display: none; }

  .tree-wires { display: block; position: absolute; overflow: visible; }
  .tree-wires--postes {
    left: var(--root-w);
    top: 50%;
    margin-top: calc(var(--tree-h) / -2);
  }
  .tree-wires--cv { left: var(--role-w); top: 0; }

  .tree-count { margin-right: -11px; }
}

.tree-wires path {
  fill: none;
  stroke: var(--branch);
  stroke-width: 2;
  stroke-linecap: round;
}

/* --- 17.6 Le repli n'existe que si le JavaScript s'exécute ----------------

   SANS JAVASCRIPT, L'ARBRE EST ENTIÈREMENT DÉPLIÉ : six liens ordinaires,
   rien de caché. La règle de repli vit donc sous @media (scripting: enabled),
   Baseline « largement disponible » depuis décembre 2023 : un navigateur
   plus ancien ignore le bloc entier et affiche l'arbre déplié — la bonne
   dégradation. Le HTML porte aria-expanded="false" dès le départ : c'est ce
   qui évite l'éclair d'un arbre déplié que le script replierait ensuite. */
@media (scripting: enabled) {
  .tree-node[aria-expanded="false"] ~ .tree-branch,
  .tree-node[aria-expanded="false"] ~ .tree-wires { display: none; }
}
@media (scripting: none) {
  .board-action { display: none; }
  .tree-node { cursor: default; }
  .tree .node .node-tile::before { animation: none; background: none; }
  .tree-node > .tree-count {
    transform: scale(.55);
    border-width: 3.6px;
    border-color: var(--branch);
    background: var(--canvas);
    color: transparent;
    font-size: 0;
  }
}

/* --- 17.7 La pousse -------------------------------------------------------

   Le seul moment animé de la page, et il répond à un geste : la branche se
   trace, puis ses enfants apparaissent, l'un après l'autre. Une animation
   CSS redémarre quand son élément passe de display:none à affiché — c'est
   tout le mécanisme, sans une ligne de JavaScript. Rien ne s'anime au
   chargement : les branches sont repliées, et sans JavaScript ce bloc ne
   s'applique pas.

   « Tout déplier » ouvre les deux niveaux d'un coup : la classe .is-cascade
   retarde alors le second, pour que les feuilles poussent APRÈS les postes
   et non en même temps.

   Sous prefers-reduced-motion, rien de ce bloc ne s'applique : les branches
   apparaissent d'un coup, tracées. La DEL de la racine, elle, est déjà
   éteinte par la section 9. */
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
  .tree-wires path {
    stroke-dasharray: 100;
    animation: tree-draw .52s var(--ease-out) both;
    animation-delay: calc(var(--delay, 0ms) + var(--i, 0) * 60ms);
  }
  .tree-branch > li {
    animation: tree-sprout .46s var(--ease-out) both;
    animation-delay: calc(var(--delay, 0ms) + 180ms + var(--i, 0) * 70ms);
  }
  .tree.is-cascade .tree-cv,
  .tree.is-cascade .tree-wires--cv { --delay: 380ms; }
}
@keyframes tree-draw {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}
@keyframes tree-sprout {
  from {
    opacity: 0;
    transform: translate(var(--sprout-x, 0px), var(--sprout-y, -6px));
  }
  to { opacity: 1; transform: none; }
}

/* ============ 18. LA TRANSITION DE L'ACCUEIL VERS LE CV ================

   Quand on clique sur la tuile « Qui suis-je · GET /cv » de l'accueil, la
   tuile quitte le graphe et vole jusqu'à sa place de racine de l'arbre ; le
   cadre du plan de travail se resserre du format de l'accueil à celui du CV.
   Le visiteur VOIT le nœud s'ouvrir sur son sous-workflow — c'est le récit
   de la page CV, raconté sans une phrase. Au retour, le mouvement se joue à
   l'envers.

   DÉCISION DE SÉBASTIEN DU 27 SEPTEMBRE 2026, et QUATRIÈME RÉOUVERTURE DU
   VERROU DE L'ACCUEIL. Pas une ligne du HTML de index.html n'a bougé : la
   tuile y est visée par son attribut href. Mais son comportement change, et
   c'est ce que le verrou protège.

   LE MÉCANISME : une transition de vue entre deux documents. Le navigateur
   photographie l'ancienne page et la nouvelle, puis anime les éléments qui
   portent le MÊME view-transition-name sur les deux. Aucun JavaScript.
   Chrome et Edge 126+, Safari 18.2+ (iPhone compris). Firefox ignore la
   règle : navigation ordinaire, rien ne casse. Sous prefers-reduced-motion,
   rien ne s'active.

   LA RÈGLE VAUT POUR TOUT LE SITE, puisque la feuille est partagée. Pour que
   rien ne change ailleurs, le fondu de page est coupé par défaut : toutes
   les autres navigations restent instantanées, comme avant. Il n'est rendu
   qu'en ARRIVANT sur le CV ou sur l'accueil — les deux seuls documents qui
   portent un arbre (.tree) ou un graphe (.flow).

   Réglé image par image au ralenti, le 27 septembre 2026, à 1440 et 390 px.
   Deux choix en sont sortis, et ils ne se « corrigent » pas :
     - le cadre passe par un fondu PAR LE NOIR, pas enchaîné : sinon le graphe
       de l'accueil et l'arbre se superposent un instant, le libellé de la
       racine agrandi par-dessus « SGS Canada » ;
     - sous 1000 px, SEULE LA TUILE VOLE : le plan de travail de l'accueil y
       est une longue colonne, et le faire rétrécir jusqu'au cadre du CV
       grossissait l'image d'arrivée au point de rendre son texte énorme.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

/* Les deux paires. Un même nom sur les deux documents : c'est tout ce qui les
   apparie. Chaque nom n'existe qu'une fois par page. */
.node--trigger[href="cv.html"] .node-tile,
.tree-root .node-tile { view-transition-name: cv-racine; }
.board { view-transition-name: plan-de-travail; }
@media (max-width: 999px) {
  .board { view-transition-name: none; }
}

::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root) { animation: none; }

html:has(.tree)::view-transition-old(root),
html:has(.flow)::view-transition-old(root) {
  animation: vt-page-out .26s ease-out both;
}
html:has(.tree)::view-transition-new(root),
html:has(.flow)::view-transition-new(root) {
  animation: vt-page-in .34s .06s ease-out both;
}

/* Le vol : la sortie exponentielle de tout le site, sans rebond. */
::view-transition-group(cv-racine),
::view-transition-group(plan-de-travail) {
  animation-duration: .52s;
  animation-timing-function: var(--ease-out);
}

/* Le cadre change de proportions : on rogne les deux images au lieu de les
   déformer, ancrées en haut à gauche, là où est la barre. */
::view-transition-image-pair(plan-de-travail) {
  overflow: clip;
  border-radius: var(--radius);
}
::view-transition-old(plan-de-travail),
::view-transition-new(plan-de-travail) {
  height: 100%;
  object-fit: cover;
  object-position: left top;
}
::view-transition-old(plan-de-travail) { animation-duration: .18s; }
::view-transition-new(plan-de-travail) {
  animation-duration: .38s;
  animation-delay: .14s;
  animation-fill-mode: both;
}

@keyframes vt-page-out { to { opacity: 0; } }
@keyframes vt-page-in { from { opacity: 0; } }

/* ============ 19. LES ANCRES DE L'ACCUEIL SOUS LA BARRE COLLÉE ============

   Les deux onglets de l'accueil, « Chiffres » et « Plan de travail », et le
   saut d'accès « Aller au graphe » visent des éléments sans creux en tête : le
   <dl> des cartes KPI et la grille du graphe. Sans marge, la cible s'arrêtait
   à 0 px, donc sous la barre collée de 63 px : le haut des cartes disparaissait,
   et le graphe arrivait amputé de la barre de titre de son cadre. Mesuré le
   27 septembre 2026 à 1440, 1280 et 1024 px.

   78 px, c'est la valeur de #profil sur la page CV (section 17) : la barre,
   plus 15 px d'air. Le graphe y ajoute les 70 px qui séparent le haut du cadre
   du haut de la grille — 65 px à 1024 px — pour que ce soit le CADRE ENTIER,
   barre de titre comprise, qui s'arrête sous la barre collée, et non son
   contenu. Si la hauteur de .board-bar change, cette valeur change avec elle.

   Écrite à côté, parce que l'accueil est verrouillé : aucune ligne de son HTML
   ne bouge. Correctif demandé par Sébastien le 27 septembre 2026.
   -------------------------------------------------------------------------- */

#chiffres { scroll-margin-top: 78px; }
#flow { scroll-margin-top: 148px; }


/* ============ 20. LE PLAN DE TRAVAIL TIENT DANS L'ÉCRAN ===================

   Le graphe exigeait 1360 px de large et 895 px de haut, quel que soit
   l'écran. Sous 1444 px de fenêtre, son cadre défilait à l'horizontale — le
   cas d'un portable de 16 pouces réglé à 150 %, ou d'un écran 2560 × 1440 à
   175 % — et sa hauteur dépassait celle d'un écran de portable : on n'en
   voyait jamais qu'une partie, d'où l'impression d'un plan de travail zoomé.
   Question ouverte depuis la session 6, tranchée par Sébastien le
   27 septembre 2026 : le cadre garde sa largeur propre (--wrap-wide), mais le
   graphe doit tenir sur chaque écran, en largeur comme en hauteur.

   CINQ RÉGLAGES, ÉCRITS À CÔTÉ. Les sections 6 et 7 sont closes avec
   l'accueil ; rien n'y est retouché, tout est redéclaré ici, et le HTML ne
   bouge pas. Rien ne change sous 1000 px : la colonne du téléphone reste
   exactement ce qu'elle était.

   1. LES COLONNES SE PARTAGENT LA LARGEUR DISPONIBLE. Le plancher de 1360 px
      disparaît, et chaque colonne devient minmax(0, 1fr) au lieu de 1fr. La
      nuance compte : 1fr refuse de descendre sous la largeur du plus long mot
      qu'il contient, et une seule colonne têtue suffit à rouvrir le
      défilement. Les treize colonnes restent égales, ce qui garde à l'axe ses
      intervalles réguliers. Le défilement de la section 6 reste en place,
      mais comme simple filet : mesuré de 1000 à 2560 px, en français comme
      en anglais, il ne se déclenche plus nulle part.

   2. LA TUILE SUIT LA LARGEUR, DE 48 À 64 PX. À 1000 px de fenêtre, une
      colonne ne mesure plus que 66 px : une tuile de 64 px y touchait ses
      voisines. 4,5vw donne 64 px à partir de 1422 px de fenêtre, et descend
      jusqu'à 48 px à 1067 px et en dessous — la taille d'une tuile n8n.

   3. LES VOIES SUIVENT LA HAUTEUR DE L'ÉCRAN, SANS SE TASSER. --lane-h
      n'est plus une constante : elle vaut la moitié de ce qui reste de la
      hauteur de la fenêtre une fois retiré tout ce qui ne se comprime pas —
      l'arrêt de l'onglet « Plan de travail » sous la barre collée
      (section 19), la barre de titre du cadre, la voie du parcours, l'axe et
      les pastilles. Mesuré, cela fait 480 px. Plafond 182 px, la valeur
      d'origine, atteint à partir de 844 px de fenêtre. La géométrie des
      courbes suit d'elle-même : elle se calcule déjà sur --lane-h
      (section 7), et les trois voies partagent la même valeur — donner à
      l'une une hauteur propre obligerait à réécrire les deux courbes qui la
      rejoignent.

      Sous 1280 px de large, les libellés passent sur une ligne de plus et
      la voie du parcours s'allonge d'autant. La formule retire alors un
      dixième de ce qui manque en largeur — 28 px à 1000 px — pour que le
      graphe reste dans l'écran d'une tablette tenue à l'horizontale.

      PLANCHER 130 PX : L'AIR PASSE AVANT LA VISIBILITÉ. Choix de Sébastien,
      le 27 septembre 2026, après un essai à 84 px où le graphe entier tenait
      partout mais se tassait. Sur une fenêtre de moins de 740 px de haut, les
      voies restent à 130 px et c'est le bas — les dates des pastilles, puis
      la note autocollante — qui passe sous l'écran. Un petit défilement
      dérange moins qu'un plan de travail écrasé. La note reste sous le
      graphe, à sa place d'origine : un essai la montait dans le coin haut
      gauche, et le plan de travail y perdait l'air qui le faisait respirer.

      Mesuré le 27 septembre 2026, en français et en anglais, cadre arrêté
      sous la barre collée — par l'onglet « Plan de travail » ou par un
      retour depuis une page du site (assets/accueil-arrivee.js) : le graphe
      entier, dates comprises, tient dans une fenêtre d'au moins 720 px de
      haut, et la note entière dans une fenêtre d'au moins 920 px. Sous
      720 px, les dates passent sous l'écran : de 26 px à 1463 × 695, un
      écran 2560 × 1440 à 175 %, jusqu'à 98 px à 1366 × 620. Aucun
      défilement horizontal de 1000 à 2560 px de large.

      SEULES LES VOIES 1 ET 3 PRENNENT CETTE HAUTEUR. La voie du parcours, la
      rangée 5, passe en hauteur automatique : c'est elle qui porte les plus
      longs libellés — CMTB et ses trois lignes — et elle ne doit jamais les
      laisser déborder sur les années de l'axe. Les courbes n'en dépendent
      pas : elles mesurent la distance entre le CENTRE des tuiles de deux
      voies, et la tuile est en haut de sa rangée. L'axe, ses graduations et
      les pastilles descendent de 22 px pour garder de l'air sous CMTB.

   4. LES PASTILLES RESTENT SUR UNE RANGÉE, ET LEURS DATES SUR UNE LIGNE.
      La règle commune leur laisse le droit de passer à la ligne, dont la
      colonne du téléphone a besoin ; sur grand écran, dans quatre colonnes
      étroites, la cinquième tombait sous les quatre autres. Trois réglages :
      - Sous 1540 px de fenêtre, la rangée gagne la colonne 13, vide à cette
        hauteur. Elle commence toujours en colonne 9, là où Sébastien l'a
        placée le 27 septembre 2026 : seul son bout s'allonge, quand quatre
        colonnes ne suffisent plus à « Power BI Data Analyst » sur une ligne.
      - Quand la place manque malgré tout — vers 1000 px —, c'est ce seul nom
        long qui passe à la ligne. Laissées libres, toutes se resserraient à
        proportion de leur largeur, et « SQL · IBM » se cassait en deux alors
        que la place ne manquait qu'à sa voisine.
      - Les dates se posent toutes sur la même ligne, en bas de la rangée.
        Sans cela, un nom sur deux lignes mettait sa fin à hauteur de la date
        voisine, et l'on lisait « Analyst mai 2026 » d'un trait. Une date ne
        se coupe pas non plus : « juil. » et « 2026 » sur deux lignes ne se
        lisent plus comme une date.
   5. LES JOINTURES NE LAISSENT PLUS DE TRAIT SOMBRE. Là où une courbe
      rejoint une ligne de voie, les deux se touchent au centième de pixel —
      mesuré —, et pourtant un filet sombre les séparait, visible à l'œil sur
      la montée vers les certifications. Le défaut précédait cette section.
      Quand deux bords tombent au milieu d'un pixel, le navigateur adoucit
      chacun à moitié, et le pixel commun ne se remplit jamais tout à fait.
      Chaque ligne de voie déborde donc d'un pixel de plus sous la courbe
      qu'elle rejoint : même couleur, même épaisseur, même direction à cet
      endroit, le recouvrement ne se voit pas, le trou si.
   -------------------------------------------------------------------------- */

@media (min-width: 1000px) {
  .board-grid .flow { min-width: 0; }

  .flow {
    grid-template-columns: repeat(var(--cols, 12), minmax(0, 1fr));
    grid-template-rows:
      var(--lane-h) var(--lane-gap)
      var(--lane-h) var(--lane-gap)
      auto auto auto;
    --tile: clamp(48px, 4.5vw, 64px);
    --lane-h: clamp(130px, calc((100vh - 480px - max(0px, (1280px - 100vw) / 10)) / 2), 182px);
    --lane-h: clamp(130px, calc((100dvh - 480px - max(0px, (1280px - 100vw) / 10)) / 2), 182px);
  }

  .lane-line { margin-inline: calc(var(--col-gap) / -2 - 1px); }

  .flow > .axis,
  .flow > .tick,
  .flow > .badges { margin-top: 22px; }

  .flow > .badges { flex-wrap: nowrap; gap: 12px; align-items: stretch; }
  .badge { flex-shrink: 0; }
  .badge:first-child { flex-shrink: 1; }
  .badge-date { white-space: nowrap; margin-top: auto; }
}

@media (min-width: 1000px) and (max-width: 1539px) {
  .flow > .badges { grid-column-end: -1; }
}


/* ============= 21. L'ONDE QUI PARCOURT LES PORTES ========================

   Demandé par Sébastien le 27 septembre 2026. Les huit portes du plan de
   travail — les sept tuiles à bordure bleue du graphe et la tuile ISO —
   émettent chacune une onde, l'une après l'autre, dans l'ordre
   chronologique, au rythme d'un cœur lent. « Me contacter » n'en fait pas
   partie : sa DEL (section 8) l'annonce déjà, et deux signaux sur la même
   tuile se brouilleraient.

   CE QUE L'ONDE DIT. La note du plan de travail écrit que les boîtes à
   bordure bleue ouvrent une page ; l'onde le montre, une porte après
   l'autre, sans qu'on ait lu la note. C'est ce qui la sépare d'une
   décoration.

   UNE ONDE, PAS UN CLIGNOTEMENT. Premier essai, le même jour : un halo
   bref, 350 ms, jugé trop rapide par Sébastien. Ce qui est retenu est un
   anneau qui quitte le bord de la tuile, s'élargit dans toutes les
   directions en ralentissant, et s'éteint peu à peu en 1,92 s, à 12 px de
   la tuile — la fin ne doit pas se remarquer. Deux réglages de Sébastien y
   ont mené, le même jour, sur une première version qui vivait 2,4 s et
   allait à 16 px : la durée réduite de 20 % sans toucher à la vitesse —
   l'onde s'éteignait plus tôt, donc à 15 px —, puis le parcours réduit de
   20 % sans toucher à la durée — l'onde va moins loin dans le même
   temps. Chaque porte en émet deux, à 450 ms
   d'écart, la seconde plus pâle : les deux pulsions demandées, comme deux
   ronds dans l'eau.

   LE RYTHME, EN TROIS NOMBRES.
     --coeur   1,1 s entre le départ de deux portes, soit 55 battements par
               minute : un cœur au repos. Une onde dure plus longtemps que
               cet écart ; elle vit donc encore quand la porte suivante part,
               ce que Sébastien a accepté explicitement.
     le cycle  dix battements, soit 11 s : huit portes, puis deux battements
               sans départ, pendant lesquels les dernières ondes s'éteignent.
               C'est exactement deux fois --beat, l'horloge de l'impulsion
               qui parcourt les câbles (section 1) : les deux animations ne
               dérivent jamais l'une par rapport à l'autre.
     le rang   écrit dans le HTML, --hb de 1 à 8, comme --row et --col :
               l'ordre se lit là où sont les tuiles. La première onde part
               1,1 s après l'ouverture de la page, pas pendant qu'elle se
               peint.

   POURQUOI UN CONTOUR (outline), ET NON UNE OMBRE. Une ombre étalée remplit
   tout l'espace entre la tuile et son bord : elle fait une tache qui grossit,
   pas une onde qui s'éloigne. Le contour, décalé par outline-offset, est un
   anneau de 2 px qui se détache de la tuile ; il suit l'arrondi des coins
   en l'élargissant — y compris le bord gauche en demi-cercle du déclencheur —
   et il garde la même épaisseur en pixels sur une tuile de 64 px comme sur
   la tuile ISO de 38 px. Il vit sur deux pseudo-éléments de la tuile :
   bordure, ombre portée, éveil au passage de l'impulsion (section 8) et
   relief au survol restent intacts.

   POUR RÉGLER.
     le tempo       --coeur seul ; le cycle en secondes suit. Les
                    pourcentages des images clés sont écrits pour un cycle de
                    dix battements — 1 % vaut un dixième de battement — et ne
                    bougent pas. Changer le NOMBRE de battements du cycle
                    oblige, lui, à les recalculer.
     la portée      les 12px de la dernière image clé ; au-delà de 20 px,
                    l'onde de la tuile ISO et celle de « Qui suis-je »
                    seraient coupées par le bord du cadre.
     l'intensité    --onde, 55 % pour la première onde, 38 % pour la seconde.
     une seule onde supprimer la règle ::before ci-dessous.

   Mouvement réduit : l'onde ne ralentit pas, elle disparaît (section 9).
   -------------------------------------------------------------------------- */

:root { --coeur: 1.1s; }

.tool--gate { position: relative; }

.node--gate:not(.node--cta) .node-tile::before,
.node--gate:not(.node--cta) .node-tile::after,
.tool--gate::before,
.tool--gate::after {
  content: "";
  position: absolute;
  inset: -1px;                   /* contre l'arête extérieure de la bordure */
  border-radius: inherit;        /* le déclencheur garde son bord arrondi */
  outline: 2px solid transparent;
  pointer-events: none;
  --onde: 55%;
  animation: onde calc(var(--coeur) * 10) linear infinite;
  animation-delay: calc(var(--hb, 0) * var(--coeur));
}

/* La seconde onde : plus pâle, 450 ms derrière la première. */
.node--gate:not(.node--cta) .node-tile::before,
.tool--gate::before {
  --onde: 38%;
  animation-delay: calc(var(--hb, 0) * var(--coeur) + .45s);
}

/* 1 % = 110 ms au tempo par défaut : l'onde vit de 0 à 17,45 %, soit
   1,92 s. Quatre images clés en mouvement linéaire dessinent la
   décélération — 7,2 px, puis 4, puis 0,8 — et un effacement qui s'étale
   jusqu'au bout. Pour changer la portée sans changer la forme du
   mouvement, multiplier les trois positions par le même facteur. */
@keyframes onde {
  0%        { outline-offset: 0;    outline-color: color-mix(in srgb, var(--accent) var(--onde), transparent); }
  7%        { outline-offset: 7.2px;  outline-color: color-mix(in srgb, var(--accent) calc(var(--onde) * .57), transparent); }
  14%       { outline-offset: 11.2px; outline-color: color-mix(in srgb, var(--accent) calc(var(--onde) * .16), transparent); }
  17.45%,
  100%      { outline-offset: 12px; outline-color: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .node--gate:not(.node--cta) .node-tile::before,
  .node--gate:not(.node--cta) .node-tile::after,
  .tool--gate::before,
  .tool--gate::after { animation: none; }
}


/* ============ 22. LE RETOUR AU PORTFOLIO DEPUIS LE CV =====================

   Demandé par Sébastien le 27 septembre 2026 : sur le plan de travail du CV,
   une tuile en haut à droite, comme la tuile ISO de l'accueil, qui ramène au
   portfolio. Elle reprend .board-canvas, .board-tools, .tool et .tool--gate
   TELS QUELS (section 6) ; ce qui s'en écarte est ajusté ici, sous
   .tool--retour, classe qui n'existe que dans cv.html. Écrit à côté, et
   APRÈS la section 21, dont une règle doit céder.

   TROIS ÉCARTS, ET AUCUN N'EST UNE FANTAISIE.
     1. L'ICÔNE EST AU TRAIT : c'est la flèche du fil d'Ariane « Mon
        portfolio ». .tool-icon pose fill: currentColor, qui la remplirait
        en bloc — le piège de .btn-icon relevé en session 11. On reprend
        donc le trait de .crumbs-icon.
     2. PAS D'ONDE. Sur l'accueil, elle parcourt huit portes l'une après
        l'autre et montre que ces boîtes s'ouvrent ; seule ici, elle
        battrait toutes les 11 s sur la sortie de la page, en concurrence
        avec la DEL de la racine, qui porte le seul geste attendu du
        visiteur. C'est la raison qui en exclut déjà « Me contacter »
        (section 21) : deux signaux au même endroit se brouillent.
     3. SOUS 1000 PX, LA RACINE LUI CÈDE LA PLACE. L'arbre y pousse vers le
        bas, et le libellé de la racine court jusqu'au bord droit, là où la
        tuile flotte. Il s'arrête désormais 52 px avant : les 38 px de la
        tuile, son écart au bord du cadre moins la marge du quadrillage
        (2 px), et 12 px d'air. Seule la racine est concernée : la tuile
        finit au-dessus de la première branche.

   LE RETOUR, LUI, N'A RIEN EN PROPRE : c'est un lien ordinaire vers
   index.html, comme le fil d'Ariane. L'accueil s'ouvre cadré sur son plan
   de travail (assets/accueil-arrivee.js) et la racine revole jusqu'à la
   tuile « Qui suis-je » (section 18), parce que ces deux mécaniques ne
   regardent que la page quittée, jamais le lien cliqué.
   -------------------------------------------------------------------------- */

.tool--retour .tool-icon {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tool--retour::before,
.tool--retour::after { content: none; }

@media (max-width: 999px) {
  .tree-root > .tree-node { padding-right: 52px; }
}
