/* =========================================================
   ANIMATIONS — la lumière, pas la structure

   TOUT LE MOUVEMENT DU SITE EST RASSEMBLÉ ICI, en une seule feuille servie à
   tout le monde. Les composants animés sont communs aux deux thèmes (ils
   viennent de skin.css et ui.css), donc les animations le sont aussi : rien
   n'est écrit deux fois, et halloween.css n'a pas une ligne à ajouter.

   RÈGLE DE TRAVAIL, tenue partout dans ce fichier :

     - on n'anime QUE `opacity`, `transform`, `filter` et les ombres ;
     - aucune propriété qui décide d'une taille ou d'une position dans le flux
       n'est touchée : la disposition ne bouge pas d'un pixel ;
     - les CADRES ne bougent jamais. Ni flottement, ni respiration, ni
       inclinaison. Ce qui vit, c'est la lumière, les cristaux, les particules,
       les illustrations et les réactions au survol ;
     - rien de rapide ni de franc : les cycles vont de 4 à 22 secondes, les
       réactions au survol de 200 à 400 ms.

   CE QUI EXISTAIT DÉJÀ et n'est pas refait : les dix particules et leur dérive
   (home.css), le système `reveal` / `in-view` et son observateur (home.css +
   site.js), la transition entre onglets, le survol des flèches du carrousel et
   la pulsation de la pastille active (skin.css). Ce fichier les affine ou s'y
   greffe, il ne les duplique pas.

   Chargée APRÈS skin.css et halloween.css : plusieurs règles ci-dessous
   affinent des transitions posées là-bas.
========================================================= */

/* =========================================================
   1. PARALLAXE DU FOND

   L'illustration de fond était collée à la fenêtre — `background-attachment:
   fixed` — donc parfaitement immobile au défilement. Elle dérive maintenant
   très légèrement, ce qui creuse la profondeur entre elle et le contenu.

   Elle passe pour cela dans un calque à part, `.background::before`, que le
   thème de base neutralisait (`display: none`) et qui était donc libre. Ce
   calque déborde de 60 px en haut et en bas de la fenêtre : c'est la réserve
   dans laquelle la translation puise, ce qui garantit qu'aucun bord ne se
   découvre jamais, quel que soit le format de l'écran.

   `.background` garde son rôle : il porte le voile clair
   (`.particles::before`) et les particules, qui se peignent par-dessus.

   L'amplitude est donnée par anim.js, qui écrit `--anim-parallaxe`. Sans
   JavaScript, la variable vaut 0 et le fond redevient simplement fixe : rien
   ne casse.
========================================================= */

.home-body .background {
  background-image: none;
}

.home-body .background::before {
  content: '';
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  top: -60px;
  bottom: -60px;
  background: var(--sk-fond) center center / cover no-repeat;
  transform: translate3d(0, var(--anim-parallaxe, 0px), 0);
  /* Pas de transition : la valeur est déjà lissée par le rAF du script, une
     transition par-dessus donnerait un retard élastique. */
  will-change: transform;
}

/* =========================================================
   2. PLUIE DE PARTICULES

   Le champ est peint dans un canevas unique, créé par pluie.js et inséré dans
   `.background`. Il hérite donc de ses trois propriétés utiles : `position:
   fixed` (la pluie couvre la fenêtre, pas la page), `z-index: 0` (au-dessus de
   l'illustration de fond, sous tout le contenu) et `pointer-events: none`
   (aucun clic ne peut être intercepté). On le redit ici malgré tout : le jour
   où `.background` change, la pluie ne doit pas devenir cliquable.

   LES TEINTES SONT DÉCLARÉES PAR LE THÈME, plus bas pour le thème de base et
   dans halloween.css pour l'autre. Le script ne connaît aucune couleur, il lit
   celles-ci. `--pluie-braise` signale que la dernière teinte est une étincelle :
   elle reçoit alors un cœur plus concentré et un scintillement plus marqué.
========================================================= */

.pluie {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* Sous l'araignée, qui est posée dans le même fond. Ces rangs ne valent
     qu'entre eux : `.background` étant positionné, l'ensemble reste sous le
     contenu de la page. */
  z-index: 1;
  /* Le canevas se peint dans sa propre couche : le reste de la page n'est pas
     repeint à chaque image. */
  will-change: transform;
}

/*
  Teintes du thème de base.

  Elles sont FRANCHEMENT saturées, et c'est un choix contraint : le ciel de
  l'illustration est presque blanc sur de larges zones. Un premier jeu de
  pastels — blanc, bleu très clair, doré pâle — s'y dissolvait au point de ne
  plus exister du tout. Sur du blanc, seule une couleur soutenue se lit.

  Le blanc garde une place sur quatre : c'est lui qui porte sur les falaises,
  l'eau et les zones sombres de l'image, où le bleu profond disparaîtrait à son
  tour. Et le cœur des étincelles est blanc de toute façon, quelle que soit
  leur teinte.
*/
:root {
  /*
    L'ÉTINCELLE DU THÈME DE BASE EST UNE PLANCHE FOURNIE.

    `sk-pluie.png` — cœur bleu, anneau doré, quatre branches — remplace les
    sprites que le script dessinait. Le fichier d'origine faisait 1254 x 1254
    pour un dessin de 329 x 349 perdu au milieu : il est recadré sur le dessin
    puis ramené à 256 px, ce qui met sa taille en rapport avec l'usage (une
    particule fait entre 6 et 44 px à l'écran) et divise son poids par deux.

    Les teintes ci-dessous ne servent alors plus — la couleur est dans l'image.
    Elles restent déclarées comme repli : si le fichier ne se charge pas, le
    script retombe sur ses sprites calculés et le champ reste là.
  */
  --pluie-sprite: url('/assets/ui/sk-pluie.png');

  /*
    Présence : taille et opacité, multipliées.

    Cette planche est une étincelle fine, et le ciel de l'illustration est
    presque blanc — elle a besoin d'être portée plus haut que les braises
    d'Halloween, qui se détachent seules sur un violet sombre. D'où ces deux
    multiplicateurs, qui ne s'appliquent qu'ici : à 1.35, une particule fait
    entre 8 et 59 px au lieu de 6 à 44.

    Ce sont les deux valeurs à toucher pour doser l'effet.
  */
  --pluie-echelle: 1.35;
  --pluie-opacite: 1.2;

  --pluie-a: 96, 170, 245;        /* bleu ciel franc */
  --pluie-b: 45, 118, 200;        /* bleu profond */
  --pluie-c: 214, 154, 42;        /* or */
  --pluie-d: 255, 255, 255;       /* blanc */
  --pluie-braise: 0;
}

/*
  Les dix particules en DOM de home.css font double emploi dès que le champ est
  en place : le script pose `pluie-active` sur <body>, ce qui les efface.

  Elles ne sont pas supprimées pour autant — sans JavaScript, ou en mouvement
  réduit, le canevas n'est jamais créé, la classe n'est jamais posée, et elles
  restent le repli.
*/
.pluie-active .particle { display: none; }

/* =========================================================
   2 bis. PARTICULES DE REPLI

   Les dix particules de home.css sont conservées telles quelles — même
   nombre, mêmes positions, mêmes couleurs. On ne corrige que leur
   SYNCHRONISME : elles partageaient une durée unique (14 s de dérive, 5 s de
   scintillement), si bien qu'elles montaient et s'allumaient ensemble, ce qui
   se lit comme une animation plutôt que comme une poussière.

   Chacune reçoit donc sa propre durée. Les valeurs sont volontairement
   premières entre elles : le motif d'ensemble ne se répète pas avant plusieurs
   minutes.
========================================================= */

.particle:nth-child(1)  { animation-duration: 17s, 6.5s; }
.particle:nth-child(2)  { animation-duration: 21s, 5.5s; }
.particle:nth-child(3)  { animation-duration: 14s, 7.5s; }
.particle:nth-child(4)  { animation-duration: 19s, 6s; }
.particle:nth-child(5)  { animation-duration: 16s, 8s; }
.particle:nth-child(6)  { animation-duration: 22s, 5s; }
.particle:nth-child(7)  { animation-duration: 15s, 7s; }
.particle:nth-child(8)  { animation-duration: 20s, 6.8s; }
.particle:nth-child(9)  { animation-duration: 18s, 5.8s; }
.particle:nth-child(10) { animation-duration: 13s, 7.2s; }

/* =========================================================
   3. CRISTAUX

   Les gemmes du site sont des images (`gemme-m.png` et ses variantes), posées
   en fond des titres de cartes. On ne peut pas en animer l'intérieur — mais on
   peut en faire varier la LUMIÈRE, ce qui suffit à leur donner vie.

   Le cycle dure 7 secondes et ne fait qu'éclaircir : la gemme n'est jamais
   plus sombre que son état de repos, donc elle ne clignote pas, elle respire.
   Les décalages par rang évitent que toutes les cartes scintillent ensemble.
========================================================= */

@keyframes anim-cristal {
  0%, 100% { filter: brightness(1)    drop-shadow(0 0 0    rgba(var(--sk-accent), 0)); }
  50%      { filter: brightness(1.28) drop-shadow(0 0 5px  rgba(var(--sk-accent), 0.55)); }
}

.gemme {
  animation: anim-cristal 7s ease-in-out infinite;
}

/* Décalages : les trois cartes qui portent une gemme ne scintillent pas
   ensemble. (« Explorez Rendrya » et « Rejoignez notre communauté » n'en ont
   pas — leur titre est seul.) */
.zone-actus    .gemme { animation-delay: 0s; }
.zone-serveurs .gemme { animation-delay: 2.3s; }
.zone-acces    .gemme { animation-delay: 4.6s; }

/* =========================================================
   4. VOYANTS DE L'ÉTAT DES SERVEURS

   Seuls les voyants EN LIGNE respirent : un voyant rouge qui pulse se lit
   comme une alarme, et l'information « hors ligne » n'a pas besoin d'être
   criée. Le halo passe de 7 à 11 px sur un cycle de 4 s — la pastille elle-même
   ne change ni de taille ni de place.
========================================================= */

@keyframes anim-voyant {
  0%, 100% { box-shadow: 0 0 6px  currentColor; }
  50%      { box-shadow: 0 0 11px currentColor; }
}

.serveurs .pastille.en-ligne {
  animation: anim-voyant 4s ease-in-out infinite;
}
/* Décalages, pour que les voyants ne battent pas à l'unisson. */
.serveurs li:nth-child(2) .pastille.en-ligne { animation-delay: 1s; }
.serveurs li:nth-child(3) .pastille.en-ligne { animation-delay: 2s; }
.serveurs li:nth-child(4) .pastille.en-ligne { animation-delay: 3s; }

/* =========================================================
   5. BOUTONS

   5a. BOUTONS PRINCIPAUX — un reflet qui passe.

   `.cta-button` est déjà `position: relative` et `overflow: hidden` dans
   style.css : le reflet peut donc vivre dans son `::after` sans rien ajouter au
   balisage, et il est proprement rogné par la forme du bouton.

   Il ne se déclenche QU'AU SURVOL et ne passe qu'une fois — un reflet en
   boucle sur un bouton au repos attire l'œil en permanence, c'est exactement
   l'effet « site qui clignote » à éviter.
*/

.cta-button::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 45%;
  background: linear-gradient(100deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.38) 50%,
    rgba(255, 255, 255, 0) 100%);
  transform: skewX(-18deg);
  opacity: 0;
  pointer-events: none;
}

/* `:not([disabled])` : un bouton grisé ne doit pas faire briller un reflet,
   il donnerait l'impression d'être cliquable. */
.cta-button:not([disabled]):hover::after {
  animation: anim-reflet 650ms ease-out;
}

@keyframes anim-reflet {
  from { left: -60%; opacity: 0; }
  20%  { opacity: 1; }
  to   { left: 115%; opacity: 0; }
}

/* Le léger soulèvement existait déjà ; on ne fait que lui donner une courbe et
   une durée, au lieu du changement sec d'origine. */
.cta-button {
  transition: filter 220ms ease, transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
              box-shadow 220ms ease;
}

/*
  5b. ONGLETS DE LA BARRE.

  Les règles visaient `.nav-bouton img` : le bouton portait alors un
  `translate(-50%, -50%)` posé par skin.css pour le placer sur le bandeau, et
  l'animer ici aurait obligé à recopier ce centrage. C'est l'icône qui bougeait.

  L'icône a disparu — l'onglet est maintenant son libellé — et le centrage
  aussi : ce sont les ZONES qui sont placées, plus les boutons un par un. Le
  bouton n'a donc plus de transform à lui, et c'est lui qu'on anime.

  Le survol, la couleur et le halo sont dans ui.css : ici on ne pose que ce qui
  relève du mouvement.
*/
.nav-bouton {
  transition: color 180ms ease, transform 260ms cubic-bezier(0.22, 0.61, 0.36, 1),
              text-shadow 260ms ease;
}
.nav-bouton:active {
  transform: translateY(0) scale(0.97);
  transition-duration: 90ms;
}

/* L'onglet courant respire très lentement : c'est un repère, pas une alerte.
   Le filet doré sous le libellé suffit à le désigner ; l'animation ne fait que
   l'entretenir. */
@keyframes anim-onglet-actif {
  0%, 100% { opacity: 0.75; transform: scaleX(0.82); }
  50%      { opacity: 1;    transform: scaleX(1); }
}
.nav-bouton.actif::after {
  animation: anim-onglet-actif 5.5s ease-in-out infinite;
}

/*
  5c. ICÔNES DES RÉSEAUX SOCIAUX.

  ui.css posait déjà `transform: translateY(-2px)` au survol, sans transition :
  le mouvement était instantané. On lui donne une courbe, on y ajoute une
  échelle et un halo, et le pictogramme s'incline très légèrement — trois
  degrés, juste de quoi le rendre vivant.
*/
.communaute-liens a {
  transition: transform 260ms cubic-bezier(0.22, 0.61, 0.36, 1),
              box-shadow 260ms ease, filter 260ms ease;
}
.communaute-liens a:hover,
.communaute-liens a:focus-visible {
  transform: translateY(-3px) scale(1.06);
  box-shadow: 0 6px 16px rgba(3, 30, 70, 0.45),
              0 0 14px 2px rgba(var(--sk-accent), 0.5);
}
.communaute-liens svg {
  transition: transform 260ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.communaute-liens a:hover svg {
  transform: rotate(-3deg) scale(1.08);
}

/* Les entrées d'« Accès rapide » : le chevron avance, la ligne s'éclaire. */
.acces-rapide a { transition: background-color 220ms ease, box-shadow 220ms ease; }
.acces-rapide a .chevron { transition: transform 240ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.acces-rapide a:hover .chevron { transform: translateX(4px); }

/* =========================================================
   6. ENTRÉE DU HÉROS

   Le titre, l'accroche, le texte, les boutons et la signature entrent l'un
   après l'autre, décalés de 90 ms. Le mouvement est court — 14 px — et la
   séquence entière tient en moins d'une seconde.

   `both` conserve l'état initial pendant le délai : sans lui, chaque ligne
   s'afficherait d'abord en clair avant de disparaître pour s'animer.

   L'animation se rejoue à chaque arrivée sur l'accueil par le routeur client,
   puisque le balisage est réinjecté — c'est le comportement voulu.
========================================================= */

@keyframes anim-entree-hero {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

.hero-texte > * {
  animation: anim-entree-hero 620ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
.hero-texte > *:nth-child(1) { animation-delay: 60ms; }
.hero-texte > *:nth-child(2) { animation-delay: 150ms; }
.hero-texte > *:nth-child(3) { animation-delay: 240ms; }
.hero-texte > *:nth-child(4) { animation-delay: 330ms; }
.hero-texte > *:nth-child(5) { animation-delay: 420ms; }

/* =========================================================
   7. APPARITION DES CARTES AU DÉFILEMENT

   La grille entière portait déjà `.reveal` : elle apparaît d'un bloc dès le
   chargement, ce qui ne dit rien des cartes du bas, situées sous la ligne de
   flottaison. Chaque carte reçoit donc sa propre apparition, déclenchée quand
   elle entre réellement dans l'écran (anim.js).

   Le héros est exclu : il a sa propre séquence, ci-dessus.

   `.anim-carte` n'est posée que par le script. Sans JavaScript, aucune carte
   n'est masquée — la règle ne s'applique qu'aux éléments qu'il a marqués.
========================================================= */

.anim-carte {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 620ms ease, transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.anim-carte.anim-vu {
  opacity: 1;
  transform: none;
}

/* =========================================================
   8. CARROUSEL « EXPLOREZ RENDRYA »

   8a. LA TRANSITION ENTRE RÉGIONS.

   skin.css la posait en fondu plus léger rétrécissement (`scale(0.985)`). Le
   rétrécissement est remplacé par un GLISSEMENT horizontal de 16 px, plus
   lisible comme un défilement de diapositives, et la durée passe de 350 à
   520 ms avec une courbe sortante.

   Seule la diapositive bouge : le cadre, lui, ne bouge pas.

   L'AIRE DES DIAPOSITIVES EST ROGNÉE. Sans cela, la diapositive qui glisse
   sort de l'ouverture du cadre pendant la transition. En thème de base elle
   passerait sous la moulure, large de 26 px, et personne ne verrait rien ; en
   thème Halloween la moulure ne fait que 6 px à cet endroit — la diapositive
   déborderait franchement du cadre. `overflow: hidden` sur l'aire règle les
   deux cas d'un coup, sans rien changer à la mise en page.
*/
.explorer-vignettes {
  overflow: hidden;
}

.explorer-vignette {
  transform: translateX(16px);
  transition: opacity 520ms ease,
              transform 520ms cubic-bezier(0.22, 0.61, 0.36, 1),
              visibility 0s linear 520ms;
}
.explorer-vignette.explo-actif {
  transform: none;
  transition: opacity 520ms ease,
              transform 520ms cubic-bezier(0.22, 0.61, 0.36, 1),
              visibility 0s;
}

/*
  8b. LE ZOOM LENT DE L'ILLUSTRATION.

  Un panoramique très lent — 22 secondes pour 6 % d'échelle — appliqué à la
  SEULE diapositive visible. L'illustration est en `object-fit: cover` dans une
  boîte à `overflow: hidden` : elle peut donc grandir sans jamais déborder du
  cadre ni déplacer quoi que ce soit.

  `alternate` fait repartir le zoom en arrière plutôt que de le couper net à la
  fin du cycle.
*/
@keyframes anim-panoramique {
  from { transform: scale(1); }
  to   { transform: scale(1.06); }
}

.explorer-vignette.explo-actif img,
.explorer-vignette.explo-actif .vignette-attente {
  animation: anim-panoramique 22s ease-in-out infinite alternate;
}

/*
  8c. LES FLÈCHES.

  skin.css leur donnait déjà un agrandissement et un `backdrop-filter` au
  survol — c'est lui qui éclaircit la flèche dessinée dans le cadre. On ne
  refait pas cet effet : on ajoute seulement la courbe d'entrée et une pointe
  de rotation, qui donne le sens du mouvement.
*/
.explo-fleche {
  transition: transform 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
              box-shadow 280ms ease,
              -webkit-backdrop-filter 280ms ease, backdrop-filter 280ms ease;
}
.explo-fleche.gauche:hover  { transform: translate(-50%, -50%) scale(1.08) rotate(-4deg); }
.explo-fleche.droite:hover  { transform: translate(-50%, -50%) scale(1.08) rotate(4deg); }

/*
  8d. LA PASTILLE ACTIVE.

  Sa pulsation (`sk-pastille`) vient de skin.css et reste inchangée. On ajoute
  uniquement l'onde qui part d'elle au moment où elle devient active : un
  anneau qui s'élargit et s'efface, une seule fois. `::after` est libre — la
  pastille n'utilisait que `::before`.
*/
.explo-point::after {
  content: '';
  position: absolute;
  inset: 12%;
  border-radius: 50%;
  border: 1px solid rgba(var(--sk-accent), 0.85);
  opacity: 0;
  pointer-events: none;
}
.explo-point[aria-current="true"]::after {
  animation: anim-onde 900ms ease-out;
}
@keyframes anim-onde {
  from { opacity: 0.9; transform: scale(0.6); }
  to   { opacity: 0;   transform: scale(2.2); }
}

/* =========================================================
   9. ARAIGNÉE DÉCORATIVE — thème saisonnier

   Le balisage n'existe que dans ce thème (voir site/_head.ejs) : ces règles
   n'ont donc rien à porter ailleurs, et aucune classe de thème n'est
   nécessaire ici. Sans le bloc, elles ne s'appliquent à rien.

   DEUX TEMPS, comme demandé — d'abord le tissage dans l'angle, ensuite la
   descente. Ils sont portés par une seule et même horloge de 40 secondes : les
   pourcentages de chaque animation se lisent sur ce cycle commun, ce qui les
   garde synchronisés sans une ligne de JavaScript.

       0 –  7 %   la toile se tisse : rayons, puis les trois spirales
       7 – 11 %   l'arachnide paraît, au centre de la toile
      11 – 43 %   descente, 12,8 s
      43 – 56 %   suspension, elle se balance sur place
      56 – 81 %   remontée, 10 s
      81 – 88 %   elle s'efface
      88 – 100 %  la toile se défait, puis le cycle reprend

   TOUT EST EN `transform` ET `opacity`. Ni hauteur ni position animées : ce
   sont les deux seules propriétés que le navigateur peut animer sans refaire
   la mise en page à chaque image, et l'araignée traverse tout l'écran.
========================================================= */

.hw-araignee {
  position: absolute;
  top: 6px;
  left: 14px;
  /* Au-dessus de la pluie, qui est posée dans le même fond. `.background`
     étant positionné, ces rangs ne valent qu'entre eux : l'ensemble reste
     sous le contenu de la page. */
  z-index: 2;
  pointer-events: none;
}

/* ---------- La toile ---------- */

.hw-toile {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
  stroke: rgba(226, 214, 240, 0.5);
  stroke-width: 1;
  stroke-linecap: round;
  /* Une ombre portée très douce détache les fils du ciel violet sans les
     épaissir. */
  filter: drop-shadow(0 0 2px rgba(20, 6, 34, 0.55));
}

/*
  Le tissage.

  Chaque tracé est transformé en pointillé d'un seul tiret, aussi long que lui
  (`stroke-dasharray`), puis ce tiret est repoussé hors du tracé
  (`stroke-dashoffset`) : le chemin est alors invisible. Ramener le décalage à
  zéro le fait apparaître PROGRESSIVEMENT, d'un bout à l'autre — c'est le
  tissage. Les valeurs de tiret sont volontairement supérieures à la longueur
  réelle de chaque chemin ; il suffit qu'elles la dépassent.

  Les rayons sont un seul chemin en cinq segments : le pointillé les parcourt
  l'un après l'autre, ce qui donne cinq fils tirés à la suite plutôt que cinq
  fils poussant ensemble.
*/
.hw-toile path {
  animation: hw-tissage 40s linear infinite;
}
.hw-toile-rayons { --long: 700; animation-delay: 0s; }
.hw-toile-spire1 { --long: 240; animation-delay: 0.9s; }
.hw-toile-spire2 { --long: 360; animation-delay: 1.4s; }
.hw-toile-spire3 { --long: 480; animation-delay: 1.9s; }

.hw-toile path {
  stroke-dasharray: var(--long);
  stroke-dashoffset: var(--long);
}

/* Les décalages de départ échelonnent les spirales sans toucher au cycle :
   chacune démarre une seconde après la précédente, si bien que la toile se
   tisse du centre vers l'extérieur au lieu de s'écrire d'un bloc. */
@keyframes hw-tissage {
  0%        { stroke-dashoffset: var(--long); opacity: 0.9; }
  7%, 88%   { stroke-dashoffset: 0; opacity: 0.9; }
  97%, 100% { stroke-dashoffset: 0; opacity: 0; }
}

/* ---------- Le pendule : fil + arachnide ---------- */

/*
  Le balancement est une ROTATION autour du point d'ancrage, pas un
  déplacement latéral de l'araignée. C'est la différence entre un pendule et
  une araignée qui glisse : en tournant, le fil reste tendu entre la toile et
  elle. Un `translateX` les aurait désolidarisés.

  Trois degrés, sur sept secondes : à peine perceptible image par image, très
  lisible à l'échelle du mouvement.
*/
.hw-pendule {
  position: absolute;
  top: 34px;
  left: 34px;
  width: 0;
  height: var(--hw-descente, calc(100vh - 150px));
  transform-origin: top center;
  animation: hw-balancement 7s ease-in-out infinite;
}

@keyframes hw-balancement {
  0%, 100% { transform: rotate(-1.5deg); }
  50%      { transform: rotate(1.5deg); }
}

/*
  Le fil s'allonge par `scaleY` depuis son sommet : il est dessiné à sa
  longueur maximale une fois pour toutes, et seule son échelle bouge. Animer sa
  hauteur aurait forcé une remise en page à chaque image.
*/
.hw-fil {
  position: absolute;
  top: 0;
  left: -0.5px;
  width: 1px;
  height: 100%;
  transform-origin: top center;
  transform: scaleY(0);
  background: linear-gradient(180deg,
    rgba(226, 214, 240, 0.65) 0%,
    rgba(226, 214, 240, 0.4) 70%,
    rgba(226, 214, 240, 0.25) 100%);
  animation: hw-fil-descend 40s ease-in-out infinite;
}

.hw-arachnide {
  position: absolute;
  top: 0;
  left: 50%;
  width: 44px;
  height: auto;
  /* Le sommet de l'image rejoint le bout du fil ; les 6 px la remontent juste
     ce qu'il faut pour que les pattes avant tiennent le fil plutôt que de
     flotter dessous. */
  margin: -6px 0 0 -22px;
  opacity: 0;
  transform: translateY(0);
  filter: drop-shadow(0 3px 6px rgba(20, 6, 34, 0.5));
  animation: hw-araignee-descend 40s ease-in-out infinite;
}

/*
  Les deux mêmes pourcentages de part et d'autre : le fil et l'arachnide
  parcourent la même course, l'un en échelle, l'autre en déplacement. C'est ce
  qui les garde bout à bout sans calcul.
*/
@keyframes hw-fil-descend {
  0%, 11%   { transform: scaleY(0); }
  43%, 56%  { transform: scaleY(1); }
  81%, 100% { transform: scaleY(0); }
}

@keyframes hw-araignee-descend {
  0%        { transform: translateY(0); opacity: 0; }
  7%        { transform: translateY(0); opacity: 0; }
  11%       { transform: translateY(0); opacity: 1; }
  43%, 56%  { transform: translateY(var(--hw-descente, calc(100vh - 150px))); opacity: 1; }
  81%       { transform: translateY(0); opacity: 1; }
  88%, 100% { transform: translateY(0); opacity: 0; }
}

/*
  Sous 1100 px, l'araignée disparaît.

  À ces largeurs la grille passe sur une ou deux colonnes et vient occuper le
  bord gauche : l'araignée passerait derrière les cartes presque tout du long,
  pour n'être qu'une gêne le reste du temps. Un ornement qu'on ne voit plus n'a
  pas à continuer de tourner.
*/
@media (max-width: 1100px) {
  .hw-araignee { display: none; }
}

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

   home.css a déjà son propre bloc pour les animations qu'il pose. Celui-ci
   couvre les ajouts de cette feuille, selon le même principe : on ne coupe pas
   seulement l'animation, on REMET l'état final — sinon le héros resterait à
   opacité 0 et les cartes invisibles.

   Ce qui subsiste : les transitions de survol, ramenées à une durée nulle.
   Elles ne sont pas du mouvement décoratif, elles disent qu'un élément répond.
========================================================= */

@media (prefers-reduced-motion: reduce) {
  .home-body .background::before,
  .gemme,
  .serveurs .pastille.en-ligne,
  .cta-button:not([disabled]):hover::after,
  .nav-bouton.actif::after,
  .hero-texte > *,
  .explorer-vignette.explo-actif img,
  .explorer-vignette.explo-actif .vignette-attente,
  .explo-point[aria-current="true"]::after,
  .hw-toile path,
  .hw-pendule,
  .hw-fil,
  .hw-arachnide {
    animation: none !important;
  }

  /*
    L'araignée est retirée, pas figée.

    C'est le seul ornement du lot qui n'existe QUE par son mouvement : arrêtée,
    il ne resterait qu'une araignée collée dans un angle, sans toile tissée ni
    fil — une image posée là sans raison. Mieux vaut ne rien montrer.
  */
  .hw-araignee { display: none; }

  /* États finaux, pour que rien ne reste masqué. */
  .hero-texte > * { opacity: 1; transform: none; }
  .anim-carte,
  .anim-carte.anim-vu { opacity: 1; transform: none; }
  .home-body .background::before { transform: none; }

  /* Réactions au survol : conservées, mais instantanées. */
  .cta-button,
  .nav-bouton,
  .communaute-liens a,
  .communaute-liens svg,
  .acces-rapide a,
  .acces-rapide a .chevron,
  .explo-fleche,
  .explorer-vignette,
  .anim-carte {
    transition-duration: 0.01ms;
  }
}
