/* apps.css — La grille des apps (page /apps)
   Structure seulement : Alex reprend la finition. Tout passe par les jetons de
   theme.css, donc la page suit le thème clair/sombre sans une seule couleur
   écrite en dur ici. */

/* LE RYTHME DES TUILES, EN UN SEUL ENDROIT.
   Deux vitesses, et la difference compte : ce qui CHANGE D'ETAT — la tuile qui
   s'agrandit, le pli qui s'ouvre, la pastille qui grossit — se regarde, donc
   prend son temps. Ce qui REPOND — une couleur au survol — doit rester
   immediat, sinon l'interface parait molle. Ralentir les deux ensemble donnerait
   une page qui traine ; ralentir la premiere seule donne une page qui se
   deploie.

   `--hub-etat` est la valeur a toucher pour changer l'allure d'ensemble. */
:root {
    /* TROIS TEMPS, ET ILS DOIVENT S'ENCHAINER. C'est le desequilibre qui
       faisait paraitre le survol trop rapide, bien plus que chaque duree prise
       seule : le cadre avait fini de bouger en un tiers de seconde pendant que
       le texte, lui, mettait une seconde pleine a s'ecrire. On voyait donc la
       carte sauter en place, puis on attendait devant elle.

       Avec la courbe reguliere de cette page, 90 % du chemin se fait a 79 % du
       temps annonce — verifie au calcul sur la bezier, pas estime. Le vrai
       etagement est donc :

         soulevement      .37s  ->  visible en 294 ms  (on voit la carte monter)
         cadre + accroche .70s  ->  visible en 556 ms  (la tuile se recompose)
         frappe           .87s                         (le texte se pose)

       Chaque etape se termine pendant que la suivante court encore, et rien ne
       s'arrete en attendant le reste. Les trois valeurs se regardent ENSEMBLE :
       en changer une seule casse l'enchainement.

       X1,5 LE 28/08/2026, ET IL FAUT SAVOIR POURQUOI. Les durees d'avant
       (1.05s / .55s, plafond de frappe a 1300 ms) ont ete reglees sur trois
       tours d'aller-retour par quelqu'un dont Windows avait les effets
       d'animation COUPES : `prefers-reduced-motion: reduce` etait vrai, donc
       aucune de ces transitions ne s'appliquait chez lui. On reglait a
       l'aveugle, sur des captures. Vues pour de vrai, elles trainaient d'un
       tiers. */
    --hub-etat: .7s;
    --hub-reponse: .37s;

    /* LA COURBE COMPTE PLUS QUE LA DUREE, et il a fallu deux essais pour la
       trouver. `--mr-ease`, l'exponentielle sortante d'origine, faisait 49 % du
       mouvement dans les 48 premieres millisecondes : l'animation durait une
       demi-seconde et se voyait finie en un sixieme de ce temps, d'ou
       l'impression de sec qui revenait apres chaque tentative de la ralentir.

       Le premier remplacant, `cubic-bezier(.32, .08, .24, 1)`, corrigeait le
       depart mais restait charge en tete : 34 % / 79 % / 96 % du mouvement aux
       trois quarts du temps, donc TOUTE LA SECONDE MOITIE etait invisible et
       le passage d'un etat a l'autre se lisait encore comme un saut suivi d'une
       derive.

       Celle-ci est une sinusoidale symetrique : 14 % / 50 % / 86 %. La moitie
       du chemin se fait a la moitie du temps, exactement. C'est ce qui
       transforme un declic en glissement — le seul profil ou l'oeil suit le
       mouvement du debut a la fin au lieu d'en voir le resultat.

       Contrepartie assumee : le depart est doux, donc la carte ne bondit pas
       sous le curseur. C'est precisement ce qu'on cherche ici, et precisement
       ce qu'il ne faut PAS pour un bouton — d'ou une variable propre a cette
       page, `--mr-ease` restant sec pour tout le reste du site. */
    --hub-courbe: cubic-bezier(.37, 0, .63, 1);

    /* LA CADENCE DE LA FRAPPE, en millisecondes par caractere — et non une
       duree totale, qui faisait se taper d'autant plus vite que la phrase etait
       longue. Le script lit cette valeur, il n'en garde pas de copie.

       ⚠️ ELLE NE DECIDE DE RIEN AUJOURD'HUI, et c'est bon a savoir avant de la
       toucher. Les trois details du catalogue font 75, 97 et 77 signes : meme a
       23 ms ils demandent 1,7 a 2,2 s, donc le PLAFOND du script les rattrape
       tous les trois et la frappe dure son maximum, pas sa cadence. Changer ce
       nombre seul ne se verrait pas — c'est la borne haute d'`apps-frappe.js`
       qu'il faut bouger avec.
       Elle reste juste pour le jour ou une app portera un detail court. */
    --hub-frappe: 23;
}

.hub {
    max-width: 1180px;
    margin: 0 auto;
    padding: 48px 24px 80px;
}

/* ── Le decor du fond ────────────────────────────────────────────────────
   DES ILOTS DE COURBES DE NIVEAU. Meme famille que la landing, la carte
   topographique, mais un dessin propre a cette page : la landing pose des ondes
   qui traversent (`.hero::before`, `.contact-content::before` dans landing.css),
   ici ce sont des anneaux emboites autour de quelques reliefs. Le trace, et
   pourquoi les reliefs tiennent dans les marges, sont dans apps.html.

   QUATRE TENTATIVES ONT ECHOUE AVANT, chacune pour sa propre raison, et elles
   valent d'etre gardees ecrites parce qu'aucune ne se voit venir.

   Un semis d'etoiles repete : toute tuile a une periode, donc une trame que
   l'oeil finit par lire. Le logo en filigrane : une forme reconnaissable posee
   derriere le catalogue se regarde, elle ne se lit pas comme un fond. Des arcs
   concentriques : un cercle a une rondeur a preserver, donc `cover`, donc un
   dessin agrandi et rogne, qui courait sous les tuiles au lieu de tenir la
   marge. Puis les ondes de la landing, justes de dessin mais DECOUPEES a la
   colonne du contenu, ce qui suit.

   Un decor de fond ne doit donc etre ni un motif qu'on decode, ni un objet
   qu'on identifie, ni une forme dont la geometrie dicte le cadrage.

   LE DECOR SE PEND A `main`, PAS A `.hub`, ET C'EST LE CORRECTIF PRINCIPAL.
   `.hub` plafonne a 1180 px et portait `overflow: hidden` : un decor pose
   dedans etait DECOUPE a cette colonne, meme demande a `100vw`. Sur un portable
   la colonne fait presque la fenetre et rien ne se voit ; en 4K il restait un
   tiers d'ecran nu de chaque cote.

   ET CE DEFAUT A SURVECU A TROIS SERIES DE MESURES, parce que
   `getBoundingClientRect` rend la boite de MISE EN PAGE et pas la surface
   PEINTE : l'element se mesurait a 1920 pendant qu'il n'en peignait que 1180.
   Un decoupage ne se lit dans aucun rectangle. Il se pointe, avec
   `elementFromPoint` pres du bord de l'ecran, qui lui respecte le decoupage.

   `main` fait toute la largeur : le decor y tient en `inset: 0`, sans `100vw`
   ni marge negative, donc sans rien a rattraper. Et c'est `main` qui prend la
   hauteur de la fenetre, parce que la grille tient en 504 px et laisserait
   sinon la moitie basse nue, le decor s'arretant avec elle en une frontiere
   horizontale au milieu de l'ecran.

   `:has(> .hub-decor)` et non une classe : `main.app-content` porte TOUTES les
   pages de l'app. La regle ne vise que celles qui posent ce decor, et suivra
   toute page qui l'adoptera. */
main.app-content:has(> .hub-decor) {
    position: relative;
    overflow: hidden;
    min-height: calc(100vh - var(--topbar-height, 64px));
}

.hub-decor {
    position: absolute;
    inset: 0;
    /* Un `svg` est un element remplace : sa hauteur automatique se deduit du
       rapport du `viewBox` AVANT de se deduire des bords. Mesure a 1920 par
       1080, `inset: 0` seul rendait 933 px au lieu de 1016, soit la largeur
       divisee par le rapport du dessin. Les deux dimensions sont donc
       explicites. */

    /* PLUS LARGE QUE LA FENETRE QUAND LES MARGES NE SUFFISENT PAS. Le dessin
       est cale sur une grille de 660 px centree : le relief gauche finit a
       0,243 de la largeur du SVG, la colonne des tuiles commence a
       `moitie - 330 px`. Les deux se croisent des que la fenetre passe sous
       1359 px — mesure a 910 : le relief allait de -84 a 212 pour une grille
       qui commence a 125, soit 87 px de courbes de niveau PAR-DESSOUS les
       cartes, des deux cotes. C'est la seule chose de cette page qui se voit
       comme un defaut.

       En posant un plancher de 1460 px, le dessin s'ecarte au lieu de se
       resserrer : les reliefs gardent au moins 25 px de jeu avec la grille a
       toutes les largeurs, verifie de 910 a 2560. Le conteneur porte deja
       `overflow: hidden`, donc ce qui depasse est coupe et aucun defilement
       lateral n'apparait.

       `left` et `transform` parce que `inset: 0` ne peut plus centrer une boite
       plus large que son conteneur. */
    width: max(100%, 1460px);
    height: 100%;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    /* `z-index: 0` et pas `-1` : un z-index negatif passerait derriere le fond
       opaque du conteneur au lieu de passer derriere le seul contenu. */
    z-index: 0;
    pointer-events: none;
    /* Le trace peint en `currentColor` : la couleur se pose donc ici, une seule
       fois, et suit le theme clair ou sombre. */
    color: var(--mr-text);
    /* Plus haut que pour une forme pleine : un trait de deux pixels couvre cent
       fois moins de surface qu'un aplat, il faut donc bien plus d'opacite pour
       la meme presence.

       LA VALEUR SE LIT EN ECART AU FOND, PAS EN OPACITE. Mesure ici : le trait
       ressort a 37 sur 255 dans les deux themes, qui tombent au meme chiffre
       parce que le fond et le texte s'echangent d'un theme a l'autre. Une seule
       valeur suffit donc, et c'est ce qui permet de la regler une fois.
       Reperes : la landing pose ses courbes a 18, et on etait a 57 ici, ce qui
       tirait l'oeil vers les bords au lieu de le laisser sur les tuiles. */
    opacity: .17;
}

/* UN DESSIN ETIRE SUPPORTE LES ECRANS, PAS LES TELEPHONES. Le decor prend le
   rapport de sa boite : de 1,7 a 2,5 sur les ecrans reels, l'ecart au dessin
   reste sous 20 % et ne se voit pas sur une forme qui n'a jamais ete ronde.
   Sous 900 px la boite devient plus haute que large, le rapport depasse 3, et
   les reliefs deviennent des ovales verticaux. Pas de decor vaut mieux qu'un
   decor deforme, et la version mobile de cette page reste a faire. */
@media (max-width: 900px) {
    .hub-decor { display: none; }
}

/* `.hub` repasse devant le decor : en flux normal, un bloc se peint SOUS toute
   couche positionnee. */
.hub { position: relative; z-index: 1; }

.hub-titre {
    font-family: var(--mr-serif);
    font-size: clamp(1.9rem, 4vw, 2.6rem);
    font-weight: 500;
    color: var(--mr-text-strong);
    margin: 0 0 8px;
}

.hub-sous {
    color: var(--mr-text-muted);
    font-size: 1rem;
    margin: 0 0 40px;
    max-width: 56ch;
}

/* Un titre sans sous-titre reprend le creux que celui-ci occupait, pour que la
   grille ne remonte pas. Releve a 1280 px : 8 px de marge du titre, 51 de texte
   sur deux lignes, 40 de marge basse, soit 99.

   Il grandit aussi : seul en haut d'une page, il n'a plus de sous-titre pour
   porter le poids de l'en-tete. La borne haute passe de 2,6 a 4,2 rem, et la
   marge basse se reduit d'autant que le titre gagne en hauteur, pour que la
   grille reste ou elle est. */
.hub-titre--seul {
    font-size: clamp(2.6rem, 6vw, 4.2rem);
    margin-bottom: 72px;
}

/* UNE RANGEE EN FLEX, ET PLUS UNE GRILLE. La tuile survolee doit POUSSER ses
   voisines, pas passer par-dessus. Une grille impose sa colonne : la seule
   facon d'y grandir est un `transform`, qui recouvre les voisines au lieu de
   les deplacer, ou un changement de `grid-column`, qui saute sans s'animer.
   En flex, `flex-basis` est animable, donc la tuile prend sa place pendant que
   les voisines cedent la leur, dans le meme mouvement.

   `align-items: flex-start` : sans lui, une rangee en flex etire toutes les
   tuiles a la hauteur de la plus grande, ce qui ecraserait le carre des
   voisines des qu'une se deplie. Chacune garde donc sa propre hauteur. */
.hub-grille {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--creux);

    /* LA GRILLE EST PLUS ETROITE QUE LA PAGE, et c'est ce qui rapetisse les
       tuiles sans toucher au nombre de colonnes. Etalees sur les 1180 px du
       bloc, trois tuiles carrees faisaient 383 px de cote : deux rangees
       depassaient l'ecran. En bornant la rangee a 660, elles tombent a 209, et
       leur contenu occupe enfin la place au lieu de flotter dedans : la
       pastille fait pres de la moitie du cote, le nom se lit de loin.
       Le nombre de colonnes, lui, ne change pas : c'est la largeur disponible
       qu'on reduit, pas la mise en page.

       660 PUIS 630 : LA TUILE PASSE DE 208 A 198 SANS QUE SON CONTENU BOUGE.
       Le vide de la tuile au repos n'en est pas : sa pastille, son nom et son
       remplissage occupent 171 px sur 208, et les 37 px qui restent sont la
       RESERVE de l'accroche qui se deplie au survol. Mesure : au survol le
       contenu demande 235 px pour une tuile de 236. Rapetisser la boite sans
       toucher au reste ecraserait donc le texte revele, en silence, puisque le
       pli est en `minmax(0, 1fr)` et se laisse comprimer.

       C'est pourquoi le survol grandit maintenant de 44 px et non de 28 : la
       tuile depliee reste au-dessus de 235.

       LE PLANCHER SE CALCULE, IL NE SE CHOISIT PAS. Avec R le cote au repos, il
       faut R + croissance >= 235 pour la tuile depliee, R - retrait >= 173 pour
       une voisine (son contenu, pastille ramenee a 88), et la rangee ne tolere
       que 3 px de jeu, donc croissance <= 2 x retrait + 3. Les trois ensemble
       donnent R >= 193.

       Premiere tentative a 192, soit un pixel sous le plancher, parce que le
       besoin d'une voisine avait ete DEDUIT a 163 au lieu d'etre mesure a 173 :
       resultat, une voisine rognee de 3 px sans que rien ne le dise. On s'arrete
       donc a 198, avec de la marge partout : 17 px au repos, 7 au survol, 3 pour
       une voisine, et 3 sur la rangee. */
    max-width: 630px;
    margin-inline: auto;

    /* LA GRILLE EST REGLEE SUR SIX APPS, pas sur un nombre quelconque. Une base
       fixe de 200 px en rangeait cinq par rangee : six donnaient cinq tuiles
       puis une seule, orpheline, sur la ligne suivante. En part de la largeur,
       elles font TROIS par rangee et deux rangees pleines.

       `--tuile` est la largeur d'un tiers, creux deduits. Tout ce qui bouge au
       survol s'exprime a partir d'elle, pour qu'un changement de nombre de
       colonnes ne demande qu'a toucher ce calcul.

       LE PIXEL RETIRE N'EST PAS UN DETAIL D'ALIGNEMENT, IL EMPECHE UN
       DEBORDEMENT. Un tiers EXACT remplit la rangee au pixel pres, et le survol
       n'y change rien : la tuile survolee prend 28 px, les deux autres en
       rendent 14 chacune, la somme reste exactement la largeur disponible.
       Mesure a 2560 par 1400, en 150 % d'echelle systeme : 0,031 px de marge
       dans les trois etats de survol. Une mise en page posee sur trois
       centiemes de pixel n'est pas sure, elle est en sursis — il suffit qu'un
       arrondi tombe de l'autre cote, sur une autre machine ou une autre police,
       pour que la troisieme tuile passe a la ligne et parte en bas a gauche.

       Un pixel retire par tuile met trois pixels de jeu dans la rangee, cent
       fois l'ecart observe, pour une tuile qui passe de 209,3 a 208,3 px : rien
       que l'oeil puisse voir. Et le jeu vaut pour TOUS les etats, au repos comme
       au survol, puisque le survol conserve la somme. */
    --creux: 16px;
    --tuile: calc((100% - 2 * var(--creux)) / 3 - 1px);
}

/* UNE TUILE CARREE, ET QUI LE RESTE UNE FOIS DEPLIEE.

   `aspect-ratio: 1` fait suivre la hauteur a la largeur : il n'y a donc rien a
   animer en hauteur, on elargit la tuile et le carre grandit tout seul. C'est
   ce qui rend le deploiement carre gratuit.

   `flex-grow: 0` ET NON `1` : une tuile qui grandit au survol ne peut pas etre
   une tuile qui s'etire deja pour remplir sa rangee. Avec `flex-grow: 1` et
   deux apps dans 1180 px, chacune prendrait 584 px de large, donc 584 px de
   haut par le ratio : deux paves geants au lieu d'un catalogue. La base vaut
   donc un tiers exact de la largeur (cf. `--tuile`), et rien ne s'etire.

   `overflow: hidden` tient la promesse du carre : `aspect-ratio` cede des que
   le contenu depasse, et une tuile se serait allongee toute seule le jour d'un
   nom sur deux lignes. */
/* `box-sizing: border-box` : cette page n'herite d'aucune remise a zero, donc
   une tuile compte en `content-box` par defaut et `flex-basis` designe la
   largeur du CONTENU seul. Mesure : 168 de base rendait 197 px a l'ecran, les
   28 de creux et les 2 de filet venant s'y ajouter. Anodin sur une rangee qui
   se range a gauche, fatal sur le demi-largeur des deux colonnes du
   telephone, ou les 30 px en trop font deborder la seconde tuile. */
.hub-app {
    box-sizing: border-box;
    flex: 0 1 var(--tuile);
    aspect-ratio: 1;
    overflow: hidden;
    /* CENTRE, comme les tuiles du menu de la barre. Elles montrent le meme
       catalogue et doivent se reconnaitre comme une seule chose : le dessin est
       identique, seule l'echelle change. `position: relative` pour l'etiquette
       « Bientot », qui se pose dans le coin. */
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 12px;
    padding: 16px;
    border: 1px solid var(--mr-border);
    /* Un rayon PROPRE a la tuile et non `--mr-radius-sm` : ce jeton sert des
       champs et des boutons, ou un arrondi discret suffit. Une tuile carree de
       209 px de cote a besoin d'un arrondi a sa mesure, sinon elle reste un
       rectangle a coins casses. */
    border-radius: 24px;
    background: var(--mr-surface-card);
    text-decoration: none;
    color: inherit;
    /* RIEN NE CHANGE DE TAILLE. C'etait le vrai defaut de cette page : la tuile
       survolee grandissait de 28 px et ses deux voisines rendaient 14 px
       chacune, logo compris. Une grille qui se comprime sous le curseur se lit
       comme une maquette qui joue, pas comme un catalogue — et pendant qu'on
       lit une tuile, les autres bougent sous l'oeil.

       Ce qui reste est ce que fait n'importe quel produit serieux devant une
       carte survolee : elle se souleve, son filet s'affirme, son fond s'eclaire.
       Trois proprietes, dont une seule — `transform` — et aucune mise en page a
       recalculer. C'est aussi ce qui la rend parfaitement lisse a 180 Hz : le
       compositeur suffit, le moteur de rendu n'est plus sollicite par image. */
    /* LA COURBE SUR LES QUATRE, y compris le filet et le fond : sans elle ils
       repartaient sur le `ease` par defaut du navigateur, une autre montee en
       charge. Deux profils sur une meme carte se voient — la couleur arrive
       avant le mouvement. */
    transition: transform var(--hub-reponse) var(--hub-courbe),
                box-shadow var(--hub-reponse) var(--hub-courbe),
                border-color var(--hub-reponse) var(--hub-courbe),
                background var(--hub-reponse) var(--hub-courbe);
}

a.hub-app:hover,
a.hub-app:focus-visible {
    /* SIX PIXELS, PARCE QUE LE MOUVEMENT DURE PLUS LONGTEMPS. Quatre pixels
       etales sur un tiers de seconde ne se lisent plus comme un soulevement
       mais comme une derive : passe une certaine duree, il faut de la distance
       pour qu'un geste garde une direction. */
    transform: translateY(-6px);
    border-color: var(--mr-border-strong);
    background: var(--mr-surface-hover);
    box-shadow: 0 14px 30px -14px rgba(0, 0, 0, .5);
}

/* L'etiquette « Bientot » se pose dans le coin, la tuile etant desormais
   centree : dans le flux, elle deplacerait le bloc pastille plus nom vers le
   bas et la tuile ne serait plus centree qu'a moitie.

   ELLE DOIT TENIR DANS LA BANDE QUE LE LOGO NE PEUT PAS ATTEINDRE, et cette
   bande retrecit. Le contenu est centre : quand une voisine est survolee, la
   tuile passe de 209 a 195 px, le logo garde sa taille et remonte donc de 7 px.
   Mesure avant correction : l'etiquette descendait a 313 et le logo commencait
   a 309, le coin bas gauche de l'une mordait sur l'autre.

   Le calcul qui borne tout : le logo commence a
   `creux + (utile - logo - creux interne - nom) / 2`, soit 29 px du haut sur la
   tuile retrecie. L'etiquette doit donc finir avant, d'ou 8 px de haut pour une
   hauteur de 17, soit 25. Il reste 4 px, sur la plus petite tuile possible. */
.hub-etiquette--coin,
.hub-app .hub-etiquette {
    position: absolute;
    top: 8px;
    right: 8px;
    padding: 1px 6px;
    font-size: .58rem;
}

/* ── Le pli du detail ────────────────────────────────────────────────────
   Replie, il ne prend aucune place et la tuile se lit d'un coup d'œil : la
   marque, le nom, la fonction. Le detail est la recompense du survol.

   `0fr` vers `1fr` sur une rangee de grille : c'est la seule facon d'animer
   vers une hauteur qu'on ne connait pas. `max-height` obligerait a deviner une
   valeur, qui couperait le texte le jour ou un detail s'allonge, et qui rend
   l'animation molle en fin de course puisqu'elle parcourt le vide restant.

   MAIS `0fr` SEUL NE VAUT PAS ZERO, et c'est le piege de cette technique. Une
   rangee en `fr` se lit `minmax(auto, 0fr)` : le maximum est bien nul, le
   MINIMUM reste `auto`, donc la taille du contenu. Mesure ici avant correction:
   la rangee se stabilisait a 13,6 px de residu, et le pli ne se fermait jamais
   tout a fait. `minmax(0, ...)` est ce qui ecrase ce minimum.

   L'`overflow: hidden` va sur l'ENFANT et non sur l'enveloppe : c'est lui qu'on
   ecrase, et sans `min-height: 0` un paragraphe refuse de descendre sous sa
   hauteur de contenu, ce qui laisse le pli ouvert en permanence. */
/* HORS DU FLUX, ET C'EST CE QUI SUPPRIME LE DEPLIAGE. L'accroche etait une
   rangee de grille qui passait de `0fr` a `1fr` : une hauteur qui s'anime, donc
   une tuile qui grandit, donc des voisines qui reculent. Posee en absolu au bas
   du carre, elle ne pese plus rien — la tuile garde exactement la meme taille,
   au repos comme au survol, et il n'y a plus une seule mise en page a recalculer
   pendant l'animation.

   Elle se leve de huit pixels en apparaissant : un texte qui surgit sur place se
   remarque comme un clignotement, le meme texte qui monte un peu se lit comme
   quelque chose qui arrive. */
.hub-app-plus {
    position: absolute;
    left: 18px;
    right: 18px;
    bottom: 14px;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity var(--hub-etat) var(--hub-courbe),
                transform var(--hub-etat) var(--hub-courbe);
}

/* ── L'interieur suit la taille de la tuile ──────────────────────────────
   Passer de cinq colonnes a trois a fait doubler la tuile, de 200 a 383 px.
   Les valeurs reglees pour la petite y devenaient minuscules : releve, le
   contenu n'occupait plus que 120 px des 381 disponibles, et la tuile se
   lisait comme une boite vide avec un logo dans un coin.

   Le vide central est voulu, c'est lui qui separe l'icone du nom. Il ne doit
   simplement pas etre les deux tiers de la tuile. */
@media (min-width: 641px) {
    /* LE CREUX EST LE BUDGET DE LA TUILE, et il est compte. Sur la tuile
       ouverte de 237 px il reste 200 px utiles une fois la marge deduite, que
       se partagent la pastille (96), le nom (29), l'accroche qui se deroule
       (43) et les deux creux (20), soit 188. A 24 px de marge il n'en restait
       que 188 pour 207 demandes : flex ecrasait alors l'accroche a 12 px de
       haut sur les 48 qu'elle voulait, et le texte revele arrivait coupe. */
    .hub-app {
        padding: 18px;
        gap: 10px;
        --pastille: 96px;
    }
    .hub-app-nom { font-size: 1.55rem; }
    .hub-app-detail { font-size: .74rem; }
}

/* L'accordeon ne s'arme QUE la ou le survol existe vraiment. Sur telephone et
   tablette, `:hover` se declenche au premier toucher et reste colle : la tuile
   s'ouvrirait au moment meme du clic qui quitte la page. */
@media (hover: hover) and (min-width: 641px) {
    /* C'est la BASE qui change, et non une part d'espace libre : les tuiles ne
       s'etirant plus, une largeur demandee est une largeur obtenue, et le carre
       suit par le ratio.

       LE COMPTE DOIT TOMBER JUSTE. La tuile survolee prend 28 px, ses deux
       voisines de rangee en cedent 14 chacune : la rangee garde exactement sa
       largeur. Sans cette compensation, une tuile depliee pousse la troisieme
       de sa rangee a la ligne suivante, et toute la grille se reorganise sous
       la souris.

       LE MOUVEMENT ETAIT DE 72 ET 36, soit un quart de la tuile : elle
       bondissait plus qu'elle ne s'ouvrait, et ses voisines se
       recroquevillaient a vue. A 28 et 14 le geste se remarque encore mais ne
       reclame plus l'attention. */
    /* ⚠️ FUSION DU 28/08/2026 : Alex avait affine cette mecanique de son cote
       (retrait de 22 px, croissance de 44) le 27 au soir, depuis `dev` — il ne
       pouvait pas voir la refonte du lendemain matin, qui la supprime. Son
       reglage porte sur une regle qui n'existe plus ; sa REDUCTION DE LA TUILE
       au repos, elle, est conservee : elle ne dependait pas du survol.

       PLUS DE REGLE SUR LES VOISINES — c'est la suppression qui compte ici.
       Elles retrecissaient de 14 px, logo compris, pour rendre a la tuile
       survolee les 28 qu'elle prenait. Toute cette arithmetique existait pour
       tenir une rangee qui se comprimait sous le curseur ; la tuile ne
       changeant plus de taille, il n'y a plus rien a compenser, plus de boucle
       `:has()` a desamorcer, et plus de tuile qui passe a la ligne quand le
       focus reste accroche au retour d'une app.

       Le mouvement se limite desormais a la tuile qu'on regarde. */

    /* `:focus-visible` ET NON `:focus-within`, et LES DEUX REGLES DOIVENT
       REAGIR AUX MEMES ETATS. C'est le bug du retour depuis une app.

       `:focus-within` s'allume aussi quand on clique a la souris : au retour
       sur la page, le lien qu'on venait d'ouvrir avait garde le focus, donc sa
       tuile restait depliee. Et comme le retrecissement des voisines ne
       repondait qu'au survol, personne ne cedait les 28 px qu'elle prenait.
       Releve : 237 + 209 + 209 plus 32 de creux font 687 pour une rangee de
       660, et la troisieme tuile passait a la ligne. La grille se retrouvait
       en deux rangees sans que rien n'ait ete survole.

       Deux corrections, et la seconde compte autant que la premiere.
       `:focus-visible` ne s'allume que pour un focus au CLAVIER, donc un clic
       souris ne laisse plus rien de deplie derriere lui. Et la regle de
       retrecissement ecoute desormais les deux etats : le jour ou le clavier
       deplie une tuile, ses voisines cedent la place comme au survol. Une
       regle qui agrandit sans regle qui compense est une rangee qui deborde. */
    /* LE CONTENU REMONTE, IL NE RETRECIT PAS. Le carre ne grandissant plus, la
       place de l'accroche se prend sur le vide qui separe le logo du bord bas :
       le bloc logo-plus-nom glisse vers le haut et libere de quoi poser deux
       lignes. Un `transform`, donc aucune mise en page — la tuile ne sait meme
       pas que son contenu a bouge.

       22 ET NON 18 DEPUIS LA FUSION du 28/08 : la tuile d'Alex fait 198 px au
       lieu de 208, et le jeu entre le nom et l'accroche etait tombe a 3 px sur
       deux tuiles sur trois. Quatre pixels de glissement en plus le ramenent a
       7, et le logo reste a 10 px du bord haut. */
    .hub-app:hover .hub-app-marque,
    .hub-app:hover .hub-app-nom,
    .hub-app:focus-visible .hub-app-marque,
    .hub-app:focus-visible .hub-app-nom {
        transform: translateY(-22px);
    }

    .hub-app:hover .hub-app-plus,
    .hub-app:focus-visible .hub-app-plus {
        opacity: 1;
        transform: translateY(0);
    }
}

/* SANS SURVOL, LE DETAIL RESTE REPLIE, et c'est un arbitrage. Le montrer
   quand meme le ferait rogner par le carre : `overflow: hidden` couperait la
   phrase en plein milieu, ce qui est pire que de ne pas l'afficher. L'accroche
   dit deja a quoi sert l'app, et un doigt sur la tuile ouvre l'app elle-meme.
   Le jour ou on prefere l'inverse, c'est le carre qu'il faut lacher sur ces
   ecrans, pas le rognage qu'il faut accepter. */

/* Le pli reste, l'animation part : ce que quelqu'un a demande a ne pas voir
   bouger, c'est le mouvement, pas le contenu. */
/* LA PASTILLE PREND SA PLACE D'ICONE D'APP. Le texte n'occupait que 95 px des
   173 utiles de la tuile : le reste etait du vide entre la pastille et lui,
   creuse par le `margin-top: auto`. C'est ce vide que la pastille reprend, pas
   la place du texte.

   Sa taille passe par une variable parce qu'elle ne peut pas etre la meme
   partout : sur la tuile de 166 px du telephone, une icone de 60 px ne laisse
   plus de quoi ecrire. La valeur se redefinit une fois, en bas de fichier,
   au lieu d'etre recopiee sur trois proprietes.

   `flex-shrink: 0` la garde carree face a un nom long. */
.hub-app-marque {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--pastille, 68px);
    height: var(--pastille, 68px);
    /* Le rayon suit la taille, sinon un arrondi fixe devient un carre presque
       vif quand la pastille grandit au survol. */
    border-radius: calc(var(--pastille, 68px) * .23);
    background: var(--mr-btn-bg);
    color: var(--mr-btn-text);
    font-family: var(--mr-serif);
    font-size: calc(var(--pastille, 68px) * .46);
    line-height: 1;
    /* LA TRANSITION PORTE SUR `width` ET `height`, PAS SUR `--pastille`. Une
       propriete personnalisee n'est pas animable tant qu'elle n'a pas ete
       declaree par `@property` : elle saute d'une valeur a l'autre. Mais les
       proprietes qui la LISENT, elles, s'animent tres bien. On change donc la
       variable d'un coup et ce sont la largeur, la hauteur et le corps de la
       lettre qui glissent. */
    /* SEUL LE GLISSEMENT S'ANIME DESORMAIS. La pastille changeait de largeur,
       de hauteur, de rayon et de corps de lettre a chaque survol — quatre
       proprietes de mise en page, recalculees a chaque image. Elle ne change
       plus de taille du tout : il ne reste qu'un `transform`, que le
       compositeur traite seul. */
    transition: transform var(--hub-etat) var(--hub-courbe);
}

.hub-app-nom {
    transition: transform var(--hub-etat) var(--hub-courbe);
    font-family: var(--mr-serif);
    font-size: 1.55rem;
    font-weight: 600;
    line-height: 1.15;
    color: var(--mr-text-strong);
    margin: 0;
}

/* L'accroche est la moitié du nom : sans elle, « Flair » ne dit rien à
   quelqu'un qui découvre le produit. Elle ne se coupe donc jamais, SAUF sur la
   tuile de 164 px du telephone, ou elle est bornee a trois lignes faute de
   place dans le carre (cf. le bloc mobile en bas de fichier).

   EN `--mr-text` ET PLUS EN `--mr-text-muted` : en theme sombre le gris attenue
   vaut #8d867a sur une carte presque noire, ce qui la rendait grise avant de la
   rendre lisible. La hierarchie avec le nom se joue maintenant sur la taille et
   la graisse, pas sur un texte qu'on peine a lire. */
.hub-app-accroche {
    color: var(--mr-text);
    font-size: 1rem;
    font-weight: 500;
    line-height: 1.35;
    margin: 0;
}

/* Plus de `margin: auto 0 0` pour le coller en bas : le detail vit maintenant
   dans un pli qui s'ouvre, et une marge automatique dans un conteneur ecrase a
   zero repousse le texte hors du cadre au lieu de le reveler.

   L'ecart au-dessus est un CREUX et non une marge, pour la meme raison : une
   marge vit hors de la boite rognee, elle survit donc a la fermeture du pli et
   laisse 12 px de vide sous l'accroche. Un creux est dedans et se rogne avec
   le texte. */
/* Le detail deplie doit rester compact lui aussi : un pli qui doublerait la
   hauteur de la tuile ferait sauter toute la rangee, et la grille se
   reorganiserait sous les yeux au moindre passage de souris.

   NI MARGE NI BORDURE ICI, uniquement du creux. Les deux vivent hors de la
   boite rognee : une marge laisse son vide sous l'entete pli ferme, et une
   bordure haute se DESSINE sur un element de hauteur nulle, puisque
   `overflow: hidden` rogne le contenu et pas la bordure de l'element lui-meme.
   On aurait un filet d'un pixel en travers de chaque tuile au repos. */
/* LE CURSEUR DE LA FRAPPE. Sans lui, des lettres qui apparaissent une a une se
   lisent comme un texte qui charge mal ; avec lui, on reconnait tout de suite
   une machine a ecrire. C'est le meme dessin qui fait la difference entre un
   effet et un defaut.

   Un filet plutot qu'un caractere : un « | » ou un « _ » prend une chasse dans
   la ligne et decalerait le texte a chaque apparition. Une bordure ne pousse
   rien. */
.hub-frappe-curseur {
    display: none;
    border-right: 1.5px solid var(--mr-text-muted);
    margin-left: 1px;
}

/* Il n'existe QUE pendant la frappe : immobile sous une tuile survolee, il
   ferait clignoter un champ de saisie qui n'existe pas. */
.est-en-frappe .hub-frappe-curseur {
    display: inline;
    animation: hub-curseur 1s steps(1) infinite;
}

@keyframes hub-curseur {
    0%, 50% { opacity: 1; }
    50.01%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .est-en-frappe .hub-frappe-curseur { animation: none; }
}

.hub-app-detail {
    /* COMPACTE, PARCE QUE LE CARRE EST COMPTE. Mesure : la tuile de 208 px offre
       172 px utiles, et le logo (96), le nom (29) et cette phrase sur deux
       lignes en reclamaient 193 — 21 de trop. C'est ce manque qui obligeait
       autrefois la tuile a grandir, donc ses voisines a reculer.
       A 0,74 rem sur un interligne de 1,35, les deux lignes font 27 px au lieu
       de 57 : tout tient, et le contenu n'a que 18 px a remonter. */
    margin: 0;
    color: var(--mr-text-muted);
    font-size: .74rem;
    line-height: 1.35;
    padding: 0;

    /* DEUX LIGNES, JAMAIS TROIS. Elle est calee par le BAS : une troisieme
       ligne ne deborderait pas de la tuile, elle monterait dans le nom. Le jeu
       mesure entre les deux est de 8 px sur l'accroche la plus longue
       d'aujourd'hui — la prochaine app ecrite un peu plus long passerait au
       travers sans que rien ne le signale. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Une app à venir ne porte aucun lien : un clic qui ne mène nulle part fait
   douter du reste de la page. */
.hub-app--bientot {
    border-style: dashed;
    background: var(--mr-surface-sunken);
    cursor: default;
}

.hub-app--bientot .hub-app-marque {
    background: transparent;
    border: 1px dashed var(--mr-border-strong);
    color: var(--mr-text-muted);
}

.hub-app--bientot .hub-app-nom,
.hub-app--bientot .hub-app-accroche {
    color: var(--mr-text-muted);
}

/* L'etiquette a rejoint l'entete et se cale a son bout : posee sous le texte,
   elle ajoutait une troisieme ligne a une tuile qu'on vient de compacter, pour
   un mot de sept lettres. `margin-left: auto` la pousse a droite, `flex-shrink`
   l'empeche de se faire ecraser par un nom long. */
.hub-etiquette {
    margin-left: auto;
    flex-shrink: 0;
    align-self: center;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--mr-surface-sunken);
    border: 1px solid var(--mr-border);
    color: var(--mr-text-muted);
    font-size: .62rem;
    letter-spacing: .04em;
    text-transform: uppercase;
}

@media (max-width: 640px) {
    .hub { padding: 32px 16px 60px; }
    /* DEUX PAR RANGEE, et pas une colonne pleine largeur : un carre de la
       largeur de l'ecran ferait 343 px de haut, soit une tuile par ecran.
       `calc(50% - 6px)` retire la moitie du creux de 12 px a chacune.

       La pastille redescend a 44 px : la tuile ne fait plus que 166 px de cote,
       et le texte y prend plus de hauteur qu'ailleurs puisqu'il se coupe sur
       une colonne deux fois plus etroite. */
    .hub-app {
        flex-basis: calc(50% - var(--creux) / 2);
        --pastille: 40px;
    }

    /* L'ACCROCHE EST BORNEE A TROIS LIGNES, ICI SEULEMENT. Sur une tuile de
       164 px, une accroche un peu longue passe a quatre lignes et reclame
       16 px de plus que le carre n'en a. Le carre gagne, puisque c'est lui
       qu'on a demande : mais `overflow: hidden` couperait alors la phrase en
       plein milieu d'une ligne, ce qui se lit comme un bogue. Trois lignes et
       des points de suspension disent qu'il y a une suite.
       Le detail complet reste accessible, il est dans l'app.
       Sur les autres tailles l'accroche n'est jamais coupee : la tuile de
       383 px a largement de quoi la porter. */
    .hub-app-accroche {
        font-size: .9rem;
        display: -webkit-box;
        -webkit-line-clamp: 3;
        line-clamp: 3;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }
}

/* ── Projets ────────────────────────────────────────────────────────────── */

.hub-alerte {
    padding: 12px 16px;
    margin: 0 0 24px;
    border: 1px solid var(--mr-border-strong);
    border-radius: var(--mr-radius-sm);
    background: var(--mr-surface-sunken);
    color: var(--mr-text);
    font-size: .9rem;
}

.hub-nouveau {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin: 0 0 32px;
}

.hub-nouveau-label {
    font-size: .78rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--mr-text-muted);
    width: 100%;
}

.hub-nouveau-champ {
    flex: 1 1 280px;
    padding: 11px 16px;
    border: 1px solid var(--mr-border);
    border-radius: 999px;
    background: var(--mr-surface-card);
    color: var(--mr-text);
    font: inherit;
    font-size: .95rem;
}

.hub-nouveau-champ:focus {
    outline: none;
    border-color: var(--mr-border-strong);
}

.hub-nouveau-btn {
    padding: 11px 22px;
    border: none;
    border-radius: 999px;
    background: var(--mr-btn-bg);
    color: var(--mr-btn-text);
    font: inherit;
    font-size: .95rem;
    cursor: pointer;
    transition: background .2s;
}

.hub-nouveau-btn:hover { background: var(--mr-btn-bg-hover); }

/* Le formulaire ne doit pas ajouter de boite autour de la tuile : c'est le
   bouton lui-meme qui porte la carte, sinon la grille compte deux niveaux et
   les hauteurs cessent de s'aligner. */
.hub-projet-forme { display: contents; }

.hub-projet {
    align-items: flex-start;
    text-align: left;
    font: inherit;
    cursor: pointer;
    min-height: 150px;
}

.hub-projet--courant {
    border-color: var(--mr-border-strong);
    background: var(--mr-surface-hover);
}

/* ── Recherche ──────────────────────────────────────────────────────────── */

/* Le second intertitre respire au-dessus, pas en dessous : sans cette marge,
   « Tes analyses » colle a la grille des apps et semble la legender. */
.hub-sous--suite { margin-top: 36px; }

/* ── La marque en image ─────────────────────────────────────────────────── */

/* LE LOGO EST DEJA UNE ICONE : il porte sa propre tuile creme et ses coins
   arrondis. La pastille ne doit donc plus rien peindre derriere lui — le fond
   sombre et le rayon feraient un cadre autour d'un cadre, et le second ne
   tomberait jamais tout a fait sur le premier. Elle ne garde que sa TAILLE,
   qui reste animee au survol comme celle de la lettre. */
.hub-app-marque.a-image {
    background: transparent;
    border-radius: 0;
}

.hub-app-marque.a-image img {
    width: 100%;
    height: 100%;
    /* `contain` et non `cover` : les deux logos n'ont pas exactement le meme
       rapport (389x383 et 411x392). En `cover`, celui qui deborde serait rogne
       d'un cote — sur une icone, c'est le bord de la tuile qu'on couperait. */
    object-fit: contain;
    display: block;
}

/* ── Le lien vers les membres d'un projet ───────────────────────────────── */

/* La tuile bascule sur le projet, ce lien mene a ses membres : deux gestes
   distincts, donc deux cibles. Le lien se pose SOUS la tuile plutot que dedans
   — un lien dans un bouton n'est ni valide ni cliquable. */
.hub-projet-bloc {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

.hub-projet-membres {
    font-size: .74rem;
    color: var(--mr-text-muted);
    text-decoration: none;
    padding: 2px 8px;
    border-radius: 999px;
    transition: color .15s, background .15s;
}

.hub-projet-membres:hover {
    color: var(--mr-text-strong);
    background: var(--mr-surface-hover);
}


/* ── Mouvement reduit ───────────────────────────────────────────────────── */

/* EN DERNIER, ET C'EST LA TOUT L'INTERET. A specificite egale c'est la derniere
   regle qui gagne : pose au milieu du fichier, ce bloc etait annule par la
   transition de `.hub-app-marque`, declaree plus bas. Quelqu'un qui demande
   moins d'animation voyait donc quand meme la pastille grandir a chaque survol.
   Constate a la mesure : 0,48 s appliquee malgre la preference.

   Toute regle d'animation ajoutee plus tard doit rester AU-DESSUS de ce bloc. */
@media (prefers-reduced-motion: reduce) {
    .hub-app,
    .hub-app-marque,
    .hub-app-nom,
    .hub-app-plus,
    .hub-projet-membres { transition: none; }
}


/* ── Les outils internes ─────────────────────────────────────────────────
   Une section a part, sous le catalogue. Meles aux apps du produit dans la
   meme grille, ces outils se lisaient comme des apps : meme taille, meme rang,
   rien pour dire qu'ils sont d'une autre nature.

   UN FILET AU-DESSUS ET NON UNE BOITE AUTOUR. Encadrer la section la ferait
   ressortir plus que le catalogue lui-meme, ce qui est l'inverse de ce qu'on
   veut : ces outils sont utiles, pas importants. */
.hub-interne {
    max-width: 630px;
    margin: 56px auto 0;
    padding-top: 28px;
    border-top: 1px solid var(--mr-border);
}

/* Le titre et sa glose sur la meme ligne, la glose en petit : « Outils
   internes » seul laisse croire a une section du produit qu'on n'aurait pas
   remarquee. La seconde moitie dit que personne d'autre ne les voit. */
.hub-interne-titre {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 10px;
    margin: 0 0 20px;
    font-family: var(--mr-serif);
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--mr-text-muted);
}

.hub-interne-titre span {
    font-family: var(--mr-sans, inherit);
    font-size: .78rem;
    font-weight: 400;
    color: var(--mr-text-muted);
    opacity: .8;
}

/* LA MEME MECANIQUE DE SURVOL QUE LE CATALOGUE, en tuiles plus petites : ce
   sont les memes cartes, et deux comportements sur une meme page se voient.
   Seule la taille dit la hierarchie.

   ⚠️ ON REGLE `--tuile`, PAS LA GRILLE. `.hub-grille` porte un nom de grille et
   c'est une FLEXBOX : la largeur d'une carte vient de `flex: 0 1 var(--tuile)`,
   et un `grid-template-columns` n'y produit rien du tout. Essaye le
   29/08/2026 : la regle s'appliquait, la tuile restait a 198 px.

   `justify-content: start` parce que deux tuiles dans un conteneur centre se
   poseraient au milieu de la ligne, sous une section dont le titre commence a
   gauche. */
.hub-grille--interne {
    --tuile: 158px;
    justify-content: flex-start;
}

@media (min-width: 641px) {
    .hub-grille--interne .hub-app {
        padding: 14px;
        gap: 8px;
        --pastille: 64px;
    }
    .hub-grille--interne .hub-app-nom { font-size: 1.15rem; }
}
