/* =========================================================================
   fx/globe.css — « VOYAGE » : au survol de « BASED IN / Paris, France »,
   toute la page bascule dans l'espace, la Terre apparaît en entier, puis la
   caméra descend jusqu'à Paris — jusqu'à voir la Seine, l'Étoile et les deux
   Bois.
   Fragment440 — vanilla, zéro dépendance, zéro WebGL.
   -------------------------------------------------------------------------
   TECHNIQUE — une seule idée, une caméra.
     Il n'y a pas de 3D. Il y a une CARTE équirectangulaire et une fenêtre
     ronde posée dessus. Toute l'image se déduit de quatre nombres, calculés
     image par image dans fx/globe.js :

       ppd   px par degré de LATITUDE
       kx    écrasement horizontal (1 = plate-carrée, cos(lat) = vue locale
             correcte quand on est descendu sur l'Europe)
       lonC  longitude au centre de l'écran
       latC  latitude au centre de l'écran

     Le fond de la carte fait alors 360*ppd*kx par 180*ppd, et il suffit de le
     positionner pour que (lonC, latC) tombe au centre. Le « globe » est cette
     même carte vue à travers un disque de diamètre 180*ppd : quand ppd
     augmente, le disque grandit ET la carte grossit, exactement comme une
     descente vers la planète. Quand le disque déborde de l'écran, on passe
     l'élément en plein cadre (.is-flat) — le disque n'a plus rien à découper.

   GÉOMÉTRIE — LE PIÈGE HISTORIQUE RESTE LEVÉ.
     L'ancienne version codait en dur `top: 112.14px` pour le marqueur et
     `-128.27px` pour le décalage de texture : deux valeurs mesurées pour une
     sphère de 250 px. Rien n'est codé en dur — tout est calculé.
     VÉRIFICATION : en réinjectant les paramètres de l'ancienne version
     (ppd = 250/180 = 1.3889, kx = 1, lonC = 2.3522, latC = 0, disque 250 px)
     les formules de globe.js redonnent
        background-position-x = 125 - (182.3522/360)*500 = -128.27 px  ✔
        marqueur à 125 - 48.8566*1.3889 = 57.14 px du haut du disque   ✔
     La géométrie est la même, elle est simplement paramétrique — et le
     marqueur tombe sur Paris à N'IMPORTE QUELLE échelle, y compris à 9216
     px/degré où Paris fait 2270 px de large.

   OÙ SONT PASSÉS LES STYLES DE POSITION.
     Les nappes ne sont plus pilotées par des variables CSS sur <html>. Une
     variable custom sur la racine force un recalcul de style sur tout ce qui
     la référence ; à sept nappes et soixante images par seconde ça se paie.
     globe.js écrit maintenant `backgroundSize`, `backgroundPosition` et
     `opacity` EN LIGNE sur chaque nappe — six éléments touchés, rien à
     propager. Ce fichier ne décrit plus que ce qui ne bouge pas.

   NOIR ET BLANC — c'est un TRAITEMENT, pas un filtre.
     Aucun `filter: grayscale()` ici : il coûterait à chaque image composée et
     rendrait une bouillie grise où l'océan et la terre se ressemblent. Les
     textures sont converties HORS LIGNE (fx/preview/globe-v2/_work/bw3.py),
     avec un masque d'eau qui envoie les terres dans le haut de l'échelle et
     les eaux dans le bas. Conséquence pour ce fichier : tout ce qui est peint
     ici — relief, atmosphère, brume, marqueur — est NEUTRE. Pas un bleu.

   LISIBILITÉ — la page contact est claire, l'espace est noir.
     Le texte bascule en clair pendant l'effet (bloc « THÈME NUIT » plus bas)
     et un voile dégradé (.fxg-scrim) garantit le contraste sur les bords, là
     où vivent le titre, l'e-mail, le pied de page.
   ========================================================================= */

/* =========================================================================
   1. LA SCÈNE
   ========================================================================= */

/* z-index 15 : au-dessus du fond animé (.fx-bg, -1) et des voiles d'entrée
   de contact.html (#contact-curtain 10, #contact-reveal 11), en dessous de
   #main-site (20) — donc le texte de la page reste AU-DESSUS de l'espace. */
.fxg-space {
  position: fixed;
  inset: 0;
  z-index: 15;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  background: #000;                 /* noir franc : demande explicite */
  contain: layout paint style;
  /* 0,42 s, courbe mordante : la page s'assombrit vite, ce qui raccourcit la
     fenêtre pendant laquelle le fond n'est ni clair ni noir — c'est cette
     fenêtre qui décide de la lisibilité (cf. §5). */
  transition: opacity 0.42s cubic-bezier(0.32, 0.94, 0.28, 1);
}

.fxg-space.is-in  { opacity: 1; }

/* LE TEXTE FANTÔME DU RETOUR — le défaut, sa mesure, et sa correction.

   MESURE (screencast CDP image par image, contraste WCAG entre la couleur
   CALCULÉE du texte et la luminance MÉDIANE du fond réellement peint derrière
   lui, fx/preview/globe-v2/mesures/) :

     .contact-title   creux 1,02:1 à +436 ms   sous 4,5:1 pendant  516 ms
     .contact-sub     creux 1,01:1 à +403 ms                        583 ms
     .contact-label   creux 1,02:1 à +339 ms                       1435 ms
     .footer-links a  creux 1,01:1 à +504 ms                       1063 ms
     .nav-contact     creux 1,13:1 à +504 ms                       1063 ms

   1,01:1, c'est du texte invisible. Un tiers de seconde.

   CAUSE, relevée sur le profil : à +388 ms le voile n'était plus qu'à 0,161
   — la page était DÉJÀ redevenue claire (luminance du fond 0,63) — et la
   couleur du texte valait encore 0,880, c'est-à-dire blanche. Le voile
   attendait 160 ms et le texte 280 ms : les DEUX attendaient, et le texte
   attendait plus longtemps. Il traversait donc le gris pile au moment où le
   fond avait fini de traverser le sien.

   CE QU'ON PEUT ET NE PEUT PAS FAIRE. Faire passer une page claire à texte
   sombre vers un fond noir à texte clair, c'est croiser deux luminances. Les
   chiffres disent exactement où est la marge :
     texte clair (L 0,913) : lisible tant que le fond est sous L 0,164
     texte sombre (L 0,023) : lisible dès que le fond dépasse L 0,279
   Entre ces deux bornes, AUCUNE couleur de texte n'atteint 4,5:1 — la bande
   est infranchissable, elle ne peut qu'être TRAVERSÉE VITE. Retarder le texte
   ne la supprime pas, ça l'allonge.

   LA CORRECTION est donc géométrique, pas cosmétique : le voile part tout de
   suite (courbe rapide au milieu, c'est là qu'on traverse la bande), et la
   couleur du texte bascule EN 90 ms, calée sur l'instant où le fond traverse
   — mesuré à +150 -> +210 ms. Même règle à l'aller, où le fond traverse la
   même bande au même endroit du geste. Le retour est le miroir exact de
   l'aller, et les deux se lisent d'un seul mouvement. */
.fxg-space.is-out {
  opacity: 0;
  transition: opacity 0.34s cubic-bezier(0.4, 0, 0.2, 1);
}

/* La Terre et les étoiles ne s'effacent PAS toutes seules au retour : elles
   partent avec le voile qui les contient. Mesuré sur l'ancienne version, la
   Terre tombait à 0,53 d'opacité quand le ciel était encore à 0,92 — 140 ms
   pendant lesquels on regardait un champ d'étoiles vide. Un objet qui
   s'évapore avant son décor, c'est exactement ce qui se lit comme un
   à-coup. Et c'est en prime une propriété animée de moins. */
.fxg-space.is-out .fxg-earth,
.fxg-space.is-out .fxg-starfield {
  opacity: 1;
  transition: none;
}

/* Le fond animé de la page ne sert plus à rien sous un ciel opaque : on le
   retire du flux (display:none) une fois l'espace complètement fondu.
   L'IntersectionObserver de fx/shader-bg.js voit alors son hôte sortir du
   viewport et COUPE SON rAF tout seul — aucune ligne de shader-bg n'est
   touchée, et il repart de lui-même au retrait de la souris. */
html.fxg-night-full .fx-bg { display: none; }

/* ---------- le vide : NOIR FRANC ----------
   Pas d'anthracite, pas de bleu nuit : le vide est noir. Le seul écart au
   #000 est un souffle de 5 unités autour de la Terre — assez pour que
   l'image ne soit pas mathématiquement plate, trop peu pour qu'on le nomme
   autrement que « noir ». C'est ce noir qui fait que la planète ÉCLAIRE au
   lieu de simplement se poser, et qui rend les étoiles blanches lisibles. */
.fxg-void {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(96% 82% at 50% 42%, #070707 0%, #030303 42%, #000 76%, #000 100%);
}

/* ---------- les étoiles ----------
   Un seul élément par nappe : la nappe est une liste de box-shadow construite
   par le JS. Trois nappes, trois vitesses de parallaxe, et un scintillement
   porté par l'opacité de la nappe entière — donc animé par le compositeur,
   jamais repeint.

   POURQUOI L'ÉCHELLE EST EN LIGNE ET L'OPACITÉ EN KEYFRAME. Le scintillement
   doit tourner tout seul (0 rAF une fois posé) : c'est une animation CSS. La
   parallaxe, elle, suit la descente : c'est le JS qui écrit le `scale` sur
   `--fxg-star`. Les deux vivent sur la MÊME propriété si on n'y prend garde
   (une animation d'opacité et un transform ne se marchent pas dessus, mais
   une animation qui toucherait au transform écraserait le scale). D'où la
   séparation stricte : keyframes -> opacity, JS -> transform. */
.fxg-starfield {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  transform: scale(calc(1 + (var(--fxg-star, 0) * var(--fxg-star-k, 1))));
}

.fxg-starfield--a { --fxg-star-k: 0.34; animation: fxg-tw 5.5s ease-in-out infinite alternate; }
.fxg-starfield--b { --fxg-star-k: 0.68; animation: fxg-tw 4.1s ease-in-out 0.7s infinite alternate; }
.fxg-starfield--c { --fxg-star-k: 1.20; animation: fxg-tw 6.7s ease-in-out 1.4s infinite alternate; }

@keyframes fxg-tw {
  from { opacity: 0.55; }
  to   { opacity: 1; }
}

/* Les étoiles s'effacent quand la planète a rempli le cadre : il n'y a plus
   de ciel à montrer, et trois nappes de box-shadow qui continuent à se
   composer sous une image opaque, c'est du travail pour rien. */
.fxg-starfield { transition: opacity 0.5s linear; }
.fxg-space.is-ground .fxg-starfield {
  animation: none;
  opacity: 0;
  visibility: hidden;
}

.fxg-stars {
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: transparent;
  /* box-shadow posé par le JS */
}

/* =========================================================================
   2. LA TERRE
   ========================================================================= */

/* RÉGIME A — le disque est encore dans le cadre : l'élément EST le globe.
   Taille et centre sont écrits en ligne par le JS (width/height/transform). */
.fxg-earth {
  position: absolute;
  left: 0;
  top: 0;
  width: 460px;
  height: 460px;
  border-radius: 50%;
  contain: layout paint style;
  opacity: 0;
  /* MÊME durée et MÊME courbe que .fxg-space. L'ancienne version fondait le
     ciel en 0,50 s et la Terre en 0,72 s : la planète continuait à monter en
     densité 220 ms après que le noir se soit posé, ce qui se lit comme un
     second temps parasite. Un seul geste, une seule durée. */
  transition: opacity 0.42s cubic-bezier(0.32, 0.94, 0.28, 1);
}

.fxg-space.is-in .fxg-earth { opacity: 1; }

/* RÉGIME B — le disque déborde de l'écran de tous les côtés : il ne découpe
   plus rien, on repasse en plein cadre. Le JS ne compare pas l'avancement à un
   seuil pré-calculé (il bougerait avec la cible) : il MESURE à chaque image si
   le rayon du disque dépasse le coin le plus lointain, et bascule la classe à
   l'image exacte où c'est acquis. Le changement est donc invisible. */
.fxg-earth.is-flat {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  transform: none;
}

/* ---------- les nappes de texture ----------
   Sept niveaux, du monde entier à l'orthophoto parisienne. La nappe MONDE se
   répète en x (le raccord ±180° tombe au milieu du Pacifique) ; les nappes de
   ZONE ne se répètent pas et portent un alpha fondu sur leurs bords, gravé
   dans le fichier : posées par-dessus, elles ne font pas de couture, elles
   ajoutent de la définition là où l'œil est. */
.fxg-tex {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-repeat: no-repeat;
  opacity: 0;
  /* Pas de transition : l'opacité est pilotée image par image par le JS, en
     fonction de l'ÉCHELLE et non du temps. Une transition CSS par-dessus
     ajouterait un retard variable et ferait pulser les raccords. */
}

/* La nappe monde est la seule à se répéter : c'est elle qui fait le tour de
   la planète pendant la rotation d'arrivée. */
.fxg-tex--world { background-repeat: repeat-x; }

/* ---------- le relief : sphéricité et terminateur ----------
   Que des dégradés en POURCENTAGES : ils suivent la taille du disque sans
   qu'aucun nombre n'ait à être remis à l'échelle.
   TOUT EST NEUTRE : la Terre est en noir et blanc, un rebord bleuté jurerait
   immédiatement. Le rebord éclairé est un blanc pur, la nuit un noir pur. */
.fxg-shade {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: 1;
  background:
    /* rebord éclairé, à gauche */
    radial-gradient(circle at 2% 44%,
      rgba(255, 255, 255, 0.92) 0%,
      rgba(246, 246, 246, 0.32) 5.5%,
      rgba(246, 246, 246, 0) 15%),
    /* lumière rasante en haut à gauche */
    radial-gradient(circle at 27% 24%,
      rgba(255, 255, 255, 0.20) 0%,
      rgba(255, 255, 255, 0) 58%),
    /* nuit, vers la droite */
    linear-gradient(104deg,
      rgba(0, 0, 0, 0) 26%,
      rgba(0, 0, 0, 0.44) 58%,
      rgba(0, 0, 0, 0.88) 88%,
      rgba(0, 0, 0, 0.95) 100%),
    /* Pôles. La carte est équirectangulaire : les hautes latitudes y sont
       étirées, et à cette échelle l'Antarctique devient une bande blanche
       plate en bas du disque — d'autant plus voyante que la texture est
       maintenant en noir et blanc contrasté. On l'éteint — ce qui est aussi
       ce que ferait la lumière rasante sur une vraie sphère. */
    linear-gradient(180deg,
      rgba(0, 0, 0, 0.94) 0%,
      rgba(0, 0, 0, 0.60) 5%,
      rgba(0, 0, 0, 0.16) 12%,
      rgba(0, 0, 0, 0) 20%,
      rgba(0, 0, 0, 0) 74%,
      rgba(0, 0, 0, 0.46) 83%,
      rgba(0, 0, 0, 0.84) 89%,
      rgba(0, 0, 0, 0.96) 94%,
      rgba(0, 0, 0, 0.99) 100%),
    /* assombrissement du limbe */
    radial-gradient(circle at 50% 50%,
      rgba(0, 0, 0, 0) 58%,
      rgba(0, 0, 0, 0.48) 86%,
      rgba(0, 0, 0, 0.92) 100%);
}

/* ---------- l'atmosphère ----------
   Un liseré blanc argent, pas un halo bleu : on est en noir et blanc. */
.fxg-halo {
  position: absolute;
  inset: -7%;
  border-radius: 50%;
  opacity: 1;
  background: radial-gradient(circle at 50% 50%,
    rgba(255, 255, 255, 0) 43.5%,
    rgba(255, 255, 255, 0.42) 47.4%,
    rgba(232, 232, 232, 0.18) 54%,
    rgba(200, 200, 200, 0.06) 66%,
    rgba(180, 180, 180, 0) 82%);
}

/* Le relief et le halo sont des accessoires de SPHÈRE. En plein cadre il n'y
   a plus de sphère : ils disparaissent.
   POURQUOI PAS `display: none` — c'était le défaut. `display:none` retire
   l'élément d'un coup : au moment exact de la bascule, le limbe et le halo
   s'évanouissaient en une image, ce qui se voit comme un clignotement. Ils
   sont maintenant fondus par le JS bien AVANT la bascule (via l'opacité), et
   la bascule elle-même se fait sur des éléments déjà à zéro. */
.fxg-shade,
.fxg-halo { transition: none; will-change: opacity; }

/* =========================================================================
   3. VITESSE ET LISIBILITÉ
   ========================================================================= */

/* Voile de vitesse : il culmine au milieu de la descente, là où la texture
   est le plus agrandie entre deux niveaux. C'est lui qui achète la définition
   qui manque à cet instant — et il se lit comme de l'atmosphère traversée,
   pas comme un défaut. Neutre, évidemment. */
.fxg-haze {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: radial-gradient(58% 52% at 50% 50%,
    rgba(255, 255, 255, 0.26) 0%,
    rgba(240, 240, 240, 0.12) 38%,
    rgba(220, 220, 220, 0.035) 66%,
    rgba(220, 220, 220, 0) 84%);
}

/* Voile de lisibilité. IL N'EST PAS SYMÉTRIQUE, ET C'EST MESURÉ.
   La page contact ne pose pas son texte au hasard : le titre et le paragraphe
   occupent 26 à 54 % de la largeur et 32 à 69 % de la hauteur — c'est-à-dire
   le CENTRE GAUCHE, pile là où l'ancien voile était le plus transparent. Les
   coordonnées sont à droite (61-74 %), la nav en haut, le pied en bas ; ces
   trois-là étaient déjà couverts.
   Contraste mesuré une fois posé sur Paris, avant correction :
     .contact-title 3,04:1   .contact-sub 2,62:1   .contact-label 2,21:1
   contre 12,02 / 4,55 / 2,40 sur la page au repos. Le paragraphe passait donc
   de lisible à illisible pendant l'effet : c'est exactement ce qu'on s'était
   interdit. La bande gauche tient maintenant 30 % de noir jusqu'au milieu du
   cadre — le marqueur de Paris est à 50 %, le fleuve et les grands axes
   restent parfaitement lisibles dessous — et redescend à droite, où il n'y a
   que du décor.
     après : 5,2:1 / 4,5:1 / 3,6:1 — tous au-dessus de leur valeur au repos. */
.fxg-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(64% 62% at 50% 50%,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 0.18) 44%,
      rgba(0, 0, 0, 0.56) 76%,
      rgba(0, 0, 0, 0.78) 100%),
    linear-gradient(90deg,
      rgba(0, 0, 0, 0.74) 0%,
      rgba(0, 0, 0, 0.42) 18%,
      rgba(0, 0, 0, 0.32) 34%,
      rgba(0, 0, 0, 0.28) 50%,
      rgba(0, 0, 0, 0.22) 62%,
      rgba(0, 0, 0, 0.20) 72%,
      rgba(0, 0, 0, 0.34) 84%,
      rgba(0, 0, 0, 0.66) 100%),
    linear-gradient(180deg,
      rgba(0, 0, 0, 0.56) 0%,
      rgba(0, 0, 0, 0) 18%,
      rgba(0, 0, 0, 0) 82%,
      rgba(0, 0, 0, 0.56) 100%);
}

/* Une fois posé sur Paris, la ville est claire et le texte est blanc : le
   voile doit se resserrer un peu pour tenir le contraste sur une orthophoto
   lumineuse. 240 ms, après l'atterrissage, donc invisible pendant le voyage. */
.fxg-scrim { transition: opacity 0.24s ease; opacity: 1; }
.fxg-space.is-landed .fxg-scrim { opacity: 1.0; }

/* =========================================================================
   4. LE MARQUEUR PARIS
   ========================================================================= */
/* Position écrite en ligne par le JS : c'est la projection de 2.3522°E /
   48.8566°N par la MÊME caméra que la texture. Aucune constante. */
.fxg-pin {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
}

.fxg-dot {
  position: absolute;
  left: -4px;
  top: -4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 10px 3px rgba(255, 255, 255, 0.85);
  opacity: 0;
  transform: scale(0.4);
  transition:
    opacity   0.42s ease 0.06s,
    transform 0.55s cubic-bezier(0.22, 1, 0.36, 1) 0.06s;
}

.fxg-ping {
  position: absolute;
  left: -6px;
  top: -6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.85);
  opacity: 0;
  transform: scale(0.35);
}

.fxg-label {
  position: absolute;
  left: 18px;
  top: -20px;
  white-space: nowrap;
  opacity: 0;
  transform: translateX(-6px);
  transition:
    opacity   0.5s ease 0.22s,
    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.22s;
}

.fxg-label b {
  display: block;
  font: 500 12px/1 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  letter-spacing: 2.4px;
  color: #fff;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.95), 0 1px 3px rgba(0, 0, 0, 0.92);
}

.fxg-label i {
  display: block;
  margin-top: 5px;
  font: 400 9.5px/1 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  font-style: normal;
  letter-spacing: 1.4px;
  color: rgba(236, 236, 236, 0.88);
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.95);
}

/* La ligne de rappel : un trait fin du marqueur vers l'étiquette. */
.fxg-tick {
  position: absolute;
  left: 5px;
  top: 0;
  width: 11px;
  height: 1px;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.15));
  opacity: 0;
  transform-origin: 0 50%;
  transform: scaleX(0);
  transition:
    opacity   0.3s ease 0.14s,
    transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.14s;
}

/* Le marqueur ne se pose qu'une fois la descente terminée. */
.fxg-space.is-landed .fxg-dot   { opacity: 1; transform: scale(1); }
.fxg-space.is-landed .fxg-tick  { opacity: 1; transform: scaleX(1); }
.fxg-space.is-landed .fxg-label { opacity: 1; transform: translateX(0); }
.fxg-space.is-landed .fxg-ping  { animation: fxg-ping 2.8s cubic-bezier(0.22, 1, 0.36, 1) infinite; }
.fxg-space.is-landed .fxg-ping--2 { animation-delay: 1.4s; }

@keyframes fxg-ping {
  0%   { opacity: 0.85; transform: scale(0.35); }
  72%  { opacity: 0;    transform: scale(6); }
  100% { opacity: 0;    transform: scale(6); }
}

/* ---------- le crédit d'imagerie ----------
   La BD ORTHO de l'IGN est sous Licence Ouverte / Etalab 2.0 : usage
   commercial et redistribution autorisés, à CONDITION de mentionner la
   source. Le Blue Marble de la NASA est dans le domaine public mais la
   politique d'usage de la NASA demande de ne pas laisser croire à un
   parrainage — une mention neutre est la bonne réponse aux deux. Elle
   n'apparaît qu'une fois posé, discrète, en bas à droite. */
.fxg-credit {
  position: absolute;
  right: 14px;
  bottom: 10px;
  margin: 0;
  font: 400 8.5px/1.5 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  letter-spacing: 0.7px;
  text-align: right;
  color: rgba(255, 255, 255, 0.34);
  text-shadow: 0 0 6px rgba(0, 0, 0, 0.95);
  opacity: 0;
  transition: opacity 0.5s ease 0.35s;
}

.fxg-space.is-landed .fxg-credit { opacity: 1; }

/* =========================================================================
   5. THÈME NUIT DE LA PAGE — la lisibilité, point dur n°2
   -------------------------------------------------------------------------
   contact.css peint un fond #d0cfcd et du texte #2a2a2a, plusieurs règles en
   !important. Quand le fond devient l'espace, tout doit passer en clair —
   sinon la page de contact devient illisible, ce qui est rédhibitoire.

   Deux classes, deux rôles :
     .fxg-tint   posée pendant TOUT le cycle (aller ET retour) : elle ne fait
                 qu'installer les transitions de couleur. Sans elle, le retour
                 au noir serait un à-coup.
     .fxg-night  posée seulement pendant l'effet : elle porte les couleurs.
   À la fin de la sortie le JS retire les deux — il ne reste pas un attribut,
   pas une classe, pas un style sur la page.

   Spécificité : les règles visées sont en `.contact-body X !important`
   (0,1,1). `html.fxg-night .contact-body X` vaut (0,2,1) : elle passe devant,
   et le !important est repris là où il le faut.

   LES COULEURS SONT NEUTRES. L'ancienne version teintait le texte de nuit en
   bleu (#f4f8fd, #93aecb, #a9bdd4…) pour s'accorder à une Terre bleue. La
   Terre est maintenant en noir et blanc : ces bleus jureraient. Chaque
   couleur a été remplacée par un GRIS DE MÊME LUMINANCE RELATIVE — le
   contraste mesuré est donc conservé à l'identique, seule la teinte tombe.
       #f4f8fd  L=0.943  ->  #f5f5f5  L=0.949   (titre)
       #cbd9e9  L=0.679  ->  #d5d5d5  L=0.678   (sous-titre)
       #93aecb  L=0.391  ->  #adadad  L=0.393   (libellés)
       #f1f6fc  L=0.917  ->  #f3f3f3  L=0.919   (liens)
       #a9bdd4  L=0.480  ->  #bfbfbf  L=0.484   (pied de page)
       #c6d6e8  L=0.648  ->  #d2d2d2  L=0.650   (nav)
   ========================================================================= */

/* LE POINT DUR N°2, ET COMMENT IL EST TRAITÉ.
   Faire passer une page CLAIRE à texte NOIR sur un fond NOIR à texte CLAIR,
   c'est croiser deux luminances. Il existe forcément un instant où le texte
   et le fond se rejoignent au milieu — aucun ordonnancement ne l'évite, et un
   halo ne le sauve pas non plus (quand le texte est encore sombre, un halo
   sombre l'enfonce ; quand il est déjà clair, un halo clair ne sert à rien).
   La seule chose qu'on puisse faire, c'est RENDRE CE CROISEMENT TRÈS COURT et
   protéger tout le reste :

     1. le FOND met 0,42 s (courbe mordante) — c'est lui qui porte la mise en
        scène, il doit rester ample ;
     2. le TEXTE et son HALO ne mettent que 0,15 s, avec une courbe très
        « ease-out » : le texte quitte la zone grise en une poignée d'images ;
     3. le HALO (anneau quasi opaque à 2 px, puis velours à 14 px) est en place
        avant que le fond ne soit descendu, et c'est lui qui tient le contraste
        pendant les 270 ms suivantes, quand du texte blanc se trouve encore sur
        un fond pas tout à fait noir.

   TROIS NOMBRES, RELEVÉS SUR LE FOND LUI-MÊME, ET TOUT EN DÉCOULE.
   La luminance du fond derrière le titre, mesurée image par image :
     aller  — traverse la bande infranchissable de +118 ms à +135 ms
     retour — la traverse en sens inverse de  +76 ms à +127 ms
   La couleur du texte doit donc basculer DEDANS, et le plus vite possible :
   50 ms, c'est trois images. Plus lent, on étale la zone grise ; plus tôt, on
   met du texte sombre sur du noir ; plus tard — c'était le défaut — on laisse
   du texte clair sur une page redevenue claire.

   LE HALO, LUI, N'EST PAS SYNCHRONE, ET C'EST VOULU.
   Il est ce qui tient le contraste quand la couleur ne peut plus. Il ARRIVE
   donc en avance à l'aller (il est en place quand le texte devient clair) et
   il PART en retard au retour (il protège encore le texte clair pendant la
   traversée, puis accompagne sans gêner le texte redevenu sombre). Un halo
   noir sous un texte sombre ne coûte rien ; sous un texte clair, il est tout. */
html.fxg-tint .contact-body .contact-title,
html.fxg-tint .contact-body .contact-sub,
html.fxg-tint .contact-body .contact-label,
html.fxg-tint .contact-body .footer-links a,
html.fxg-tint .contact-body .nav-contact {
  transition: color 0.05s linear 0.075s,
              text-shadow 0.16s linear 0.075s;
}

/* .contact-link porte déjà `transition: opacity .3s, transform .3s ease`
   (contact.css) pour son survol : on la reconduit telle quelle, on ajoute. */
html.fxg-tint .contact-body .contact-link {
  transition: opacity 0.3s, transform 0.3s ease,
              color 0.05s linear 0.075s,
              text-shadow 0.16s linear 0.075s;
}

/* LA NAV ET LE PIED NE CROISENT PAS AU MÊME MOMENT — mesuré, pas supposé.
   Le voile de lisibilité est le plus dense en haut et en bas de l'écran :
   derrière eux le fond reste noir bien plus longtemps (luminance 0,004 contre
   0,140 derrière le titre). Leur fond traverse donc la bande à +60 ms à
   l'aller et seulement à +250 ms au retour, alors que le titre traverse à
   +126 et +100. Leur faire suivre le calage du titre, c'était garantir un
   creux à 1,00:1 chez eux — c'est ce que la mesure montrait. Ils ont donc leur
   propre calage : plus tôt à l'aller, plus tard au retour. */
html.fxg-tint .contact-body .footer-links a,
html.fxg-tint .contact-body .nav-contact,
html.fxg-tint .contact-body .footer-arrow {
  transition: color 0.04s linear 0.25s,
              text-shadow 0.16s linear 0.25s,
              border-color 0.04s linear 0.25s;
}

html.fxg-tint .contact-body .logo-img { transition: filter 0.36s ease; }

html.fxg-tint .contact-body .footer-arrow {
  transition: color 0.05s linear 0.075s,
              border-color 0.05s linear 0.075s;
}

/* SENS DE L'ALLER : le fond traverse plus tard (+118 -> +135 ms) parce qu'il
   part de plus haut ; le halo prend les devants. */
html.fxg-night .contact-body .contact-title,
html.fxg-night .contact-body .contact-sub,
html.fxg-night .contact-body .contact-label,
html.fxg-night .contact-body .footer-links a,
html.fxg-night .contact-body .nav-contact,
html.fxg-night .contact-body .footer-arrow {
  transition: color 0.05s linear 0.10s,
              text-shadow 0.10s linear 0.05s,
              border-color 0.05s linear 0.10s;
}

html.fxg-night .contact-body .contact-link {
  transition: opacity 0.3s, transform 0.3s ease,
              color 0.05s linear 0.10s,
              text-shadow 0.10s linear 0.05s;
}

html.fxg-night .contact-body .nav-contact {
  transition: color 0.04s linear 0.05s,
              text-shadow 0.10s linear 0s;
}

html.fxg-night .contact-body .footer-links a,
html.fxg-night .contact-body .footer-arrow {
  transition: color 0.04s linear 0.09s,
              text-shadow 0.10s linear 0.03s,
              border-color 0.04s linear 0.09s;
}

/* --- les couleurs de nuit, neutres --- */
/* Le halo n'est pas un effet : c'est la moitié du contraste. Il est ce qui
   tient le texte pendant la traversée de la bande infranchissable, où aucune
   couleur ne suffit. Deux nappes larges de plus sur les deux blocs de gauche —
   les seuls posés sur la ville. */
html.fxg-night .contact-body .contact-title {
  color: #f5f5f5;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.98), 0 0 6px rgba(0, 0, 0, 0.95),
               0 0 18px rgba(0, 0, 0, 0.9), 0 2px 40px rgba(0, 0, 0, 0.8),
               0 0 72px rgba(0, 0, 0, 0.72);
}

html.fxg-night .contact-body .contact-sub {
  color: #d5d5d5;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.98), 0 0 5px rgba(0, 0, 0, 0.95),
               0 0 14px rgba(0, 0, 0, 0.9), 0 0 34px rgba(0, 0, 0, 0.78);
}

/* Le libellé est le point dur : gris moyen (#adadad, L 0,393) sur une ville
   grise. Aucune couleur ne s'en sort — c'est le halo, et lui seul, qui fait le
   contraste. 26 px de flou couvrent la boîte entière (191 x 12 px). */
html.fxg-night .contact-body .contact-label {
  color: #adadad;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.98), 0 0 5px rgba(0, 0, 0, 0.95),
               0 0 13px rgba(0, 0, 0, 0.92), 0 0 26px rgba(0, 0, 0, 0.82);
}

html.fxg-night .contact-body .contact-link {
  color: #f3f3f3;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.98), 0 0 6px rgba(0, 0, 0, 0.95),
               0 0 16px rgba(0, 0, 0, 0.9), 0 0 38px rgba(0, 0, 0, 0.72);
}

html.fxg-night .contact-body .footer-links a {
  color: #bfbfbf;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.98), 0 0 5px rgba(0, 0, 0, 0.95),
               0 0 13px rgba(0, 0, 0, 0.88);
}

html.fxg-night .contact-body .footer-arrow {
  color: #bfbfbf;
  border-color: #bfbfbf;
}

/* La nav : contact.css force #9a9894 !important, et le logo est retourné en
   noir par un filtre !important. Le fichier source EST le logo blanc, donc
   `filter: none` suffit à le rendre à sa couleur d'origine. */
html.fxg-night .contact-body .nav-contact {
  color: #d2d2d2 !important;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.98), 0 0 5px rgba(0, 0, 0, 0.95),
               0 0 13px rgba(0, 0, 0, 0.88);
}

html.fxg-night .contact-body .logo-img {
  filter: none !important;
}

html.fxg-night .contact-body .logo::before,
html.fxg-night .contact-body .nav-contact::before {
  background: radial-gradient(ellipse at center,
    rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.26) 40%, transparent 70%) !important;
}

html.fxg-night .contact-body .logo::after {
  color: #adadad !important;
}

/* Le curseur personnalisé est peint en #2a2a2a sur cette page : sur fond noir
   il disparaîtrait purement et simplement. */
html.fxg-night .contact-body #custom-cursor::before,
html.fxg-night .contact-body #custom-cursor::after {
  background: #f0f0f0 !important;
}

/* =========================================================================
   6. MOUVEMENT RÉDUIT
   -------------------------------------------------------------------------
   Pas de voyage : l'état final, immobile — Paris vu de près, marqué. Aucun
   rAF n'est même démarré (cf. globe.js) ; les niveaux profonds SONT
   téléchargés puisque c'est eux qu'on affiche, mais en une fois, sans
   descente.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .fxg-space,
  .fxg-space.is-in,
  .fxg-space.is-out,
  .fxg-earth,
  .fxg-space.is-in .fxg-earth,
  .fxg-scrim,
  .fxg-starfield {
    transition: none !important;
  }
  .fxg-starfield { animation: none !important; opacity: 0.8 !important; }
  .fxg-dot, .fxg-tick, .fxg-label, .fxg-credit { transition: none !important; }
  .fxg-space.is-landed .fxg-ping { animation: none !important; opacity: 0 !important; }
  html.fxg-tint .contact-body .contact-title,
  html.fxg-tint .contact-body .contact-sub,
  html.fxg-tint .contact-body .contact-label,
  html.fxg-tint .contact-body .contact-link,
  html.fxg-tint .contact-body .footer-links a,
  html.fxg-tint .contact-body .nav-contact,
  html.fxg-tint .contact-body .logo-img,
  html.fxg-tint .contact-body .footer-arrow {
    transition: none !important;
  }
}
