/* ============================================================
   Motionary, demos C
   Polish et effets, performance, principes
   ============================================================ */

/* ---------------- polish et effets ---------------- */
/* Deux proprietes que tout le monde confond, montrees ensemble sur le
   meme fond raye et sur le meme cycle. Le panneau de gauche porte un
   filter, il part dans le flou avec son propre texte. Celui de droite
   porte un backdrop-filter, son texte reste net et c'est le fond
   derriere lui qui se brouille. */
.blr{position:relative;width:176px;height:98px;border-radius:11px;overflow:hidden;background:var(--s0)}
.blr .bg{position:absolute;inset:0;background:repeating-linear-gradient(45deg,var(--lime) 0 7px,var(--accent-2) 7px 14px)}
.blr .pane{position:absolute;top:19px;width:72px;height:60px;border-radius:9px;
  display:grid;place-items:center;border:1px solid var(--line-2)}
.blr .pane i{font:700 9px/1 var(--mono);letter-spacing:.02em}
.blr .f{left:10px;background:var(--card);animation:aBlurF 3.6s ease-in-out infinite}
.blr .f i{color:var(--ink)}
.blr .b{right:10px;background:rgba(var(--void-rgb),.34);animation:aBlurB 3.6s ease-in-out infinite}
.blr .b i{color:var(--ink)}
@keyframes aBlurF{0%,14%{filter:blur(0)}52%,66%{filter:blur(3.5px)}96%,100%{filter:blur(0)}}
@keyframes aBlurB{0%,14%{-webkit-backdrop-filter:blur(0);backdrop-filter:blur(0)}
  52%,66%{-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}
  96%,100%{-webkit-backdrop-filter:blur(0);backdrop-filter:blur(0)}}

/* Rayures a 45 degres pour que la coupe se lise. Contre un aplat uni on
   ne verrait que la silhouette, pas la nettete du bord.
   Quatre points a chaque etat, dont le triangle qui double son dernier
   sommet, sinon le navigateur ne sait pas interpoler et la forme saute. */
.clp .shape{display:block;width:92px;height:92px;
  background:repeating-linear-gradient(45deg,var(--lime) 0 8px,var(--accent-2) 8px 16px);
  animation:aClip 4.4s cubic-bezier(.4,0,.2,1) infinite}
@keyframes aClip{
  0%,10%{clip-path:polygon(0 0,100% 0,100% 100%,0 100%)}
  32%,42%{clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%)}
  64%,74%{clip-path:polygon(0 0,100% 0,50% 100%,50% 100%)}
  96%,100%{clip-path:polygon(0 0,100% 0,100% 100%,0 100%)}}

/* Meme remplissage raye que clip-path, volontairement. Les deux cartes
   sont voisines, seule la qualite du bord doit changer d'une a l'autre,
   franc la haut et degrade ici. */
.msk .shape{display:block;width:92px;height:70px;border-radius:10px;
  background:repeating-linear-gradient(45deg,var(--lime) 0 8px,var(--accent-2) 8px 16px);
  -webkit-mask-image:linear-gradient(90deg,#000 34%,transparent 72%);mask-image:linear-gradient(90deg,#000 34%,transparent 72%);
  -webkit-mask-size:250% 100%;mask-size:250% 100%;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  animation:aMask 4.4s cubic-bezier(.4,0,.2,1) infinite}
@keyframes aMask{0%,10%{-webkit-mask-position:120% 0;mask-position:120% 0}54%,74%{-webkit-mask-position:-60% 0;mask-position:-60% 0}96%,100%{-webkit-mask-position:120% 0;mask-position:120% 0}}

.ba{width:124px;height:84px}
.ba .before{position:absolute;inset:0;background:repeating-linear-gradient(45deg,var(--s2) 0 8px,var(--s1) 8px 16px)}
.ba .after{position:absolute;inset:0;background:linear-gradient(135deg,var(--lime),var(--accent-2));animation:aBA 4s cubic-bezier(.4,0,.2,1) infinite}
/* la poignee fait 2px de large, sans la marge negative son bord gauche
   tombe sur la frontiere de decoupe et la barre est decalee d'un pixel */
.ba .handle{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--ink);animation:aBAh 4s cubic-bezier(.4,0,.2,1) infinite}
.ba .handle::after{content:"";position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;background:var(--ink)}
/* La couche retouchee occupe la DROITE, comme dans l'exemple de la
   carte. L'inverse et les deux temps de la meme carte se contrediraient,
   avant a droite dans l'un et a gauche dans l'autre.
   La poignee porte exactement la valeur gauche de l'inset. */
@keyframes aBA{0%,8%{clip-path:inset(0 0 0 74%)}52%,68%{clip-path:inset(0 0 0 16%)}96%,100%{clip-path:inset(0 0 0 74%)}}
@keyframes aBAh{0%,8%{left:74%}52%,68%{left:16%}96%,100%{left:74%}}

/* Avec pathLength a 1, le dasharray vaut 1 quelle que soit la longueur
   reelle du chemin. Plus besoin de getTotalLength ni de nombre magique
   a remettre a jour des que le trace change. */
.ld{position:relative;display:grid;place-items:center;padding-bottom:16px}
.ld svg{width:124px;height:72px}
.ld path{stroke:var(--lime);stroke-width:3;stroke-linecap:round;
  stroke-dasharray:1;stroke-dashoffset:1;animation:aDraw 3.6s cubic-bezier(.5,0,.2,1) infinite}
.ld .lbl{position:absolute;left:2px;bottom:0;font:600 9px/1 var(--mono);font-style:normal;color:var(--ink-3)}
@keyframes aDraw{0%,6%{stroke-dashoffset:1}56%,84%{stroke-dashoffset:0}98%,100%{stroke-dashoffset:1}}

/* PAUSED vers PLAYED. Les lettres communes restent grises et immobiles,
   les trois qui changent roulent en vert, decalees de 60ms l'une de
   l'autre. Boite de 26px de haut, glyphe de 26px, translation de -26px,
   le second glyphe arrive donc exactement en place. */
.tm{display:flex;gap:2px;font:800 21px/26px var(--mono);font-variant-numeric:tabular-nums}
.tm .ch{display:block;width:15px;height:26px;text-align:center}
.tm .st{color:var(--ink-3)}
.tm .ro{overflow:hidden;color:var(--lime)}
.tm .ro b{display:block;height:26px;animation:aMorphTxt 3.4s cubic-bezier(.3,.9,.3,1) infinite}
.tm .d2 b{animation-delay:.06s}
.tm .d3 b{animation-delay:.12s}
@keyframes aMorphTxt{0%,24%{transform:translateY(0)}44%,80%{transform:translateY(-26px)}96%,100%{transform:translateY(0)}}

.skl{position:relative;display:flex;flex-direction:column;gap:9px;width:112px;overflow:hidden;border-radius:8px;padding:2px}
.skl .bar{height:12px;border-radius:6px;background:var(--s2)}
.skl .w1{width:100%}.skl .w2{width:76%}.skl .w3{width:52%}
.skl .sheen{position:absolute;top:0;bottom:0;width:60px;background:linear-gradient(90deg,transparent,rgba(var(--accent-rgb),.28),transparent);animation:aSheen 2s linear infinite}
@keyframes aSheen{from{transform:translateX(-70px)}to{transform:translateX(130px)}}

/* Glyphe de 38px, fenetre de 38px. Les dizaines portent cinq glyphes
   et s'arretent a -152px, soit 4 fois 38 pile. Les unites en portent
   dix et vont a -342px, soit 9 fois 38. Tout multiple non exact
   laisserait le chiffre final coupe en deux au milieu de la fenetre. */
.nt{display:flex;gap:5px}
.nt .win{height:38px;overflow:hidden;width:24px;border-radius:6px;background:var(--s1);display:grid}
.nt .reel{display:flex;flex-direction:column;font:800 26px/38px var(--mono);color:var(--lime);
  text-align:center;font-variant-numeric:tabular-nums;
  animation-duration:3.4s;animation-timing-function:cubic-bezier(.3,.9,.3,1);animation-iteration-count:infinite}
.nt .reel b{height:38px}
.nt .r1{animation-name:aReelTens}
.nt .r2{animation-name:aReelUnits}
@keyframes aReelTens{0%,8%{transform:translateY(0)}70%,88%{transform:translateY(-152px)}98%,100%{transform:translateY(0)}}
@keyframes aReelUnits{0%,8%{transform:translateY(0)}70%,88%{transform:translateY(-342px)}98%,100%{transform:translateY(0)}}

/* La meme valeur alterne entre 111.11 et 000.00 dans les deux rangees.
   Le carre bleu est colle au bord droit du nombre, il suit donc sa
   largeur reelle. En haut la fonte est proportionnelle, le 1 est plus
   etroit que le 0, le carre saute a chaque changement. En bas les
   chiffres ont tous la meme chasse et le carre ne bouge plus.
   Le trait vertical a droite est le repere fixe des deux rangees. */
/* Le trait de repere est place juste apres le nombre le plus etroit,
   pas au bout de la carte. Un ecart de cinq pixels contre un espace de
   cinquante ne se verrait pas, contre un espace de huit il saute aux
   yeux, et c'est ce petit ecart qui est tout le sujet du terme. */
.tbn{display:flex;flex-direction:column;gap:15px;width:120px}
.tbn .row{display:flex;align-items:center;gap:8px}
.tbn .tag{display:block;flex:0 0 auto;width:28px;font:600 8px/1 var(--mono);color:var(--ink-3)}
.tbn .stk{position:relative;flex:0 0 auto;width:80px;height:24px}
.tbn .stk::after{content:"";position:absolute;right:0;top:-4px;bottom:-4px;width:1px;background:var(--line-2)}
.tbn .stk b{position:absolute;left:0;top:0;display:block;white-space:nowrap;
  font:700 17px/24px var(--font);font-variant-numeric:proportional-nums;color:var(--lime)}
.tbn .stk .v1{animation:aTbnA 3.2s steps(1,end) infinite}
.tbn .stk .v2{animation:aTbnB 3.2s steps(1,end) infinite}
.tbn .t b{font-family:var(--mono);font-variant-numeric:tabular-nums}
.tbn .stk b i{display:inline-block;width:7px;height:13px;margin-left:5px;border-radius:2px;
  background:var(--a2);vertical-align:-1px}
@keyframes aTbnA{0%,2%{opacity:1}50%,52%{opacity:0}98%,100%{opacity:1}}
@keyframes aTbnB{0%,2%{opacity:0}50%,52%{opacity:1}98%,100%{opacity:0}}

/* La largeur est exprimee en ch, l'unite qui vaut exactement la chasse
   d'un caractere. Dix sept caracteres, dix sept pas, chaque palier
   tombe donc pile sur une frontiere de glyphe quelle que soit la
   taille de la fonte. En pixels le pas ne tomberait juste que par
   hasard et les dernieres lettres arriveraient toutes ensemble. */
.tw2{display:flex;align-items:center;gap:3px;width:calc(17ch + 8px);font:600 14px/22px var(--mono)}
.tw2 .txt2{display:block;overflow:hidden;white-space:nowrap;font:inherit;color:var(--lime);width:0;
  animation:aType 4.4s steps(17,end) infinite}
.tw2 .caret{display:block;flex:0 0 auto;width:2px;height:18px;background:var(--lime);
  animation:aCaret .9s steps(1,end) infinite}
@keyframes aType{0%{width:0}55%,80%{width:17ch}96%,100%{width:0}}
/* la discontinuite entre 100 pour cent et 0 pour cent EST le clignotement */
@keyframes aCaret{0%,50%{opacity:1}51%,100%{opacity:0}}

/* ---------------- performance ----------------
   Support commun a fps, jank et dropped-frame. Douze images en haut,
   la grille vsync en bas. Le cycle fait 2.4s et la grille bat toutes
   les 200ms, exactement, sur les trois cartes. Les images utilisent
   la MEME keyframe et la MEME fenetre d'allumage, seuls les delais
   changent, donc la seule chose que l'oeil compare est le decalage
   entre une image et son battement. */
.frz{position:relative;width:172px;padding-bottom:20px}
.frz .slots{display:flex;gap:4px;height:42px}
.frz .slots i{display:block;flex:1;height:42px;border-radius:3px;background:var(--s2);box-sizing:border-box}
.frz .gri{display:flex;gap:4px;margin-top:6px}
.frz .gri u{display:block;flex:1;height:5px;border-radius:1px;background:var(--s3);text-decoration:none;
  animation:aFrsBeat 2.4s steps(1,end) infinite}
.frz .lbl{position:absolute;left:0;right:0;bottom:0;font:600 9px/1 var(--mono);color:var(--ink-2)}
/* Depart et arrivee sur l'etat eteint, la boucle se referme d'elle meme.
   Fenetre allumee de 5 a 11 pour cent, soit 144ms sur un cycle de 2.4s,
   plus courte que les 200ms qui separent deux battements, donc jamais
   deux voyants allumes en meme temps. */
@keyframes aFrsBeat{0%,2%{background:var(--s3)}5%,11%{background:var(--ink-2)}14%,100%{background:var(--s3)}}
@keyframes aFrsOn{0%,2%{background:var(--s2)}5%,11%{background:var(--lime)}14%,100%{background:var(--s2)}}
@keyframes aFrsLate{0%,2%{background:var(--s2)}5%,11%{background:var(--warn)}14%,100%{background:var(--s2)}}
@keyframes aFrsGone{0%,2%{border-color:var(--s3)}5%,11%{border-color:var(--danger)}14%,100%{border-color:var(--s3)}}
.frz .gri u:nth-child(1){animation-delay:0s}
.frz .gri u:nth-child(2){animation-delay:.2s}
.frz .gri u:nth-child(3){animation-delay:.4s}
.frz .gri u:nth-child(4){animation-delay:.6s}
.frz .gri u:nth-child(5){animation-delay:.8s}
.frz .gri u:nth-child(6){animation-delay:1s}
.frz .gri u:nth-child(7){animation-delay:1.2s}
.frz .gri u:nth-child(8){animation-delay:1.4s}
.frz .gri u:nth-child(9){animation-delay:1.6s}
.frz .gri u:nth-child(10){animation-delay:1.8s}
.frz .gri u:nth-child(11){animation-delay:2s}
.frz .gri u:nth-child(12){animation-delay:2.2s}

/* fps, chaque image tombe exactement sur son battement, 0ms de retard */
.fps .slots i{animation:aFrsOn 2.4s steps(1,end) infinite}
.fps .slots i:nth-child(1){animation-delay:0s}
.fps .slots i:nth-child(2){animation-delay:.2s}
.fps .slots i:nth-child(3){animation-delay:.4s}
.fps .slots i:nth-child(4){animation-delay:.6s}
.fps .slots i:nth-child(5){animation-delay:.8s}
.fps .slots i:nth-child(6){animation-delay:1s}
.fps .slots i:nth-child(7){animation-delay:1.2s}
.fps .slots i:nth-child(8){animation-delay:1.4s}
.fps .slots i:nth-child(9){animation-delay:1.6s}
.fps .slots i:nth-child(10){animation-delay:1.8s}
.fps .slots i:nth-child(11){animation-delay:2s}
.fps .slots i:nth-child(12){animation-delay:2.2s}

/* jank, retards de 0, 20, 60, 0, 80, 140, 0, 60, 120, 0, 40 puis 0ms.
   Jamais en avance, jamais deux fois le meme retard. C'est la variation
   du retard que l'oeil lit comme une saccade, pas le retard lui meme. */
.jnk .slots i{animation:aFrsLate 2.4s steps(1,end) infinite}
.jnk .slots i:nth-child(1){animation-delay:0s;animation-name:aFrsOn}
.jnk .slots i:nth-child(2){animation-delay:.22s}
.jnk .slots i:nth-child(3){animation-delay:.46s}
.jnk .slots i:nth-child(4){animation-delay:.6s;animation-name:aFrsOn}
.jnk .slots i:nth-child(5){animation-delay:.88s}
.jnk .slots i:nth-child(6){animation-delay:1.14s}
.jnk .slots i:nth-child(7){animation-delay:1.2s;animation-name:aFrsOn}
.jnk .slots i:nth-child(8){animation-delay:1.46s}
.jnk .slots i:nth-child(9){animation-delay:1.72s}
.jnk .slots i:nth-child(10){animation-delay:1.8s;animation-name:aFrsOn}
.jnk .slots i:nth-child(11){animation-delay:2.04s}
.jnk .slots i:nth-child(12){animation-delay:2.2s;animation-name:aFrsOn}

/* dropped-frame, tout est a l'heure sauf la cinquieme. Son battement
   arrive, le contour rougit, et aucune image n'est prete. */
.df .slots i{animation:aFrsOn 2.4s steps(1,end) infinite}
.df .slots i:nth-child(1){animation-delay:0s}
.df .slots i:nth-child(2){animation-delay:.2s}
.df .slots i:nth-child(3){animation-delay:.4s}
.df .slots i:nth-child(4){animation-delay:.6s}
.df .slots i:nth-child(6){animation-delay:1s}
.df .slots i:nth-child(7){animation-delay:1.2s}
.df .slots i:nth-child(8){animation-delay:1.4s}
.df .slots i:nth-child(9){animation-delay:1.6s}
.df .slots i:nth-child(10){animation-delay:1.8s}
.df .slots i:nth-child(11){animation-delay:2s}
.df .slots i:nth-child(12){animation-delay:2.2s}
.df .slots .gone{background:transparent;border:1.5px dashed var(--s3);
  animation:aFrsGone 2.4s steps(1,end) infinite;animation-delay:.8s}

/* Le pipeline reste froid sur layout et paint pendant tout le
   deplacement, seul composite s'allume. Le fond raye derriere prouve
   que rien n'est redessine, il ne bouge jamais d'un pixel. */
.cpz{position:relative;width:172px;display:flex;flex-direction:column;gap:13px}
.cpz .stage2{position:relative;height:54px;border-radius:9px;background:var(--s1);overflow:hidden}
.cpz .pl{position:absolute;inset:0;background:repeating-linear-gradient(45deg,var(--s2) 0 6px,var(--s1) 6px 12px)}
.cpz .mv{position:absolute;left:9px;top:11px;width:32px;height:32px;border-radius:8px;background:var(--lime);
  box-shadow:0 6px 14px rgba(var(--shadow-rgb),.45);animation:aCmpMv 3.2s cubic-bezier(.45,0,.25,1) infinite}
.cpz .pipe{display:flex;gap:6px}
.cpz .pipe b{display:block;flex:1;height:19px;border-radius:5px;background:var(--s2);color:var(--ink-3);
  font:600 8px/19px var(--mono);text-align:center}
.cpz .p3{animation:aCmpHot 3.2s linear infinite}
@keyframes aCmpMv{0%,10%{transform:translateX(0)}52%,62%{transform:translateX(122px)}96%,100%{transform:translateX(0)}}
@keyframes aCmpHot{0%,6%{background:var(--s2);color:var(--ink-3)}
  12%,92%{background:var(--lime);color:var(--on-accent)}98%,100%{background:var(--s2);color:var(--ink-3)}}

/* La couche est un enfant du conteneur qui se deplace, elle voyage
   donc avec le carre. Une couche restee au point de depart pendant
   que l'element s'en va dirait exactement le contraire d'une promotion.
   L'ordre est le sujet du terme. La couche apparait a 22 pour cent, le
   mouvement demarre a 34, s'arrete a 70, et la couche est rendue a 94.
   Annoncee avant, retiree apres, jamais pendant. */
.wc{position:relative;width:150px;height:78px}
.wc .mover{position:absolute;left:4px;top:8px;width:52px;height:52px;
  animation:aWillSq 3.4s cubic-bezier(.2,.9,.25,1) infinite}
.wc .layer{position:absolute;inset:0;border-radius:13px;border:1.5px dashed var(--lime);opacity:0;
  animation:aWillLayer 3.4s ease infinite}
.wc .sq{position:absolute;left:4px;top:4px;width:44px;height:44px;border-radius:10px;background:var(--lime)}
.wc .lbl{position:absolute;left:0;bottom:0;font:600 9px/1 var(--mono);color:var(--lime);
  animation:aWillLayer 3.4s ease infinite}
@keyframes aWillLayer{0%,10%{opacity:0}22%,84%{opacity:1}94%,100%{opacity:0}}
@keyframes aWillSq{0%,34%{transform:translateX(0)}70%,88%{transform:translateX(90px)}100%{transform:translateX(0)}}

/* Six operations identiques dans les deux rangees, trois lectures en
   bleu et trois ecritures en gris, sur le meme cycle de 3s et les
   memes 500ms d'intervalle. Seul l'ordre change.
   En haut elles alternent, et chaque lecture solde la mise en page que
   le navigateur comptait differer, donc trois recalculs, chacun sous
   la lecture qui l'a provoque. En bas elles sont groupees et un seul
   recalcul suffit, apres la derniere ecriture.
   Largeur 172, six cases a flex 1 et cinq espaces de 5px, donc une
   case fait (172 - 25) / 6 = 24.5px et le pas vaut 29.5px. Les
   lectures sont aux index 0, 2 et 4, soit 0, 59 et 118px. */
.lt{display:flex;flex-direction:column;gap:15px;width:172px}
/* .col et non .row. La primitive partagee .row veut dire rangee
   HORIZONTALE centree, et son align-items:center empechait .ops de
   s'etirer, ce qui laissait ses marques a largeur nulle. */
.lt .col{display:flex;flex-direction:column;gap:6px}
.lt .tag{display:block;font:600 8px/1 var(--mono)}
.lt .bad .tag{color:var(--danger)}
.lt .good .tag{color:var(--lime)}
.lt .ops{display:flex;gap:5px}
.lt .ops i{display:block;flex:1;height:15px;border-radius:4px;opacity:.35;
  animation:aThrOp 3s steps(1,end) infinite}
.lt .ops .r{background:var(--a2)}
.lt .ops .w{background:var(--s5)}
.lt .ops i:nth-child(1){animation-delay:0s}
.lt .ops i:nth-child(2){animation-delay:.5s}
.lt .ops i:nth-child(3){animation-delay:1s}
.lt .ops i:nth-child(4){animation-delay:1.5s}
.lt .ops i:nth-child(5){animation-delay:2s}
.lt .ops i:nth-child(6){animation-delay:2.5s}
.lt .lay{position:relative;display:block;height:8px;border-radius:3px;background:var(--s1)}
.lt .lay b{position:absolute;top:0;bottom:0;border-radius:3px;background:var(--danger);opacity:0;
  animation:aThrFlash 3s steps(1,end) infinite}
.lt .f1{left:0;width:25px;animation-delay:.05s}
.lt .f2{left:59px;width:25px;animation-delay:1.05s}
.lt .f3{left:118px;width:25px;animation-delay:2.05s}
.lt .g1{left:0;right:0;background:var(--lime);animation-name:aThrFlashOne;animation-delay:0s}
@keyframes aThrOp{0%,2%{opacity:.35}5%,14%{opacity:1}17%,100%{opacity:.35}}
@keyframes aThrFlash{0%,2%{opacity:0}3%,16%{opacity:1}18%,100%{opacity:0}}
/* un seul recalcul, apres la derniere ecriture de 2.5s */
@keyframes aThrFlashOne{0%,86%{opacity:0}88%,97%{opacity:1}99%,100%{opacity:0}}

/* ---------------- principes ---------------- */
/* Meme duree, meme amplitude de 106px, meme courbe. La rangee du haut
   part et revient exactement d'ou elle venait, elle n'a rien appris a
   personne. Celle du bas quitte une source et se pose dans une cible,
   elle a dit d'ou et vers ou. Le cout est le meme, l'information non. */
.pur{display:flex;flex-direction:column;gap:14px;width:172px}
.pur .row{display:flex;align-items:center;gap:8px}
.pur .tag{display:block;flex:0 0 auto;width:26px;font:600 8px/1 var(--mono);color:var(--ink-3)}
.pur .b .tag{color:var(--lime)}
.pur .fld{position:relative;flex:1;height:34px;border-radius:7px;background:var(--s1)}
.pur .dot,.pur .dot2,.pur .src,.pur .slot{position:absolute;top:9px;width:16px;height:16px;
  border-radius:5px;box-sizing:border-box}
.pur .dot{left:8px;background:var(--s5);animation:aPurDeco 3.4s cubic-bezier(.4,0,.2,1) infinite}
.pur .src{left:8px;border:1.5px dashed var(--line-2)}
.pur .slot{left:114px;border:1.5px dashed var(--lime)}
.pur .dot2{left:8px;background:var(--lime);animation:aPurInfo 3.4s cubic-bezier(.4,0,.2,1) infinite}
@keyframes aPurDeco{0%,10%{transform:translateX(0)}46%,58%{transform:translateX(106px)}94%,100%{transform:translateX(0)}}
/* il se pose dans la cible et s'y eteint, il ne revient jamais sur ses
   pas. Le retour au depart se fait a opacite nulle, hors de vue. */
@keyframes aPurInfo{0%{transform:translateX(0);opacity:0}
  8%{transform:translateX(0);opacity:1}
  46%,88%{transform:translateX(106px);opacity:1}
  93%{transform:translateX(106px);opacity:0}
  93.001%,100%{transform:translateX(0);opacity:0}}

/* Le fantome gris part au meme instant, sans elan, et arrive au meme
   instant. Seul le depart differe, c'est la seule chose a lire.
   Recul de 8px sur 130 de course, soit 6 pour cent, ce que dit la
   consigne. Compression a 0.88 pendant l'elan puis etirement a 1.14
   au lancement, l'effort se voit. */
.ant{display:flex;flex-direction:column;gap:12px;width:172px}
.ant .lane{position:relative;display:block;height:32px}
.ant .mk{position:absolute;left:164px;top:-3px;bottom:-3px;width:2px;margin-left:-1px;
  background:repeating-linear-gradient(180deg,var(--line-2) 0 3px,transparent 3px 6px)}
.ant .gh,.ant .sq{position:absolute;left:4px;top:1px;width:30px;height:30px;border-radius:8px}
.ant .gh{background:var(--s4);animation:aAnticGh 2.8s cubic-bezier(.3,0,.2,1) infinite}
.ant .sq{background:var(--lime);animation:aAntic 2.8s cubic-bezier(.3,0,.2,1) infinite}
@keyframes aAnticGh{0%,18%{transform:translateX(0) scaleX(1)}
  62%,88%{transform:translateX(130px) scaleX(1)}
  100%{transform:translateX(0) scaleX(1)}}
@keyframes aAntic{0%,10%{transform:translateX(0) scaleX(1)}
  18%{transform:translateX(-8px) scaleX(.88)}
  32%{transform:translateX(34px) scaleX(1.14)}
  62%,88%{transform:translateX(130px) scaleX(1)}
  100%{transform:translateX(0) scaleX(1)}}

/* Le fantome du haut est rigide, corps et appendice partagent la meme
   keyframe et arrivent donc ensemble. En bas le corps s'arrete a 54
   pour cent et l'appendice ne se pose qu'a 84, avec un depassement
   puis un seul retour. La vignette exagere ce retard pour qu'il se
   voie, le prompt donne la vraie fourchette de 40 a 80ms. */
.flw{display:flex;flex-direction:column;gap:12px;width:172px}
.flw .lane{position:relative;display:block;height:34px}
.flw .gb,.flw .sq{position:absolute;left:4px;top:0;width:30px;height:30px;border-radius:8px}
.flw .gt,.flw .tail{position:absolute;left:6px;top:23px;width:22px;height:7px;border-radius:4px;
  transform-origin:left center}
.flw .gb{background:var(--s3);animation:aFtRigid 3.2s cubic-bezier(.2,.9,.25,1) infinite}
.flw .gt{background:var(--s4);animation:aFtRigid 3.2s cubic-bezier(.2,.9,.25,1) infinite}
.flw .sq{background:var(--lime);animation:aFollow 3.2s cubic-bezier(.2,.9,.25,1) infinite}
.flw .tail{background:rgba(var(--accent-rgb),.55);animation:aFollowTail 3.2s cubic-bezier(.2,1.4,.3,1) infinite}
@keyframes aFtRigid{0%,10%{transform:translateX(0) rotate(0)}54%,86%{transform:translateX(112px) rotate(0)}100%{transform:translateX(0) rotate(0)}}
@keyframes aFollow{0%,10%{transform:translateX(0)}54%,86%{transform:translateX(112px)}100%{transform:translateX(0)}}
@keyframes aFollowTail{0%,10%{transform:translateX(0) rotate(0)}
  40%{transform:translateX(64px) rotate(-14deg)}
  62%{transform:translateX(112px) rotate(10deg)}
  74%{transform:translateX(112px) rotate(-4deg)}
  84%,86%{transform:translateX(112px) rotate(0)}
  100%{transform:translateX(0) rotate(0)}}

/* Volume conserve a chaque etape, le produit des deux facteurs vaut 1.
   .847 x 1.18 = 0.9995, .893 x 1.12 = 1.0002, 1.3 x .769 = 0.9997,
   .877 x 1.14 = 0.9998, .862 x 1.16 = 0.9999.
   transform-origin sur le bas, sinon la balle s'enfonce dans le sol
   au moment ou elle l'ecrase, ce qui est exactement l'inverse. */
.ss .ball{transform-origin:center bottom;animation:aSquash 2s cubic-bezier(.4,0,.6,1) infinite}
@keyframes aSquash{
  0%{transform:translateY(-46px) scale(.847,1.18)}
  34%{transform:translateY(-6px) scale(.893,1.12)}
  42%{transform:translateY(0) scale(1.3,.769)}
  52%{transform:translateY(-8px) scale(.877,1.14)}
  80%{transform:translateY(-42px) scale(.862,1.16)}
  100%{transform:translateY(-46px) scale(.847,1.18)}}

/* Les deux colonnes attendent rigoureusement la meme duree, et la
   barre du bas en est la preuve, elle est unique et sert aux deux.
   Seul le contenu de l'attente change, donc seul le ressenti change. */
.pp2{display:flex;flex-direction:column;gap:14px;width:172px}
.pp2 .cols{display:flex;gap:14px}
.pp2 .col{flex:1;display:flex;flex-direction:column;align-items:center;gap:8px}
.pp2 .lbl{display:block;font:600 8px/1 var(--mono);color:var(--ink-3)}
.pp2 .box{position:relative;display:grid;place-items:center;width:100%;height:54px;
  border-radius:8px;background:var(--s1);overflow:hidden}
.pp2 .spin{display:block;width:24px;height:24px;border-radius:50%;border:3px solid var(--s3);
  border-top-color:var(--lime);animation:aSpin .9s linear infinite}
.pp2 .sk{display:flex;flex-direction:column;gap:5px;width:56px;position:relative;overflow:hidden;border-radius:5px}
.pp2 .sk i{display:block;height:8px;border-radius:4px;background:var(--s3)}
.pp2 .sk i:nth-child(2){width:74%}
.pp2 .sk i:last-child{width:52%}
.pp2 .sk::after{content:"";position:absolute;top:0;bottom:0;width:26px;background:linear-gradient(90deg,transparent,rgba(var(--accent-rgb),.32),transparent);animation:aSheen2 1.5s linear infinite}
.pp2 .wait{position:relative;display:block;height:4px;border-radius:2px;background:var(--s2);overflow:hidden}
.pp2 .wait b{position:absolute;left:0;top:0;bottom:0;width:0;border-radius:2px;background:var(--ink-3);
  animation:aPpWait 3.6s linear infinite}
@keyframes aSpin{to{transform:rotate(360deg)}}
@keyframes aSheen2{from{transform:translateX(-30px)}to{transform:translateX(60px)}}
@keyframes aPpWait{0%{width:0}92%{width:100%}92.001%,100%{width:0}}

.frq{display:flex;flex-direction:column;gap:14px}
.frq .col{gap:6px}
.frq .track{position:relative;display:block;width:104px;height:8px;border-radius:4px;background:var(--s2)}
.frq .track i{position:absolute;left:0;top:0;width:8px;height:8px;border-radius:4px;background:var(--lime);display:block}
/* 3.3s contre 1.1s, rapport de trois EXACTEMENT. Les deux pistes se
   resynchronisent donc a chaque tour et on compte trois passages
   courts pour un passage long, ce qui est tout le sujet du terme.
   La courbe de la rare depasse, celle du quotidien non, l'expressivite
   se paye aussi en depassement. */
.frq .big{animation:aFreqBig 3.3s cubic-bezier(.2,1.3,.3,1) infinite}
.frq .small{animation:aFreqSmall 1.1s cubic-bezier(.2,.9,.25,1) infinite}
@keyframes aFreqBig{0%,8%{transform:translateX(0) scale(1.5)}64%,86%{transform:translateX(96px) scale(1.5)}100%{transform:translateX(0) scale(1.5)}}
@keyframes aFreqSmall{0%,10%{transform:translateX(0)}60%,88%{transform:translateX(96px)}100%{transform:translateX(0)}}

/* Deux couloirs identiques, meme aller de 100px, meme cycle. En haut
   le carre revient par la ou il est parti. En bas il sort par la droite
   et rentre par la gauche, et le retour a zero se fait hors champ a
   opacite nulle, donc c'est bien un aller retour incoherent qu'on voit,
   pas un artefact de boucle.
   Zone A a gauche 24 sur 48 de large, centre a 48. Le carre fait 18,
   pose a 39, centre a 48 lui aussi. Zone B collee a droite, centre a
   148. La course vaut donc 100 exactement. */
.spc{display:flex;flex-direction:column;gap:13px;width:172px}
.spc .lane{position:relative;display:block;height:38px}
.spc .tag{position:absolute;left:0;top:14px;width:16px;font:600 8px/1 var(--mono);font-style:normal}
.spc .ok .tag{color:var(--lime)}
.spc .ko .tag{color:var(--danger)}
.spc .zn{position:absolute;top:0;width:48px;height:38px;border-radius:9px;background:var(--s1);
  border:1px solid var(--line);box-sizing:border-box}
.spc .zn.a{left:24px}
.spc .zn.b{left:124px}
.spc .sq{position:absolute;top:10px;left:39px;width:18px;height:18px;border-radius:5px;background:var(--lime)}
.spc .ok .sq{animation:aSpcOk 3.6s cubic-bezier(.4,0,.2,1) infinite}
.spc .ko .sq{background:var(--danger);animation:aSpcKo 3.6s cubic-bezier(.4,0,.2,1) infinite}
@keyframes aSpcOk{0%,12%{transform:translateX(0)}46%,64%{transform:translateX(100px)}94%,100%{transform:translateX(0)}}
@keyframes aSpcKo{0%,12%{transform:translateX(0);opacity:1}
  46%,64%{transform:translateX(100px);opacity:1}
  74%{transform:translateX(140px);opacity:0}
  74.001%{transform:translateX(-46px);opacity:0}
  82%{transform:translateX(-46px);opacity:1}
  94%,100%{transform:translateX(0);opacity:1}}

/* La liste, pas une puce qui clignote. Les quatre proprietes tombent
   chacune dans son couloir, transform et opacity dans celui du GPU,
   width et top sur le fil principal. Un seul cycle pour les quatre.
   Les deux premieres tiennent sur une ligne, 65 plus 6 plus 54 font
   125, la troisieme ferait 174 et passe donc a la ligne, ce qui donne
   pile la bonne separation sans rien forcer. */
.ha{position:relative;width:172px;height:104px}
.ha .props{position:absolute;left:0;top:0;right:0;display:flex;flex-wrap:wrap;gap:6px}
.ha .props b{display:block;padding:4px 8px;border-radius:6px;font:600 9px/9px var(--mono)}
.ha .go{background:rgba(var(--accent-rgb),.18);color:var(--lime);animation:aHaGo 3.4s cubic-bezier(.3,0,.2,1) infinite}
.ha .no{background:rgba(var(--danger-rgb),.16);color:var(--danger);animation:aHaNo 3.4s cubic-bezier(.3,0,.2,1) infinite}
.ha .lanes{position:absolute;left:0;right:0;top:50px;display:flex;flex-direction:column;gap:6px}
.ha .lanes em{display:block;height:22px;border-radius:7px;font:600 8px/20px var(--mono);
  font-style:normal;text-align:right;padding-right:9px;box-sizing:border-box}
.ha .gpu{background:rgba(var(--accent-rgb),.1);color:var(--lime);border:1px dashed rgba(var(--accent-rgb),.45)}
.ha .cpu{background:rgba(var(--danger-rgb),.09);color:var(--danger);border:1px dashed rgba(var(--danger-rgb),.4)}
@keyframes aHaGo{0%,16%{transform:translateY(0)}44%,84%{transform:translateY(53px)}96%,100%{transform:translateY(0)}}
@keyframes aHaNo{0%,16%{transform:translateY(0)}44%,84%{transform:translateY(58px)}96%,100%{transform:translateY(0)}}

/* Memes instants exactement, 10, 46, 74 et 96 pour cent, meme duree.
   A gauche l'evenement voyage, tourne et grossit. A droite il ne fait
   que passer d'invisible a visible. Le changement d'etat est annonce
   dans les deux cas, seul le mouvement disparait. */
.rm{display:flex;gap:14px;width:172px}
.rm .col{flex:1;display:flex;flex-direction:column;align-items:center;gap:8px}
.rm .lbl{display:block;font:600 8px/1 var(--mono);color:var(--ink-3);font-style:normal}
.rm .pane{position:relative;display:block;width:100%;height:62px;border-radius:9px;
  background:var(--s1);overflow:hidden}
.rm .sq{position:absolute;left:50%;margin-left:-16px;top:30px;width:32px;height:32px;
  border-radius:8px;background:var(--lime)}
.rm .big{animation:aRmBig 3.4s cubic-bezier(.2,1.3,.3,1) infinite}
.rm .soft{animation:aRmSoft 3.4s ease infinite}
@keyframes aRmBig{0%,10%{transform:translateY(0) rotate(0) scale(1);opacity:0}
  46%,74%{transform:translateY(-24px) rotate(-8deg) scale(1.1);opacity:1}
  96%,100%{transform:translateY(0) rotate(0) scale(1);opacity:0}}
@keyframes aRmSoft{0%,10%{opacity:0}46%,74%{opacity:1}96%,100%{opacity:0}}
