/* ============================================================
   AGROSOFT — site vitrine une page · SODEPA
   ============================================================ */

/* Palette « marine & feuille » — trois couleurs venues du logo, et chacune
   n'a qu'un seul emploi :

   · le MARINE #031C55 (celui du mot « AGROSOFT ») est la MATIÈRE — fond,
     encre, filets, voiles, et jusqu'au graphite du moniteur 3D. Le site
     n'a plus un seul gris taupe : il a la température du logotype ;
   · la FEUILLE #058A28 accompagne une PHRASE : le mot qui compte dedans, la
     puce qui l'énumère, le bouton qui la conclut. Elle souligne, elle ne
     structure pas ;
   · le TURQUOISE du swoosh #39A9BC est la MACHINE : il ne vit que dans
     l'écran 5250, et nulle part ailleurs (voir --t-*).

   · la TERRE CUITE #FE4A0F — l'orange du filet spectral — est la VOIX DU
     SITE : ce qu'il dit de lui-même, et non du produit. Le commentaire de
     l'accroche, les repères 01…06, les phrases de conclusion, le libellé du
     chargeur, l'invite à faire défiler. Elle structure, elle ne souligne pas.

   D'où une règle de lecture qui n'a pas besoin d'être expliquée :
   marine = la matière, vert = ce que le produit fait pour vous,
   terre cuite = ce que le site vous dit, turquoise = ce que dit la machine.

   Le partage entre le vert et la terre cuite se teste en une question : est-ce
   DANS une phrase, ou AUTOUR d'elle ? Les numéros de section étaient verts et
   n'auraient jamais dû l'être — un repère n'est pas un accent, et le vert y
   perdait sa règle. Les « em » restent verts : ils sont dans la phrase.

   Pourquoi cette teinte chaude et pas une autre : l'ambre serait plus doux,
   mais sur PAPIER une encre doit descendre à 7,6:1, et plus une teinte chaude
   va vers le jaune plus elle s'effondre en kaki. Mesuré à saturation maximale
   tenable, le chroma vaut 131 à 14°, 113 à 26°, 99 à 36° et 87 à l'ambre —
   contre 92 pour le marine qu'elle remplace. Passé 26°, la « couleur chaude »
   est donc MOINS colorée que le bleu. Seul le bout orange survit aux deux
   thèmes (voir --ink-2 plus bas).

   L'ÉCHELLE DES ENCRES, elle, se construit sur la CLARTÉ et jamais sur la
   saturation. C'est une correction, et elle se mesure : --soft et --muted
   étaient tombés à 32 % et 20 % de saturation là où --ink en tient 56 %
   (93 % sur papier). À ce compte-là, une encre secondaire n'est plus une
   couleur mais un gris bleuté — et un texte gris se saute, quel que soit son
   contraste. Les trois encres tiennent donc désormais la même saturation, à
   la teinte du marine, et ne se distinguent que par la clarté. Le vert des
   « em » suit : il est recalé À CHAQUE FOIS AU-DESSUS de --muted, pour qu'un
   mot accentué ne soit jamais plus faible que le paragraphe qui le porte.
   Sans cette règle, remonter l'encre secondaire enterre l'accent.

   Pourquoi l'écran n'est plus ambre, alors que les vraies machines le sont :
   l'ambre était la seule couleur chaude d'un site entièrement froid, et il y
   détonnait — c'est le client qui l'a relevé, et il a tranché. Le turquoise
   n'est pas pour autant hors sujet : c'est l'une des sept couleurs des 5250
   COULEUR (3179, 3477, 5292), et c'est celle du logo.

   Pourquoi pas le vert, qui serait pourtant d'époque : c'est le cliché de
   l'AS/400, et le site montre un produit d'aujourd'hui. Le vert est d'ailleurs
   déjà pris ci-dessus, et lui donner un second emploi aurait brouillé la seule
   règle de lecture du site.

   La teinte dominante a été calée sur la LUMINANCE de l'ancien ambre (0,4873
   contre 0,4894) : le seul réglage de la refonte qui ne se décide pas au
   calcul — le seuil du bloom — n'a donc pas eu à bouger. Un vert aurait tout
   changé de ce côté-là (il pèse 0,7152 dans la luminance contre 0,2126 pour
   le rouge).

   En thème sombre le marine descend jusqu'à la nuit : le fond n'est pas un
   noir neutre mais un bleu très profond, ce qui suffit à faire chanter le
   vert sans avoir à le crier. */
:root, :root[data-theme="dark"] {
  --bg:        #060a14;   /* marine poussé à la nuit — jamais un noir neutre */
  --bg-2:      #111a2b;
  --ink:       #eaf0f9;   /* blanc bleuté : le blanc pur jurerait sur ce fond */
  --soft:      #b7cbea;   /* corps de texte : panneaux, listes, accroche du
                             contact — 10,43:1 sur le pire fond (le sommet d'un
                             panneau translucide, #121b2e). C'est du CORPS DE
                             TEXTE, pas une légende : il se lit à pleine force,
                             et c'est la taille du titre qui fait la hiérarchie */
  /* LA SECONDE VOIX DU HERO, et rien d'autre dans tout le site.
     Teinte 14,3°, soit l'orange #FE4A0F du filet spectral du logo, à la
     force exacte de --muted (7,65:1) : le second paragraphe change de
     TEINTE, pas de force — il reste aussi présent que le premier.

     Pourquoi cette teinte-là et pas l'ambre, qui serait plus douce : sur
     PAPIER une teinte chaude doit descendre à 7,6:1, et plus elle va vers le
     jaune plus elle s'effondre en kaki. Mesuré, à saturation maximale tenable :
     chroma 131 à 14°, 113 à 26°, 99 à 36°, 87 à l'ambre — contre 92 pour le
     marine de --muted. Passé 26°, la « couleur chaude » est donc MOINS colorée
     que le bleu qu'elle remplace. Seul le bout orange survit aux deux thèmes.

     Deux voisinages surveillés, tous deux sans conséquence :
     · le vert des « em » à l'intérieur de ce paragraphe est à 119° de là, et
       porte en plus la graisse 700 — il reste lisible comme accent, y compris
       pour un œil qui confond le rouge et le vert ;
     · le rouge d'alerte du terminal (--t-red) est proche, mais il ne paraît
       que sur des écrans plus loin dans la page, quand ce texte a défilé. */
  --ink-2:     #f49476;
  --muted:     #8faedf;   /* 7,60:1 sur ce même pire fond. Le CHROME, désormais :
                             liens de la barre, libellé du chargeur, scroll-hint.
                             Il ne porte plus une ligne d'argumentaire.
                             L'ancien #8291ab tenait 5,39:1 pour 20 % de
                             saturation : conforme, et illisible d'intention */
  --line:      rgba(168, 196, 235, 0.16);   /* filets bleutés, plus la lueur chaude d'avant */

  /* Le vert de la feuille éclairci pour la nuit, à TEINTE CONSTANTE : 135,8°
     pour #058A28, 132,8° ici. C'est une dérivation, pas une ressemblance —
     et c'est ce qui permet à l'accent d'être le même vert dans les deux
     thèmes, à la clarté près. Il tient les SURFACES — bouton, puces, barre
     du chargeur — et c'est aussi le vert de screens.js (DARK.green) : ces
     deux-là doivent rester identiques au caractère près.
     L'encre, elle, est désormais dédoublée dans les DEUX thèmes. Elle ne
     l'était pas ici, et la raison invoquée — « il est déjà conforme » — ne
     répondait pas à la bonne question : ce n'est pas au fond qu'un mot
     accentué doit se comparer, c'est au paragraphe qui l'entoure. À 7,47:1
     contre les 7,60:1 de --muted, le vert serait passé SOUS son propre
     texte. 8,20:1 le remet au-dessus, sans changer de teinte (133°). */
  --accent:     #4cc165;
  --accent-ink: #4acb66;
  /* Bordures et états au repos UNIQUEMENT : 3,38:1 au pire, soit le seuil des
     éléments non textuels. Ne jamais y poser de texte. */
  --accent-dim: #2f7d43;
  --accent-rgb: 76, 193, 101;

  /* Les trois arrêts recopient makeBg() (main.js) au caractère près : la page
     se prolonge dans la scène 3D, et tout écart se verrait sur les bords du
     canvas. Toute retouche ici vaut là-bas. */
  --page-grad: radial-gradient(120% 120% at 50% -10%, #101a2e 0%, #060a14 45%, #00030a 100%);
  --panel-bg:  linear-gradient(160deg, rgba(18,27,46,0.76), rgba(8,13,24,0.56));
  --panel-solid: rgba(15,23,40,0.44);
  --vignette:  radial-gradient(110% 90% at 50% 40%, transparent 55%, rgba(0,2,9,0.58) 100%);
  /* En portrait, le moniteur se replie dans un bandeau en haut et le texte
     passe dessous : la vignette se resserre autour du bandeau au lieu d'être
     centrée sur la fenêtre. Elle reste une VARIABLE, et c'est le point —
     écrite en dur, elle était noire à 60 % dans les deux thèmes et grisait la
     moitié basse du thème clair, qui est pourtant le thème par défaut. */
  --vignette-tel: radial-gradient(120% 60% at 50% 22%, transparent 45%, rgba(0,2,9,0.62) 100%);
  --scrim:     rgba(5, 9, 18, 0.68);
  --grain-op:  0.05;
  /* Le bouton EST le logo : feuille verte, mot marine. 7,01:1, et 8,15:1 au
     survol. En thème clair la polarité s'inverse — voir la note là-bas. */
  --cta-ink:   #031c55;
  --maxw:      1320px;
  --ease:      cubic-bezier(0.22, 1, 0.36, 1);

  /* Le filet spectral du logo, relevé au pixel et remis dans l'ordre.
     Il n'a droit qu'à UN emploi dans toute la page — la barre du chargeur —
     et toujours sur sa piste sombre, jamais sur le fond de page. La raison
     est mesurée : le violet #5F1AF7 plafonne à 2,98:1 sur du NOIR PUR, il ne
     peut donc atteindre 3:1 sur aucun fond existant ; et sur le papier du
     thème clair, c'est l'autre bout du filet qui s'efface (#F1B805 à 1,70:1).
     Le filet ne peut donc jamais porter de sens. Sur sa piste, il redevient
     ce qu'il est : un ornement de 2 px visible une seconde, pendant que le
     libellé « CHARGEMENT » et le mouvement portent l'information. */
  --spectre: linear-gradient(90deg,
    #5F1AF7, #A607AD, #ED0E55, #FE4A0F, #FF9905, #F1B805,
    #A3B505, #57AA0B, #23A323, #08963B, #058D3A);
  --spectre-track: #04060c;   /* la même piste dans les DEUX thèmes : c'est ce qui
                                 fait que le filet se lit à l'identique de part et
                                 d'autre de la bascule */

  /* Les trois pièces du logo. Chaque thème a ses propres fichiers : le
     logotype est écrit en marine, et marine sur presque-noir ne se lit plus.
     La variante sombre n'inverse pas l'image — elle ne remplace que le
     marine, et laisse la feuille, le filet et les pictogrammes intacts
     (voir tools/decoupe-logo.mjs).

     Passer par des variables et non par des <img> tient à une raison
     précise : le thème est posé sur <html> AVANT le premier rendu, donc le
     navigateur ne va chercher qu'un seul jeu de fichiers. Deux <img>
     superposées en auraient chargé deux, et un src piloté par le JS aurait
     chargé le mauvais avant de le corriger — c'est-à-dire un scintillement
     sur la première chose que le visiteur voit. */
  --logo-embleme:  url("../img/logo-embleme-dark.png");
  --logo-mot:      url("../img/logo-mot-dark.png");

  /* Palette du terminal de contact. Elle recopie l'objet DARK de screens.js :
     le panneau HTML doit rester indiscernable de la texture 3D au moment de
     la bascule de fin de zoom. Toute retouche vaut pour les deux. */
  --t-bg:    #06090f;   /* verre marine-noir, un cran sous le fond de page */
  --t-gold:  #3fcbd8;   /* libellés, logotype ASCII, bandeaux — 10,20:1 */
  --t-ink:   #e9ecf3;
  --t-dim:   #868da0;   /* 6,01:1 — lisible jusqu'aux touches de fonction, en 13 px */
  --t-cyan:  #8fa8ff;   /* valeurs modifiables : le bleu du poisson du logo
                           #0265D4, éclairci — 8,74:1 */
  --t-field: #6376a8;   /* pointillés de saisie : 4,45:1, très au-dessus du seuil
                           de 3:1 des éléments non textuels */
  --t-red:   #ef7360;   /* les deux rouges du terminal étaient écrits en dur, donc
                           insensibles au thème. La variable recopie la clé `red`. */
  --t-pad:   rgba(143, 168, 255, 0.15);   /* = --t-cyan à 0,15 ; doit rester égal au
                                            littéral de fieldBox() dans screens.js */
}

/* Thème clair — c'est le thème PAR DÉFAUT du site, donc celui qui doit être
   irréprochable, et celui dont chaque valeur est vérifiée sur le pire fond
   RÉEL : le bas du dégradé de page (#dfe5f0) et le bas d'un panneau
   translucide posé dessus (#ecf0f8). Jamais sur du blanc pur, qui n'existe
   qu'au sommet du dégradé et qui flatterait tous les chiffres.

   Le fond n'est pas un blanc mais un papier très légèrement bleuté : c'est
   lui qui fait tenir ensemble le marine de l'encre et le vert de l'accent,
   là où un blanc franc les laisserait chacun de son côté. */
:root[data-theme="light"] {
  --bg:        #f6f8fc;   /* papier bleuté, jamais #ffffff */
  --bg-2:      #e8edf7;
  --ink:       #031C55;   /* le marine du logo, tel quel — 12,76:1 au pire.
                             body pose color: var(--ink) : tout ce qui ne
                             redéfinit pas sa couleur hérite de ce marine
                             saturé, les titres de panneau compris. C'est
                             assumé, d'où la mesure sur le pire fond. */
  --soft:      #1a2e5a;   /* corps de texte — 10,49:1 au pire (le bas du dégradé,
                             #dfe5f0). Il frôle --ink, et c'est assumé : sur papier
                             le corps de texte se lit à pleine encre, la hiérarchie
                             étant portée par le corps et la graisse du titre. */
  /* La même terre cuite sur papier — teinte 14,4°, 7,60:1 au bas du dégradé.
     Elle y devient une brique, là où le thème sombre en fait un corail : c'est
     la clarté qui s'inverse, jamais la teinte, comme pour le vert et le
     marine. Voir la note complète côté sombre. */
  --ink-2:     #7f260a;
  --muted:     #254281;   /* 7,63:1 au pire. L'ancien #4f5d7c s'arrêtait à 5,21:1
                             pour 22 % de saturation — le « gris » qui faisait
                             sauter la seconde voix du héros. (Avant lui, le
                             #6d685f tombait même à 4,32:1 : non conforme.) */
  --line:      rgba(3, 28, 85, 0.16);   /* filets tirés de l'encre, pas d'un gris quelconque */

  /* La feuille assombrie à TEINTE CONSTANTE (135,8° -> 134,0°). Le vert brut
     du logo, #058A28, ne fait que 4,23:1 sur ce papier : il rate le seuil de
     peu, et c'est là toute la raison de l'écart — la teinte, elle, ne bouge
     pas. Il tient les SURFACES (bouton, puces, barre du chargeur). */
  --accent:     #0b7223;
  /* La même feuille, plus appuyée, pour l'accent porté EN TEXTE : 8,21:1 même
     au bas du dégradé. C'est ce qui met « em » — son seul porteur depuis que
     les numéros de section sont passés à la terre cuite — à l'abri des fonds
     de panneau, où --accent descendrait à 5,34:1 — encore
     conforme, mais sans marge. Le calage vient de --muted (7,63:1) et non du
     seuil : l'accent doit rester au-dessus du texte qu'il ponctue, sinon un
     mot vert dans un paragraphe marine se lit comme une mise en retrait.
     L'ancien #075c1b ne faisait que 6,50:1 et serait passé dessous. */
  --accent-ink: #044b15;
  --accent-dim: #4f8a5d;   /* 3,24:1 au pire : bordures et états au repos, jamais de texte */
  --accent-rgb: 11, 114, 35;

  --page-grad: radial-gradient(120% 120% at 50% -10%, #ffffff 0%, #f2f5fb 45%, #dfe5f0 100%);
  --panel-bg:  linear-gradient(160deg, rgba(255,255,255,0.90), rgba(240,244,251,0.74));
  --panel-solid: rgba(255,255,255,0.64);
  --vignette:  radial-gradient(110% 90% at 50% 38%, transparent 60%, rgba(3,28,85,0.14) 100%);
  /* Sur papier une vignette ne doit pas assombrir, seulement refermer : c'est
     le même marine que la vignette de bureau, à la même densité. */
  --vignette-tel: radial-gradient(120% 60% at 50% 22%, transparent 52%, rgba(3,28,85,0.16) 100%);
  --scrim:     rgba(247, 249, 253, 0.72);
  --grain-op:  0.03;
  /* Ici la polarité du bouton s'inverse, et ce n'est pas un caprice : pour
     porter le marine à 4,5:1, un vert de fond devrait être si clair que la
     puce de 8 px des listes tomberait à 3,0:1 au fond d'un panneau — conforme
     par accident. On garde donc le vert profond, et c'est l'encre qui devient
     le papier : 5,83:1, et 5,18:1 même au survol. Le couple reste « la
     feuille et le mot », seulement retourné.
     L'ancien #fffaf0 sur #b57516 ne faisait que 3,66:1 : encore une
     non-conformité existante qui disparaît. */
  --cta-ink:   #f7fbf6;

  /* Le filet ne change pas d'un thème à l'autre : ce sont les relevés du logo,
     posés sur la même piste sombre des deux côtés. C'est précisément ce qui
     les rend identiques à la bascule — sur le papier, le tiers ambre du filet
     disparaîtrait purement et simplement. */
  --spectre: linear-gradient(90deg,
    #5F1AF7, #A607AD, #ED0E55, #FE4A0F, #FF9905, #F1B805,
    #A3B505, #57AA0B, #23A323, #08963B, #058D3A);
  --spectre-track: #04060c;

  --logo-embleme:  url("../img/logo-embleme.png");
  --logo-mot:      url("../img/logo-mot.png");

  /* Terminal « papier » — recopie l'objet LIGHT de screens.js.
     Le fond de l'écran est très légèrement PLUS CHAUD que celui de la page
     (#fcfbf8 contre #f6f8fc) : un écran n'est jamais tout à fait du même blanc
     que ce qui l'entoure, et ce point d'écart le décolle du fond sans qu'on
     ait à lui dessiner un cadre. */
  --t-bg:    #fcfbf8;
  --t-gold:  #0e6d80;   /* le turquoise devenu encre — 5,77:1 */
  --t-ink:   #1e2433;   /* encre marine-graphite — 14,97:1 */
  --t-dim:   #656c7e;   /* 5,07:1 sur l'écran, 4,94:1 sur la page quand le panneau
                           est hors zoom : les deux mesurés, parce que les touches
                           de fonction sont du vrai texte en 13 px.
                           L'ancien #7d766a n'était qu'à 4,31:1. */
  --t-cyan:  #3159c4;   /* valeurs modifiables : le bleu du poisson, densifié
                           pour le papier — 6,05:1 */
  --t-field: #7b88a8;   /* 3,33:1 sur la page — l'ancien #8a93ad était à 2,95:1,
                           donc sous le seuil des éléments non textuels */
  --t-red:   #a83320;   /* la pomme du logo, assombrie */
  --t-pad:   rgba(49, 89, 196, 0.12);   /* = --t-cyan à 0,12 */
}

* { margin: 0; padding: 0; box-sizing: border-box; }

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

body {
  background: var(--page-grad);
  color: var(--ink);
  font-family: "IBM Plex Sans", system-ui, sans-serif;
  font-size: 18px;
  line-height: 1.55;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  cursor: default;
  transition: background 0.5s var(--ease), color 0.5s var(--ease);
}

a { color: inherit; text-decoration: none; }
/* « em » est le SEUL mot-clé du corps de texte, et il porte deux signaux à
   la fois : la couleur d'accent et la graisse. Les deux ensemble, parce que
   la page emploie déjà des paragraphes en 600 (.hero__lead) où une simple
   mise en gras ne se verrait pas, et des paragraphes en 400 où la couleur
   seule passe inaperçue au survol du regard. 700 est donc au-dessus des deux.
   C'est ce qui rend <strong> inutile autour de <em> : le balisage double qui
   traînait dans index.html ne faisait que rattraper l'absence de graisse. */
em { font-style: normal; font-weight: 700; color: var(--accent-ink); }
strong { color: var(--ink); font-weight: 600; }

/* ---------- Fixed WebGL stage ----------
   Pas de 100vw ni de 100vh ici : `inset: 0` suffit, et surtout il ne dit pas
   la même chose. `100vw` COMPREND la barre de défilement classique — celle de
   Windows, qui prend 15 px de large ; `inset: 0` s'arrête à la zone visible.
   Le canvas était donc 15 px plus large que la fenêtre utile, et l'écran 3D,
   centré dedans, tombait 7,5 px à droite du panneau HTML qui vient s'y
   substituer en fin de vol — un décalage visible au raccord, sous Windows
   seulement (macOS a des barres flottantes, qui ne prennent aucune place).
   Même raison pour la hauteur : sur Safari iOS, `100vh` est la fenêtre
   BARRES COMPRISES, alors que le panneau, lui, se pose dans la fenêtre
   visible.

   En pourcentages et non en `auto` : un <canvas> est un élément REMPLACÉ, il a
   une taille intrinsèque (celle de son tampon de dessin). Avec `inset: 0` et
   pas de dimension, il garde cette taille intrinsèque au lieu de remplir la
   fenêtre — et comme sizeOf() mesure le canvas pour en déduire celle du
   tampon, la scène se serait figée à 300 × 150. */
#stage {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  display: block;
  pointer-events: none;
}

/* ---------- Overlays ---------- */
.fx-vignette {
  position: fixed; inset: 0; z-index: 2; pointer-events: none;
  background: var(--vignette);
  mix-blend-mode: multiply;
}
.fx-grain {
  position: fixed; inset: -50%; z-index: 3; pointer-events: none;
  opacity: var(--grain-op);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain 0.6s steps(2) infinite;
}
@keyframes grain {
  0% { transform: translate(0,0); } 100% { transform: translate(-6%, 4%); }
}

/* ---------- Loader ---------- */
.loader {
  position: fixed; inset: 0; z-index: 60;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px;
  background: var(--bg);
  transition: opacity 0.7s var(--ease), visibility 0.7s;
}
.loader.is-done { opacity: 0; visibility: hidden; }
/* Le filet du logo passe ici, et nulle part ailleurs : c'est le seul endroit
   de la page où ses onze couleurs peuvent coexister sans avoir à porter de
   sens — ni contraste à tenir, ni lecture à assurer, une seconde de présence.

   C'est le bloc MOBILE qui le porte, et non la piste. L'inverse avait été
   envisagé — une piste arc-en-ciel balayée par un bloc sombre — mais une
   barre déjà pleine ne se lit plus comme un chargement : elle se lit comme
   une barre remplie que quelque chose vient effacer. Le geste conventionnel
   est le bon, seule sa couleur change.

   Le halo est retiré : une lueur d'une seule couleur sous onze les salit
   toutes. La piste est la même dans les deux thèmes, pour que le filet s'y
   lise à l'identique de part et d'autre de la bascule. */
.loader__bar {
  width: 220px; height: 2px; background: var(--spectre-track); overflow: hidden;
}
.loader__bar span {
  display: block; height: 100%; width: 40%;
  background: var(--spectre);
  animation: load 1.1s var(--ease) infinite;
}
@keyframes load {
  0% { transform: translateX(-120%); } 100% { transform: translateX(360%); }
}
/* Le site annonce son propre état : terre cuite, comme tout ce qu'il dit de
   lui-même. 8,80:1 en sombre et 9,04:1 en clair — le fond du chargeur est
   plat, donc plus clément que le dégradé de la page. */
.loader__label {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px; letter-spacing: 0.35em; color: var(--ink-2);
}

/* ---------- Le logo, en trois pièces ----------
   Le logotype fourni est un verrou vertical : l'emblème (tablette, anneau,
   produits, feuille), le mot « AGROSOFT by SODEPA » avec son filet spectral,
   et la bande des cinq filières. Le site s'en sert en trois endroits et à
   trois moments différents, d'où le découpage (tools/decoupe-logo.mjs).

   · l'EMBLÈME est posé dans la barre dès l'ouverture et n'en bouge plus ;
   · le MOT tient lieu de titre du hero, et le défilement le fait remonter et
     rétrécir jusqu'à venir se caler à droite de l'emblème — les deux moitiés
     du logotype se retrouvent alors, à l'horizontale cette fois ;
   · les FILIÈRES restent dans le hero, sous l'accroche.

   Les pièces sont posées en fond d'un élément et non par des <img>, pour deux
   raisons. Le thème change le fichier, et le thème est écrit sur <html> avant
   le premier rendu : le navigateur ne va donc chercher qu'un seul jeu. Et
   `aspect-ratio` donne à la boîte sa hauteur SANS attendre l'image — ce qui
   compte doublement ici, puisque initBrand() mesure cette boîte pour calculer
   toute la course, et qu'une mesure faite avant l'arrivée du fichier aurait
   été fausse.

   Trois réglages : la largeur du mot dans le hero, sa hauteur une fois posé
   dans la barre, et la hauteur de l'emblème. */
:root {
  /* Largeur du mot dans le hero. Le plafond n'est pas esthétique mais
     géométrique : au-delà, le mot passe derrière le cadre du moniteur 3D, qui
     commence aux alentours de 40 % de la largeur de la fenêtre — et c'est le
     « T » de SOFT, tracé en filet, qui s'y fait couper. */
  --brand-size:    clamp(230px, 33vw, 520px);
  --brand-nav-h:   38px;                        /* hauteur du mot, une fois posé */
  --embleme-h:     64px;                        /* hauteur de l'emblème dans la barre */
  --embleme-ratio: 653 / 300;
  --mot-ratio:     1200 / 259;
}
.brand {
  position: fixed; top: 0; left: 0; z-index: 45;
  width: max-content; margin: 0;
  transform-origin: top center;
  pointer-events: none;
  /* posé et retiré par place() (main.js) : voir le commentaire là-bas */
  will-change: auto;
}
/* tant que les mesures ne sont pas faites, on ne montre rien : évite
   d'apercevoir la marque en haut à gauche pendant une image */
.brand:not(.is-placed) { opacity: 0; }
.brand__in { display: block; }
/* Le mot est mis à l'échelle par une transformation, jamais par un changement
   de largeur : la boîte garde donc la taille qu'elle a dans le hero, et le
   compositeur ne fait que RÉDUIRE une image déjà tramée en grand. C'est le
   sens qui pardonne — l'agrandissement d'une trame se voit, sa réduction non.
   C'est aussi ce qui permet à la course de rester à soixante images par
   seconde : rien n'est remis en page pendant qu'elle se joue. */
.brand__mot {
  display: block;
  width: var(--brand-size);
  aspect-ratio: var(--mot-ratio);
  background: var(--logo-mot) center / contain no-repeat;
}
/* Le nom en toutes lettres, pour les lecteurs d'écran et les moteurs : le mot
   est une image, et un attribut alt ne se lit pas comme un titre de page. */
.brand__nom {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ---------- Content layering ---------- */
main { position: relative; z-index: 10; }

section { position: relative; }

/* ---------- Nav ---------- */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  display: flex; align-items: center; justify-content: space-between;
  padding: 22px clamp(20px, 5vw, 60px);
  mix-blend-mode: normal;
}
/* Le verrou de la marque : l'emblème, posé là dès l'ouverture, et à sa droite
   l'emplacement où le mot viendra se caler. */
.nav__brand { display: flex; align-items: center; gap: 13px; position: relative; }
/* La barre n'a pas de fond, et c'est voulu : le contenu passe dessous. Mais
   depuis que le logo y est posé — une image en couleurs, non plus du texte
   blanc — une ligne de texte qui lui passe derrière le brouille. On lui met
   donc le même voile feutré qu'à l'accroche du hero (.hero__plate) : un
   dégradé tiré du fond de page, qui ne se voit pas tant qu'il n'a rien à
   couvrir, et qui détache le logo dès qu'il en a besoin.

   Son opacité est pilotée par setZoom() (main.js) via --halo : pendant le vol
   vers le terminal, l'écran 5250 remplit le cadre et le logo se pose dans son
   coin vide. Un voile flou y trahirait la bascule. */
.nav__brand::before {
  content: ""; position: absolute; z-index: -1;
  inset: -18px -26px -18px -22px;
  opacity: var(--halo, 1);
  background: radial-gradient(70% 120% at 40% 50%, var(--scrim) 0%, var(--scrim) 46%, transparent 82%);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  -webkit-mask: radial-gradient(70% 120% at 40% 50%, #000 0%, #000 50%, transparent 82%);
  mask: radial-gradient(70% 120% at 40% 50%, #000 0%, #000 50%, transparent 82%);
  pointer-events: none;
}
.nav__embleme {
  display: block;
  height: var(--embleme-h);
  aspect-ratio: var(--embleme-ratio);
  background: var(--logo-embleme) center / contain no-repeat;
}
/* Volontairement vide : c'est la boîte d'arrivée du mot. Ses dimensions
   définitives sont posées par initBrand() d'après le mot lui-même ; celles
   écrites ici ne servent qu'avant l'exécution du JS, pour que la barre ne
   sursaute pas — et elles tombent juste, puisque le rapport de l'image est
   connu d'avance. */
.nav__dock {
  display: block;
  height: var(--brand-nav-h);
  aspect-ratio: var(--mot-ratio);
}
/* Sur un téléphone, la barre n'a plus la place du verrou entier : l'emblème,
   le mot posé, la bascule de thème et le bouton d'appel font ensemble plus
   large que l'écran, et le bouton passait à la ligne.

   On garde donc ce qui compte — l'emblème, qui EST le logo, et le bouton — et
   on retire les deux redondances : le mot posé (à dix-sept pixels de haut,
   « by SODEPA » n'y est plus qu'un trait) et le libellé de la bascule, dont
   la pastille suffit à dire l'état.

   Le mot ne disparaît pas pour autant : il reste en grand dans le hero, et
   c'est en s'effaçant dans l'emblème qu'il le rejoint (voir place(), main.js).
   Le geste demandé est le même, seule son arrivée change. */
@media (max-width: 560px) {
  :root { --embleme-h: 56px; }
  .nav__dock { display: none; }
  .theme-toggle__label { display: none; }
  .theme-toggle { padding: 8px; }
  .theme-toggle__dot { width: 12px; height: 12px; }
}
.nav__links { display: flex; align-items: center; gap: clamp(14px, 2vw, 30px); font-size: 14px; }
.nav__links a { color: var(--muted); transition: color 0.3s; }
.nav__links a:hover { color: var(--ink); }
.nav__cta {
  color: var(--cta-ink) !important; background: var(--accent);
  padding: 9px 16px; border-radius: 100px; font-weight: 600;
  box-shadow: 0 0 22px rgba(var(--accent-rgb),0.30);
}
.nav__cta:hover { filter: brightness(1.08); }

/* Theme toggle */
.theme-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  background: transparent; border: 1px solid var(--line); color: var(--muted);
  font: inherit; font-size: 13px; cursor: pointer;
  padding: 7px 13px; border-radius: 100px;
  transition: color 0.3s, border-color 0.3s;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--accent); }
.theme-toggle__dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 10px var(--accent);
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 15px 26px; border-radius: 100px; font-weight: 600; font-size: 15px;
  transition: transform 0.35s var(--ease), background 0.3s, border-color 0.3s;
}
.btn--primary { background: var(--accent); color: var(--cta-ink); box-shadow: 0 8px 40px rgba(var(--accent-rgb),0.26); }
.btn--primary:hover { transform: translateY(-3px); filter: brightness(1.08); }

/* ---------- HERO ---------- */
.hero {
  min-height: 100vh;
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 clamp(20px, 5vw, 70px);
}
.hero__inner { max-width: 640px; }
/* Réserve dans le flux la place du titre, qui est en position fixe.
   Sa hauteur est posée par initBrand() d'après la marque elle-même. */
.hero__brandslot { margin: 18px 0 26px; }
/* La mesure a DEUX bornes, et il en faut deux.
   · 42 ch, c'est la limite de confort de lecture : au-delà, l'œil perd sa
     ligne en revenant à la marge.
   · 35 vw, c'est la limite GÉOMÉTRIQUE : le moniteur 3D commence entre 39 %
     (1024 px) et 43 % (1920 px) de la fenêtre. Sous 1280 px il n'y a
     matériellement pas la place de tenir 42 ch avant lui, et c'est le vw qui
     prend la main ; au-dessus, c'est le ch.
   Mesuré : le texte mord au pire de 4 px sur le moniteur, à 1024 px — moins
   que les 30 ch d'avant, qui mordaient de 11 px. Et sur un écran large
   l'accroche passe de quatorze lignes à dix. */
/* GRAISSE 400, ET C'EST UN RÉGLAGE DE L'EMPHASE, PAS DU PARAGRAPHE.
   Ce bloc a été en 600 le temps que la coupure avec le second repose sur la
   graisse. Elle repose sur la teinte depuis que --ink-2 existe, et le 600 ne
   servait donc plus qu'à une chose : écraser les « em ».

   Mesuré : dans CE paragraphe, le vert des « em » est plus faible que le
   texte qui le porte — 1,92× plus sombre en thème sombre, 3,40× PLUS CLAIR
   en thème clair. Le mot accentué n'avance pas, il recule. Il ne lui restait
   donc que la graisse pour se signaler, et 600 → 700 ne fait que cent
   unités : dans IBM Plex Sans, c'est SemiBold contre Bold, autant dire rien.
   À 400 l'écart passe à trois cents, exactement celui du second bloc, et la
   page marque l'emphase de la même façon partout.

   SI L'ON REMONTE CETTE GRAISSE UN JOUR, il faut remettre un font-weight
   explicite sur .hero__lead--suite : ce paragraphe-là porte les DEUX classes
   et suivrait en silence. Le garde-fou est ici, et non là-bas, parce que
   c'est ici que la décision se prend. */
.hero__lead {
  font-size: clamp(17px, 2vw, 22px); font-weight: 400;
  color: var(--ink); max-width: min(42ch, 35vw);
}
/* LA CASSURE NE TIENT PLUS QU'À L'ENCRE, et c'est suffisant — mais il faut
   lire « encre » au complet : la teinte (marine contre terre cuite) ET la
   clarté (15:1 contre 7,6:1, soit un facteur 2,1 de luminance). Ce sont deux
   signaux indépendants, pas un seul, et le second survit au niveau de gris
   comme au daltonisme.

   IL Y EN A EU UN TROISIÈME — la graisse — et son retrait n'est pas un
   oubli : quand il a été posé, le second bloc était en --muted, c'est-à-dire
   en gris, et la graisse du premier compensait cette faiblesse. La terre
   cuite n'a pas cette faiblesse. Le 600 ne payait donc plus que son coût, qui
   était d'écraser les « em » du premier bloc (voir .hero__lead ci-dessus).

   PAS DE font-weight ICI. Il y en avait un, nécessaire tant que la base était
   en 600 ; il serait maintenant une redondance — une ligne qui a l'air
   porteuse et ne l'est pas, ce qui est le pire des deux défauts. Le garde-fou
   a été déplacé dans .hero__lead, là où la décision se prend.

   Le CORPS ne bouge toujours pas : réduire aussi la taille ferait de la fin
   du propos une note de bas de page, alors que « un empilement de bon sens »
   est un argument. */
.hero__lead--suite { margin-top: 26px; color: var(--ink-2); }

/* FENÊTRE TROP COURTE : initBrand() (main.js) a posé le mot du logo d'emblée
   dans la barre, et refermé le réservoir qui lui gardait sa place dans le
   hero. Le contenu remonte donc de toute la hauteur du mot — et il passerait
   SOUS la barre, qui n'a pas de fond, si on ne lui rendait rien.
   On lui rend exactement la hauteur de la barre (l'emblème plus ses deux fois
   22 px de padding) et huit pixels d'air, et l'empilement repart du haut :
   comme en portrait, un bloc qui commence en haut ne remonte plus quand il
   s'allonge.

   La condition d'orientation n'est pas décorative : elle ÉVITE UN PIÈGE DE
   SPÉCIFICITÉ. `.marque-en-barre .hero` pèse plus lourd que le `.hero` de la
   règle portrait plus bas dans le fichier, et l'emporterait donc sur elle
   malgré l'ordre du source — lui écrasant son `padding-top: 48vh`. Le cas
   ne peut pas se produire (une fenêtre serrée est par définition plus large
   que haute), mais une règle qui ne dépend que de l'ordre pour ne pas nuire
   est une règle qui finira par nuire. */
@media (orientation: landscape) {
  .marque-en-barre .hero {
    padding-top: calc(var(--embleme-h) + 52px);
    justify-content: flex-start;
  }
}

/* VOILE FEUTRÉ derrière l'accroche, pour qu'elle reste lisible par-dessus le
   moniteur 3D.

   Il a longtemps été une tache ovale (`radial-gradient(120% 130% at 28% 46%)`)
   et c'était un piège de géométrie : le dégradé n'atteignait sa transparence
   qu'à 0,94 fois la largeur de sa boîte À PARTIR DE SON CENTRE, alors que le
   bord gauche n'est qu'à 0,28 de ce centre. Autrement dit il était encore
   **opaque à 100 % en haut, en bas et à gauche, et à 64 % à droite** quand il
   rencontrait le bord de son propre élément : quatre coupures franches. Sur
   le fond de page uni cela ne se remarquait pas, mais le bord droit tombe en
   travers de l'écran du moniteur — là où il y a du détail fin et du contraste
   — et s'y lisait comme une couture verticale sur la machine.

   D'où un voile piloté par deux fondus LINÉAIRES, un par axe, croisés par
   `mask-composite`. Chaque fondu commence exactement au bord du texte et meurt
   au bord de la boîte, donc le voile couvre l'accroche à pleine opacité et
   s'éteint sans jamais présenter d'arête :
     · à gauche, il n'y a même pas de fondu — le calc() sort la boîte de la
       fenêtre, le bord est hors écran ;
     · à droite, 180 px de fondu, ce qui le fait mourir bien avant la fin de
       l'écran du moniteur ;
     · en haut et en bas, des fondus en pixels (même raison qu'en portrait :
       un pourcentage suivrait la hauteur du texte et mangerait la première
       ligne quand l'accroche s'allonge).

   Les deux couches de masque doivent être CROISÉES, pas additionnées : c'est
   tout l'objet de `mask-composite: intersect`. Sans lui elles s'ajouteraient
   et l'on retrouverait un rectangle plein — le défaut d'origine, donc une
   dégradation sans casse sur un moteur qui ne saurait pas composer.
   Et le masque doit porter les MÊMES fondus que le fond, sans quoi c'est le
   `backdrop-filter` qui garde l'arête : le flou, lui, ne se dégrade pas tout
   seul. */
.hero__plate { position: relative; display: inline-block; }
.hero__plate > * { position: relative; z-index: 1; }
.hero__plate::before {
  content: ""; position: absolute; z-index: 0;
  inset: -34px -180px -60px calc(-1 * clamp(20px, 5vw, 70px) - 30px);
  background: var(--scrim);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  -webkit-mask:
    linear-gradient(to right, #000 calc(100% - 180px), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 34px, #000 calc(100% - 60px), transparent 100%);
  -webkit-mask-composite: source-in;
  mask:
    linear-gradient(to right, #000 calc(100% - 180px), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 34px, #000 calc(100% - 60px), transparent 100%);
  mask-composite: intersect;
  pointer-events: none;
}

.scroll-hint {
  position: absolute; left: 50%; bottom: 34px; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  font-family: "IBM Plex Mono", monospace; font-size: 10px; letter-spacing: 0.3em; color: var(--ink-2);
}
/* Le trait qui tombe suit le libellé : l'invite est d'un seul tenant, et elle
   dit comment lire la page — pas ce que le produit fait. */
.scroll-hint i {
  width: 1px; height: 46px;
  background: linear-gradient(var(--ink-2), transparent);
  animation: fall 1.8s var(--ease) infinite;
}
@keyframes fall { 0% { transform: scaleY(0); transform-origin: top; } 60% { transform: scaleY(1); transform-origin: top; } 100% { transform: scaleY(1); transform-origin: bottom; opacity: 0; } }

/* ---------- FEATURES ---------- */
.feature {
  min-height: 100vh;
  display: flex; align-items: center;
  padding: 0 clamp(20px, 5vw, 70px);
}
.feature--left  { justify-content: flex-start; }
.feature--right { justify-content: flex-end; }

.panel {
  max-width: 440px;
  background: var(--panel-bg);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 34px 34px 38px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
/* Le repère de section — terre cuite, et non vert. C'est le site qui
   numérote son propre propos : rien n'est proposé, rien n'est cliquable. Le
   vert reste dans le panneau, sur les puces, où il énumère pour de bon. */
.panel__num {
  font-family: "IBM Plex Mono", monospace; font-size: 13px; letter-spacing: 0.3em;
  color: var(--ink-2); display: block; margin-bottom: 16px;
}
.panel__title {
  font-family: "Space Grotesk", sans-serif; font-weight: 600;
  font-size: clamp(30px, 4vw, 46px); line-height: 1.02; letter-spacing: -0.02em;
  margin-bottom: 16px;
}
.panel__text { color: var(--soft); margin-bottom: 22px; font-size: 21.5px; }
/* Phrase de conclusion, après la liste. Même nature que le second bloc du
   hero — le panneau cesse de décrire et s'adresse à vous — d'où la même encre.

   ELLE EST AU CORPS DU TEXTE COURANT, et c'est un retrait volontaire : elle
   faisait 26,5 px, et portait alors TROIS signaux pour deux messages. Le corps
   et la graisse disent la même chose — « ceci compte » — pendant que la couleur
   en dit une autre — « c'est le site qui parle ». L'échelle du panneau le
   disait aussi : 46 / 26,5 / 21,5 / 20, soit quatre corps dans un bloc de huit
   lignes dont deux presque identiques. Personne ne lit ça comme délibéré.

   C'est le CORPS qui a été retiré et non la graisse, parce que les deux
   moitiés ne coûtent pas la même chose : à 7,60:1, une terre cuite en 400 se
   lit plus mince que son rapport ne le laisse croire — les traits fins perdent
   du contraste perçu. En 600 elle tient. Restent donc graisse + couleur,
   c'est-à-dire exactement le mécanisme des « em ».

   Le blanc passe de 24 à 30 px : l'écart de corps ne sépare plus, il faut donc
   que la gouttière reprenne ce travail.

   Ce n'est pas en contradiction avec .hero__lead--suite, qui est en 400 pour
   la même encre : le hero COMMENTE (il doit être l'égal du bloc du dessus),
   l'outro CONCLUT (il doit tomber plus fort que la liste qu'il ferme). */
.panel__outro { color: var(--ink-2); margin-top: 30px; font-size: 21.5px; font-weight: 600; }
.panel__list { list-style: none; display: grid; gap: 12px; }
.panel__list li {
  position: relative; padding-left: 26px; font-size: 20px; color: var(--soft);
}
.panel__list li::before {
  content: ""; position: absolute; left: 0; top: 9px;
  width: 8px; height: 8px; background: var(--accent); border-radius: 2px;
  box-shadow: 0 0 10px var(--accent); transform: rotate(45deg);
}

/* ---------- CONTACT ----------
   Le terminal 3D est décalé à droite, comme dans le hero : la colonne de
   texte doit donc rester assez étroite pour ne pas passer devant. */
.contact {
  min-height: 100vh; display: flex; flex-direction: column; justify-content: center;
  padding: 90px clamp(20px, 5vw, 70px) 0;
}
/* Deux bornes, exactement comme l'accroche du hero : 660 px est le confort de
   lecture, 34 vw est la GÉOMÉTRIE — le moniteur 3D commence entre 39 % et
   43 % de la largeur de la fenêtre, et sous 1440 px la colonne de 660 px
   passait carrément devant lui. Mesuré avant correction : le titre débordait
   de 242 px sur l'écran à 1024, de 206 à 900, de 50 à 1152 — et l'amorce
   elle-même de 7 px à 1024. */
.contact__inner { max-width: min(660px, 34vw); }
/* Les deux questions ouvrent la section, le titre y répond : c'est le motif
   « amorce », d'où un texte discret placé AVANT un titre très grand. */
.contact__lead {
  font-size: clamp(17px, 2vw, 22px); color: var(--soft); margin-bottom: 14px;
}
/* Le corps suit la même contrainte, et pour une raison que la largeur de la
   colonne ne suffit pas à couvrir : « Contactez-nous » ne se coupe QU'AU
   TRAIT D'UNION. Le morceau « Contactez- » est insécable, et s'il ne tient
   pas dans la colonne il déborde quand même — une largeur maximale ne peut
   rien contre un mot trop long. C'est donc le corps qu'il faut brider.
   À 6 vw, le pire cas mesuré garde 46 px de marge avant le moniteur, et le
   plafond de 104 px est atteint dès 1735 px : sur grand écran le titre est
   exactement celui d'avant. */
.contact__title {
  font-family: "Space Grotesk", sans-serif; font-weight: 700;
  font-size: clamp(34px, 6vw, 104px); line-height: 0.94; letter-spacing: -0.03em;
  margin: 0;
}
/* LE FILET DU LOGO, une seconde fois — et la dernière.
   La page s'ouvre sur AGROSOFT surmontant son filet spectral ; elle se ferme
   sur « Contactez-nous » traité de la même façon. Ce n'est pas un ornement de
   plus : c'est le même geste aux deux bouts, et c'est ce qui en fait une
   fermeture plutôt qu'une décoration. Un troisième emploi le banaliserait.

   Il n'a pas besoin de sa piste sombre ici, contrairement à la barre du
   chargeur. Cette règle-là vient de la FINESSE, pas du thème : à deux
   pixels, le tiers clair du filet s'efface sur le papier. À cinq, sous un
   titre de cent, il se lit exactement comme celui du logotype — qui est posé
   sur le même papier, quatre écrans plus haut, et qui tient.

   Il disparaît avec le titre en fin de zoom, puisqu'il en fait partie. */
.contact__title::after {
  content: ""; display: block;
  /* 88 % et non 100 % : la colonne du contact est bornee a 34 vw pour ne pas
     croiser le moniteur 3D, mais le TEXTE s'arrete au dernier mot alors qu'un
     trait, lui, remplit la colonne. A 1024 px la marge tombait a 15 px pour un
     moniteur qui flotte de 12 — mesure, pas estime (voir BUILD.md). A 88 % elle
     remonte a 57 px, et le filet cesse de buter contre le bord de sa colonne,
     ce qui se voit. */
  height: 5px; width: min(88%, 440px); margin-top: 26px; border-radius: 3px;
  background: var(--spectre);
}

/* ---------- TERMINAL DE CONTACT ----------
   Deux états, un seul balisage :

   · au repos (mobile, WebGL absent, JavaScript absent) c'est un panneau
     ordinaire en fin de page, mise en page verticale et lisible ;
   · en mode zoom (.zoom-on), il devient une boîte 16:9 en position fixe,
     calée au pixel près sur le rectangle projeté de l'écran 3D.

   Les six variables ci-dessous sont posées par layoutTerm() (main.js)
   d'après la taille réelle de cette boîte. --term-cw et --term-ch sont la
   cellule de la grille 80 × 26 ; --term-ls étire l'avance de la police
   jusqu'à cette cellule, exactement comme le fait metrics() côté canvas.
   C'est ce qui rend les deux rendus superposables. */
.term {
  --term-w: 100%;
  --term-h: auto;
  --term-px: 22px;
  --term-py: 26px;
  --term-cw: 8px;
  --term-ch: 26px;
  --term-fs: 15px;
  --term-ls: 0px;

  max-width: 940px;
  margin: 0 auto;
  padding: 0 clamp(20px, 5vw, 70px) 70px;

  /* Le panneau est hors de <main> : sans plan à lui, il resterait un bloc non
     positionné, donc peint SOUS le canvas 3D (#stage, z-index 1) et sous les
     voiles. On lui donne le même plan qu'à <main>. C'est ce qui manquait
     partout où le vol ne prend pas la main — sans WebGL, sans JavaScript : le
     formulaire s'y affichait derrière l'écran 3D. */
  position: relative;
  z-index: 10;
}
.term__screen {
  background: var(--t-bg);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 30px 26px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 15px;
  line-height: 1.75;
  color: var(--t-ink);
}
.term__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px 20px; }
.term__title { border-bottom: 1px solid currentColor; }
.term__date { color: var(--t-cyan); margin-left: auto; }
.term__lead { color: var(--t-gold); margin-top: 14px; }
.term__rule { border: 0; border-top: 1px solid var(--t-ink); margin: 18px 0; }

.tfield { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 12px; }
.tfield__k { color: var(--t-gold); white-space: pre; }
.tfield__v {
  font: inherit; letter-spacing: inherit; color: var(--t-ink);
  background: var(--t-pad);
  border: 0; border-bottom: 1px solid var(--t-field);
  padding: 6px 8px; flex: 1 1 14ch; min-width: 0;
}
.tfield__v:focus { outline: 2px solid var(--t-gold); outline-offset: 1px; }

.term__send { margin-top: 20px; color: var(--t-gold); }
/* Vidéo inverse : c'est ainsi qu'un 5250 met une zone en évidence, et ça se
   lit sans hésitation comme un bouton. */
.term__btn {
  font: inherit; letter-spacing: inherit; cursor: pointer;
  background: var(--t-ink); color: var(--t-bg);
  border: 0; padding: 4px 12px; margin-right: 8px;
}
.term__btn:hover { background: var(--t-gold); }
/* L'engagement de rappel, en vidéo inverse. Le boss la voulait plus grosse ;
   la grille 80 × 26 l'interdit, chaque caractère y a une taille imposée. Le
   terminal, lui, a toujours eu une réponse à cela : inverser le fond et
   l'encre. La phrase garde son corps et sa couleur, elle ne change que de rôle.
   Le pavé déborde d'une colonne de chaque côté, exactement comme le pavé peint
   par drawDemande() (screens.js) — les deux doivent rester d'accord, sinon la
   bascule en fin de zoom se voit. */
.term__note { color: var(--t-cyan); }
.term__note span {
  background: var(--t-cyan); color: var(--t-bg);
  padding: 0 var(--term-cw);
}
.term__msg {
  margin-top: 16px; padding: 8px 12px;
  border-left: 3px solid var(--t-cyan); color: var(--t-ink);
}
.term__msg.is-err { border-left-color: var(--t-red); }

/* Les touches de fonction tiennent lieu de pied de page */
.term__fkeys {
  display: flex; flex-wrap: wrap; gap: 10px 26px;
  margin-top: 26px; padding-top: 14px;
  border-top: 1px solid var(--line);
  color: var(--t-dim); font-size: 13px;
}
.term__fkeys a { color: var(--t-ink); }
.term__fkeys a:hover { color: var(--t-gold); }

/* Piège à robots : hors du champ de vision et hors du flux de tabulation.
   Ni display:none ni visibility:hidden — les automates les détectent. */
.cform__trap {
  position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden;
}

/* ---------- Terminal en mode zoom ----------
   Le panneau quitte le flux et vient prendre la place de l'écran 3D. Deux
   manières de s'y poser, et le vol se joue dans les deux :

   · `.zoom-grille` — la manière exacte : le panneau se cale au pixel sur le
     rectangle projeté de l'écran, en cellules de la grille 80 × 26. C'est le
     cas de tout écran assez large, y compris un navigateur zoomé à l'extrême
     (une fenêtre CSS étroite y reste lisible, chaque pixel CSS s'affichant sur
     deux ou trois pixels réels).

   · sans `.zoom-grille` — la manière lisible, pour les écrans où la grille
     80 × 26 ne tiendrait pas : un téléphone en portrait n'offre que 4,4 px par
     caractère, et en paysage les champs descendraient sous 16 px, taille au
     dessous de laquelle Safari iOS zoome de lui-même dès qu'on tape dans un
     champ — ce qui ferait exploser la mise en page fixe. Le panneau y garde sa
     mise en page ordinaire et prend toute la fenêtre. La substitution n'est
     alors plus superposable, mais le scintillement CRT la couvre : c'est lui,
     et non l'alignement, qui rend la bascule invisible.

   C'est main.js qui tranche (`volEnGrille()`), d'après la géométrie réelle et
   la taille de police qui en découle. */
.zoom-on .term {
  position: fixed; inset: 0;
  width: auto; height: auto; max-width: none; margin: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  /* le fond déborde de la boîte 16:9 : sur un écran qui n'est pas au format,
     c'est ce qui évite d'apercevoir le cadre du moniteur derrière */
  background: var(--t-bg);
  z-index: 20;
  opacity: 0; visibility: hidden; pointer-events: none;
}
.zoom-on .term.is-shown { visibility: visible; }
.zoom-on .term.is-live { pointer-events: auto; }
/* La boîte fait exactement --term-w × --term-h, la taille sur laquelle
   l'écran 3D se projette : c'est `align-items: center` du parent qui la
   centre, comme la caméra centre l'écran dans la fenêtre. Un `height: 100%`
   traînait ici et l'emportait sur --term-h : la boîte prenait alors toute la
   hauteur de la fenêtre, et dès que celle-ci n'était pas au format 16:9 —
   1440 × 900 d'un MacBook, par exemple — la grille commençait 45 px trop haut.
   Le scintillement CRT masquait le saut, mais il était bien là. */
.zoom-grille .term__screen {
  width: var(--term-w); height: var(--term-h);
  /* aucun rétrécissement toléré : la boîte est mesurée, pas négociée */
  flex: none;
  display: grid;
  grid-template-rows: repeat(26, var(--term-ch));
  padding: var(--term-py) var(--term-px);
  border: 0; border-radius: 0;
  font-size: var(--term-fs);
  line-height: var(--term-ch);
  letter-spacing: var(--term-ls);
}
/* La grille du 5250 est indicée à partir de 0, celle de CSS à partir de 1 :
   d'où le décalage d'une unité par rapport aux lignes de drawDemande(). */
/* Colonnes de l'en-tête, identiques à celles de drawDemande() : rien à
   gauche, le titre en 27, la date au bord droit. */
.zoom-grille .term__head   { grid-row: 2; margin: 0; gap: 0; }
.zoom-grille .term__title  { margin-left: calc(27 * var(--term-cw)); }
.zoom-grille .term__lead   { grid-row: 4 / span 2; margin: 0; padding-left: calc(4 * var(--term-cw)); }
.zoom-grille .term__rule   { grid-row: 7; margin: 0; align-self: center; }
/* Le <form> s'efface de la mise en page pour que ses champs deviennent des
   cases de la grille de l'écran. La soumission, elle, ne dépend pas de la
   mise en page : le formulaire reste parfaitement fonctionnel. */
.zoom-grille .term__form { display: contents; }
.zoom-grille .tfield {
  margin: 0; gap: 0; flex-wrap: nowrap;
  padding-left: calc(4 * var(--term-cw));
}
.zoom-grille .tfield--1 { grid-row: 9; }
.zoom-grille .tfield--2 { grid-row: 11; }
.zoom-grille .tfield--3 { grid-row: 13; }
.zoom-grille .tfield--4 { grid-row: 15; }
.zoom-grille .tfield--5 { grid-row: 17; }
.zoom-grille .tfield__v {
  flex: 0 0 auto;
  margin-left: calc(1 * var(--term-cw));
  height: calc(1.5 * var(--term-ch));
  padding: 0 calc(0.3 * var(--term-cw));
}
.zoom-grille .tfield__v--18 { width: calc(18 * var(--term-cw)); }
.zoom-grille .tfield__v--30 { width: calc(30 * var(--term-cw)); }
.zoom-grille .tfield__v--44 { width: calc(44 * var(--term-cw)); }
.zoom-grille .term__send { grid-row: 20; margin: 0; padding-left: calc(4 * var(--term-cw)); }
.zoom-grille .term__btn  { padding: 0; margin-right: calc(1 * var(--term-cw)); }
/* 3 colonnes et non 4 : le pavé commence une colonne avant le texte, c'est sa
   colonne de marge. Le texte retombe donc bien en 4, comme les lignes voisines. */
.zoom-grille .term__note { grid-row: 21; margin: 0; padding-left: calc(3 * var(--term-cw)); }
.zoom-grille .term__msg  { grid-row: 23; margin: 0; padding: 0 0 0 calc(4 * var(--term-cw)); border: 0; }
.zoom-grille .term__msg.is-err { color: var(--t-red); }
.zoom-grille .term__fkeys {
  grid-row: 25; margin: 0; padding: 0 0 0 calc(3 * var(--term-cw));
  border: 0; font-size: inherit; gap: calc(4 * var(--term-cw));
}

/* ---------- Terminal en mode lisible ----------
   Là où la grille ne tient pas, le panneau prend toute la fenêtre et garde sa
   mise en page ordinaire, agrandie et resserrée pour tenir sans défilement
   interne. On arrive au même endroit — dans l'écran — par un autre chemin.

   Deux exigences moins évidentes que le reste :

   · **16 px dans les champs, au moins.** En dessous, Safari iOS zoome la page
     de lui-même dès qu'on tape dans un champ ; sur un panneau en position fixe
     le résultat est ingérable. C'est la contrainte qui a décidé de ce mode.
   · **pas de piège au défilement.** Le panneau tient dans la fenêtre, donc
     rien à faire défiler dedans, donc le geste continue de passer à la page :
     on ressort du terminal en remontant, comme sur un ordinateur. Le
     `overflow-y: auto` n'est qu'un filet pour les très petits écrans, et sans
     `overscroll-behavior: contain` — arrivé au bout, le geste reprend sa
     course sur la page. */
.zoom-on:not(.zoom-grille) .term {
  align-items: flex-start;
  overflow-y: auto;
}
.zoom-on:not(.zoom-grille) .term__screen {
  width: 100%; min-height: 100%;
  border: 0; border-radius: 0;
  padding: clamp(18px, 6vh, 60px) clamp(16px, 5vw, 40px);
  display: flex; flex-direction: column; justify-content: center;
  font-size: clamp(13px, 3.6vw, 17px);
  line-height: 1.7;
}
/* Le libellé garde ses points de conduite — c'est la signature du 5250 — mais
   il ne doit plus renvoyer le champ à la ligne : sur 390 px les deux tiennent
   côte à côte si on laisse le champ prendre ce qui reste. */
.zoom-on:not(.zoom-grille) .tfield { flex-wrap: nowrap; gap: 8px; margin-bottom: clamp(8px, 1.6vh, 18px); }
.zoom-on:not(.zoom-grille) .tfield__k { flex: 0 0 auto; font-size: 0.86em; }
.zoom-on:not(.zoom-grille) .tfield__v {
  flex: 1 1 auto; width: auto; min-width: 0;
  font-size: max(16px, 1em);          /* le plancher de Safari iOS */
  padding: 7px 8px;
}
.zoom-on:not(.zoom-grille) .term__lead { margin-top: clamp(8px, 2vh, 20px); }
.zoom-on:not(.zoom-grille) .term__rule { margin: clamp(10px, 2.4vh, 22px) 0; }
.zoom-on:not(.zoom-grille) .term__send { margin-top: clamp(10px, 2.4vh, 22px); }
.zoom-on:not(.zoom-grille) .term__btn { padding: 6px 14px; }
.zoom-on:not(.zoom-grille) .term__fkeys {
  margin-top: clamp(14px, 3vh, 30px); padding-top: 12px;
}
/* Téléphone en paysage : fenêtre large et très basse — 390 px de haut, parfois
   320. En une colonne le formulaire y fait une fois et demie la hauteur
   disponible ; en deux, il tient d'un coup d'œil et le panneau n'a plus rien à
   faire défiler. Le piège à robots est en position absolue : il ne prend pas de
   case dans la grille. */
@media (orientation: landscape) and (max-height: 540px) {
  /* minmax(0, 1fr) et non 1fr : le minimum automatique d'une colonne vaut le
     min-content de ce qu'elle porte, et le champ y compte pour sa largeur
     intrinsèque d'<input> (une vingtaine de caractères). Les colonnes
     s'élargissaient donc à 417 px et le panneau débordait de la fenêtre. */
  .zoom-on:not(.zoom-grille) .term__form {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(16px, 4vw, 44px);
  }
  .zoom-on:not(.zoom-grille) .term__send,
  .zoom-on:not(.zoom-grille) .term__note,
  .zoom-on:not(.zoom-grille) .term__msg { grid-column: 1 / -1; }
  /* Et l'interligne se resserre : c'est ce qui fait tenir les douze lignes dans
     320 px de haut, la fenêtre d'un vieil iPhone tourné. Les champs, eux, ne
     descendent pas — 16 px reste le plancher, quoi qu'il arrive. */
  .zoom-on:not(.zoom-grille) .term__screen {
    padding: 12px clamp(16px, 5vw, 40px); line-height: 1.4;
  }
  .zoom-on:not(.zoom-grille) .term__lead { margin-top: 2px; }
  .zoom-on:not(.zoom-grille) .term__rule { margin: 8px 0; }
  .zoom-on:not(.zoom-grille) .tfield { margin-bottom: 6px; }
  .zoom-on:not(.zoom-grille) .term__send { margin-top: 8px; }
  .zoom-on:not(.zoom-grille) .term__fkeys { margin-top: 10px; padding-top: 8px; }
}

/* La hauteur de la section EST la course du vol : c'est elle qu'on allonge
   ou qu'on raccourcit pour rendre le zoom plus lent ou plus vif.
   L'accroche reste collée pendant la première moitié, le temps qu'elle
   s'efface — son opacité est pilotée par setZoom() (main.js). */
.zoom-on .contact {
  min-height: 230vh;
  justify-content: flex-start;
  padding-top: 32vh;
}
.zoom-on .contact__inner { position: sticky; top: 32vh; }

/* ---------- Reveal animation base (JS toggles) ---------- */
.panel, .contact__inner, .hero__inner { will-change: transform, opacity; }

/* ---------- Light theme tweaks ----------
   (Le `mix-blend-mode: normal` qui neutralisait ici le mode « screen » de la
   marque a disparu avec lui : le mot est maintenant une image, qui a ses
   propres couleurs et son propre fichier par thème.) */
:root[data-theme="light"] .panel { -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
:root[data-theme="light"] .loader { background: var(--bg); }
:root[data-theme="light"] .nav__cta { box-shadow: 0 6px 22px rgba(var(--accent-rgb),0.22); }

/* ---------- No-WebGL fallback ---------- */
body.no-webgl #stage { display: none; }
body.no-webgl .feature, body.no-webgl .hero { min-height: auto; padding-top: 120px; padding-bottom: 60px; }

/* Sans JavaScript, ou sans WebGL, initBrand() n'est jamais atteint : le mot du
   logo n'obtient jamais « .is-placed » et resterait donc invisible — le hero
   n'aurait plus de titre du tout, et le visiteur ne verrait de la marque que
   l'emblème de la barre. C'est un cas qui arrive pour de vrai : WebGL se
   bloque par stratégie d'entreprise, et c'est courant sur un poste géré.

   On rend alors au mot ce qu'il est au départ, un titre ordinaire posé en tête
   de page. Il ne remonte plus au défilement — il n'y a plus rien pour le faire
   remonter — mais il est là, et la page a un titre.

   Ces deux sélecteurs viennent APRÈS « .brand:not(.is-placed) », à spécificité
   égale : c'est l'ordre qui les fait gagner. Les déplacer plus haut les
   annulerait. */
.no-js .brand,
body.no-webgl .brand {
  position: static;
  opacity: 1;
  margin: 0 clamp(20px, 5vw, 70px) 30px;
  padding-top: 118px;   /* la barre est en position fixe : elle ne réserve rien */
}
.no-js .hero,
body.no-webgl .hero { padding-top: 8px; }
.no-js .hero__brandslot,
body.no-webgl .hero__brandslot { display: none; }

/* Et pendant qu'on y est, deux éléments que le JavaScript seul fait vivre.

   L'écran d'attente n'est retiré que par le script : sans lui, il reste posé
   sur toute la page, opaque, indéfiniment — la page était donc entièrement
   noire, et pas seulement privée de sa 3D. (Le défaut ne date pas de cette
   version : la version en ligne se comporte de la même façon.) Il n'y a rien
   à attendre si le script ne vient pas, donc on ne l'affiche pas.

   La bascule de thème, elle, n'aurait plus de gestionnaire de clic : un
   bouton qui ne répond pas vaut moins qu'un bouton absent. Le site s'ouvre
   alors dans le thème que la feuille de style donne par défaut. */
.no-js .loader { display: none; }
.no-js .theme-toggle { display: none; }

/* ---------- Responsive ----------
   Deux régimes, et le second n'est pas « petit écran » mais **écran haut**.

   La distinction compte, et elle a longtemps été mal placée. Le régime
   téléphone était déclenché par `max-width: 820px` seul, ce qui rangeait dans
   la même case deux situations qui n'ont rien à voir :

   · un téléphone en portrait — écran physiquement petit, plus haut que large,
     où le moniteur doit se replier dans un bandeau et le texte passer dessous ;
   · une fenêtre de bureau au zoom poussé — 200 % sur un écran de 1920 px ne
     laisse que 960 px CSS, 300 % en laisse 640. La fenêtre CSS est étroite,
     mais **chaque pixel CSS s'affiche sur deux ou trois pixels réels** : rien
     n'est petit à l'œil, et la mise en page large reste la bonne. Or c'est
     précisément le visiteur qui zoome parce qu'il voit mal qui se voyait
     priver de tout l'effet.

   D'où la condition d'orientation. Ce qui dépend vraiment de la largeur — la
   barre de nav qui se dégarnit — reste sur la largeur. */
@media (max-width: 820px) {
  .nav__links a:not(.nav__cta) { display: none; }
  /* deux colonnes de champs deviennent illisibles sous 820 px */
  .cform__grid { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 820px) and (orientation: portrait) {
  .feature--left, .feature--right { justify-content: center; }
  .feature { padding-top: 60vh; padding-bottom: 40px; min-height: auto; }
  .panel { max-width: 560px; width: 100%; }
  /* Le hero s'aligne sur les autres sections : son contenu passe SOUS le
     bandeau du moniteur, au lieu d'être centré dans la fenêtre. Centré, il
     remontait dès qu'on lui ajoutait quelque chose — et l'ajout de la bande
     des filières a suffi à faire monter le mot du logo en plein sur l'écran
     3D. Un empilement qui commence en haut ne bouge plus quand il s'allonge :
     c'est toute la raison du flex-start. */
  .hero { padding-top: 48vh; padding-bottom: 40px; justify-content: flex-start; }
  #stage { height: 56vh; top: 0; bottom: auto; }
  .fx-vignette { background: var(--vignette-tel); }

  /* PLEINE LARGEUR SUR TÉLÉPHONE.
     Sur grand écran, l'accroche est une colonne étroite posée à gauche du
     moniteur : son voile est une tache ovale qui se perd de tous les côtés,
     et on ne voit jamais où le flou s'arrête. En portrait il n'y a plus de
     colonne — le texte occupe toute la fenêtre — et cette même tache ovale
     s'arrêtait EN PLEIN DANS le texte : la limite du flou se lisait comme
     une barre en travers du paragraphe, d'autant plus visible que
     l'accroche s'est allongée.
     Donc : le bloc prend toute la largeur, et son voile déborde des marges
     de la section pour aller mourir HORS de l'écran. Il ne reste alors qu'un
     fondu haut/bas, le seul qui ait un bord à montrer ici. Le calc() annule
     exactement le padding du hero : le voile touche les deux bords, sans
     jamais les dépasser (pas de barre de défilement horizontale). */
  .hero__inner { max-width: none; }
  .hero__plate { display: block; }
  /* Le VOILE prend toute la largeur ; le TEXTE, lui, garde une mesure.
     Sur téléphone la question ne se pose pas — 52 caractères sont plus larges
     que l'écran, la règle ne mord jamais et le texte va bien d'un bord à
     l'autre. Elle ne sert qu'aux portraits larges (tablette), où la pleine
     largeur donnerait des lignes de quatre-vingt-dix caractères, illisibles.
     Les 30 ch de la version bureau, eux, étaient beaucoup trop étroits ici. */
  .hero__lead { max-width: 52ch; }
  .hero__plate::before {
    inset: -24px calc(-1 * clamp(20px, 5vw, 70px)) -64px;
    background: var(--scrim);
    /* Le masque porte AUSSI le flou : sans lui, backdrop-filter garderait un
       bord net en haut et en bas du rectangle.
       Les fondus sont donnés EN PIXELS, pas en pourcentage. En pourcentage
       ils suivent la hauteur du bloc : l'accroche fait ici une dizaine de
       lignes, et 10 % faisaient un dégradé de quarante pixels qui mangeait
       la première ligne — celle-là même qui passe par-dessus le moniteur au
       défilement, donc celle qui a le plus besoin du voile. En pixels, le
       fondu se termine juste avant le texte quelle que soit sa longueur. */
    -webkit-mask: linear-gradient(to bottom, transparent 0, #000 26px, #000 calc(100% - 66px), transparent 100%);
    mask: linear-gradient(to bottom, transparent 0, #000 26px, #000 calc(100% - 66px), transparent 100%);
    /* Ici le masque n'a plus qu'UNE couche — les deux bords latéraux sortent
       déjà de l'écran, il n'y a rien à croiser. Le mode de composition hérité
       de la règle de bureau doit donc être remis au neutre : `source-in` sur
       une couche unique n'a rien contre quoi composer. */
    -webkit-mask-composite: source-over;
    mask-composite: add;
  }

  /* Les deux bornes en vw du contact n'ont plus lieu d'être : en portrait le
     moniteur est un bandeau EN HAUT, il n'y a plus de colonne à sa gauche
     dont il faudrait se tenir à l'écart. Les laisser reviendrait à écraser le
     titre dans 34 % de la largeur d'un téléphone — 133 px à 390 px de
     fenêtre, moins que le seul mot « Contactez- ». */
  .contact__inner { max-width: 660px; }
  .contact__title { font-size: clamp(40px, 8vw, 104px); }

  /* Le repère de défilement retourne DANS LE FLUX, sous l'accroche.
     Il était calé en absolu au bas de la section : tant que le hero tenait
     exactement une fenêtre, il tombait juste. Ici la section est plus haute
     que l'écran (bandeau du moniteur + accroche), son bas n'est plus le bas
     de la fenêtre, et le repère venait s'écrire par-dessus les dernières
     lignes du texte. Dans le flux, il ne peut plus rien chevaucher. */
  .scroll-hint {
    position: static; left: auto; bottom: auto; transform: none;
    margin-top: 30px;
  }

  /* Le vol dans ce régime :
     · l'accroche passe SOUS le bandeau du moniteur, comme tous les autres
       textes de la version téléphone (`padding-top: 60vh` ci-dessus). À 32 vh
       elle lui passait en travers — l'écran n'occupe ici que le haut, et
       l'accroche reste visible bien plus longtemps qu'en mode grille, puisque
       c'est elle qui tient la moitié basse pendant l'approche ;
     · 230 vh font un long parcours au pouce pour un seul effet, et le vol est
       plus court à jouer — l'écran ne part que du bandeau. */
  .zoom-on .contact { min-height: 165vh; padding-top: 62vh; }
  .zoom-on .contact__inner { top: 62vh; }
}

/* Animations réduites : on coupe ce qui bouge TOUT SEUL, et cela seulement.
   Le vol de caméra, lui, est mené par le défilement du visiteur — il ne part
   jamais sans qu'on l'ait demandé, et il s'arrête, revient et repart au doigt.
   Le lui retirer privait de l'effet des postes qui n'avaient rien demandé de
   tel : sous Windows 11, « Effets d'animation » est un réglage d'apparence
   qu'on croise désactivé sur bien des machines gérées. Le flottement du
   moniteur, la dérive, la parallaxe à la souris et le battement de la lueur
   d'écran sont neutralisés dans animate() (`calme`, main.js), et les entrées
   au défilement ne sont pas jouées. */
@media (prefers-reduced-motion: reduce) {
  .fx-grain, .scroll-hint i, .loader__bar span { animation: none; }
}
