/* ===========================================================================
   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%; }
}
