/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5
 * Hallmark · genre: editorial · macrostructure: Map / Diagram · theme: Le Plan
 * enrichment: SVG construit (traces + pouls) · nav: aucune · footer: Ft2 inline
 * accent: rouge #ef4444 · axes: papier sombre / grotesque technique / accent chaud
 *
 * La page n'est pas une liste : c'est un PLAN. Maxime au centre, ce qu'il
 * fabrique en satellites, reliés par des traits. On voit d'un coup que le bot
 * appartient au jeu et que les prestations sortent de la boutique — une liste
 * ne peut pas dire ça.
 *
 * ⚠️ Le `?v=` de l'import n'est pas décoratif : sans en-tête Cache-Control
 * (python http.server, mutualisé OVH…) le navigateur sert une feuille périmée
 * sans le moindre signe. L'incrémenter à chaque modif de tokens.css.
 */

@import url("fonts/fonts.css");
@import url("tokens.css?v=9");

/* ------------------------------------------------------------------ base */

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

html, body { margin: 0; padding: 0; overflow-x: clip; }

body {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: safe center;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--gutter) var(--space-md);
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { text-decoration: none; color: inherit; }
h1 { margin: 0; }
p  { margin: 0; }

:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

/* ------------------------------------------------------------ le décor */

/* Le poste de travail, de biais derrière tout le reste.
   ⚠️ `z-index: -1` est obligatoire, pas un réglage de goût : `.chapeau` et
   `.repli` ne sont pas positionnés, donc un décor à z-index 0 passerait
   PAR-DESSUS le nom et les liens. Le négatif le renvoie derrière le contenu
   tout en le laissant devant le fond du body. */
.decor {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

/* Le personnage, debout à gauche, en vis-à-vis du poste. Il vient de la même
   character sheet que la marionnette des vidéos, donc le même Maxime dessiné
   sert aux deux usages.
   ⚠️ Pas de masque en bas : il a des pieds, un fondu les ferait fondre dans
   le vide. Il est simplement ancré sous le bord de l'écran. */
.decor__perso {
  position: absolute;
  left: clamp(8px, 3vw, 64px);
  bottom: -3vh;
  height: clamp(380px, 95vh, 1080px);
  width: auto;
  /* Retourné, pour qu'il tienne son portable du côté du poste et non du bord
     de l'écran. `transform` et non `scale`, et le centrage mobile passe par
     `translate` : les trois propriétés sont séparées, aucune n'écrase l'autre. */
  transform: scaleX(-1);
  /* ⚠️ Plus effacé que le bureau, à dessein : sa peau claire est la seule
     grande plage lumineuse du décor, à opacité égale il sautait aux yeux
     alors que le poste, presque noir, restait en retrait. */
  opacity: .13;
}

/* Ancré en bas à droite et débordant volontairement : le plateau et la tour
   sont coupés par le cadre de l'image source, ce débord met la coupe hors
   de l'écran. Le masque efface ce qu'il en resterait. */
.decor__bureau {
  position: absolute;
  right: -1vw;
  bottom: -14vh;
  width: auto;
  height: clamp(300px, 72vh, 980px);
  transform: rotate(-7deg);
  transform-origin: 100% 100%;
  opacity: .16;
  /* Un seul dégradé, horizontal, et court : il ne sert qu'à noyer la coupe du
     cadre sur le bord gauche de l'image. Plus long, il mangerait l'écran de
     gauche, celui qui affiche justement cette page-ci.
     ⚠️ Il y avait AUSSI un fondu vertical, hérité de la version où la scène
     portait encore son mur : il noyait la ligne franche du plateau, qui sinon
     se lisait comme un trait posé en travers de la page. Depuis que l'image
     est détourée, ce bord n'existe plus — et le fondu, lui, effaçait tout ce
     qui dépassait 22 % de la hauteur. Les écrans et la tour disparaissaient. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 7%);
  mask-image: linear-gradient(to right, transparent, #000 7%);
}

/* ---------------------------------------------------------- le chapeau */

.chapeau {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
}

.tete { text-align: center; }
.tete h1 { font-size: var(--text-xl); font-weight: 700; letter-spacing: -.035em; }
.tete .h {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-accent);
}
.tete .role { margin-top: var(--space-2xs); font-size: var(--text-sm); color: var(--color-ink-2); }

/* La vue éclatée et la langue, côte à côte : voir le ⚠️ dans index.html, c'est
   48 px de chapeau rendus au plan. */
.outils { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; justify-content: center; }

/* -------------------------------------------------------- la recherche */

/* ⚠️ `position: relative` sur le bloc, et RIEN dans le flux en dessous : la
   liste de résultats est ancrée ici en absolu. La hauteur du chapeau est une
   constante du calcul de la carte (voir la réserve de `.plan`) ; si la liste
   poussait le flux, la carte rétrécirait à chaque frappe. */
.cherche { position: relative; width: min(430px, 100%); }

.cherche__barre {
  display: flex;
  align-items: stretch;
  gap: var(--space-2xs);
}

/* Le sélecteur de catégorie est un vrai `<select>`, pas une liste maison :
   au clavier et sur mobile, le natif fait mieux que tout ce qu'on écrirait. */
.cherche__cat {
  flex: none;
  max-width: 40%;
  padding: 0 24px 0 12px;
  border: var(--rule-hair) solid var(--color-line-2);
  border-radius: 999px;
  background: transparent;
  color: var(--color-ink-2);
  font: 600 var(--text-3xs) / 1 var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
  appearance: none;
  /* le chevron, en fond : `appearance: none` retire celui du système */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 12px top 50%, right 7px top 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: color var(--dur-short) var(--ease-out),
              border-color var(--dur-short) var(--ease-out);
}

.cherche__cat:hover { color: var(--color-ink); border-color: var(--color-accent-line); }
/* ⚠️ Le menu déroulant natif se dessine avec les couleurs du SYSTÈME, pas du
   site : sans ça, texte clair sur fond clair chez qui a un thème clair. */
.cherche__cat option { background: var(--color-paper-2); color: var(--color-ink); }

.cherche__champ {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
  padding: 0 14px;
  border: var(--rule-hair) solid var(--color-line-2);
  border-radius: 999px;
  transition: border-color var(--dur-short) var(--ease-out);
}

.cherche__champ:focus-within { border-color: var(--color-accent); }
.cherche__loupe { width: 14px; height: 14px; flex: none; color: var(--color-ink-3); }

.cherche__champ input {
  flex: 1 1 auto;
  min-width: 0;
  height: 34px;                 /* cible tactile, aligné sur `.eclat` */
  border: 0;
  background: none;
  color: var(--color-ink);
  font: 400 var(--text-sm) / 1 var(--font-body);
}

.cherche__champ input::placeholder { color: var(--color-ink-3); }
.cherche__champ input:focus { outline: none; }   /* le cadre passe au parent */
/* la croix native de `type="search"` est grise sur noir et hors voix : on la
   retire, Échap et le bouton effacent déjà */
.cherche__champ input::-webkit-search-cancel-button { display: none; }

/* ------------------------------------------------------ les résultats */

.trouve {
  position: absolute;
  z-index: var(--z-dessus);
  top: calc(100% + var(--space-2xs));
  left: 0;
  right: 0;
  max-height: 46vh;
  overflow-y: auto;
  padding: var(--space-2xs);
  border: var(--rule-hair) solid var(--color-line-2);
  border-radius: 14px;
  background: var(--color-paper-2);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .55);
  text-align: left;
}

.trouve__r {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: 9px;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.trouve__r[aria-selected="true"],
.trouve__r:hover { background: var(--color-accent-soft); }

.trouve__r .pastille { width: 26px; height: 26px; flex: none; }
.trouve__r .pastille svg { width: 12px; height: 12px; }

.trouve__t { min-width: 0; flex: 1 1 auto; }
.trouve__t b {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.trouve__t span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
  color: var(--color-ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* le chemin : d'où vient ce résultat dans la carte */
.trouve__ou {
  flex: none;
  max-width: 38%;
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
  color: var(--color-ink-3);
  letter-spacing: .04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.trouve__rien {
  padding: 12px 10px;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--color-ink-3);
}

/* --------------------------------------------------------- vue éclatée */

.eclat {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 9px 16px 9px 12px;   /* cible tactile : ~34 px de haut */
  border: var(--rule-hair) solid var(--color-line-2);
  border-radius: 999px;
  background: transparent;
  color: var(--color-ink-2);
  font: 600 var(--text-3xs) / 1 var(--font-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--dur-short) var(--ease-out),
              border-color var(--dur-short) var(--ease-out);
}

.eclat:hover { color: var(--color-ink); border-color: var(--color-accent-line); }
.eclat[aria-pressed="true"] { color: var(--color-accent); border-color: var(--color-accent); }

/* les cinq points : serrés au repos, écartés une fois la carte dépliée —
   le bouton montre ce qu'il fait */
.eclat__marque { position: relative; width: 15px; height: 15px; flex: none; }

.eclat__marque i {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 3px;
  height: 3px;
  margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: currentColor;
  transition: translate 260ms var(--ease-out);
}

.eclat__marque i:nth-child(2) { translate: -3px -3px; }
.eclat__marque i:nth-child(3) { translate:  3px -3px; }
.eclat__marque i:nth-child(4) { translate: -3px  3px; }
.eclat__marque i:nth-child(5) { translate:  3px  3px; }

.eclat[aria-pressed="true"] .eclat__marque i:nth-child(2) { translate: -6px -6px; }
.eclat[aria-pressed="true"] .eclat__marque i:nth-child(3) { translate:  6px -6px; }
.eclat[aria-pressed="true"] .eclat__marque i:nth-child(4) { translate: -6px  6px; }
.eclat[aria-pressed="true"] .eclat__marque i:nth-child(5) { translate:  6px  6px; }

/* ------------------------------------------------------- bascule FR / EN */

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

.langue {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border: var(--rule-hair) solid var(--color-line-2);
  border-radius: 999px;
}

.langue button {
  font: 600 var(--text-3xs) / 1 var(--font-mono);
  letter-spacing: .1em;
  white-space: nowrap;
  padding: 11px 15px;          /* cible tactile : ~35 px de haut */
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--color-ink-3);
  cursor: pointer;
  transition: background-color var(--dur-short) var(--ease-out),
              color var(--dur-short) var(--ease-out);
}

.langue button:hover { color: var(--color-ink); background: var(--color-paper-2); }
.langue button[aria-pressed="true"] { background: var(--color-accent); color: var(--color-accent-ink); }
.langue button[disabled] { cursor: default; }

/* ══════════════════════════════════════════════════════════ le plan ═══ */

/* ⚠️ Le rapport doit suivre `VUE` dans index.html : les positions des nœuds
   sont des pourcentages calculés sur ces dimensions. Les désaccorder décale
   tous les nœuds par rapport à leurs traits.

   ⚠️ La largeur est bornée par la HAUTEUR disponible, pas seulement par la
   fenêtre : depuis que le nom est sorti du plan, le chapeau prend 165 px et la
   page se mettait à défiler sous 990 px de haut (mesuré : 281 px de trop en
   1280×800).

   ⚠️⚠️ Cette réserve est la SOMME de tout ce qui n'est pas le plan, et il faut
   la reprendre dès qu'on ajoute ou retire un élément dans le `flex` du body —
   sa gouttière comprise. Elle valait 265 px, écrite quand la page finissait sur
   le plan ; le pied de page des mentions légales est arrivé après (16 px + 32 px
   de gouttière) et personne n'a corrigé le chiffre. Résultat : 32 px de trop,
   la page défilait, et le nom se coupait en haut dès qu'on touchait la molette.
   Le détail, mesuré au navigateur :
     32 (marge haute) + 166 (chapeau) + 32 (gouttière) + 32 (gouttière)
     + 16 (pied) + 20 (marge basse) + 1 (arrondi) = 298.
   ⚠️ La barre de recherche a été ajoutée SANS toucher à ce chiffre, et c'est
   volontaire : empilée, elle coûtait 48 px et faisait passer un 1512×982 sous
   le plancher de 880 px — la page se remettait à défiler. « Vue éclatée » et
   la langue ont donc été mises côte à côte, ce qui rend exactement la place
   qu'elle prend. Le chapeau vaut toujours 166. Rajouter une ligne ici, c'est
   soit en retirer une ailleurs, soit reprendre ce chiffre.

   ⚠️ `container-type: inline-size` va avec : TOUT ce qu'il y a dedans est
   dimensionné en `cqw`, donc la carte rapetisse d'un bloc. Repasser une taille
   en px ferait grossir cet élément quand le plan rétrécit, et les étiquettes
   se percuteraient sur les petits écrans — alors que rien ne bouge sur grand. */
.plan {
  position: relative;
  container-type: inline-size;
  /* ⚠️ Le plancher à 880 px n'est pas négociable : sans lui, un écran court
     (1280×800) rétrécissait la carte à 810 px, les pastilles tombaient sous
     30 px et les étiquettes devenaient illisibles. Passé ce plancher on laisse
     la page défiler de quelques dizaines de pixels — c'est le moindre mal. */
  width: min(1120px, 100%, max(880px, (100dvh - 298px) * 1180 / 780));
  aspect-ratio: 1180 / 780;
}

/* ------------------------------------------------------------- traits */

.traits {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;        /* ⚠️ sinon le SVG mange les clics des nœuds */
  z-index: var(--z-plan);
}

/* ⚠️ `pathLength="1"` normalise tous les traits : sans lui un trait court se
   dessine plus vite qu'un long et la construction paraît boiteuse. C'est un
   ATTRIBUT SVG, posé en JS à la création — il n'a pas d'équivalent CSS. */
.traits line {
  stroke-width: 1;
  fill: none;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}

.traits .base      { stroke: var(--color-line); }
.traits .base--vif { stroke: var(--color-accent-line); }

/* Une mère ouverte : son trait est celui que le portrait vient d'emprunter, il
   reste allumé ; les autres reculent avec leurs mères. */
.plan.ouvert .base--vif       { stroke: var(--color-line); }
.plan.ouvert .base--vif.suivi { stroke: var(--color-accent); }

/* le pouls : un court segment qui court du parent vers le nœud, au survol */
.traits .pouls {
  stroke: var(--color-accent);
  stroke-width: 1.6;
  opacity: 0;
  stroke-dasharray: .12 .88;
  stroke-dashoffset: 1;
}

.traits .pouls.actif {
  opacity: 1;
  animation: courir 900ms cubic-bezier(.4, 0, .2, 1) infinite;
}

@keyframes courir { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }

/* --------------------------------------------------- entrée orchestrée */

.dessine .base { animation: tracer var(--dur-trace) var(--ease-out) forwards; }
@keyframes tracer { to { stroke-dashoffset: 0; } }

@keyframes poser {
  from { opacity: 0; scale: .82; }
  to   { opacity: 1; scale: 1; }
}

/* ----------------------------------------------------------- les nœuds */

.noeud {
  position: absolute;
  translate: -50% -50%;
  z-index: var(--z-noeud);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  text-align: center;
  color: var(--color-ink-2);
  opacity: 0;
  /* reset : les mères sont des <button> */
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  transition: color var(--dur-short) var(--ease-out),
              opacity var(--dur-short) var(--ease-out);
}

.dessine .noeud { animation: poser var(--dur-poser) var(--ease-out) forwards; }

/* ----------------------------------------------------- mères et enfants */

/* La mère est une ÉTAPE, pas une destination : plus grosse, nommée en clair,
   et son cerclé reste allumé pour dire « c'est cliquable, ça ne part pas ». */
.noeud--mere .pastille { width: 5.71cqw; height: 5.71cqw; border-color: var(--color-accent-line); }
.noeud--mere .pastille svg { width: 2.32cqw; height: 2.32cqw; }
.noeud--mere .n { font-size: 1.43cqw; color: var(--color-ink); }

/* ⚠️ L'animation d'entrée est en `forwards` : tant qu'elle est accrochée, elle
   garde la main sur `opacity` et AUCUNE règle normale ne peut atténuer une
   mère (une animation bat la cascade ordinaire). On la retire donc une fois
   les quatre posées, sinon le fondu des mères ci-dessous ne ferait rien. */
.plan.fini .noeud--mere { animation: none; opacity: 1; }

/* ⚠️ Une mère ouverte s'EFFACE : le portrait vient prendre sa place, les deux
   superposés feraient une bouillie. Elle reste dans le flux (pas de
   `display:none`) pour que son trait et sa position ne bougent pas. */
.plan.ouvert.fini .noeud--mere       { opacity: .34; }
.plan.ouvert.fini .noeud--mere:hover { opacity: 1; }
.plan.ouvert.fini .noeud--mere.actif { opacity: 0; pointer-events: none; }

.noeud--enfant .pastille { width: 4.46cqw; height: 4.46cqw; }
.noeud--enfant .pastille svg { width: 1.88cqw; height: 1.88cqw; }

/* un enfant qui a lui-même des enfants reste une ÉTAPE : même cerclé que les
   mères, pour qu'on voie qu'il ouvre au lieu de partir */
.noeud--etape .pastille { border-color: var(--color-accent-line); }
.plan.ouvert.fini .noeud--etape.actif { opacity: 0; pointer-events: none; }

/* ⚠️⚠️ LE MÊME PIÈGE QUE POUR LES MÈRES, une marche plus bas, et il a coûté
   cher. Tant que l'animation d'entrée reste accrochée en `forwards`, elle garde
   la main sur `opacity` et AUCUNE règle normale ne s'applique — la ligne
   juste au-dessus, celle qui efface l'étape ouverte pour que le portrait vienne
   s'y poser, ne faisait donc RIEN : l'étiquette de « Black Desert Online »
   restait lisible sous le portrait, à moitié cachée par lui. Les mères ont leur
   décrochage (`.plan.fini`), les enfants n'en avaient aucun.
   `posee` est ce décrochage, posé en JS nœud par nœud une fois la pose jouée :
   par minuteur et non par `animationend`, qui ne viendrait jamais en mouvement
   réduit. Il doit rendre `opacity: 1`, sinon le nœud retombe sur le `0` de
   `.noeud` et disparaît. */
.plan .noeud--enfant.posee { animation: none; opacity: 1; }

/* ------------------------------------------------------ la vue éclatée */

/* ⚠️ Tout est déplié en même temps : à taille normale, 22 nœuds sur la même
   carte se percutent. Les étiquettes rapetissent et les notes disparaissent —
   c'est une vue d'ensemble, pas une vue de lecture. */
.plan.eclate .noeud--enfant .pastille { width: 3.57cqw; height: 3.57cqw; }
.plan.eclate .noeud--enfant .pastille svg { width: 1.52cqw; height: 1.52cqw; }
.plan.eclate .noeud--enfant .n { font-size: .94cqw; }
.plan.eclate .noeud--enfant .f { display: none; }
.plan.eclate .noeud--mere .pastille { width: 4.64cqw; height: 4.64cqw; }
.plan.eclate .noeud--mere .f { display: none; }

.pastille {
  display: grid;
  place-items: center;
  width: 5cqw;
  height: 5cqw;
  border-radius: 50%;
  background: var(--color-paper);
  border: var(--rule-hair) solid var(--color-line);
  transition: border-color var(--dur-short) var(--ease-out),
              background-color var(--dur-short) var(--ease-out),
              scale 220ms var(--ease-out);
}

.noeud:hover { color: var(--color-accent); }
.noeud:hover .pastille,
.noeud:focus-visible .pastille {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  scale: 1.12;
}

.pastille svg { width: 2.05cqw; height: 2.05cqw; fill: currentColor; }

.noeud .n { font-size: 1.29cqw; font-weight: 600; letter-spacing: -.02em; white-space: nowrap; }
.noeud .f { font-family: var(--font-mono); font-size: .89cqw; color: var(--color-ink-3); white-space: nowrap; }

/* ------------------------------------------------------------ le centre */

/* le siège vide : un anneau discret, révélé seulement quand le portrait est
   parti — au repos il serait caché sous la photo de toute façon.
   ⚠️ C'est un BOUTON (il ramène au centre d'un coup), donc il faut défaire
   l'apparence native : sans `appearance: none` + `background: none`, Windows
   pose un rectangle gris plein au milieu de la carte. */
/* ⚠️ LA CIBLE ET L'ANNEAU SONT DEUX BOÎTES. Le bouton fait 44 px au minimum,
   l'anneau visible reste à 3.04cqw : dessiné à même le bouton, il descendait
   sous 28 px dès qu'on rétrécit la fenêtre (le plan vit jusqu'à 880 px), et le
   banc de mesure le rapporte comme cible trop petite — à juste titre, on ne
   vise pas un anneau de 26 px à la souris. */
.siege {
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(3.04cqw, 44px);
  height: max(3.04cqw, 44px);
  translate: -50% -50%;
  appearance: none;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  z-index: var(--z-plan);
  transition: opacity var(--dur-short) var(--ease-out);
}

.siege::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 3.04cqw;
  height: 3.04cqw;
  translate: -50% -50%;
  border: var(--rule-hair) dashed var(--color-line-2);
  border-radius: 50%;
  transition: border-color var(--dur-short) var(--ease-out),
              scale var(--dur-short) var(--ease-out);
}

/* ⚠️ `pointer-events` revient AVEC l'opacité, jamais avant : au repos le siège
   est exactement sous le portrait, et resté cliquable il lui volerait le clic
   — d'autant qu'il est maintenant plus large que l'anneau qu'on voit. */
.plan.ouvert .siege { opacity: 1; pointer-events: auto; cursor: pointer; }

.plan.ouvert .siege:hover::before,
.plan.ouvert .siege:focus-visible::before {
  border-color: var(--color-accent);
  border-style: solid;
  scale: 1.18;
}

/* ⚠️ `.moi` ne contient QUE le portrait dans le flux : c'est ce qui le laisse
   exactement au centre géométrique, là où convergent les traits. Le nom, le
   pseudo et le rôle sont posés au-dessus, hors du flux. */
/* ⚠️ La durée doit rester égale à `VOYAGE` dans index.html : c'est elle qui
   décide du moment où la fratrie apparaît. Les désaccorder fait entrer les
   enfants avant l'arrivée du portrait, ou les fait attendre dans le vide. */
.moi {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  z-index: var(--z-dessus);
  opacity: 0;
  transition: left 620ms var(--ease-out), top 620ms var(--ease-out);
}

/* ⚠️ Deux allures : lente au départ du centre, vive en transit d'une mère à
   l'autre. La durée réelle est posée EN LIGNE par `basculer` (sur la
   transition et sur `--vol`) ; les 620 ms écrites ici ne sont qu'un repli. */

/* --- le portrait est un bouton de retour une fois qu'on a quitté le centre */

/* ⚠️ `position` + `z-index` ne sont pas décoratifs : le halo est un `::before`
   à `z-index: -1`, il lui faut ce contexte d'empilement pour rester DERRIÈRE
   le portrait mais DEVANT le papier. Et c'est ce bloc, halo compris, qui est
   mis à l'échelle pendant le voyage — un portrait qui rétrécit en laissant son
   halo à taille réelle ferait un trou lumineux. */
.moi__bouton {
  position: relative;
  z-index: 0;
  display: block;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: 50%;
}

.moi__bouton[disabled] { cursor: default; }

/* Le portrait rétrécit en chemin et retrouve sa taille en arrivant : il file
   sur le trait au lieu de glisser. La durée doit rester celle du voyage. */
.moi--vol .moi__bouton { animation: filer var(--vol, 620ms) var(--ease-out); }

@keyframes filer {
  0%, 100% { scale: 1; }
  46%      { scale: .46; }
}

/* ⚠️ `--ox`/`--oy` sont posés en JS depuis l'angle de la mère ouverte : le nom
   se range du côté du centre, à l'opposé de l'éventail. Voir `basculer`. */
.moi__ou {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: calc(-50% + var(--ox, 0px)) calc(-50% + var(--oy, 104px));
  width: max-content;
  text-align: center;
}

.moi__ou b {
  display: block;
  font-size: 1.43cqw;
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--color-ink);
}

.moi__ou span {
  display: block;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: .89cqw;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-accent);
}

/* la flèche vit sur la ligne du retour, pas avant le nom : posée sur `.moi__ou`
   elle tomberait seule sur sa propre ligne, `<b>` étant en bloc */
.moi__ou span::before { content: "\2190\00a0"; }

.moi__bouton:not([disabled]):hover img,
.moi__bouton:focus-visible img { box-shadow: 0 0 0 1px var(--color-accent), 0 0 70px -14px rgba(239, 68, 68, .7); }

.dessine .moi { animation: poser 520ms var(--ease-out) forwards; }

.moi img {
  display: block;
  width: 9.64cqw;
  height: 9.64cqw;
  border-radius: 50%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.15);
  box-shadow: 0 0 0 1px var(--color-line), 0 0 70px -18px rgba(239, 68, 68, .45);
}

/* halo qui bat très lentement, derrière le portrait.
   ⚠️ Accroché au BOUTON et non à `.moi` : `.moi` porte aussi le nom et la
   ligne de retour, un halo posé là suivrait le texte et non la photo. */
.moi__bouton::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16.96cqw;
  height: 16.96cqw;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(rgba(239, 68, 68, .18), transparent 68%);
  animation: battre 5.5s ease-in-out infinite;
  pointer-events: none;
  z-index: -1;
}

@keyframes battre {
  0%, 100% { opacity: .55; scale: .94; }
  50%      { opacity: 1;   scale: 1.06; }
}

/* --------------------------------- dérive : l'image respire, sans bouger */

.derive { animation: deriver var(--t, 13s) ease-in-out infinite alternate; }
@keyframes deriver {
  from { transform: translate(0, 0); }
  to   { transform: translate(var(--dx, 4px), var(--dy, -5px)); }
}

/* ═════════════════════════════════════ repli : la liste hiérarchique ═══ */

/* ⚠️ Sous 880 px un plan devient illisible : nœuds superposés, traits croisés.
   La liste garde l'information de PARENTÉ (le retrait) sans la position. */
.repli { display: none; width: min(430px, 100%); }

.repli__tete {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.repli__tete img {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.15);
  box-shadow: 0 0 0 1px var(--color-line);
}

.repli__tete h1 { font-size: var(--text-lg); font-weight: 700; letter-spacing: -.03em; }
.repli__tete .role { margin-top: 2px; font-size: var(--text-xs); color: var(--color-ink-2); }
.repli__tete .h {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.liste__lien {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 11px 0;
  border-bottom: var(--rule-hair) solid var(--color-line);
  color: var(--color-ink-2);
  transition: color var(--dur-short) var(--ease-out);
}

.liste__lien:hover { color: var(--color-accent); }
.liste__lien svg { width: 18px; height: 18px; fill: currentColor; flex: none; }
.liste__lien b { font-weight: 600; color: var(--color-ink); font-size: var(--text-sm); }
.liste__lien span { margin-left: auto; font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--color-ink-3); }

/* ⚠️ Les mères ne sont PAS des liens ici non plus : sur le repli il n'y a rien
   à ouvrir, tout est déjà déplié. Elles ne sont que des intertitres. */
.liste__mere {
  margin-top: var(--space-md);
  padding-bottom: var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.liste__mere--sous {
  margin-top: var(--space-sm);
  padding-left: 26px;
  color: var(--color-ink-3);
}

.liste__lien--enfant { padding-left: 26px; }

@media (max-width: 880px) {
  .plan  { display: none; }
  .repli { display: block; }
  /* ⚠️ Le repli est une LISTE de liens posée en plein sur le décor, là où le
     plan ne pose que des pastilles espacées. À la même opacité, les écrans et
     le tapis passaient derrière les libellés et les rendaient sales. */
  .decor img { opacity: .10; }

  /* ⚠️ Le bouton de vue éclatée ne pilote QUE `.plan`, qui est masqué juste
     au-dessus : sous 880 px il ne pouvait rien déplier. Le repli montre déjà
     toute l'arborescence, il n'y a rien à éclater. */
  .eclat { display: none; }

  /* Le personnage remonte en haut : à gauche il n'y a plus de vide sous 880 px,
     le repli occupe toute la largeur et il tomberait pile derrière les libellés
     des liens. En haut il occupe le blanc du chapeau, où rien ne se lit. */
  .decor__perso {
    top: 1vh;
    bottom: auto;
    left: 50%;
    right: auto;
    translate: -50% 0;      /* pas `transform` : le décor n'en a pas ici, mais
                               la propriété séparée ne peut rien écraser */
    height: 52vh;
    opacity: .15;
  }
}


/* ═════════════════════════════════════════════ mouvement réduit ═══ */

/* ⚠️ Tout est coupé, et surtout traits et nœuds redeviennent VISIBLES : leur
   état initial est « invisible », donc sans ces règles la page resterait vide
   pour qui a désactivé les animations. C'est l'erreur classique du motif. */
@media (prefers-reduced-motion: reduce) {
  .traits line   { stroke-dashoffset: 0 !important; animation: none !important; }
  .traits .pouls { display: none; }
  .noeud, .moi   { opacity: 1 !important; animation: none !important; }
  .derive, .moi__bouton::before, .moi--vol .moi__bouton { animation: none !important; }
  /* le portrait se téléporte au lieu de voyager ; le JS suit (`LENT`) et fait
     entrer la fratrie sans attendre */
  .moi { transition: none !important; }
  /* ⚠️ le `opacity: 1 !important` ci-dessus rendrait la mère ouverte visible
     SOUS le portrait posé dessus — il faut la remasquer avec la même force */
  .plan.ouvert.fini .noeud--mere       { opacity: .34 !important; }
  .plan.ouvert.fini .noeud--mere.actif { opacity: 0 !important; }
  *, *::before, *::after { transition-duration: 120ms !important; }
}

/* ------------------------------------------------------- pied de page */

/* ⚠️ Exigé par les plateformes qui examinent une application développeur :
   les liens vers la politique de confidentialité et les conditions doivent
   être atteignables depuis le site SANS ouvrir de menu. D'où un pied en clair
   plutôt qu'un nœud de plus dans le plan, qui serait un menu déguisé.
   Volontairement effacé : il ne doit pas concurrencer la constellation. */
.pied {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--color-ink-3);
  letter-spacing: .04em;
}

.pied a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}

.pied a:hover { color: var(--color-ink-2); border-bottom-color: var(--color-line-2); }
.pied a:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

/* --------------------------------------------------------- impression */

@media print {
  .plan, .langue, .decor { display: none; }
  .repli { display: block; }
}
