/* ============================================
   lgpix.ch — The Toolbox
   Couche de mouvement — motion.css
   Aucune dépendance, aucun build. CSS natif uniquement.

   Chargé après components.css, comme toute feuille de composants.
   Aucun token existant n'est redéfini ici : --transition-fast et
   --transition-normal restent la propriété de main.css.
   ============================================ */

/* --------------------------------------------
   1. Tokens de mouvement
   Identiques dans les deux thèmes : le mouvement
   ne dépend pas de la lumière.
   -------------------------------------------- */
:root {
  /* Durées */
  --duration-instant: 90ms;
  --duration-fast: 140ms;
  --duration-base: 240ms;
  --duration-slow: 420ms;

  /* Facteur de la révélation. Le seul endroit où toucher à sa vitesse.
     Durées et décalages sont multipliés ensemble : les proportions ne
     bougent pas, le rythme en trois temps est conservé, seule l'échelle
     de temps change.
       1 : minutage de référence, fin de séquence à 235 ms.
       4 : instrument d'observation, fin de séquence à 940 ms.
     Au-delà de 1, la séquence sort du budget de 250 ms qui gouverne le
     reste du site. Revenir à 1 est un seul caractère. */
  --motion-reveal-scale: 4;

  /* Rattrapage du bloc. .tool-content est en flex: 1 dans body.tool-page :
     sa boîte remplit la hauteur restante, son contenu non. Mesuré, le
     contenu occupe de 35 % à 96 % de la boîte selon l'outil. Le rideau
     parcourt la boîte, donc une partie de sa course ne dévoile plus rien
     et le bloc paraît toujours plus rapide que les deux autres temps,
     même ralenti. Ce facteur rend au temps utile ce que la boîte vide lui
     prend. Il ne peut pas être exact, l'occupation dépend de l'outil.
     À 1, la durée nominale de la spécification. */
  --motion-reveal-block-slack: 2;

  /* Durées de la révélation d'une page d'outil.
     Autonomes, jamais dérivées de --duration-base ni de --duration-fast :
     le mode réduit ramène ces deux-là à 120 ms alors que la révélation
     doit garder son minutage à l'identique. */
  --duration-reveal-icon: calc(130ms * var(--motion-reveal-scale));
  --duration-reveal-title: calc(190ms * var(--motion-reveal-scale));
  --duration-reveal-block: calc(150ms * var(--motion-reveal-scale) * var(--motion-reveal-block-slack));

  /* Décalages de la révélation. Le titre part assez tard pour que l'œil
     distingue deux gestes, le bloc quand le titre est à moitié écrit.
     Celui de la vignette reste littéral : zéro multiplié reste zéro. */
  --delay-reveal-icon: 0ms;
  --delay-reveal-title: calc(25ms * var(--motion-reveal-scale));
  --delay-reveal-block: calc(85ms * var(--motion-reveal-scale));

  /* Courbes.
     --ease-out-expo est reprise à l'identique dans --transition-fast et
     --transition-normal de main.css : toute correction ici doit y être
     reportée. */
  --ease-out-expo: linear(0, 0.263 2.3%, 0.492 4.9%, 0.681 7.9%, 0.83 11.4%, 0.9 13.6%, 0.951 16%, 0.985 19%, 1.001 23.5%, 1);
  --ease-editorial: linear(0, 0.045 4%, 0.18 9.5%, 0.62 22%, 0.79 30%, 0.892 38%, 0.955 48%, 0.99 62%, 1);
  --ease-spring: linear(0, 0.13 3.5%, 0.51 8.5%, 0.88 14%, 1.04 17.5%, 1.08 21%, 1.03 27%, 0.99 34%, 0.998 48%, 1);
  --ease-exit: linear(0, 0.28 12%, 0.52 24%, 0.78 44%, 0.93 66%, 1);
  --ease-hold: linear(0, 0.008 8%, 0.09 20%, 0.36 36%, 0.72 55%, 0.94 74%, 1);

  /* Amplitudes */
  --motion-lift: -2px;
  --motion-nudge: 3px;
  --motion-rise: 10px;
  --motion-press: 0.995;

  /* Amplitudes de la révélation.
     --motion-reveal : fraction de sa propre boîte que le volet parcourt.
       C'est le token du mode réduit, il y tombe à 0 et le volet ne
       parcourt plus rien. Les keyframes le multiplient, elles n'écrivent
       jamais 100% en dur, sans quoi le mode réduit n'aurait aucun effet.
     --motion-reveal-fade : opacité de départ. À 1, aucun fondu, le volet
       suffit et le bord reste net. À 0, le fondu reprend l'information.
     --motion-reveal-bleed : débord vertical du volet du titre. Sans lui,
       clip-path coupe les jambages et les accents du serif. */
  --motion-reveal: 1;
  --motion-reveal-fade: 1;
  --motion-reveal-bleed: -14%;
}

/* --------------------------------------------
   2. Transitions de vue inter-documents
   Un fondu de page, et rien d'autre. Le morph de la carte vers la page
   a été retiré : le trajet dépendait de la largeur de fenêtre, il
   demandait un nom de transition posé par script sur la carte cliquée,
   et sur écran large l'œil se mettait à poursuivre l'objet au lieu de
   le retrouver à l'arrivée. Ce que le morph disait est désormais dit
   par la révélation de la section 2 bis, en place, sans trajet.
   -------------------------------------------- */
@view-transition {
  navigation: auto;
}

/* Le décor cède la place et le nouveau entre aussitôt : un délai sur une
   réponse à un clic est du temps mort, quelle que soit la largeur.
   Le fondu du nouveau tient à 240 ms parce que la révélation ne joue pas
   sur l'opacité : le fondu est uniforme, la révélation est géométrique,
   les deux ne se disputent pas la même propriété. */
::view-transition-old(root) {
  animation-duration: var(--duration-fast);
  animation-timing-function: var(--ease-exit);
}

/* Vers l'accueil, le fondu est court. Rien ne l'accompagne : la grille
   arrive pleine et statique, les 240 ms n'y recouvrent aucun mouvement et
   se lisent comme une latence au retour. Mesuré, le document est prêt à
   peindre à 83 ms, tout ce qui suit est du fondu pur. */
::view-transition-new(root) {
  animation-duration: var(--duration-fast);
  animation-timing-function: var(--ease-exit);
}

/* Vers une page d'outil, les 240 ms d'origine. Les pseudo-éléments d'une
   transition inter-documents sont stylés par le document d'arrivée, donc
   ce sélecteur porte bien sur la destination et non sur le départ. Ici le
   fondu recouvre la révélation, qui est géométrique : les deux ne se
   disputent pas l'opacité, c'est ce qui justifiait la durée longue. */
html:has(body.tool-page)::view-transition-new(root) {
  animation-duration: var(--duration-base);
}

/* L'en-tête est nommé de part et d'autre : il reste donc en place
   pendant que le reste change. L'accueil et les pages d'outil n'ont
   pas la même barre, d'où les deux sélecteurs. C'est le seul nom de
   transition du site, et la seule continuité entre les deux documents. */
.site-header,
.tool-header {
  view-transition-name: site-header;
}

/* --------------------------------------------
   2 bis. La révélation
   Trois temps sur une page d'outil : la vignette, puis le nom, puis
   l'outil. Chacun est un volet qui s'ouvre, jamais un déplacement :
   le contenu est en place dès la première image, seul le découpage
   bouge. Fin de séquence à 85 + 150 = 235 ms.

   animation-fill-mode: backwards, jamais both. Avec both, le clip-path
   final resterait appliqué indéfiniment : sur .tool-content il y
   créerait un contexte d'empilement, découperait tout débordement et
   ferait du bloc un conteneur pour ses descendants en position fixed.
   Avec backwards, l'état de départ vaut pendant le décalage et plus
   rien n'est appliqué une fois l'animation terminée.

   Aucun élément ne porte d'état masqué en propre : ni opacity, ni
   clip-path dans une règle statique. Tout vit dans les @keyframes.
   Si clip-path n'est pas supporté, ou si les keyframes manquent, les
   trois éléments s'affichent normaux dès la première image.

   Les keyframes pilotent le déplacement par --motion-reveal et jamais
   par une valeur littérale : c'est ce token, et lui seul, que le mode
   réduit ramène à 0.
   -------------------------------------------- */

/* La vignette : un volet vers le haut. Le filet de 1 px se dessine avec
   lui, puisqu'il est dans la boîte découpée. Un tampon qui se pose. */
@keyframes tool-reveal-up {
  from {
    clip-path: inset(calc(100% * var(--motion-reveal)) 0 0 0);
    opacity: var(--motion-reveal-fade);
  }
  to {
    clip-path: inset(0 0 0 0);
    opacity: 1;
  }
}

/* Le titre : un balayage de gauche à droite, dans le sens de la lecture.
   Le trajet est celui du texte, borné par le nombre de lettres, donc à
   peu près constant de 375 à 2552 px de large. Le débord de -4% à
   l'arrivée protège le dernier caractère, il suit --motion-reveal pour
   que le mode réduit n'ait plus rien à parcourir non plus. */
@keyframes tool-reveal-right {
  from {
    clip-path: inset(
      var(--motion-reveal-bleed)
      calc(100% * var(--motion-reveal))
      var(--motion-reveal-bleed)
      0
    );
    opacity: var(--motion-reveal-fade);
  }
  to {
    clip-path: inset(
      var(--motion-reveal-bleed)
      calc(-4% * var(--motion-reveal))
      var(--motion-reveal-bleed)
      0
    );
    opacity: 1;
  }
}

/* Le bloc : un volet vers le bas, dans le sens de la chute. Le bloc
   entier, jamais ses enfants : les 26 outils n'ont pas la même
   structure interne et aucune règle générique ne peut les cadencer. */
@keyframes tool-reveal-down {
  from {
    clip-path: inset(0 0 calc(100% * var(--motion-reveal)) 0);
    opacity: var(--motion-reveal-fade);
  }
  to {
    clip-path: inset(0 0 0 0);
    opacity: 1;
  }
}

.tool-hero-icon {
  animation: tool-reveal-up var(--duration-reveal-icon) var(--ease-editorial)
    var(--delay-reveal-icon) backwards;
}

.tool-hero-title {
  animation: tool-reveal-right var(--duration-reveal-title) var(--ease-editorial)
    var(--delay-reveal-title) backwards;
}

/* linear, et non --ease-exit comme les deux autres temps : c'est un
   rideau, pas une arrivée, et la spécification demandait déjà « la course
   la plus régulière ». --ease-exit ne l'est pas assez ici, elle atteint
   52 % de course à 24 % du temps ; combinée à une boîte plus haute que son
   contenu, elle finissait de tout dévoiler dans le premier quart de la
   durée et le reste s'écoulait sur du vide. En linear, le temps écoulé
   est proportionnel à la course, donc au contenu réellement dévoilé.
   Repasser à var(--ease-exit) est un mot. */
.tool-content {
  animation: tool-reveal-down var(--duration-reveal-block) linear
    var(--delay-reveal-block) backwards;
}

/* .tool-header ne reçoit rien : il garde son view-transition-name et
   reste la continuité, sans géométrie. */

/* --------------------------------------------
   3. Cascade pilotée par le défilement
   animation-duration: auto est le mécanisme de dégradation :
   sans chronologie de défilement, auto vaut 0s, l'animation
   s'achève sur sa dernière image et la carte est visible
   immédiatement. Aucun script, aucun observateur, jamais de
   contenu masqué en attente.

   Les trois animations de cette section sont en linear, jamais en
   courbe : sur une chronologie de défilement, la courbe désynchronise
   le geste et la réponse. Le défilement est une entrée directe, la
   réponse lui est proportionnelle. La déclaration suit chaque fois le
   raccourci animation, qui remettrait sinon la valeur par défaut ease.
   -------------------------------------------- */
@keyframes tool-card-rise {
  from {
    opacity: 0;
    transform: translateY(var(--motion-rise));
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* La plage s'achève dans la phase d'entrée, jamais dans « cover ».
   Avec « cover », une carte n'atteint sa pleine opacité qu'une fois
   suffisamment défilée ; une carte déjà posée dans le viewport au repos
   n'y arrive donc jamais, puisque plus rien ne défile. Mesuré avec
   l'ancienne plage : sur 21 cartes visibles à l'arrivée, 9 seulement
   étaient opaques, les autres figées entre 0.06 et 0.89. La page
   paraissait ne pas avoir fini de charger. */
.tools-grid .tool-card,
.tools-section-grid .tool-card {
  animation: tool-card-rise auto both;
  animation-timing-function: linear;
  animation-timeline: view();
  animation-range: entry 0% entry 20%;
}

/* Le décalage vient de la position dans la rangée, via animation-range,
   jamais d'un animation-delay indexé : sur 27 cartes, un délai indexé
   ferait attendre la dernière plusieurs secondes. Les quatre plages
   restent bornées à l'intérieur de la phase d'entrée, pour la raison
   ci-dessus. */
@media (min-width: 1024px) {
  .tools-grid .tool-card:nth-child(4n + 2),
  .tools-section-grid .tool-card:nth-child(4n + 2) {
    animation-range: entry 4% entry 24%;
  }
  .tools-grid .tool-card:nth-child(4n + 3),
  .tools-section-grid .tool-card:nth-child(4n + 3) {
    animation-range: entry 8% entry 28%;
  }
  .tools-grid .tool-card:nth-child(4n + 4),
  .tools-section-grid .tool-card:nth-child(4n + 4) {
    animation-range: entry 12% entry 32%;
  }
}

/* Intertitre de section : le filet se tire comme un trait de plume. */
@keyframes section-rule {
  from {
    opacity: 0.35;
    transform: scaleX(0);
  }
  to {
    opacity: 1;
    transform: scaleX(1);
  }
}

/* Le filet statique de main.css cède la place à celui qui se tire :
   les deux superposés donnaient un trait de 2 px, et le trait déjà
   présent rendait l'animation invisible. Sans cette feuille, la
   bordure d'origine reste en place. */
.tools-section-head {
  position: relative;
  border-bottom-color: transparent;
}

.tools-section-head::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background-color: var(--border-card);
  transform-origin: left;
  animation: section-rule auto both;
  animation-timing-function: linear;
  animation-timeline: view();
  animation-range: entry 0% entry 60%;
}

/* --------------------------------------------
   4. Survol des cartes — « Deux vitesses »
   La couleur répond en 140 ms, la masse suit en 240 ms,
   la sortie prend 420 ms. C'est le décalage entre les deux
   qui donne la sensation de matière, pas l'amplitude.
   -------------------------------------------- */
.tool-card {
  --nudge: 0px;
  transition:
    background-color var(--duration-slow) var(--ease-exit),
    border-color var(--duration-slow) var(--ease-exit),
    transform var(--duration-slow) var(--ease-exit);
}

.tool-card:hover,
.tool-card:focus-visible {
  --nudge: var(--motion-nudge);
  transform: translateY(var(--motion-lift));
  transition:
    background-color var(--duration-fast) var(--ease-out-expo),
    border-color var(--duration-fast) var(--ease-out-expo),
    transform var(--duration-base) var(--ease-out-expo);
}

/* Le doigt écrase l'objet : accuse réception avant la navigation. */
.tool-card:active {
  transform: translateY(0) scale(var(--motion-press));
  transition: transform var(--duration-instant) var(--ease-out-expo);
}

/* La carte désactivée ne bouge pas. */
.tool-card--disabled:hover,
.tool-card--disabled:active {
  transform: none;
}

/* Chevron du pied de carte. L'accent est réservé à la carte survolée :
   vingt-sept flèches accentuées en permanence contrediraient la règle
   du site (un seul accent, utilisé rarement). */
.tool-card-go {
  margin-left: auto;
  color: var(--text-tertiary);
  transform: translateX(var(--nudge));
  transition:
    transform var(--duration-base) var(--ease-out-expo),
    color var(--duration-fast) var(--ease-out-expo);
}

.tool-card:hover .tool-card-go,
.tool-card:focus-visible .tool-card-go {
  color: var(--accent);
}

.tool-card--disabled:hover .tool-card-go {
  color: var(--text-tertiary);
}

/* --------------------------------------------
   5. En-tête et navigation
   -------------------------------------------- */

/* Le filet apparaît exactement quand le contenu passe dessous.
   Cause à effet, pas décoration. */
@keyframes header-settle {
  from {
    border-bottom-color: transparent;
  }
  to {
    border-bottom-color: var(--border-card);
  }
}

.site-header {
  animation: header-settle auto both;
  animation-timing-function: linear;
  animation-timeline: scroll(root);
  animation-range: 0 64px;
}

/* La barre suit la bascule de thème au même rythme que la page,
   sinon elle saute d'un fond à l'autre pendant que le corps glisse. */
.site-header,
.tool-header {
  transition:
    background-color var(--duration-slow) var(--ease-hold),
    border-color var(--duration-slow) var(--ease-hold);
}

/* Onglet de catégorie actif : le seul rebond du site, plafonné à 8 %. */
.filter-pill {
  position: relative;
  transition: color var(--duration-fast) var(--ease-out-expo);
}

.filter-pill::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background-color: var(--accent);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--duration-base) var(--ease-spring);
}

/* Même raison que l'intertitre de section : le soulignement de l'onglet
   actif est désormais porté par ::after, qui se déploie, et non plus par
   la bordure, qui apparaissait d'un coup sous lui. */
.filter-pill.active {
  border-bottom-color: transparent;
}

.filter-pill.active::after {
  transform: scaleX(1);
}

/* Un outil de saisie ne fait jamais attendre. */
.search-input {
  transition:
    border-color var(--duration-fast) var(--ease-out-expo),
    box-shadow var(--duration-fast) var(--ease-out-expo);
}

/* Bascule de thème : le jour tombe sur la page. Lent parce que
   c'est un changement d'ambiance, pas une navigation. */
body {
  transition:
    background-color var(--duration-slow) var(--ease-hold),
    color var(--duration-slow) var(--ease-hold);
}

/* --------------------------------------------
   6. Mouvement réduit — une seconde partition
   On ne supprime pas l'information portée par le mouvement,
   on change son support : ce qu'un déplacement disait reste
   dit par l'opacité, la bordure et la couleur.
   -------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 120ms;
    --duration-base: 120ms;
    --duration-slow: 120ms;
    --ease-out-expo: linear(0, 1);
    --ease-editorial: linear(0, 1);
    --ease-spring: linear(0, 1);
    --ease-exit: linear(0, 1);
    --ease-hold: linear(0, 1);
    --motion-lift: 0px;
    --motion-nudge: 0px;
    --motion-rise: 0px;
    --motion-press: 1;

    /* La révélation change de support, pas de rythme. Le volet ne
       parcourt plus rien, clip-path vaut inset(0) aux deux bouts, et
       l'opacité de départ reprend exactement l'information qu'il
       portait. Durées et décalages restent ceux du mode normal :
       0 / 25 / 85 et 130 / 190 / 150. Ce qui subsiste est le rythme en
       trois temps, c'est lui qui dit « la vignette, puis le nom, puis
       l'outil ». */
    --motion-reveal: 0;
    --motion-reveal-fade: 0;
  }

  /* La cascade n'est pas coupée, elle est réécrite : --motion-rise vaut 0,
     seule l'opacité subsiste, toujours pilotée par le défilement.
     Le rythme éditorial demeure, le déplacement disparaît. */

  /* main.css impose animation-duration: 0.01ms !important sur *, ce qui
     écraserait les trois durées de la révélation sans toucher aux
     décalages : on obtiendrait trois apparitions sèches étalées sur
     85 ms. Ces trois déclarations les rétablissent. Elles portent sur
     des sélecteurs de classe, qui l'emportent sur * à !important égal ;
     le bloc global de main.css n'est pas modifié, il protège les
     27 outils. */
  .tool-hero-icon {
    animation-duration: var(--duration-reveal-icon) !important;
  }

  .tool-hero-title {
    animation-duration: var(--duration-reveal-title) !important;
  }

  .tool-content {
    animation-duration: var(--duration-reveal-block) !important;
  }

  /* Le fondu de page se resserre, mais rien n'est coupé : sans lui
     l'arrivée serait un saut. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: var(--duration-base);
    animation-timing-function: linear;
  }

  /* Le soulignement d'onglet réapparaît à sa place sans glisser :
     le déplacement latéral est précisément le mouvement le plus mal supporté. */
  .filter-pill::after {
    transition: opacity var(--duration-fast) linear;
    transform: scaleX(1);
    opacity: 0;
  }

  .filter-pill.active::after {
    opacity: 1;
  }

  /* Le filet de section apparaît sans se tirer. */
  .tools-section-head::after {
    animation: none;
  }
}

/* --------------------------------------------
   7. Dégradation
   Sans @view-transition : navigation instantanée, rien de cassé. La
   révélation joue quand même, elle est déclenchée par le chargement du
   document et non par la transition de vue : un accès direct ou un
   favori y a droit aussi.
   Sans animation-timeline : animation-duration auto vaut 0s,
   tout le contenu est visible dès la première image.
   Sans clip-path : la déclaration est ignorée et les trois éléments
   de la révélation s'affichent normaux dès la première image.
   Sans linear() : la courbe retombe sur ease, la séquence reste juste.
   Sans JavaScript : sans effet, cette couche n'en contient pas une ligne.
   Jamais de contenu masqué en attente d'une animation.
   -------------------------------------------- */
