/* ==========================================================================
   Fabian Branas — feuille de style principale
   --------------------------------------------------------------------------
   Les animations image par image sont dans _sprites.css, GÉNÉRÉ par
   tools/build-assets.sh. Ce fichier-ci s'écrit à la main.

   Sommaire
     1.  Variables
     2.  Reset & base
     3.  Curseur personnalisé
     4.  Accessibilité
     5.  Boutons-images (motif réutilisable)
     6.  Annotations manuscrites
     7.  En-tête
     8.  Pied de page
     9.  Page d'accueil
     10. Masques de découpe
     11. Contenu sobre (en attente des dessins de Fabian)
     12. Menus déroulants
     13. Modales et formulaires
   ========================================================================== */

/* ==========================================================================
   1. VARIABLES
   ========================================================================== */

:root {
  /* Palette relevée à la pipette directement dans les PNG de Fabian.
     Une seule couleur d'accent sur tout le site : le vert EST le signal
     d'interaction. Ne pas en ajouter une deuxième. */
  --fb-vert:  #359b3c;
  --fb-noir:  #000000;
  --fb-blanc: #ffffff;
  --fb-rose:  #ef87aa; /* signature « nickel. » uniquement */

  /* Gris utilitaires — n'existent pas dans les maquettes, réservés aux
     éléments que Fabian n'a pas encore dessinés (formulaires, tableaux…). */
  --fb-gris:       #767676; /* contraste 4.6:1 sur blanc : lisible */
  --fb-gris-clair: #e6e6e6;

  /* Typographie : Adobe Garamond Pro via Typekit (kit clr2hux).
     ⚠ Les kits Adobe Fonts sont restreints par domaine. Si la police ne
     s'affiche pas, vérifier que fabian-branas, localhost et fabianbranas.art
     sont autorisés dans les réglages du kit. La pile de secours ci-dessous
     assure que le site reste lisible entre-temps.

     ⚠ CETTE LIGNE N'EST PLUS LE DERNIER MOT (20 octobre 2026). Le réglage
     « Police du site » (Apparence → Personnaliser) peut redéfinir
     `--fb-serif` juste après ce fichier, avec l'une des deux polices
     embarquées dans le thème — Fraunces ou DM Sans. Le code est dans
     inc/typographie.php, les fichiers dans assets/fonts/. Ce qui est
     écrit ici reste le DÉFAUT, et le défaut reste Garamond : tant que
     personne n'a touché au réglage, le site charge ce qu'il a toujours
     chargé.

     ⚠ ET C'EST POURQUOI AUCUNE RÈGLE DU THÈME NE DOIT ÉCRIRE UN NOM DE
     POLICE EN DUR. Les dix `font-family` du projet passent toutes par cette
     variable (plus une dans galerie.css) ; c'est exactement ce qui a permis
     d'ajouter le réglage en touchant zéro règle de style. Une seule police
     écrite en dur quelque part, et le site se mettrait à panacher.

     ⚠ LE NOM DIT « serif » ET LA VALEUR PEUT NE PAS EN ÊTRE UNE. Choisir
     DM Sans met une sans serif dans une variable qui s'appelle `--fb-serif`.
     On garde le nom : le renommer voudrait dire toucher onze règles réparties
     dans 345 Ko de CSS pour un gain purement cosmétique. À lire comme « la
     police du site ». */
  --fb-serif: "adobe-garamond-pro", "EB Garamond", "Adobe Garamond Pro",
              Garamond, Georgia, "Times New Roman", serif;

  /* Échelle typographique fluide : la taille suit la largeur de l'écran,
     sans paliers brutaux. clamp(mini, idéal, maxi). */
  --fb-texte: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --fb-petit: clamp(0.8125rem, 0.78rem + 0.16vw, 0.9375rem);
  --fb-titre: clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem);

  /* Rythme */
  --fb-gouttiere: clamp(1.25rem, 4vw, 3rem);
  --fb-largeur:   900px;

  /* HAUTEUR DES DEUX OBJETS POSÉS DANS LES COINS HAUTS : « IN ENGLISH PLEASE. »
     à gauche, le panier à droite. Une seule valeur parce qu'ils doivent se
     répondre — deux `clamp()` indépendants, ce qu'il y avait ici, divergent au
     premier réglage fait d'un seul côté. Le bilingue étant masqué pour
     l'instant, la divergence ne se verrait même pas tout de suite : elle
     apparaîtrait le jour où on rallume le lien, longtemps après la cause.

     Remontée le 23 septembre 2026 (1 → 1,75 rem au plancher, 2 → 2,9 rem au
     plafond) : le panier est FIXE au-dessus de 1250 px, donc calé sur le bord
     de la fenêtre et non sur la page — à 2 rem il touchait presque le haut de
     l'écran. « Le décoller un peu », Fabian. */
  --fb-coin-haut: clamp(1.75rem, 3.4vw, 2.9rem);

  /* Échelle calée sur la page de maintenance dessinée par Fabian, qui sert de
     référence : composition de 620 px de large, titre de 560 px, boutons de
     65 px, gouttière de 20 px. Le site reprend ces proportions.

     Les assets restent exportés en 380 px pour un affichage à ~124 px : plus de
     2x, donc parfaitement nets sur écran Retina — et il y a de la marge si on
     décide plus tard de les agrandir. */
  --fb-compo:  790px;                       /* largeur max de la composition d'accueil */
  /* ×1,2 le 9 septembre 2026, à la demande de Fabian (« environ 120 % ») :
     124 → 149 px au plafond, et la composition suit (660 → 790). Les assets
     sont exportés en 380 px : encore 2,5× la taille affichée, nets sur Retina. */
  --fb-bouton: clamp(84px, 11.4vw, 149px);

  /* Plancher des vignettes d'accueil, plus bas que --fb-bouton À DESSEIN.
     Les 5 vignettes doivent tenir sur une seule ligne le plus longtemps
     possible : c'est la lecture voulue de la navigation. En dessous de cette
     taille on renonce et on passe à 4 colonnes — vers 340 px de large, donc
     seulement sur les tout petits téléphones. */
  --fb-vignette-mini: 56px;
  /* Hauteur des pilules de navigation. Réduite une deuxième fois (40 → 34 px au
     plafond) : le menu doit rester un sous-titre du logo, pas une barre qui lui
     tient tête. Le plancher ne descend pas sous 25 px — en dessous, le contour
     dessiné à la main de « À PROPOS » devient un trait continu à l'écran. */
  --fb-nav-h:  clamp(25px, 2.5vw, 34px);
  --fb-ecart:  clamp(0.35rem, 0.8vw, 0.7rem);

  /* GÉOMÉTRIE DE LA RANGÉE DE RÉSEAUX SOCIAUX.
     Déclarée ici, dans `:root`, et pas sur `.fb-reseaux` où elle est utilisée,
     parce qu'un deuxième élément en dépend : le bouton « Me soutenir » doit
     faire exactement la largeur des quatre icônes réunies, et c'est son FRÈRE,
     pas son descendant. Une variable descend l'arbre, elle ne traverse pas.
     Trois valeurs à un seul endroit, donc, et une largeur qui reste juste quand
     on retouche la taille des icônes ou leur écart. */
  --fb-reseau-l:     clamp(26px, 2.4vw, 28px);
  --fb-reseau-ecart: clamp(0.25rem, 0.65vw, 0.5rem);
  --fb-reseaux-l:    calc(var(--fb-reseau-l) * 4 + var(--fb-reseau-ecart) * 3);

  /* Hauteur du bord déchiré qui ouvre le pied de page.

     Déclarée ICI et pas sur `.fb-pied`, où elle vivait, parce qu'elle sert
     désormais à quelqu'un qui n'est pas un descendant du pied de page : sur
     l'accueil, le contenu mesure `100svh` MOINS cette hauteur, pour que la
     déchirure affleure le bas de l'écran. Une variable ne franchit pas
     l'arbre, seulement la descendance — d'où la remontée à la racine. */
  /* Hauteur du bord déchiré. Ramenée de 40 à 26 px avec le passage du pied en
     position fixe : c'est du noir permanent à l'écran, il se paie cher.
     L'image source fait 3307 × 133 et s'étire en `100% 100%` — le trait est
     donc comprimé d'un facteur 2,2 au lieu de 1,4. Sur un trait dessiné à la
     main, irrégulier par nature, ça ne se lit pas. En dessous de ~20 px, si. */
  --fb-dechirure-h: clamp(14px, 1.9vw, 26px);

  /* DE COMBIEN LA DÉCHIRURE S'ENFONCE SOUS LA BANDE NOIRE.
     La bande remonte de cette valeur (margin négatif sur la déchirure) et vient
     recouvrir le bas du dessin. Ce bas est du noir plein — on n'y perd donc
     aucun trait, on gagne 10 px de budget et le bord déchiré se lit comme
     mordant dans la bande plutôt que posé dessus, ce que demandait Fabian.

     Écrite ici, en un seul endroit, parce que deux règles en dépendent : la
     déchirure elle-même et la bulle « nickel. », qui doit remonter au-dessus de
     tout le noir et donc savoir combien il y en a. */
  --fb-dechirure-enfoncement: 10px;

  --fb-transition: 140ms ease-out;
}

/* ==========================================================================
   2. RESET & BASE
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* LA BARRE DE DÉFILEMENT EN VERT (Fabian, 17 septembre 2026).
   Deux mécanismes, parce que les navigateurs n'en ont pas un seul :
   `scrollbar-color` est le standard (Firefox, Chrome et Edge depuis 121) ;
   `::-webkit-scrollbar` est ce que Safari comprend. Là où le standard est
   reconnu, il gagne et les règles ::-webkit- sont ignorées — c'est voulu par
   Chrome, et les deux donnent la même chose. Sur macOS avec les barres « au
   défilement », Safari affiche une vraie barre dès qu'on la stylise : un
   effet secondaire, pas un bug — la barre verte est justement ce qu'on veut
   voir. On ne touche pas à la largeur : celle du système reste la plus
   facile à attraper. */
html { scrollbar-color: var(--fb-vert) transparent; }
::-webkit-scrollbar { width: 14px; height: 14px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  /* Le rail est transparent (Fabian, 17 sept. 2026) : le poignet est écarté
     du bord par une bordure TRANSPARENTE, et `background-clip: padding-box`
     empêche le vert de passer dessous — une bordure blanche aurait dessiné
     un liseré sur le bandeau noir du pied. */
  background: var(--fb-vert);
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 7px;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* LE PIED DE PAGE EST EN `position: fixed` — CE QUE ÇA CHANGE ICI
   ---------------------------------------------------------------
   ⚠ AU-DESSUS DE 700 px SEULEMENT. Sur téléphone il est rendu au flux et
   cette réserve est ramenée à sa seule gouttière — tout est expliqué dans le
   bloc `@media (max-width: 700px)` de la section 8. Ce qui suit décrit donc
   le cas de l'ordinateur, qui reste le cas général.
   Avant, `body` était une colonne flex avec `min-height: 100vh` : le pied était
   poussé en bas par `.site-main { flex: 1 0 auto }`, et il défilait avec la
   page. Fabian le veut maintenant toujours visible.

   Un élément en `position: fixed` est sorti du flux : le contenu passerait
   DERRIÈRE lui, et les derniers paragraphes de chaque page seraient illisibles.
   On réserve donc la place avec un `padding-bottom` égal à sa hauteur.

   Cette hauteur dépend des `clamp()` du CSS et du nombre de lignes que prend le
   bloc clair : le CSS ne sait pas la mesurer pour s'en servir ailleurs. C'est
   assets/js/interface.js qui la recopie dans `--fb-pied-h`, via un
   ResizeObserver. La valeur de repli ci-dessous est volontairement généreuse :
   sans JavaScript, mieux vaut un blanc de trop qu'un lien caché.

   `min-height` et la colonne flex ne servent plus à rien — c'était la mécanique
   du pied « collé en bas », remplacée par le `fixed`.

   ON NE RÉSERVE PLUS LA HAUTEUR DU PIED ENTIER, SEULEMENT CELLE DE LA BANDE.
   Le bloc clair (réseaux + « Me soutenir ») n'a plus de fond : la page défile
   DERRIÈRE lui, volontairement, les icônes se posant dans les marges de part et
   d'autre du texte. Lui réserver de la place reviendrait à annuler l'effet en
   laissant un grand vide blanc sous chaque page.
   La bande noire, elle, est opaque : ce qui passe dessous est perdu, donc sa
   hauteur reste due. C'est `--fb-pied-bande-h`, mesurée par interface.js. */
body {
  margin: 0;
  /* La hauteur de la bande, PLUS une gouttière blanche.
     Cette gouttière n'est pas du confort : les bulles d'annotation des réseaux
     sociaux s'affichent AU-DESSUS de leurs icônes (`bottom: 100%`), donc
     au-dessus du pied, donc par-dessus la page. Sans une bande blanche pour les
     recevoir, un dessin au trait noir se superposerait au dernier paragraphe —
     deux choses noires l'une sur l'autre, illisibles toutes les deux. C'est le
     même raisonnement que la marge haute de `.fb-entete`, qui réserve la place
     de « RETOUR À L'ACCUEIL. ». */
  padding-bottom: calc(var(--fb-pied-bande-h, 80px) + clamp(1.5rem, 4vw, 3rem));
  background: var(--fb-blanc);
  color: var(--fb-noir);
  font-family: var(--fb-serif);
  font-size: var(--fb-texte);
  line-height: 1.6;
}

img { max-width: 100%; height: auto; display: block; }

/* Les listes purement structurelles (grilles, réseaux sociaux) n'ont pas à
   afficher de puces. On ne cible que celles marquées role="list" : l'attribut
   est là pour que les lecteurs d'écran continuent d'annoncer « liste de N
   éléments » malgré la suppression du style — Safari cesse de le faire dès
   qu'on retire list-style. */
ul[role="list"], ol[role="list"] {
  list-style: none;
  margin: 0;
  padding: 0;
}

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.15em; }

h1, h2, h3 { font-weight: 700; line-height: 1.2; margin: 0 0 0.5em; }

button { font: inherit; }

.fb-conteneur {
  width: 100%;
  max-width: var(--fb-largeur);
  margin-inline: auto;
  padding-inline: var(--fb-gouttiere);
}

/* --------------------------------------------------------------------------
   PLUS DE ZOOM AU DOUBLE-TAP
   --------------------------------------------------------------------------
   Fabian, 7 octobre 2026 : « désactiver le zoom sur le double clic ». Sur
   téléphone, un double-tap rapide agrandit la page — et comme tout le site est
   fait de dessins qu'on tape (les onglets, les vignettes, les plus et les
   moins du panier), deux appuis un peu rapprochés déclenchaient un zoom à la
   place d'un double achat d'attention.

   ⚠ CE N'EST PAS `user-scalable=no`, ET IL NE FAUT SURTOUT PAS LE METTRE.
   La solution qu'on trouve partout consiste à écrire
   `<meta name="viewport" content="…, user-scalable=no, maximum-scale=1">`.
   Elle supprime bien le double-tap — en supprimant AUSSI le zoom à deux
   doigts, c'est-à-dire le seul moyen qu'a une personne malvoyante de lire un
   texte trop petit. C'est un échec au critère 1.4.4 des WCAG, et c'est pour
   cette raison que Safari l'ignore depuis iOS 10.

   `touch-action: manipulation` dit exactement ce qu'on veut dire : « sur cet
   élément, le navigateur garde le défilement et le pincement, et abandonne le
   double-tap ». Le zoom à deux doigts reste entier.

   SUR `(pointer: coarse)` ALORS QUE LA PROPRIÉTÉ NE CONCERNE QUE LE TACTILE.
   Elle n'a en effet aucun effet à la souris, la limiter est donc théoriquement
   inutile. On la limite quand même, parce que la consigne de cette mise à jour
   est que rien de ce qui est écrit pour le téléphone ne puisse atteindre le
   grand écran : une règle sans garde-fou est une règle dont il faut vérifier
   qu'elle est inoffensive, et c'est une vérification de plus à refaire à chaque
   relecture.

   ⚠ SUR `body` ET PAS SUR `html` : `touch-action` s'hérite mal — elle ne
   s'hérite pas du tout, mais elle s'applique à l'élément et à ses descendants
   qui ne la redéclarent pas. Posée sur `html`, elle laisse passer les éléments
   qui créent leur propre contexte de défilement. Sur `body`, elle couvre tout
   ce qui est dessiné dans la page. */
@media (pointer: coarse) {
  body { touch-action: manipulation; }
}

/* ==========================================================================
   3. CURSEUR PERSONNALISÉ
   --------------------------------------------------------------------------
   Fait avec la propriété CSS `cursor`, pas avec un <div> qui suit la souris en
   JavaScript. La différence est nette : le curseur natif est dessiné par le
   système d'exploitation, donc il ne peut PAS avoir de retard à l'affichage.
   Un curseur en JS accuse toujours une image de latence, ce qui se voit.

   Les deux nombres après l'URL sont le « point d'accroche » : le pixel de
   l'image qui correspond à la position réelle du clic. Pour le crayon, c'est
   sa pointe (en haut à gauche) ; pour la main, le bout de l'index.

   ⚠ LES RÈGLES ELLES-MÊMES SONT DANS _sprites.css, généré par le build.
   Raison : `cursor` ne sait pas redimensionner une image, elle s'affiche à la
   taille du fichier. La taille se règle donc à la génération
   (`CURSEUR_ECHELLE` dans tools/build-assets.sh), et le point d'accroche doit
   suivre la même échelle. Les garder ici obligerait à modifier deux fichiers
   en même temps, et un oubli décalerait le clic par rapport au dessin.

   Pour changer la taille : modifier CURSEUR_ECHELLE, relancer le build.

   ⚠ IL N'Y A AUCUN `cursor:` DANS CETTE FEUILLE, ET C'EST VOULU (23 septembre
   2026). Fabian : « à travers TOUT le site, que ce soit toujours soit le
   crayon soit la main ; il y a encore beaucoup d'endroits qui ne prennent pas
   ça en compte — fais en sorte que ce soit par défaut, sans devoir l'ajouter
   sur chaque bouton. »

   Le crayon était posé sur `body` et comptait sur l'héritage. Ça tient tant que
   personne ne redéclare `cursor` en chemin — et tout le monde le redéclare :
   WooCommerce sur ses boutons, PhotoSwipe sur ses images, cette feuille-ci sur
   une quinzaine de règles. Chaque `cursor: pointer` un peu plus spécifique
   rendait le curseur du système, à l'endroit précis où l'on clique.

   Les trois curseurs sont donc déclarés une seule fois, dans _sprites.css, en
   `!important` : le crayon sur `*`, la main sur tout ce qui se clique ET ses
   descendants, le curseur texte sur les champs de saisie. Les `cursor: pointer`
   qui vivaient ici sont partis avec — ils ne faisaient plus rien, et ils
   laissaient croire qu'il fallait y penser bouton par bouton.

   Le seul cas qui demande encore un geste : un élément rendu cliquable par du
   JavaScript, sans <a>, sans <button> et sans `role`. La classe `.fb-cliquable`
   est là pour ça (voir CIBLES_MAIN_JS dans tools/build-assets.sh) — mais c'est
   d'abord le signe qu'il manque une balise, pas un curseur.
   ========================================================================== */

/* ==========================================================================
   4. ACCESSIBILITÉ
   ========================================================================== */

/* Masque un texte à l'œil mais le laisse lisible par les lecteurs d'écran.
   Indispensable ici : au survol, le mot « SHOP » est remplacé par un dessin de
   caddie. Le mot doit continuer d'exister dans la page, sinon un utilisateur
   aveugle n'a plus aucune navigation. On masque, on ne remplace jamais. */
.screen-reader-text {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 999;
  padding: 0.75rem 1.25rem;
  background: var(--fb-noir);
  color: var(--fb-blanc);
  text-decoration: none;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* Anneau de focus visible au clavier. Il n'apparaît PAS au clic souris
   (`:focus-visible` gère la distinction), donc il n'abîme pas le design. */
a:focus-visible, button:focus-visible,
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: 3px solid var(--fb-vert);
  outline-offset: 3px;
}

/* Respect du réglage système « réduire les animations ». Une personne sujette
   au mal des transports ou aux migraines l'active pour de bonnes raisons :
   33 images qui s'agitent au survol seraient très pénibles. On coupe tout
   mouvement, mais on garde les changements d'état (les survols restent lisibles). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   5. BOUTONS-IMAGES — le motif réutilisable du site
   --------------------------------------------------------------------------
   Presque toute l'interface est faite d'images avec 2 ou 3 états. Plutôt que
   d'échanger un `src` en JavaScript, on empile les états en calques et on joue
   sur l'opacité.

   Deux raisons :
     • Aucun clignotement au premier survol. Si on changeait l'image au moment
       du survol, le navigateur commencerait seulement à la télécharger à cet
       instant → le bouton disparaîtrait un instant. Ici les états sont déjà
       peints (juste transparents), donc déjà chargés.
     • Aucun JavaScript.

   Les calques sont ::before et ::after, donc ça ne coûte aucune balise HTML.
   ========================================================================== */

.fb-img {
  position: relative;
  display: inline-block;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  text-decoration: none;
  vertical-align: middle;
}

.fb-img::before,
.fb-img::after {
  content: "";
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  transition: opacity var(--fb-transition);
}

/* ⚠ LE CALQUE DU REPOS NE S'EFFACE PAS AU SURVOL, ET C'EST UNE CORRECTION.
   ----------------------------------------------------------------------------
   Il s'effaçait, en fondu croisé avec celui du survol : `::before` 1 → 0 pendant
   que `::after` 0 → 1. C'est le réflexe, et c'est faux — ça produisait un
   ÉCLAIR BLANC au milieu de chaque survol, sur tout le site (signalé par Thoma,
   24 septembre 2026).

   L'arithmétique de la superposition l'explique. Deux calques empilés à 0,5
   d'opacité ne couvrent pas la page : il en passe 1 − 0,5 × 0,5 = 0,25. Au
   point médian du fondu, un quart du blanc de la page traversait donc les deux
   dessins — sur un bouton noir, ça se voit comme un flash. Le passage par le
   milieu était INÉVITABLE, puisque les deux transitions étaient symétriques.

   La correction : le repos reste opaque, le survol se pose DESSUS. Il n'y a
   plus de moment où le fond transparaît.

   CE QUI RENDAIT ÇA SÛR A ÉTÉ MESURÉ, PAS SUPPOSÉ. Si un dessin de survol
   laissait des zones transparentes là où celui du repos a de l'encre, on
   verrait les deux superposés — un fantôme immobile sous le nouveau dessin.
   Les 37 paires `*-base.png` / `*-survol.png` du thème ont donc été comparées
   canal alpha contre canal alpha : 36 sont intégralement couvertes. La seule
   exception est le bouton NEWS de la navigation, traité à part dans la section
   « navigation ».

   Le même piège avait déjà été rencontré — et résolu — sur les tuiles de
   l'accueil, où il portait l'autre nom du même phénomène : le fantôme. Voir
   `.fb-btn__anim`, dont le commentaire dit déjà « un fondu, même court, ferait
   apparaître les deux images à demi opaques en même temps ». La leçon n'avait
   simplement jamais été reportée ici. */
.fb-img::before { opacity: 1; } /* état de repos — toujours opaque */
.fb-img::after  { opacity: 0; } /* état de survol — se pose par-dessus */

.fb-img:hover::after,
.fb-img:focus-visible::after  { opacity: 1; }

/* Page courante : l'état « sélection » est porté par le fond de l'élément
   lui-même, et les deux calques s'effacent. */
.fb-img.is-current::before,
.fb-img.is-current::after { opacity: 0; }

/* LE DESSIN SANS ÉTAT — `.fb-dessin`.
   Même mécanique que `.fb-img` (un calque ::before qui porte l'image, le
   rapport largeur/hauteur écrit par le build dans _sprites.css), mais SANS
   survol : la pastille de prix, le cadre du champ de quantité, la scène du
   panier vide n'ont qu'un seul état. Poser `.fb-img` dessus les aurait fait
   DISPARAÎTRE au survol — `.fb-img:hover::before { opacity: 0 }` éteint le
   repos pour laisser voir un survol qui, ici, n'existe pas. */
.fb-dessin {
  position: relative;
  display: inline-block;
}
.fb-dessin::before {
  content: "";
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  pointer-events: none;
}

/* ==========================================================================
   6. ANNOTATIONS MANUSCRITES
   --------------------------------------------------------------------------
   « RETOUR À L'ACCUEIL. », « DANS MON PANIER, J'AI MIS… », « INSTA »…
   Visibles au survol uniquement, à chaque survol.

   `pointer-events: none` : l'annotation ne doit jamais intercepter la souris,
   sinon elle se placerait entre le curseur et le bouton et déclencherait un
   clignotement infini (survol → l'annotation apparaît sous la souris → le
   bouton n'est plus survolé → l'annotation disparaît → etc.).
   ========================================================================== */

.fb-annote { position: relative; display: inline-flex; }

.fb-annote__bulle {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity var(--fb-transition), transform var(--fb-transition);
  background-repeat: no-repeat;
  background-size: contain;
  background-position: center;
}

.fb-annote:hover .fb-annote__bulle,
.fb-annote:focus-within .fb-annote__bulle {
  opacity: 1;
  transform: translateY(0);
}

/* Sur écran tactile il n'y a pas de survol : ces annotations ne peuvent jamais
   apparaître. On les retire complètement — ce qui supprime au passage un
   défaut invisible à l'œil : elles restent dans le flux même à `opacity: 0`,
   et celle de l'en-tête, placée à DROITE d'un bouton lui-même collé au bord
   de l'écran, provoquait 12 px de défilement horizontal sur un écran de
   320 px. Sur toutes les pages, en silence. */
@media (hover: none) {
  .fb-annote__bulle { display: none; }
}

/* ==========================================================================
   7. EN-TÊTE
   ========================================================================== */

/* La marge haute n'est pas décorative : elle réserve la place de l'annotation
   « RETOUR À L'ACCUEIL. », qui s'affiche AU-DESSUS du logo (`bottom: 100%`).
   Avec les 2 rem d'avant, la bulle sortait par le haut de la fenêtre — elle
   s'affichait bien, mais hors champ, donc invisible. Rien ne la rattrapait :
   on ne peut pas faire défiler vers le haut.

   Les deux valeurs vont donc de pair : si on réduit cette marge ou si on
   agrandit la bulle, il faut revérifier que la seconde tient dans la première.
   Voir `.fb-entete .fb-annote__bulle--accueil` plus bas.

   La marge BASSE, elle, a été resserrée à la demande de Fabian : le bloc
   titre + baseline + navigation flottait trop haut au-dessus du contenu. */
.fb-entete {
  position: relative;
  padding: clamp(3rem, 7vw, 5.5rem) 0 clamp(0.6rem, 1.8vw, 1.4rem);
}

/* « IN ENGLISH PLEASE. » — coin haut gauche.
   Le bilingue arrive en phase 2 ; d'ici là le lien est masqué côté PHP. */
.fb-english {
  position: absolute;
  top: var(--fb-coin-haut);
  left: var(--fb-gouttiere);
  width: clamp(120px, 16vw, 157px);
  aspect-ratio: 314 / 49;
}
.fb-english::before { background-image: url("../img/accueil/english-base.png"); }
.fb-english::after  { background-image: url("../img/accueil/english-survol.png"); }

/* Panier — coin haut droit. */
.fb-panier-zone {
  position: absolute;
  top: var(--fb-coin-haut);
  right: var(--fb-gouttiere);
}
/* LE PANIER SUIT LE DÉFILEMENT SUR GRAND ÉCRAN (Fabian, 9 septembre 2026 :
   « fixe sur la page, comme les réseaux sociaux »). Même seuil que le bloc
   social du pied (section 8) : au-dessus de 1250 px, les marges de part et
   d'autre de la colonne de texte sont assez larges pour qu'un objet fixé n'y
   recouvre jamais un mot. En dessous, il redevient ce qu'il est ici — posé dans
   l'en-tête, il part avec lui.

   `position: fixed` le sort de `.fb-conteneur` : `right` se compte alors depuis
   le bord de la FENÊTRE, plus depuis celui de la colonne. C'est voulu — c'est
   aussi là que sont les icônes des réseaux. `z-index: 21` : un cran au-dessus
   du pied fixe (20), pour que sa bulle « dans mon panier… » passe devant si
   les deux se croisent sur une fenêtre basse. */
@media (min-width: 1251px) {
  .fb-panier-zone {
    position: fixed;
    z-index: 21;
  }
}
/* Réduit d'un quart (56 → 42 px) : à sa taille d'origine, le panier pesait
   autant que le logo alors qu'il n'est qu'un raccourci. */
.fb-panier {
  width: clamp(29px, 3.75vw, 42px);
  aspect-ratio: 111 / 102;
}
.fb-panier::before { background-image: url("../img/nav/panier-base.png"); }
.fb-panier::after  { background-image: url("../img/nav/panier-survol.png"); }
.fb-panier.is-current { background-image: url("../img/nav/panier-selection.png"); }

/* « DANS MON PANIER, J'AI MIS… », désormais dans l'angle bas-GAUCHE du panier,
   à la demande de Fabian.

   Comme pour la bulle « nickel. », la pointe n'est pas au milieu du dessin.
   Mesurée dans le PNG : x ≈ 123,5 sur 167, soit **74 % de la largeur** depuis
   la gauche — donc 26 % depuis la droite — et elle touche le bord haut de
   l'image (y = 2 sur 194).

   `right: calc(100% - 26 %)` amène cette pointe pile sur le bord gauche de
   l'icône, la bulle s'étalant ensuite vers la gauche. C'est ce sens-là qu'il
   faut : le panier est calé sur la gouttière droite, une bulle qui partirait
   vers la droite sortirait de la fenêtre.

   Le facteur et la largeur vont ensemble : changer `--fb-bulle-panier-l` sans
   toucher au reste garde la visée juste, mais recadrer le PNG la fausserait
   en silence — c'est le dessin qui porte la mesure, pas la feuille de style. */
.fb-panier-zone .fb-annote__bulle {
  --fb-bulle-panier-l: clamp(54px, 6.8vw, 64px);

  top: calc(100% + 0.15rem);
  right: calc(100% - var(--fb-bulle-panier-l) * 0.26);
  left: auto;
  width: var(--fb-bulle-panier-l);
  aspect-ratio: 167 / 194;
  background-image: url("../img/annotations/panier.png");
}

/* Pastille du nombre d'articles. Aucun asset n'a été dessiné pour ça :
   on reste sobre, en attendant que Fabian s'en occupe.

   ⚠ LES DEUX DÉCALAGES SONT EN `em`, DONC RELATIFS À `--fb-petit`, la taille du
   chiffre — pas à la taille de l'icône. C'est ce qu'on veut : la pastille reste
   proportionnée à son propre contenu quand l'écran rétrécit.

   Remontée le 23 septembre 2026 (−0,35 → −0,85em) : Fabian la voulait « bien au
   coin ». Le dessin du panier remplit sa toile à 2 px près (109 × 99 sur
   111 × 102, mesuré au `-trim`), l'angle de la boîte EST donc l'angle du
   dessin — il n'y a pas de marge transparente à rattraper, seul le décalage
   compte. */
.fb-panier__compte {
  position: absolute;
  top: -0.85em;
  right: -0.5em;
  z-index: 2;
  min-width: 1.5em;
  padding: 0.1em 0.35em;
  border-radius: 1em;
  background: var(--fb-vert);
  color: var(--fb-blanc);
  font-size: var(--fb-petit);
  font-weight: 700;
  line-height: 1.4;
  text-align: center;
}

/* Panier vide : la pastille reste dans le HTML (le script doit pouvoir écrire
   dedans, voir header.php) mais disparaît de l'écran. `display: none` et non
   `opacity: 0` : à zéro d'opacité, le rond vert continuerait d'intercepter la
   souris dans l'angle du panier. */
.fb-panier__compte--vide { display: none; }

/* Bloc central : le logo « HEY! Moi c'est FABIAN » + la baseline.
   Écart resserré en deux fois (0.6 → 0.35 → 0.1em) : Fabian veut le titre, le
   slogan et le menu solidaires, lus comme un seul bloc et pas comme trois
   éléments empilés.

   ⚠ 0.1em, c'est presque zéro, et c'est le plancher : l'unité est `em`, donc
   l'écart suit la taille du texte du bloc. Le mettre à 0 collerait le bas des
   lettres de « FABIAN » au haut du slogan à toutes les tailles d'écran. */
.fb-marque {
  /* LARGEUR DU LOGO, EN UN SEUL ENDROIT.
     « HEY! Moi c'est » et l'étiquette FABIAN sont deux images distinctes
     dessinées sur des toiles de MÊME hauteur (175 px) : 434 px de large pour la
     première, 294 px pour la seconde. Tant que les deux largeurs restent dans ce
     rapport, les deux dessins sont à la même échelle et leurs lignes de base se
     répondent. Deux `clamp()` indépendants — ce qu'il y avait ici — pouvaient
     dériver l'un de l'autre au premier réglage fait d'un seul côté ; c'est
     arrivé. La seconde largeur se déduit donc de la première. */
  --fb-logo-l: clamp(110px, 15.3vw, 183px);

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1em;
  text-align: center;
}

.fb-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.4rem, 1vw, 0.85rem);
  text-decoration: none;
}

.fb-logo__hey {
  width: var(--fb-logo-l);
  aspect-ratio: 434 / 175;
  background: url("../img/accueil/hey.png") center / contain no-repeat;
}

/* L'étiquette verte animée. Son animation et son `aspect-ratio` viennent de
   _sprites.css (classe .fb-fabian) ; seule la largeur est réglée ici.

   ⚠ `.fb-fabian` sert AUSSI sur l'accueil, où elle n'est pas dans `.fb-marque`
   et n'hérite donc pas de `--fb-logo-l`. Ce n'est pas un oubli : la règle
   `.fb-accueil .fb-fabian` (section 9) est plus spécifique et impose sa propre
   largeur, bien plus grande. Le titre d'accueil et celui de l'en-tête n'ont
   jamais eu la même taille. */
.fb-fabian {
  width: calc(var(--fb-logo-l) * 294 / 434);
  background-repeat: no-repeat;
}

/* UNE VARIANTE TIRÉE AU SORT, TENUE PENDANT LA VISITE
   ---------------------------------------------------
   Sur l'accueil, les 6 variantes de l'étiquette défilent en boucle : c'est
   l'effet « carnet de croquis » voulu. Dans l'en-tête des pages internes, la
   même boucle tourne en permanence dans le coin de l'œil pendant qu'on lit —
   Fabian a demandé de la figer.

   On ne fige pas sur la première : ce serait toujours la même, et les cinq
   autres ne se verraient jamais. Le tirage et sa mémorisation sont faits par le
   script en ligne du <head> (inc/identite.php), qui pose la position de fond
   dans `--fb-fabian-frame` et la classe ci-dessous sur <html>.

   Sans JavaScript, la classe n'apparaît pas et l'animation continue de tourner :
   c'est l'ancien comportement, pas une page cassée. */
.fb-identite-js .fb-logo .fb-fabian {
  animation: none;
  background-position-x: var(--fb-fabian-frame, 0%);
}

/* Bulle réduite deux fois : 92 → 72 → 56 px au plafond. À 92 px elle mesurait
   99 px de haut, soit trois fois la marge dont elle disposait ; à 56 px elle en
   fait 60, et il lui reste largement la place sous le `padding-top` de
   `.fb-entete` — plancher d'écran compris.

   ELLE EST AUSSI RENTRÉE VERS LA GAUCHE ET DESCENDUE. Elle partait du coin haut
   droit exact du logo (`left: 100%` / `bottom: 100%`), c'est-à-dire aussi loin
   que possible de ce qu'elle désigne : sa flèche visait « FABIAN » depuis
   l'autre bout d'un grand vide.

   ⚠ LES DEUX DÉCALAGES SONT PROPORTIONNELS À LA BULLE, PAS EN PIXELS FIXES.
   Écrits en dur (« 40 px à gauche »), ils seraient justes à 1440 px et faux
   partout ailleurs : à 420 px la bulle ne fait plus que 34 px de large, et un
   décalage de 40 px la ferait passer entièrement de l'autre côté de son point
   d'ancrage — elle ne désignerait plus rien. Exprimés en fraction de sa propre
   largeur, ils suivent le `clamp()` : ≈ 40 px de gauche et 5 px de descente au
   plafond, ≈ 24 et 3 au plancher.

   La descente a été ramenée de 20 à 5 px après coup : la bulle tombait juste,
   mais trop bas. Le `bottom` négatif ne la fait toujours pas mordre sur le
   dessin — le PNG « HEY! » a une bande transparente de 52 px sur 175 en haut de
   sa toile, soit ≈ 22 px à l'écran, et la bulle descend dans ce vide, pas sur
   les lettres. Il reste donc ≈ 17 px de marge avant le premier trait. */
.fb-entete .fb-annote__bulle--accueil {
  --fb-bulle-accueil-l: clamp(34px, 4.6vw, 56px);

  width: var(--fb-bulle-accueil-l);
  aspect-ratio: 152 / 164;
  left:   calc(100% - var(--fb-bulle-accueil-l) * 0.72);
  bottom: calc(100% - var(--fb-bulle-accueil-l) * 0.16);
  background-image: url("../img/annotations/accueil.png");
}

/* Fenêtre étroite avec une souris : la bulle bascule de l'autre côté de son
   bouton, au lieu de sortir de l'écran et de provoquer un défilement
   horizontal. (Sur écran tactile elle est déjà retirée, voir section 6.)

   ⚠ Cette règle doit rester APRÈS celle du dessus : elle a la même
   spécificité, c'est donc sa position dans le fichier qui la fait gagner.
   Placée plus haut, elle n'avait aucun effet. */
@media (max-width: 420px) {
  .fb-entete .fb-annote__bulle--accueil {
    left: auto;
    right: calc(100% - var(--fb-bulle-accueil-l) * 0.72);
  }
}

/* LES 4 BASELINES
   ===============
   Les 4 images sont toutes dans le HTML, empilées au même endroit, et on fait
   varier leur opacité. Le bloc garde donc une hauteur fixe : aucun à-coup de
   mise en page quand la phrase change.

   ⚠ UN BUG A VÉCU ICI PENDANT DES SEMAINES, ET IL VAUT D'ÊTRE RACONTÉ.
   La version précédente écrivait `.fb-phrases__item:nth-child(1)` à `(4)`. Or
   `:nth-child` compte TOUS les frères, pas seulement ceux qui correspondent au
   sélecteur — et le premier enfant de `.fb-phrases` est le `<span
   class="screen-reader-text">` qui porte le texte lu par les lecteurs d'écran
   (voir header.php). Tout était décalé d'un cran :

     :nth-child(1) → le texte lecteur d'écran     → règle sans effet
     :nth-child(2) → 1re vignette  → phrase-02      (phrase-01 jamais affichée)
     :nth-child(3) → 2e  vignette  → phrase-03
     :nth-child(4) → 3e  vignette  → phrase-04
     :nth-child(5) → 4e  vignette  → AUCUNE RÈGLE : ni image, ni délai

   La quatrième vignette était donc transparente et prenait le créneau 0 → 6 s :
   c'est la « frame blanche avec rien dedans » signalée par Fabian. Et le repli
   `prefers-reduced-motion` réaffichait `:nth-child(1)`, c'est-à-dire le texte
   lecteur d'écran — donc rien du tout.

   Les index ci-dessous partent maintenant de 2, et l'ordre est explicite. Si on
   ajoute un élément dans `.fb-phrases`, il faut les décaler : c'est le prix de
   `:nth-child`, et c'est pour ça qu'on l'écrit noir sur blanc. */
/* ⚠ `margin: 0` N'EST PAS COSMÉTIQUE, C'EST LA CAUSE DU TROU.
   `.fb-phrases` est un `<p>`, et la feuille de style du navigateur donne à tout
   paragraphe `margin: 1em 0` — 16 px en haut ET en bas. Le thème remet à zéro
   `body`, `img`, `h1`-`h3` et les listes, mais jamais `p`.
   Dans un conteneur `flex`, les marges ne se fusionnent pas avec celles des
   voisins : ces 32 px s'ajoutaient tels quels, ce qui explique qu'on ait resserré
   deux fois `.fb-marque { gap }` (0.6 → 0.35 → 0.1em) sans presque rien voir
   bouger — on rabotait 8 px à côté de 32 px qu'on ne voyait pas. Mesuré sur
   capture : 45 px entre le bas du logo et le haut du slogan, pour un `gap`
   annoncé à 1,6 px. */
.fb-phrases {
  position: relative;
  margin: 0;
  width: clamp(155px, 24vw, 315px);
  aspect-ratio: 742 / 64;
}
.fb-phrases__item {
  position: absolute;
  inset: 0;
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  opacity: 0;
  transition: opacity 400ms ease-out;
}
.fb-phrases__item:nth-child(2) { background-image: url("../img/nav/phrase-01.png"); }
.fb-phrases__item:nth-child(3) { background-image: url("../img/nav/phrase-02.png"); }
.fb-phrases__item:nth-child(4) { background-image: url("../img/nav/phrase-03.png"); }
.fb-phrases__item:nth-child(5) { background-image: url("../img/nav/phrase-04.png"); }

/* Sans JavaScript : la première phrase, affichée en fixe. C'est peu, mais c'est
   juste — mieux qu'un carrousel qui ne s'arrête jamais sur rien. */
.fb-phrases__item:nth-child(2) { opacity: 1; }

/* Avec JavaScript : c'est `data-fb-phrase` sur <html> qui décide, posé par le
   script de inc/identite.php. La phrase est tirée au sort, tenue pendant toute
   la navigation, et ne change qu'au bout d'une minute ou à un rafraîchissement.

   L'ancienne version changeait toutes les 6 secondes — on n'avait pas le temps
   de lire la phrase avant qu'elle ne parte. */
.fb-identite-js .fb-phrases__item { opacity: 0; }
.fb-identite-js[data-fb-phrase="0"] .fb-phrases__item:nth-child(2),
.fb-identite-js[data-fb-phrase="1"] .fb-phrases__item:nth-child(3),
.fb-identite-js[data-fb-phrase="2"] .fb-phrases__item:nth-child(4),
.fb-identite-js[data-fb-phrase="3"] .fb-phrases__item:nth-child(5) { opacity: 1; }

/* Navigation principale.
   Écarts resserrés deux fois (gap 1.1 → 0.6 → 0.4rem, marge haute 1.5 → 0.8 →
   0.35rem) : les quatre onglets dessinés se lisaient comme quatre boutons
   indépendants plutôt que comme une barre, et la barre elle-même flottait loin
   sous le slogan. Le `flex-wrap` reste : sur écran étroit, mieux vaut deux
   lignes qu'un débordement.

   ⚠ Le `gap` a un plancher qu'on ne descend pas : les onglets sont des pilules
   à contour noir dessinées à la main, dont le tracé déborde légèrement de la
   boîte. Collés, leurs contours se touchent et on lit une seule forme. */
/* L'ÉTOILE EST SORTIE DU FLUX.
   ============================
   C'est ce que montre le croquis de Fabian : elle flotte dans le blanc à gauche,
   à hauteur du slogan, juste au-dessus de la barre de navigation.

          ★    Je raconte des histoires.
     SHOP   PORTFOLIO   À PROPOS   CONTACT

   L'étoile est le bouton « Nouveautés » (`.fb-nav__news`), et il n'existe que si
   une destination a été saisie dans Apparence → Personnaliser : c'est un
   cinquième bouton FACULTATIF dans une barre qui en compte quatre. Tant qu'il
   restait dans le flux, il coûtait quelque chose à tout le monde — sur la même
   ligne il décalait le menu vers la droite, sur une ligne à lui il creusait un
   cran de plus sous le slogan. Dans les deux cas, la mise en page changeait
   selon qu'un réglage d'administration était rempli ou non.

   En `position: absolute`, il ne pèse plus rien : la barre reste centrée sous le
   logo, le menu remonte contre le slogan, et retirer le bouton ne déplace
   strictement aucun pixel.

   ⚠ DEUX CONDITIONS, ET ELLES SONT LIÉES.

   1. `width: fit-content` sur `.fb-nav`. Un conteneur flex est un bloc : il
      occupe toute la largeur disponible, et `left: 0` viserait alors le bord de
      la page, pas celui de « SHOP ». En le faisant se rétracter sur son contenu,
      son bord gauche DEVIENT celui de la première pilule.

   2. Le slogan ne doit pas venir jusque-là. Les deux sont centrés et l'étoile
      est à ~68 px du bord gauche du menu : à 1440 px, le menu commence à 185 px
      et le slogan à 292 px — 40 px de marge. Fabian le confirme, le slogan ne
      grandira pas. C'est une superposition assumée, pas un calcul qui tient tout
      seul : si une phrase plus longue arrivait un jour, c'est ici qu'il faudrait
      revenir. */
.fb-nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  width: fit-content;

  /* Rendue à l'œil, mais réglée à la mesure : l'écart visible entre le bas du
     logo et le haut du slogan est de 24 px à 1440 px, on vise le même en
     dessous. La marge n'en fait pas 24 pour autant — les deux dessins ont du
     vide dans leur PNG (6 px sous le slogan, ~1 px sur le contour des pilules)
     et il compte lui aussi. C'est l'encre qui doit être à égale distance, pas
     les boîtes. */
  margin: clamp(0.55rem, 1.2vw, 1rem) auto 0;

  /* Nommé plutôt qu'écrit deux fois : le bouton « Retour » se pose à cet écart
     du bord gauche de la barre (section 12), et un menu dont les pilules sont
     serrées mais dont le retour flotte au loin se verrait tout de suite.

     ⚠ LA DÉCLARATION A DÉMÉNAGÉ SUR `.fb-nav-zone`, JUSTE AU-DESSUS.
     Elle vivait ici. Depuis que le bouton « Retour » est sorti du <nav> (voir
     header.php, 20 octobre 2026), il n'est plus un descendant de `.fb-nav` :
     une variable déclarée ici ne lui parvient plus, et son `right: calc(100% +
     var(--fb-nav-ecart) * 2)` serait tombé sur une valeur invalide, donc sur
     `right: auto` — le bouton revenu se coller au bord droit du menu, sans
     message d'erreur. Déclarée sur la zone, elle descend aux DEUX. */
  gap: var(--fb-nav-ecart);
}

/* LA BOÎTE QUI N'EN EST PAS UNE — `display: contents`.
   `.fb-nav__liste` enveloppe les pilules dans le HTML, et sur grand écran le
   navigateur la TRAVERSE : elle ne génère aucune boîte, ses enfants restent les
   enfants directs du <nav>, et la barre est celle d'avant au pixel près. Elle
   n'existe que pour le panneau sur téléphone, qui a besoin de deux éléments
   pour s'animer (voir « LA BARRE DEVIENT UN PANNEAU », plus bas).
   ⚠ C'est un <div> sans rôle : `display: contents` ne lui retire aucune
   sémantique, puisqu'il n'en portait pas. Sur un <ul> ou un <nav>, la même
   déclaration retirerait l'élément de l'arbre d'accessibilité. */
.fb-nav__liste { display: contents; }

.fb-nav__item {
  height: var(--fb-nav-h);
  aspect-ratio: 372 / 97;
}

/* LA RANGÉE DU BOUTON MENU — INEXISTANTE SUR GRAND ÉCRAN, ET AU SENS PROPRE.
   `display: contents` fait traverser la boîte par le navigateur : ses deux
   enfants (le bouton et la pastille de la page en cours) redeviennent des
   enfants directs de `.fb-nav-zone`, et comme ils sont tous les deux en
   `display: none` au-dessus de 700 px, il ne reste RIEN — pas même une boîte
   vide qui porterait sa marge. Avec un `display: none` sur la rangée elle-même
   on obtiendrait le même rendu, mais on perdrait la raison : ce n'est pas la
   rangée qu'on cache, ce sont ses deux occupants qui n'ont pas lieu d'être. */
.fb-nav-barre { display: contents; }

/* ⚠ ELLE SUIT `.fb-img` DANS LE FICHIER, ET C'EST OBLIGATOIRE.
   `.fb-img` déclare `display: inline-block` et pèse exactement le même poids
   (0,1,0) : c'est l'ordre d'écriture qui départage. La même précaution que
   pour `.fb-burger`, section 3, et pour la même raison. */
.fb-nav__ici { display: none; }

.fb-nav__news {
  position: absolute;
  left: 0;                          /* le bord gauche de « SHOP » */
  bottom: calc(100% + clamp(0.1rem, 0.3vw, 0.25rem));
  height: var(--fb-nav-h);
  aspect-ratio: 164 / 82;
}

/* Écran étroit : l'étoile revient dans le flux, en début de ligne. Le menu y
   occupe toute la largeur disponible — le blanc à gauche où elle se posait
   n'existe plus, et elle viendrait chevaucher le slogan.

   ⚠ `relative`, SURTOUT PAS `static`, ET ÇA S'EST VU À L'ÉCRAN.
   Le bouton est un `.fb-img` : son dessin n'est pas dans l'élément mais dans ses
   deux pseudo-éléments, en `position: absolute; inset: 0`. Un élément absolu se
   cale sur son premier ancêtre POSITIONNÉ. En `static`, le bouton cesse d'en
   être un et ses deux calques sautent au suivant — c'est-à-dire `.fb-nav`, que
   la règle du dessus vient justement de passer en `relative`. Ils s'étalaient
   alors sur toute la barre, et comme ils sont en `background-size: contain`,
   l'étoile se retrouvait dessinée au MILIEU du menu, par-dessus « PORTFOLIO ».
   `relative` sans décalage occupe la même place que `static` dans le flux, mais
   garde le bouton comme repère pour ses calques.

   `inset: auto` n'est pas décoratif non plus : en `relative`, `left` et `bottom`
   ne placent plus, ils DÉCALENT. Le `bottom: calc(100% + …)` de la règle du
   dessus remonterait l'étoile d'une hauteur de barre au lieu de ne rien faire. */
/* ⚠ LE SEUIL EST PASSÉ DE 560 À 700 PX POUR SUIVRE LE MENU BURGER.
   Il valait 560 : la largeur à laquelle le blanc à gauche de la barre
   disparaissait. Depuis que la barre devient un panneau déroulant sous 700 px
   (voir « L'EN-TÊTE SUR TÉLÉPHONE », plus bas), elle n'a plus de bord gauche du
   tout quand elle est fermée — et une étoile en `absolute` calée sur
   `bottom: 100%` d'un panneau fermé se serait posée au-dessus de lui, c'est-à-
   dire en plein milieu de l'en-tête, visible alors que le menu ne l'est pas.
   Les deux seuils sont liés : les changer séparément fait réapparaître
   l'étoile orpheline. */
@media (max-width: 700px) {
  .fb-nav__news {
    position: relative;
    inset: auto;
  }
}
.fb-nav__news::before { background-image: url("../img/nav/news-base.png"); }
.fb-nav__news::after  { background-image: url("../img/nav/news-survol.png"); }

/* LA SEULE EXCEPTION DU SITE : ICI, LE CALQUE DU REPOS DOIT VRAIMENT PARTIR.
   ----------------------------------------------------------------------------
   Partout ailleurs, le dessin de survol recouvre entièrement celui du repos, ce
   qui permet de laisser le repos opaque et d'éviter l'éclair blanc du fondu
   croisé (voir la section 3). Ces deux dessins-ci sont construits autrement :
   la même pastille verte, PERCÉE une fois d'une étoile et une fois du mot
   NEWS. Les lettres sont donc du transparent, et le vert du repos les
   remplirait — on obtiendrait une pastille avec un trou en forme d'étoile et
   plus de mot du tout. Mesuré : 15 % du dessin de repos n'est pas couvert.

   D'où la coupure NETTE, sans transition, exactement comme les tuiles de
   l'accueil et pour la même raison : un fondu, même de 140 ms, montrerait les
   deux découpes à demi opaques en même temps. Les deux images étant déjà
   chargées, une coupure franche ne clignote pas.

   ⚠ Si Fabian livre un jour ses propres images NEWS (réglages du Customizer),
   elles passeront par ici aussi. Deux dessins pleins s'en accommoderont sans
   qu'on ait rien à changer ; la coupure sera juste un peu plus sèche qu'un
   fondu. */
.fb-nav__news::before,
.fb-nav__news::after { transition: none; }
.fb-nav__news:hover::before,
.fb-nav__news:focus-visible::before { opacity: 0; }

/* Un bloc par entrée : c'est volontairement explicite. Ajouter une entrée de
   menu suppose que Fabian en dessine les 3 états, donc ça ne peut de toute
   façon pas se faire depuis l'administration.

   ⚠ L'image « selection » n'est posée QUE sur .is-current. L'état de repos est
   une pilule à contour noir dont l'intérieur est transparent : si on laissait
   la pastille noire en fond permanent, elle transparaîtrait à travers, et tous
   les onglets auraient l'air sélectionnés. */
.fb-nav__item--shop::before            { background-image: url("../img/nav/shop-base.png"); }
.fb-nav__item--shop::after             { background-image: url("../img/nav/shop-survol.png"); }
.fb-nav__item--shop.is-current         { background-image: url("../img/nav/shop-selection.png"); }

.fb-nav__item--portfolio::before       { background-image: url("../img/nav/portfolio-base.png"); }
.fb-nav__item--portfolio::after        { background-image: url("../img/nav/portfolio-survol.png"); }
.fb-nav__item--portfolio.is-current    { background-image: url("../img/nav/portfolio-selection.png"); }

.fb-nav__item--a-propos::before        { background-image: url("../img/nav/a-propos-base.png"); }
.fb-nav__item--a-propos::after         { background-image: url("../img/nav/a-propos-survol.png"); }
.fb-nav__item--a-propos.is-current     { background-image: url("../img/nav/a-propos-selection.png"); }

.fb-nav__item--contact::before         { background-image: url("../img/nav/contact-base.png"); }
.fb-nav__item--contact::after          { background-image: url("../img/nav/contact-survol.png"); }
.fb-nav__item--contact.is-current      { background-image: url("../img/nav/contact-selection.png"); }

/* --------------------------------------------------------------------------
   LE BOUTON MENU — le dessin de Fabian, et un morphing en cinq images
   --------------------------------------------------------------------------
   IL ÉTAIT LE SEUL ÉLÉMENT D'INTERFACE DU SITE À NE PAS ÊTRE UN DESSIN.
   Trois rectangles noirs tracés en CSS, faute de mieux, avec une bascule en
   croix obtenue par deux rotations. Le commentaire qui vivait ici disait :
   « il est fait pour être remplacé », et décrivait ce qu'il faudrait faire le
   jour où le dessin arriverait. Fabian l'a livré le 19 octobre 2026.

   IL N'A PAS LIVRÉ DEUX IMAGES, IL EN A LIVRÉ SEPT.
   Les quatre états qu'on attendait — fermé/ouvert × repos/survol — et TROIS
   images intermédiaires qui font passer les trois traits à la croix : ils se
   resserrent, se confondent en un seul trait, puis s'ouvrent en X. Le bouton
   ne change plus d'état, il se transforme.

   ⚠ LA SÉQUENCE N'EXISTE QUE DANS LE JEU SOMBRE, ET ÇA COMMANDE TOUT LE RESTE.
   Les quatre dessins se répartissent en deux jeux inversés : au repos un carré
   clair à traits noirs, au survol un carré noir à traits blancs. Les trois
   images du milieu sont dans le jeu SOMBRE. Le morphing ne peut donc pas se
   jouer par-dessus l'état de repos sans que le bouton s'inverse le temps de la
   bascule — et c'est très bien ainsi : il s'allume pendant qu'il se transforme,
   puis se rend. Sur un écran tactile, où aucun survol n'arrive jamais, c'est
   même le SEUL moment où le jeu sombre se voit. Ne pas « corriger » cette
   inversion : elle est la seule lecture possible de ce que Fabian a dessiné.

   TROIS CALQUES, DONC, ET UN SEUL EN PLUS DU MOTIF HABITUEL :
     ::before            le repos      — traits, ou croix si le menu est ouvert
     ::after             le survol     — le même, en sombre (jamais vu au doigt)
     .fb-burger__film    la pellicule  — les cinq images sombres, bout à bout

   ⚠ LE FILM PORTE UN `z-index`, ET CE N'EST PAS DE LA PRÉCAUTION.
   `::after` est, dans l'ordre de peinture, le DERNIER enfant de la boîte — donc
   au-dessus d'un `<span>` écrit dans le HTML. Sans `z-index: 1` sur le film,
   survoler le bouton pendant la bascule recouvrirait l'animation par l'image de
   survol : le morphing existerait, et ne se verrait pas. Invisible au doigt,
   visible à la souris dans une fenêtre étroite.

   PAS DE `cursor` ICI : les trois curseurs du site sont déclarés une fois pour
   toutes dans _sprites.css, et un `<button>` y reçoit déjà la main (voir la
   note de la section 3). En écrire un ici rendrait le curseur du système. */
.fb-burger {
  /* Le nombre d'images sert DEUX FOIS — à étaler la pellicule et à découper
     l'animation — et c'est exactement pour ça qu'il est écrit une seule fois.
     Une pellicule de 5 images lue en 4 pas montre 4 dessins sur 5, et le défaut
     ne se voit pas : il se lit comme un morphing un peu sec. */
  --fb-burger-images: 5;
  --fb-burger-l: 46px;

  /* 5 dessins en 0,24 s, soit 48 ms chacun — ~21 images/seconde, le régime des
     séquences de Fabian (le script de build vise 24). Les 140 ms de
     `--fb-transition`, qui règlent tous les autres changements d'état du site,
     auraient donné 28 ms par dessin : on aurait vu passer quelque chose sans
     voir quoi. Au-delà de 0,3 s, à l'inverse, le bouton traînerait derrière le
     panneau.
     ⚠ CE NOMBRE EST RECOPIÉ DANS `--fb-nav-duree` (section menu mobile), ET
     CE N'EST PAS UN HASARD : depuis que le panneau se déroule au lieu de
     s'afficher d'un coup, les deux animations se jouent ensemble et doivent
     finir ensemble. Changer l'une sans l'autre les fait arriver en décalé. */
  --fb-burger-duree: 0.24s;

  /* Masqué partout sauf sur téléphone. La barre de navigation dessinée reste
     la navigation du site : le burger ne la remplace que là où elle ne tient
     pas.
     ⚠ CE `display` DOIT RESTER APRÈS CELUI DE `.fb-img` DANS LE FICHIER. Les
     deux classes pèsent le même poids (0,1,0) ; c'est l'ordre d'écriture qui
     départage, et `.fb-img` est en section 5, bien plus haut. */
  display: none;

  position: absolute;
  top: var(--fb-coin-haut);
  left: var(--fb-gouttiere);
  z-index: 3;
  width: var(--fb-burger-l);
  height: var(--fb-burger-l);
  padding: 0;
  border: 0;
  background: none;
}

/* LES DEUX CALQUES DE REPOS ET DE SURVOL, PILOTÉS PAR `aria-expanded`.
   L'état est lu sur l'attribut que le script doit tenir à jour de toute façon
   pour les lecteurs d'écran — pas sur une classe en plus. Une seule source de
   vérité : si l'attribut est faux, l'apparence l'est aussi, et un oubli côté
   script se voit immédiatement à l'écran au lieu de ne se trahir qu'au lecteur
   d'écran. C'est la règle qui valait déjà pour la bascule en croix tracée en
   CSS ; le dessin a changé, pas le principe. */
.fb-burger::before                       { background-image: url("../img/nav/burger-base.png"); }
.fb-burger::after                        { background-image: url("../img/nav/burger-survol.png"); }
.fb-burger[aria-expanded="true"]::before { background-image: url("../img/nav/burger-fermer.png"); }
.fb-burger[aria-expanded="true"]::after  { background-image: url("../img/nav/burger-fermer-survol.png"); }

/* LA PELLICULE — 5 × 86 px de large, lue derrière une fenêtre de 86.
   Même mécanique que les tuiles de l'accueil : `background-size` en POURCENTAGE
   (5 × 100 %) pour que la pellicule suive la largeur du bouton quelle qu'elle
   soit, et `background-position-x` de 0 à 100 % découpé par `steps()`.
   `jump-none` garantit que les deux extrémités — les trois traits et la croix —
   sont réellement atteintes : sans lui, CSS s'arrête à l'avant-dernière image
   et la croix n'arrive jamais. */
.fb-burger__film {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: url("../img/nav/burger-anim.png");
  background-repeat: no-repeat;
  background-size: calc(var(--fb-burger-images) * 100%) 100%;
  background-position-x: 0%;
  /* Au repos, la pellicule n'est pas là. Elle n'apparaît que pendant la
     bascule, et disparaît en même temps qu'elle finit (voir `…-voile`). */
  opacity: 0;
}

/* ⚠ LA CLASSE `is-anime`, ET POURQUOI ELLE N'EST PAS DÉCORATIVE.
   `aria-expanded="false"` est écrit dans le HTML servi : sans cette classe, la
   règle « fermé → joue la séquence à l'envers » s'appliquerait AU CHARGEMENT de
   chaque page. Le bouton se serait inversé et aurait déroulé une fermeture que
   personne n'a demandée, sur toutes les pages du site. Le script pose
   `is-anime` au premier clic, et plus jamais ne la retire : avant le premier
   geste il n'y a rien à animer, après il y a toujours quelque chose.

   DEUX NOMS DE KEYFRAMES PLUTÔT QU'UN `reverse`. Le sens inverse s'obtiendrait
   avec `animation-direction: reverse`, mais la direction s'appliquerait aussi
   au voile posé dans le même raccourci — qui partirait donc invisible pour
   finir opaque, c'est-à-dire laisserait la pellicule affichée par-dessus le
   dessin. Deux jeux de keyframes coûtent quatre lignes et ne se trompent pas.

   ⚠ LE CHANGEMENT DE NOM EST CE QUI RELANCE L'ANIMATION — ET LE VOILE AUSSI
   DOIT CHANGER DE NOM, ALORS QUE SES DEUX VERSIONS SONT IDENTIQUES.
   C'est la correction d'un défaut mesuré, pas une précaution. Le navigateur
   apparie les animations par RANG dans la liste : rebasculer remplaçait
   `…-ouvre` par `…-ferme` au rang 1, qui repartait donc à zéro, mais laissait
   `fb-burger-voile` au rang 2 — même nom, même rang, donc la MÊME animation,
   déjà terminée et figée par son `forwards`. Le voile restait à 0.

   Relevé en mesure le 20 octobre 2026 : à 60 ms de la fermeture, la pellicule
   était bien en train de défiler (`background-position-x: 75%`) avec une
   `opacity` de 0. Le morphing de fermeture se jouait intégralement, et
   personne ne pouvait le voir. Le bouton passait de la croix aux traits d'un
   coup, exactement comme avant que Fabian dessine la séquence.

   Les deux jeux de keyframes du voile sont donc rigoureusement les mêmes, et
   leurs deux noms ne servent qu'à dire au navigateur « ce n'est plus la même
   animation ». Les fusionner reproduirait le défaut. */
.fb-burger.is-anime[aria-expanded="true"] .fb-burger__film {
  animation:
    fb-burger-ouvre       var(--fb-burger-duree) steps(var(--fb-burger-images), jump-none) forwards,
    fb-burger-voile-ouvre var(--fb-burger-duree) step-end                                  forwards;
}
.fb-burger.is-anime[aria-expanded="false"] .fb-burger__film {
  animation:
    fb-burger-ferme       var(--fb-burger-duree) steps(var(--fb-burger-images), jump-none) forwards,
    fb-burger-voile-ferme var(--fb-burger-duree) step-end                                  forwards;
}

@keyframes fb-burger-ouvre {
  from { background-position-x: 0%; }
  to   { background-position-x: 100%; }
}
@keyframes fb-burger-ferme {
  from { background-position-x: 100%; }
  to   { background-position-x: 0%; }
}

/* LE VOILE — la pellicule est opaque pendant toute la bascule, puis plus du
   tout, dans un sens comme dans l'autre — d'où deux jeux jumeaux, voir la note
   ci-dessus. `step-end` tient la première valeur jusqu'au bout et ne bascule qu'à
   l'instant final ; `forwards` l'y laisse. C'est sous ce voile que `::before`
   échange les traits contre la croix, ce qui rend l'échange invisible.
   ⚠ `opacity` ICI NE PEUT PAS FAIRE DE DÉGÂTS : le film est un élément à part,
   pas le parent des deux autres calques. Le piège inverse — un `opacity` sur un
   élément qui emporte ses propres pseudo-éléments — avait justement fait
   DISPARAÎTRE le burger tracé en CSS, et c'est la raison pour laquelle son
   trait du milieu s'effaçait par sa couleur et non par son opacité.
   Et pour qui demande moins de mouvement, la règle globale de la section 4
   ramène la durée à 0,001 ms : la pellicule est masquée avant d'avoir été vue,
   le bouton change d'image d'un coup, sans clignotement. */
@keyframes fb-burger-voile-ouvre {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes fb-burger-voile-ferme {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* --------------------------------------------------------------------------
   L'EN-TÊTE SUR TÉLÉPHONE — menu déroulant, titre en grand, panier agrandi
   --------------------------------------------------------------------------
   Tout est dans un `max-width: 700px` : au-dessus, l'en-tête est celui de la
   maquette BASE.jpg et rien ici ne l'atteint.

   TROIS DEMANDES DE FABIAN DU 7 OCTOBRE 2026, QUI SE TIENNENT :
     « mettre le menu en burger »
     « afficher hey moi c'est fabian en grand, prendre toute la largeur max »
       et « agrandir le texte en dessous »
     « agrandir le bouton Panier »

   Elles se tiennent parce qu'elles se gênent : le titre agrandi vient occuper
   la largeur où se trouvent le panier et le burger, tous deux en `absolute`
   dans les coins hauts. C'est le `padding-top` qui règle la collision, et il
   est calculé, pas choisi — voir plus bas.
   -------------------------------------------------------------------------- */

@media (max-width: 700px) {

  /* ── LE BOUTON MENU PASSE SOUS LA PHRASE ─────────────────────────────────
     Fabian, 20 octobre 2026 : « passer le menu burger sous la phrase sous le
     titre ». Il était dans le coin haut gauche, en `position: absolute`, face
     au panier ; il est maintenant dans le flux, juste au-dessus du panneau
     qu'il déplie (le déplacement dans l'arbre est dans header.php).

     ⚠ `relative`, SURTOUT PAS `static`, ET ON SAIT DÉJÀ POURQUOI.
     Le bouton est un `.fb-img` : son dessin vit dans ses pseudo-éléments, en
     `position: absolute; inset: 0`, qui se calent sur le premier ancêtre
     POSITIONNÉ. En `static`, ils sauteraient à `.fb-nav-zone` et le burger
     s'étalerait sur toute la largeur du menu. C'est mot pour mot ce qui est
     arrivé à l'étoile NEWS (section 3) le jour où on lui a retiré son
     `position` — la note y est, elle vaut ici aussi.

     `inset: auto` annule les `top`/`left` de la règle de base : en `relative`,
     ils ne placent plus, ils DÉCALENT — le bouton partirait de 28 px vers le
     bas et de 20 vers la droite. */
  .fb-burger {
    display: block;
    position: relative;
    inset: auto;
  }

  .fb-nav__ici { display: inline-block; }

  /* La rangée redevient une vraie boîte. Sa marge haute reprend celle que le
     panneau portait quand il était seul sous le slogan (`.fb-nav.is-ouvert`,
     plus bas) : c'est le même air, rendu au même endroit — simplement, ce
     n'est plus le panneau qui le porte, c'est ce qui le précède. */
  .fb-nav-barre {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(0.4rem, 2vw, 0.7rem);
    margin-top: clamp(0.55rem, 1.2vw, 1rem);
  }

  /* ── LE PANIER, ASSEZ GRAND POUR ÊTRE TOUCHÉ ─────────────────────────────
     Il valait `clamp(29px, 3.75vw, 42px)`, soit 29 px sur un téléphone : le
     plancher du clamp, parce que 3.75vw de 390 ne fait que 14,6. 29 px de
     large pour 27 de haut (rapport 111/102), c'est sous les 44 px que les
     recommandations d'Apple comme de Google donnent pour une cible tactile —
     et ça se sentait, c'est pour ça que Fabian l'a signalé.

     46 px de large donnent 42 px de haut : la cible fait 46 × 42, ce qui est
     la bonne taille une fois compté le vide transparent du PNG autour du
     dessin. Même valeur que le burger, pour que les deux coins pèsent pareil. */
  .fb-panier { width: var(--fb-coin-objet-l); }

  /* ── LE TITRE EN GRAND ───────────────────────────────────────────────────
     `--fb-logo-l` valait `clamp(110px, 15.3vw, 183px)` : 110 px sur un
     téléphone, là encore le plancher.

     LE 50vw N'EST PAS UN CHIFFRE ROND, C'EST UNE CONTRAINTE RÉSOLUE.
     Les deux dessins restent côte à côte (contrairement à l'accueil, où ils
     s'empilent : ici le logo est une marque dans un en-tête, pas la
     composition d'une page d'atterrissage). La paire occupe donc
     `--fb-logo-l` × (1 + 294/434) = 1,677 × --fb-logo-l, plus l'écart de
     `.fb-logo`. Elle doit tenir dans la rangée utile, c'est-à-dire
     100vw − 2 gouttières, et la gouttière vaut 20 px en dessous de 500 px de
     large. En posant --fb-logo-l = k × 100vw :

         1,677 k × 100vw + 6,4 ≤ 100vw − 40
         k ≤ (100vw − 46,4) / (1,677 × 100vw)

     À k = 0,5, l'inégalité tient dès 287 px de large — sous le plus étroit des
     téléphones en circulation. À 0,54 elle lâchait à 360 px, une taille encore
     courante. D'où 50vw, et pas 55.

     Le plafond de 200 px prend le relais à partir de 400 px de large : au-delà,
     le titre n'a plus besoin de grandir, il a besoin de rester un titre.
     C'est tout de même 200 px contre 110 — il double presque. */
  .fb-marque { --fb-logo-l: min(50vw, 200px); }

  /* Le slogan suit : « agrandir le texte en dessous ». Il valait
     `clamp(155px, 24vw, 315px)`, soit 155 px au plancher. 76vw le porte à
     296 px sur un téléphone de 390 — il tient dans les 350 de la rangée utile
     avec 54 px de marge, et il reste sous la largeur du titre au-dessus de
     400 px, ce qui garde la hiérarchie. */
  .fb-phrases { width: min(76vw, 300px); }

  /* ── L'AIR SOUS LES DEUX COINS, CALCULÉ ──────────────────────────────────
     ⚠ SANS CE PADDING, LE TITRE PASSE SOUS LE PANIER.
     Le panier est en `position: absolute` : il ne pousse rien. Tant que le
     titre faisait 110 px de large il passait à côté ; à 334 px (200 × 1,677)
     il va d'un bord à l'autre et le rencontre.

     ⚠ LE BURGER ÉTAIT LE SECOND TERME DE CE CALCUL, IL N'Y EST PLUS.
     Il occupait le coin haut gauche, symétrique du panier, et c'est ce qui
     rendait la réserve nécessaire DES DEUX CÔTÉS. Depuis le 20 octobre 2026
     il est passé sous la phrase, dans le flux. Le calcul ne change pas pour
     autant : il n'a jamais compté qu'une hauteur d'objet, et le panier a
     exactement la même (`--fb-coin-objet-l`, 46 px, posée pour que les deux
     coins pèsent pareil). C'est devenu une réserve pour un seul coin, et le
     titre reste centré sans que rien ne l'écrase.

     Le calcul, au lieu d'un nombre relevé à l'œil :
       --fb-coin-haut        où commencent les deux objets    = 28 px
       --fb-coin-objet-l     leur hauteur de boîte            = 46 px
       0.75rem               l'air entre eux et le titre      = 12 px
                                                              ──────
                                                                86 px
     Chacun des trois termes est déclaré quelque part : si l'un bouge, le
     padding suit tout seul. C'était 48 px (le plancher du clamp d'origine). */
  .fb-entete {
    --fb-coin-objet-l: 46px;
    padding-top: calc(var(--fb-coin-haut) + var(--fb-coin-objet-l) + 0.75rem);
  }

  /* ── LA BARRE DEVIENT UN PANNEAU, ET IL SE DÉPLIE ────────────────────────
     OUVERTE, ELLE EST DANS LE FLUX et pousse le contenu vers le bas, au lieu
     de flotter par-dessus. C'est délibéré : un panneau en `fixed` ou
     `absolute` au-dessus de la page, c'est une boîte invisible de plus à qui
     il faut penser à retirer les clics quand elle est fermée — le défaut qui a
     coûté le plus cher sur ce site (voir la note `pointer-events` du pied
     fixe). Dans le flux, fermé veut dire absent, et il n'y a rien à neutraliser.

     ⚠ ELLE S'OUVRAIT D'UN COUP, ET LE COMMENTAIRE QUI VIVAIT ICI EXPLIQUAIT
     POURQUOI C'ÉTAIT INÉVITABLE. Il disait : « `display` ne s'anime pas,
     l'ouverture est franche ; l'animer supposerait une hauteur connue d'avance,
     que le nombre d'onglets ne garantit pas. » La prémisse est juste — le menu
     a quatre onglets, cinq si Fabian remplit NEWS, et leur hauteur dépend du
     `clamp` des pilules. La conclusion, non : on peut animer une hauteur sans
     la connaître.

     LA GRILLE DE `0fr` À `1fr`, ET POURQUOI ELLE RÉPOND EXACTEMENT À ÇA.
     `fr` est une part de l'espace libre, et l'espace libre d'une grille dont la
     hauteur n'est pas fixée vaut… la hauteur de son contenu. Une rangée à `1fr`
     fait donc la taille de ce qu'elle contient, quelle qu'elle soit, et une
     rangée à `0fr` fait zéro. Entre les deux, `grid-template-rows` s'interpole
     comme n'importe quelle longueur : le navigateur mesure le contenu pour
     nous, à chaque image. Rien n'est écrit en dur, rien n'est à tenir à jour si
     un onglet s'ajoute.

     Il faut DEUX éléments pour ça : la grille qui se déplie (`.fb-nav`) et
     l'élément rogné pendant qu'elle le fait (`.fb-nav__liste`, en
     `overflow: hidden`). D'où le <div> ajouté dans header.php — invisible sur
     grand écran, où il est en `display: contents`.

     ⚠ `min-height: 0` SUR LA LISTE N'EST PAS DÉCORATIF. La taille minimale par
     défaut d'un élément de grille est `auto`, c'est-à-dire la hauteur de son
     contenu : sans cette ligne, la rangée refuserait de descendre sous les
     ~240 px des pilules et il ne se passerait rien du tout.

     ⚠ ET `visibility` FAIT CE QUE `display: none` FAISAIT AVANT.
     C'était le vrai service rendu par `display: none` : retirer les liens du
     parcours clavier. Un menu invisible mais tabulable est pire qu'un menu
     absent, on tabule dans le vide sans savoir où on est. Une rangée de zéro
     pixel en `overflow: hidden` ne suffit PAS — les liens y restent
     focalisables, c'est le défaut classique de ce motif. `visibility: hidden`
     les retire ; il ne s'interpole pas, mais il se RETARDE, et c'est tout ce
     qu'on demande : visible immédiatement à l'ouverture, caché seulement une
     fois le panneau replié.

     ⚠ `width: fit-content` est HÉRITÉ de la règle de base et doit le rester :
     c'est lui qui fait que `margin-inline: auto` centre le panneau. */
  .fb-nav {
    /* Même durée que le morphing du bouton (`--fb-burger-duree`), et pour une
       raison : les deux gestes sont LE MÊME geste. Le bouton qui finit de se
       transformer pendant que le panneau finit de se déplier se lit comme une
       seule animation ; un décalage, même de 100 ms, se lit comme deux. */
    --fb-nav-duree: 0.24s;

    display: grid;
    grid-template-rows: 0fr;
    margin-inline: auto;

    /* ⚠ `align-items` DOIT REVENIR À `stretch`, ET LE DÉFAUT NE SUFFIT PAS.
       La règle de base déclare `align-items: center` pour la BARRE, où elle
       aligne les pilules entre elles. En grille, elle ferait tout autre chose :
       l'élément prendrait la hauteur de son contenu au lieu de celle de la
       rangée, et se centrerait dans une rangée de zéro pixel — les pilules
       déborderaient de part et d'autre, hors de toute boîte qui les rogne.
       Le dépliage ne se verrait pas : on verrait un menu qui ne bouge pas. */
    align-items: stretch;

    /* L'AIR SOUS LE SLOGAN FAIT PARTIE DE L'ANIMATION, SANS QUOI ELLE BOITE.
       La règle de base pose `margin-top: clamp(0.55rem, 1.2vw, 1rem)`. Tant que
       le panneau était en `display: none`, il n'avait pas de boîte, donc pas de
       marge : elle apparaissait avec lui. Maintenant qu'il existe en permanence,
       cette marge serait là même replié — une dizaine de pixels de vide sous le
       slogan sur toutes les pages — et surtout elle ne s'animerait pas : le
       contenu sauterait de 9 px avant de se mettre à glisser. Elle est donc
       remise à zéro au repos, rendue à l'ouverture, et transportée avec le
       reste. La valeur est répétée telle quelle depuis la règle de base : si
       l'une change, l'autre doit suivre. */
    margin-top: 0;

    transition:
      grid-template-rows var(--fb-nav-duree) ease-out,
      margin-top         var(--fb-nav-duree) ease-out;
  }

  .fb-nav.is-ouvert {
    grid-template-rows: 1fr;
    margin-top: clamp(0.55rem, 1.2vw, 1rem);
  }

  .fb-nav__liste {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(0.3rem, 1.6vw, 0.6rem);

    min-height: 0;
    overflow: hidden;

    /* Le fondu accompagne le dépliage au lieu de le doubler : les pilules
       seraient sinon pleinement opaques alors qu'on n'en voit encore qu'un
       tiers, ce qui donne l'impression d'un rideau qu'on tire sur une image
       déjà là. Elles arrivent avec la place qu'on leur fait. */
    opacity: 0;

    /* ⚠ LES DEUX `transition-delay` SONT L'ESSENTIEL DE CETTE RÈGLE.
       À la FERMETURE (cet état), `visibility` attend la fin du repli avant de
       passer à `hidden` : sans ce retard, les pilules disparaîtraient
       instantanément et le panneau se refermerait sur du vide. À l'OUVERTURE,
       le retard retombe à zéro — voir la règle suivante. */
    visibility: hidden;
    transition:
      opacity    var(--fb-nav-duree) ease-out,
      visibility 0s linear var(--fb-nav-duree);
  }

  .fb-nav.is-ouvert .fb-nav__liste {
    opacity: 1;
    visibility: visible;
    transition:
      opacity    var(--fb-nav-duree) ease-out,
      visibility 0s linear 0s;
  }

  /* Les pilules grandissent avec le panneau : empilées, elles ont toute la
     largeur pour elles, et 25 px de haut (le plancher de `--fb-nav-h`) faisait
     des cibles de 96 × 25 dans un menu qu'on ouvre justement au doigt.
     40 px de haut donnent 153 px de large — au-dessus des 44 px recommandés
     dans la dimension qui compte, celle qu'on vise.

     ⚠ LA HAUTEUR NE DÉPEND PLUS DE `is-ouvert`, ET C'EST UNE CORRECTION.
     La règle visait `.fb-nav.is-ouvert .fb-nav__item` tant que le panneau
     n'existait pas en dehors de son ouverture. Depuis qu'il se replie au lieu
     de disparaître, la classe tombe AU DÉBUT du repli : les pilules
     repassaient de 40 à 25 px de haut — et de 153 à 96 px de large — d'un coup,
     pendant qu'elles étaient encore à 58 % d'opacité. Mesuré, et visible : une
     secousse au premier tiers de chaque fermeture.
     Sous 700 px, ces pilules ne sont jamais ailleurs que dans le panneau ; la
     condition n'apportait rien et coûtait ça. */
  .fb-nav__item,
  .fb-nav__news {
    height: 40px;

    /* ⚠ `flex-shrink: 0` N'EST PAS UNE PRÉCAUTION, C'EST LA CORRECTION D'UN
       DÉFAUT MESURÉ. Dans une colonne flex, `flex-shrink` joue sur la HAUTEUR,
       et la hauteur du conteneur est ici pilotée de l'extérieur par la rangée
       de grille : à `0fr`, elle vaut zéro. Avec la valeur par défaut (1), les
       cinq pilules se comprimaient pour tenir dedans — relevé 10,7 px au lieu
       de 40 à mi-parcours, c'est-à-dire cinq dessins de Fabian écrasés au quart
       de leur hauteur pendant toute l'animation, et pas un seul instant la
       bonne proportion. À 0, elles gardent leurs 40 px et c'est l'`overflow:
       hidden` du conteneur qui les découpe : le panneau se déroule au lieu de
       se tasser. */
    flex-shrink: 0;
  }
}

/* ==========================================================================
   8. PIED DE PAGE
   --------------------------------------------------------------------------
   ⚠ Les assets de la bande sont BLANCS sur fond transparent : ils sont
   invisibles hors du bandeau noir.

   TAILLES — d'où viennent les nombres.
   Elles ne sont pas choisies à l'œil : elles sont relevées dans BASE.jpg, la
   maquette de Fabian, et exprimées en part de la rangée de contenu.

   Dans la maquette (3307 px de large, rangée utile de 3177 px) :
     newsletter  823 px = 25.9 %      téléphone  337 px = 10.6 %
     e-mail      473 px = 14.9 %      nickel     181 px =  5.7 %
     icône réseau 72 px =  2.3 %      me soutenir 349 px = 11.0 %

   Notre rangée fait au plus 1144 px (--fb-largeur 1240 moins deux gouttières
   de 48). Les maxima ci-dessous sont ces pourcentages appliqués à 1144.

   Les valeurs précédentes étaient toutes 30 à 50 % au-dessus : la bande prenait
   132 px de haut pour un texte de 32 px. C'est ce qui donnait au pied de page
   son air massif — la composition interne était juste, l'échelle ne l'était pas.

   ⚠ LE PIED EST MAINTENANT FIXE, ET IL EST DONC BUDGÉTÉ.
   Fabian le veut visible en permanence, la bande noire ne dépassant pas 50 à
   70 px. Ce n'est plus un réglage esthétique : chaque pixel du pied est un pixel
   retiré à la lecture, sur toutes les pages et pour toute la durée de la visite.
   Le budget se répartit ainsi, à la largeur maximale (1144 px de rangée) :

     déchirure       26 px   (--fb-dechirure-h, sur :root)
     bouton le + haut 24 px   (newsletter, 18,5 px de lettrage × 71/55)
     air bas          11 px
     ─────────────────────
     total          ≈ 61 px

   Le deuxième poste a fait l'aller-retour : 17 px à l'origine, doublé le
   9 septembre 2026 (35 px), ramené à × 1,75 le 17 (30 px), puis calé le 23 sur
   la taille du lettrage des FILTRES (24 px) — la référence que Fabian a donnée
   cette fois. Voir la note sur `--fb-pied-mot-h` plus bas.

   Agrandir l'un de ces nombres, c'est sortir du budget. Le plus piégeux est
   le deuxième : c'est le PLUS HAUT des éléments de `.fb-pied__inner` qui dicte
   la hauteur de la bande, pas la moyenne. Élargir le seul bouton newsletter
   suffit à faire grossir tout le pied de page.
   ========================================================================== */

/* POSITION FIXE — voir aussi la règle `body` de la section 2, qui réserve la
   place, et assets/js/interface.js, qui mesure la hauteur réelle.

   `z-index: 20` : au-dessus du contenu, mais volontairement bas. Les bulles
   d'annotation (`.fb-annote__bulle`) sortent du pied par le haut et doivent
   rester lisibles — elles héritent de ce contexte d'empilement. */
.fb-pied {
  position: fixed;
  inset: auto 0 0;
  z-index: 20;

  /* ⚠ LE PIED ENTIER LAISSE PASSER LA SOURIS — c'est la correction du 11 août
     2026, et elle vaut d'être expliquée parce que le symptôme ne désignait pas
     sa cause.

     `.fb-pied__clair` portait déjà `pointer-events: none` (voir plus bas), et
     ça ne suffisait pas : ce qui interceptait les clics n'était pas le bloc
     clair, c'était SON PARENT. `.fb-pied` est en `position: fixed` sur toute la
     largeur, et sa boîte couvre les 150 derniers pixels de la fenêtre. Sans
     fond, on ne la voit pas ; mais une boîte invisible reste une boîte, et le
     navigateur lui donne le clic dès que l'enfant visé s'en désintéresse.

     Le résultat, à l'écran : dans la bande basse de la page, les liens ne
     répondaient plus et le curseur restait le crayon au lieu de passer à la
     main — puisque la souris était en réalité sur le <footer>, pas sur le lien
     qu'elle survolait visiblement. Un seul défaut, deux symptômes qui n'avaient
     pas l'air liés.

     On rend donc le pied traversant PAR DÉFAUT, et on rallume la souris
     seulement sur ce qui est vraiment dessiné : la bande noire (opaque, elle
     cache ce qui passe dessous) et les enfants directs du bloc clair. */
  pointer-events: none;

  /* PLUS DE FOND ICI — ET C'EST UN CHANGEMENT DE PARTI PRIS.
     La version précédente peignait tout le pied en blanc opaque, pour que rien
     ne défile derrière. Fabian veut l'inverse : le bloc clair doit laisser voir
     la page, ses icônes flottant dans les marges. Le fond est donc retiré.

     ⚠ Une seule zone doit rester bouchée, et elle n'est pas évidente :
     `ligne-noire.png` n'est noire que SOUS le trait dessiné — au-dessus, elle
     est transparente. Sans rien derrière, ce trou laisse passer le contenu au
     ras de la bande, ce qui donne des bouts de vignettes hachés par le tracé.
     Le blanc est donc reposé sur la seule déchirure, plus bas. */

  /* `--fb-dechirure-h` était déclarée ici : elle est maintenant sur `:root`,
     parce que l'accueil s'en sert pour se dimensionner. Voir la section 1. */

  /* L'air du pied de page, déclaré UNE fois et posé en `padding-block` sur les
     deux zones. Une seule valeur, donc le haut et le bas sont égaux par
     construction et pas par vigilance : on ne peut plus en modifier un sans
     l'autre. */
  --fb-pied-air: clamp(0.4rem, 1.2vw, 0.7rem);

  /* La bande noire respire moins que le bloc clair, et c'est volontaire : la
     déchirure lui apporte déjà du noir au-dessus du texte. Lui donner le même
     `--fb-pied-air` revenait à payer l'air deux fois. */
  --fb-bande-air: clamp(0.45rem, 1vw, 0.7rem);
}

/* Bloc clair au-dessus de la bande : réseaux sociaux et « Me soutenir ».

   IL ÉTAIT EN COLONNE, IL EST MAINTENANT EN RANGÉE. Empilés, les deux blocs
   prenaient une centaine de pixels — soutenable quand le pied défilait avec la
   page, hors de question quand il occupe l'écran en permanence. Côte à côte
   avec `space-between`, ils tiennent sur une ligne de la hauteur du plus haut.

   IL EST AUSSI PASSÉ PLEINE LARGEUR. Il portait `.fb-conteneur`, donc les mêmes
   1240 px que le corps de la page : les réseaux se calaient sur le bord gauche
   du texte et « Me soutenir » sur son bord droit, ce qui les faisait lire comme
   deux éléments DU contenu. Aux quatre coins de l'écran, ils redeviennent ce
   qu'ils sont — de la signalétique posée sur la page.

   ET IL N'A PLUS DE FOND. Le contenu défile derrière, c'est voulu (voir
   `.fb-pied`). Conséquence à assumer : les icônes se posent sur ce qui passe.
   Sur écran large elles tombent dans les marges, à gauche et à droite de la
   colonne de texte, et rien ne se gêne. Sous 700 px cette marge n'existe plus —
   la bascule est en bas de section. */
.fb-pied__clair {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  align-items: left;
  justify-content: space-between;
  gap: clamp(0.5rem, 1.6vw, 1rem);
  padding: var(--fb-pied-air) var(--fb-gouttiere);

  /* Le bloc occupe toute la largeur mais n'est plein qu'à ses deux extrémités :
     entre les deux, c'est une bande transparente d'un bon millier de pixels
     posée par-dessus la page. Sans ceci, elle intercepterait les clics et la
     sélection de texte sur toute la largeur de l'écran — on ne pourrait plus
     ni cliquer un lien ni sélectionner une phrase à cette hauteur, sans que
     rien à l'écran n'explique pourquoi. Les enfants les reprennent.

     Redondant depuis que `.fb-pied` est traversant, et gardé quand même : cette
     règle dit ce que fait CE bloc, indépendamment de son parent. Si le pied
     cessait un jour d'être en `position: fixed`, elle resterait juste. */
  pointer-events: none;
}

/* ON NE RALLUME LA SOURIS QUE SUR LES LIENS EUX-MÊMES.
   La règle était `.fb-pied__clair > *`, et elle annulait tout le bénéfice de la
   précédente. Le bloc clair est une COLONNE (`flex-direction: column`) : ses
   enfants directs — la liste des réseaux, le bouton « Me soutenir » — s'étirent
   donc sur toute la largeur de l'écran, même si le dessin qu'ils contiennent
   fait 250 px. Rallumer la souris sur eux revenait à reposer, à dix pixels
   près, exactement la barrière qu'on venait d'enlever.

   Ça ne se voyait pas en lisant la règle : `> *` a l'air d'être une précaution
   (« seulement les enfants directs »), alors que le problème n'était pas la
   profondeur mais la LARGEUR. La vérification qui l'a montré tient en une
   ligne — demander au navigateur ce qu'il y a sous le milieu de la bande, et
   constater qu'il répondait « la liste des réseaux » au lieu du texte de la
   page.

   Les <ul>, <li> et autres enveloppes restent donc traversants. Les bulles
   d'annotation continuent de s'afficher : `:hover` remonte aux ancêtres
   indépendamment de `pointer-events`, c'est le survol du lien qui les
   déclenche. */
.fb-pied__clair a,
.fb-pied__clair button { pointer-events: auto; }

/* La bande noire, elle, est opaque : elle cache ce qui défile dessous, donc
   c'est bien elle qu'on vise quand on clique dedans. Rien à laisser passer.

   La déchirure est incluse alors qu'elle est à moitié transparente — au-dessus
   du trait, on voit la page. C'est un arbitrage entre deux petits défauts sur
   26 px de haut : la laisser traversante permettrait de cliquer, à travers
   elle, des choses à moitié recouvertes par un trait noir ; la bloquer rend
   inertes 26 px où l'on distingue encore le contenu. On choisit le second, qui
   se comprend — « le pied de page commence ici » — plutôt que le premier, qui
   donne des clics au hasard sur des liens coupés en deux. */
.fb-pied__bande,
.fb-pied__dechirure { pointer-events: auto; }

/* Le bord déchiré. C'est une image de 3307 px de large : en `background-size:
   100% 100%` elle s'étire sur toute largeur d'écran, et comme c'est un trait
   dessiné à la main, l'étirement ne se voit pas.

   La hauteur est déclarée en variable sur `.fb-pied` (plus haut) parce qu'elle
   ne sert pas qu'ici : la bulle « nickel. » doit remonter au-dessus d'elle pour
   rester visible. Voir `.fb-annote__bulle--nickel`. */
.fb-pied__dechirure {
  height: var(--fb-dechirure-h);

  /* Deux couches. Le dessin, puis du BLANC derrière lui — c'est le bouchon dont
     parle `.fb-pied` : au-dessus du trait, l'image est transparente, et depuis
     que le pied n'a plus de fond, ce qui défile passerait dans l'ouverture. */
  background:
    url("../img/pied/ligne-noire.png") bottom center / 100% 100% no-repeat;

  /* La bande remonte de 10 px et recouvre le bas du dessin : le bord déchiré
     mord dans le noir au lieu d'être posé dessus. Voir la variable sur :root. */
  margin-bottom: calc(-1 * var(--fb-dechirure-enfoncement));
}

/* PLUS DE `padding-top` DU TOUT — c'est la dernière étape d'un raisonnement
   entamé plus haut.
   La déchirure est noire sur toute sa hauteur sauf le trait dessiné, tout en
   haut de l'image. Elle ne sépare donc pas la bande du reste : elle EN FAIT
   PARTIE. Le noir au-dessus des boutons valait déjà `déchirure + air`, soit à
   peu près le double de l'air du bas — la bande paraissait décentrée vers le
   bas alors que ses deux `padding` étaient égaux.
   En retirant l'air du haut, ce que voit l'œil s'équilibre : `déchirure` en
   haut, `--fb-bande-air` en bas. Et on récupère une dizaine de pixels sur un
   budget de 65.

   Les gouttières latérales sont ici, et pas sur `.fb-pied__inner` : la rangée
   ne porte plus `.fb-conteneur` (voir footer.php), elle occupe toute la largeur
   de l'écran, et c'est le noir qui doit la border. */
.fb-pied__bande {
  background: var(--fb-noir);
  color: var(--fb-blanc);
  padding: 0 var(--fb-gouttiere) var(--fb-bande-air);
}

/* PLEINE LARGEUR, comme le bloc clair. Les quatre éléments (newsletter, e-mail,
   téléphone, nickel.) étaient contenus dans les 1240 px du corps de texte : sur
   un grand écran, la bande noire s'étendait d'un bord à l'autre mais son contenu
   restait groupé au milieu, avec deux grands vides noirs de chaque côté.
   Étalés d'un bord à l'autre, ils occupent la bande qu'ils habitent. */
.fb-pied__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(0.75rem, 2vw, 1.75rem);
}

/* Le bouton newsletter ouvre maintenant une modale (section 13) : ce n'est plus
   un <details> qui se déplie sur place. Il ne fait donc plus varier la hauteur
   de la bande, et les rapports mesurés dans BASE.jpg tiennent quel que soit
   l'état de la page. */
/* ⚠ CE BOUTON EST LE PLUS HAUT DE LA BANDE : c'est lui qui en fixe la hauteur.
   Les quatre largeurs ci-dessous ont été réduites d'environ 30 % pour tenir le
   budget de 65 px décrit en tête de section. Les rapports entre elles sont ceux
   relevés dans BASE.jpg et n'ont pas changé. */
/* LES TROIS MOTS SE MESURENT EN HAUTEUR DE DESSIN, PLUS EN LARGEUR DE BOÎTE.
   Fabian (9 septembre 2026) : « doubler la taille des textes du pied, en
   faisant attention à ce que tous soient égaux en hauteur, en comptant les
   espaces vides ». La seconde moitié de la phrase est la vraie consigne : les
   trois PNG ont des marges transparentes DIFFÉRENTES (newsletter 5 px sur 71,
   e-mail 8 sur 64, téléphone 5 sur 63 — mesurées par `-trim`), et en posant
   des largeurs, comme avant, on obtenait trois lettrages de 13, 11 et 13 px
   de haut. Presque égaux, donc visiblement faux. (L'e-mail portait en plus un
   rapport 495/64 alors que le fichier fait 551 × 64 : `contain` le réduisait
   en silence.)

   On fixe donc une seule hauteur de LETTRAGE, `--fb-pied-mot-h`, et chaque
   boîte se calcule en remontant de la hauteur dessinée à la hauteur du fichier
   (× hauteur du fichier ÷ hauteur du trait). La largeur suit par aspect-ratio.
   Résultat : trois mots dont les lettres font la même hauteur au pixel près,
   quelle que soit la façon dont Fabian a cadré ses exports.

   ⚠ Les trois rapports « fichier ÷ trait » sont RELEVÉS sur les fichiers
   livrés. Si Fabian relivre un des trois dessins, les remesurer :
   `magick X.png -trim -format '%h' info:` donne la hauteur du trait.

   L'ÉCHELLE, ET COMMENT ELLE SE CALCULE MAINTENANT.
   ~13,5 px de trait à l'origine, doublés à 27 le 9 septembre 2026, ramenés à
   × 1,75 (≈ 23,5 px) le 17 — « un entre-deux » — puis jugés « trop grands »
   le 23. Fabian a cette fois donné une RÉFÉRENCE au lieu d'un facteur : « à
   peu près la même taille que le texte des filtres du portfolio ou du shop ».
   C'est une bien meilleure consigne, parce qu'elle se vérifie.

   Le calcul, à refaire si l'un des deux bouge :
     • un filtre fait `clamp(22px, 2.6vw, 34px)` de HAUTEUR DE BOÎTE
       (`.fb-filtre`, section « composants partagés ») ;
     • dans ces boîtes, le lettrage occupe 43 px sur 78 — mesuré au `-trim` sur
       portfolio-illustration-base.png, qui est en capitales sans accent ;
     • donc le trait des filtres fait 34 × 43/78 ≈ 18,7 px au plafond et
       22 × 43/78 ≈ 12,1 px au plancher.
   D'où le clamp ci-dessous, et le 1,45vw qui rejoint les deux bornes à la même
   largeur de fenêtre que celui des filtres (~1275 px).

   La variable est posée sur la BANDE et non sur `.fb-img` : la signature
   « nickel. » (un SVG, pas un `.fb-img`) doit avoir la même hauteur que les
   trois mots — elle la lit ici.

   `width: auto` : c'est `height` + `aspect-ratio` qui portent la taille ici. */
.fb-pied__bande { --fb-pied-mot-h: clamp(12px, 1.45vw, 18.5px); }
.fb-pied__bande .fb-img { width: auto; }

.fb-newsletter-lien { height: calc(var(--fb-pied-mot-h) * 71 / 55); aspect-ratio: 861 / 71; }
.fb-newsletter-lien::before { background-image: url("../img/pied/newsletter-base.png"); }
.fb-newsletter-lien::after  { background-image: url("../img/pied/newsletter-survol.png"); }

.fb-pied__email { height: calc(var(--fb-pied-mot-h) * 64 / 50); aspect-ratio: 551 / 64; }
.fb-pied__email::before { background-image: url("../img/pied/email-base.png"); }
.fb-pied__email::after  { background-image: url("../img/pied/email-survol.png"); }

.fb-pied__tel { height: calc(var(--fb-pied-mot-h) * 63 / 52); aspect-ratio: 357 / 63; }
.fb-pied__tel::before { background-image: url("../img/pied/telephone-base.png"); }
.fb-pied__tel::after  { background-image: url("../img/pied/telephone-survol.png"); }

/* --------------------------------------------------------------------------
   Signature de l'agence + sa bulle.
   -------------------------------------------------------------------------- */

.fb-nickel { flex: 0 0 auto; }

.fb-nickel__lien { display: block; line-height: 0; }

/* Trois classes et une balise, à dessein : sur les pages de la boutique,
   WooCommerce écrit `.woocommerce-page img { height: auto }` (deux classes et
   une balise) et effaçait cette hauteur — le logo disparaissait du pied de
   la boutique, et de là seulement. */
.fb-pied .fb-nickel img.fb-nickel__logo {
  /* Même hauteur de lettrage que les trois mots de la bande (Fabian, 17
     septembre 2026). Le SVG (101.95 × 25.56) n'a pas de marge : ses lettres
     vont d'un bord à l'autre de la boîte, la hauteur de boîte EST celle du
     trait. La largeur suit le rapport du fichier. */
  height: var(--fb-pied-mot-h, 20px);
  width: auto;
  /* Le SVG porte sa propre couleur (#f386aa). --fb-rose n'existe donc plus que
     comme note : les deux roses sont à un cheveu l'un de l'autre, mais c'est le
     fichier qui décide, pas la feuille de style. */
}

/* La bulle de la maquette (nickel-bulle.png, en haut à droite du bandeau dans
   BASE.jpg). Elle est dans les assets depuis le début et n'était pas posée.

   POURQUOI CELLE-CI EST INTERACTIVE, ALORS QUE LES AUTRES ANNOTATIONS NE LE
   SONT PAS : elle peut contenir un lien écrit par Fabian. Un lien qu'on voit et
   qu'on ne peut pas cliquer serait pire que pas de lien du tout.

   Mais `pointer-events: auto` en permanence poserait un vrai problème : la
   bulle occupe sa place même invisible, soit ~265 px de haut au-dessus de la
   signature. Elle intercepterait les clics dans le vide, et surtout la survoler
   suffirait à déclencher `:hover` sur son parent — la bulle apparaîtrait quand
   la souris passe dans une zone vide bien au-dessus du logo. On ne l'active
   donc qu'une fois qu'elle est déjà ouverte. Comme elle est collée au logo
   (`bottom: 100%`, aucun interstice) et qu'elle en est un descendant, le survol
   ne se perd pas en chemin. */
.fb-annote__bulle--nickel {
  --fb-bulle-l: clamp(150px, 13vw, 185px);

  /* La queue du dessin n'est pas centrée : sa pointe est à 71.5 % de la largeur
     (mesuré dans le PNG). On décale donc la bulle de ces 71.5 % pour que la
     pointe tombe sur le milieu du logo, comme dans la maquette. */
  left: calc(50% - var(--fb-bulle-l) * 0.715);

  /* Le dégagement n'est pas un choix esthétique. Le dessin est NOIR, et la
     signature est posée sur le bandeau noir : collée au logo, la bulle
     disparaissait purement et simplement — on ne voyait ni sa forme ni sa
     queue, seulement le texte blanc en suspension.

     Elle doit donc remonter au-dessus de tout le noir : la bande elle-même
     (le logo est à peu près centré dans une rangée de ~24 px, d'où le demi-rem)
     puis la déchirure. C'est exactement ce que montre la maquette, où la bulle
     s'arrête une vingtaine de pixels au-dessus du bord déchiré.

     ⚠ CE CALCUL SUIT EXACTEMENT LE NOIR QU'IL Y A AU-DESSUS DU LOGO, terme à
     terme — c'est pour ça qu'il change dès qu'on touche au pied.
     `--fb-bande-air` en a disparu : la bande n'a plus de `padding-top`, le logo
     n'est donc plus poussé vers le bas et il n'y a plus rien à rattraper.
     `--fb-dechirure-enfoncement` s'en retranche : la bande recouvre les 10 px
     bas du dessin, il y a donc 10 px de noir en moins à franchir.
     Si on remet de l'air en haut de la bande ou qu'on change l'enfoncement, ce
     calcul est le premier endroit à rouvrir : trop court, la bulle retombe dans
     le noir et devient invisible (dessin noir sur fond noir). */
  bottom: calc(100% + var(--fb-dechirure-h) - var(--fb-dechirure-enfoncement) + 1.75rem);

  width: var(--fb-bulle-l);
  min-height: calc(var(--fb-bulle-l) * 529 / 369);

  /* `100% 100%` et pas `contain` : si le texte dépasse, la boîte grandit et le
     dessin s'étire avec elle plutôt que de laisser le texte sortir du cadre.
     Sur une forme tracée à la main, un étirement modéré ne se voit pas. */
  background-image: url("../img/pied/nickel-bulle.png");
  background-size: 100% 100%;

  display: flex;
  align-items: center;
  justify-content: center;
  /* Le bas doit dégager la queue, qui occupe les 10 % inférieurs du dessin. */
  padding:
    calc(var(--fb-bulle-l) * 0.09)
    calc(var(--fb-bulle-l) * 0.11)
    calc(var(--fb-bulle-l) * 0.20);
}

.fb-nickel:hover .fb-annote__bulle--nickel,
.fb-nickel:focus-within .fb-annote__bulle--nickel { pointer-events: auto; }

/* La signature n'est plus au bout de la bande, elle est sur sa propre ligne :
   la bulle, qui s'ouvre vers le haut et pointe vers le bas, se retrouve
   n'importe où. Mesuré à 480 px avant l'empilement, elle sortait de 64 px par
   la gauche et sa queue retombait dans le noir du bandeau.

   On ne la répare pas, on la retire. Elle est déjà absente sur écran tactile
   (voir section 6) et rien ne se perd : la signature reste un lien vers nckl.ch.

   ⚠ LE SEUIL EST PASSÉ DE 559 À 700 PX, ET IL DOIT SUIVRE CELUI DE LA BANDE.
   Il valait 559 parce que c'était la largeur à laquelle la bande passait sur
   deux lignes par débordement. La bande s'empile maintenant délibérément dès
   700 px (voir juste en dessous) : laissé à 559, ce seuil aurait rendu la bulle
   entre 560 et 700 px, c'est-à-dire exactement là où la signature a cessé
   d'être au bout d'une ligne. Les deux nombres sont liés — changer l'un sans
   l'autre fait réapparaître le défaut. */
@media (max-width: 700px) {
  .fb-annote__bulle--nickel { display: none; }
}

/* --------------------------------------------------------------------------
   LA BANDE SUR TÉLÉPHONE — les informations les unes sous les autres
   --------------------------------------------------------------------------
   Demande de Fabian, 7 octobre 2026 : « faire un footer fixe en bas avec les
   informations les unes en dessous des autres », suivie de « tester des
   combinaisons » — il hésite, et on va voir pourquoi.

   CE QUI SE PASSAIT AVANT. Les quatre éléments sont une ligne en
   `space-between` avec `flex-wrap`. À 390 px, la rangée utile fait 350 px et
   les trois mots en demandent 402 à eux seuls : ils débordaient donc, et
   `wrap` les répartissait en trois lignes bancales — newsletter seule, puis
   e-mail et téléphone, puis la signature — avec les vides de `space-between`
   au milieu. Ce n'était pas une mise en page, c'était un débordement.

   ⚠ ET VOICI LE COÛT, PARCE QU'IL EST RÉEL ET QU'IL FAUT LE DIRE.
   Le pied est FIXE : chaque pixel qu'il prend est retiré à la lecture, en
   permanence, sur toutes les pages. L'en-tête de cette section fixe le budget
   à ~61 px, et Fabian lui-même avait demandé « la bande noire ne dépassant pas
   50 à 70 px ». Quatre lignes ne tiennent pas dans ce budget :

     déchirure                        14 px   (--fb-dechirure-h au plancher)
     newsletter  14 × 71/55        =  18,1
     e-mail      14 × 64/50        =  17,9
     téléphone   14 × 63/52        =  17,0
     nickel.                          14
     3 écarts de 5                 =  15
     air bas                          7,2
     ────────────────────────────────────
     total                         ≈  89 px

   Soit environ 10 % d'un écran de téléphone, en permanence. C'est le prix de
   la demande, et il est assumé — mais il est chiffré ici pour que la question
   puisse se trancher à l'œil et non à l'estime.

   `--fb-pied-mot-h` était descendu à 11 px (contre 12 au plancher du clamp)
   pour tenir dans ce budget. ⚠ IL EST REMONTÉ À 14 PX LE 20 OCTOBRE 2026, et
   le budget saute pour de bon : Fabian, qui l'avait fixé, est aussi celui qui
   a écrit « texte trop petit dans le pied de page ». Les deux demandes se
   contredisent et c'est la seconde qui tranche — elle est postérieure, et elle
   porte sur ce qu'il voit, pas sur ce qu'il estimait.

   LE CHIFFRE EXACT, MESURÉ APRÈS COUP : la bande passe de 99 à 114 px sur un
   390. La variante à deux colonnes décrite ci-dessous reste le seul moyen de
   rendre ces 15 px sans reperdre la lisibilité — elle redescendrait à ≈ 72 px
   AVEC le lettrage à 14.

   ⚠ LA COMBINAISON À TESTER, PUISQU'IL EN A DEMANDÉ — ELLE TIENT EN UNE LIGNE.
   Remplacer `flex-direction: column` par une grille à deux colonnes :

       display: grid;
       grid-template-columns: repeat(2, auto);
       justify-content: center;
       justify-items: center;

   Deux rangées au lieu de quatre, soit ≈ 60 px au total : dans le budget, et
   les quatre éléments restent chacun lisible. C'est la variante à montrer à
   Fabian s'il trouve le pied trop haut. Rien d'autre ne bouge.
   -------------------------------------------------------------------------- */

@media (max-width: 700px) {
  /* ⚠ SUR TÉLÉPHONE, LE PIED N'EST JAMAIS FIXE — PARTOUT, PAS SEULEMENT SUR
     L'ACCUEIL. Thomas, 20 octobre 2026 : « le footer ne doit jamais être fixe
     sur mobile, corrige partout, il doit toujours être en bas ».

     C'est la généralisation d'une demande de Fabian du matin même, qui ne
     visait que l'accueil (« le footer ne doit pas être fixe, il est en bas et
     on le voit seulement si on scroll »). On avait alors argumenté que
     l'exception se justifiait par la page : une page d'atterrissage qui tient
     en un écran ne peut pas céder 10 % de sa hauteur. ⚠ L'ARGUMENT ÉTAIT BON
     ET LA CONCLUSION TROP ÉTROITE — ce qui coûte 10 % sur l'accueil en coûte
     autant partout ailleurs, et la bande vient de passer de 76 à 99 px en
     reprenant son air. Sur un écran de 844, c'est 12 % retirés à chaque page.

     CE QUE LE PIED FIXE APPORTAIT, ET POURQUOI ÇA NE TIENT QUE SUR GRAND
     ÉCRAN. Fabian l'avait demandé le 7 octobre pour que l'e-mail et le
     téléphone soient toujours atteignables. Sur un ordinateur, la bande ne
     coûte que 35 px sur 1000, soit 3,5 % : le marché est bon. Sur un
     téléphone, elle coûte trois fois plus cher pour rendre le même service —
     et le geste de remonter en bas de page, lui, est devenu si courant qu'il
     ne se négocie plus. Le pied fixe n'est pas supprimé, il reste la règle
     au-dessus de 700 px ; il est seulement rendu à son usage.

     DEUX RÈGLES, ET C'EST LA SECONDE QU'ON OUBLIE.

     1. `position: static` remet le pied dans le flux : il se place après le
        contenu, donc sous l'écran, et ne se voit qu'en défilant. `inset` et
        `z-index` deviennent sans effet d'eux-mêmes, il n'y a rien à annuler.

     2. ⚠ LA RÉSERVE FAITE SUR `body` TOMBE À ZÉRO, et son effet est à l'autre
        bout du fichier (section 2). Elle valait la hauteur de la bande PLUS
        une gouttière blanche. La hauteur de la bande n'est plus due : un pied
        dans le flux ne recouvre rien, et la garder laisserait une centaine de
        pixels de blanc entre le contenu et la déchirure.

        ⚠ LA GOUTTIÈRE NON PLUS NE PEUT PAS RESTER LÀ, ET LA RAISON N'EST PAS
        CELLE QU'ON CROIT. Elle existait pour recevoir les bulles des réseaux
        sociaux, qui n'ont pas lieu d'être ici (`.fb-pied__clair` est masqué
        sous 700 px) — mais l'air entre le contenu et le bord déchiré, lui,
        reste nécessaire. Ce qui l'interdit, c'est qu'un `padding` de `body`
        est DANS la boîte dont on vient de fixer la hauteur minimale : sur une
        page courte, le pied se colle en bas de la zone de contenu, et les
        24 px de padding s'étalent SOUS lui. Mesuré : pied à 717 au lieu de
        741, et un liseré blanc sous la déchirure. L'air est donc redonné en
        `margin-top` au pied lui-même, où il sépare ce qu'il doit séparer et
        où le calcul du flex le compte comme il faut.

     `--fb-pied-bande-h` continue d'être mesurée par interface.js et reste
     juste ; elle n'est simplement plus utilisée ici. Les deux autres éléments
     qui s'en servaient ne concernent pas le téléphone : le bouton « retour en
     haut » est en `display: none` sous 700 px, et le récapitulatif du panier
     repasse en `position: static` sous 900 px. Rien d'autre à reprendre. */
  .fb-pied { position: static; }
  body { padding-bottom: 0; }
  .fb-pied { margin-top: clamp(1.5rem, 4vw, 3rem); }

  /* ⚠ « IL DOIT TOUJOURS ÊTRE EN BAS » — ET `static` SEUL NE LE PROMET PAS.
     Un pied rendu au flux se place après le contenu, ce qui n'est « en bas »
     que si le contenu remplit l'écran. Sur une page courte — le panier vide en
     est l'exemple type, relevé à la mesure : pied à 653 px dans une fenêtre de
     844 — il remonte au milieu de l'écran et laisse un grand blanc dessous. Le
     défaut ne se voit sur AUCUNE des pages de contenu du site, qui défilent
     toutes ; il ne se serait manifesté que le jour où quelqu'un ouvre un panier
     vide ou un résultat de recherche sans réponse.

     C'est le motif du « pied collé en bas », et c'est exactement la mécanique
     que le `position: fixed` avait remplacée en octobre (voir la note de la
     section 2, qui dit depuis que `min-height` et la colonne flex « ne servent
     plus à rien »). Sur téléphone, ils resservent : `.site-main` absorbe
     l'espace libre avec `flex: 1 0 auto`, et le pied est poussé contre le bord
     bas. Quand le contenu dépasse, `1 0 auto` ne rétrécit pas — le `0` est là
     pour ça — et tout se comporte comme avant.

     ⚠ `dvh` ET NON `svh`, ALORS QUE LE RESTE DU FICHIER UTILISE `svh`. Les
     deux unités ne répondent pas à la même question. Sur l'accueil, on veut le
     pied SOUS la ligne de flottaison quoi qu'il arrive : il faut la plus PETITE
     hauteur possible, barres d'interface déployées, donc `svh`. Ici on veut
     l'inverse — aucun blanc sous le pied : il faut la hauteur RÉELLE du moment,
     donc `dvh`. Avec `svh`, une barre d'URL qui se rétracte découvrirait une
     bande blanche sous la déchirure. Le reproche habituel fait à `dvh`, les
     reflux pendant le défilement, ne s'applique pas : cette hauteur ne décide
     de rien dès que la page est plus haute que l'écran, c'est-à-dire dans tous
     les cas où l'on défile. */
  body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;   /* repli : navigateurs d'avant 2022 */
    min-height: 100dvh;
  }

  .site-main { flex: 1 0 auto; }

  .fb-pied__bande { --fb-pied-mot-h: 14px; }

  /* ⚠ L'AIR DU HAUT, QUI AVAIT ÉTÉ RETIRÉ EXPRÈS, EST REMIS.
     Thomas, 20 octobre 2026 : « laisser respirer un peu plus le footer, il
     est tout cougné c'est moche ». Il a raison, et le raisonnement qui a
     produit ce défaut est écrit en tête de section : sur grand écran, la
     déchirure du bord haut TIENT LIEU d'air, donc `padding-top: 0` et l'œil
     s'équilibre. C'est vrai à 1440 px, où la déchirure fait 26 px de haut.

     ⚠ CE N'EST PLUS VRAI À 390 px, OÙ ELLE N'EN FAIT QUE 14 — et seulement 4
     visibles, les 10 autres passant sous la bande par un `margin-bottom: -10px`
     qui, lui, ne se réduit pas avec l'écran. L'argument d'origine tenait à une
     hauteur de dessin qui a fondu de moitié : la première ligne se retrouvait
     collée sous le déchirement. C'est le même piège que les seuils de 560 et
     700 px — une décision juste à une taille, muette quand la taille change.

     LES TROIS VALEURS, MESURÉES PLUTÔT QU'ESTIMÉES. Les quatre lignes font
     14 + 14 + 13 + 11 = 52 px d'encre ; le reste est de l'air et c'est lui
     qu'on règle. On vise une respiration ÉGALE en haut, entre les lignes et
     en bas — l'encre est si fine que toute asymétrie se lit comme une erreur.

       haut    0      → 8,6 px   (la déchirure ne suffit plus toute seule)
       entre   5,5 px → 9,4 px   (trois fois, soit 28 au lieu de 16)
       bas     7,2 px → 10,1 px  (`--fb-bande-air`, qui reprend le relais)

     La bande passe de 76 à ≈ 99 px, soit 103 avec la déchirure visible : de
     9,5 % à 12 % d'un écran de 844. ⚠ C'EST LE BUDGET DE 65 px ANNONCÉ EN
     TÊTE DE SECTION QUI SAUTE, et il faut le dire : il avait été tenu en
     serrant tout, c'est exactement ce que Thomas trouve moche. La réserve
     faite sur `body` suit toute seule — elle est calculée en JS depuis la
     hauteur réelle du pied (`--fb-pied-bande-h`, voir interface.js), il n'y a
     donc aucune constante à corriger ailleurs.

     ⚠ LA VARIANTE EN DEUX COLONNES, DÉCRITE EN TÊTE DE SECTION, REDEVIENT LA
     VRAIE RÉPONSE SI FABIAN TROUVE LE PIED TROP HAUT. Elle passe de quatre
     rangées à deux : avec ces écarts-là, ≈ 70 px au lieu de 99 — donc plus
     d'air ET moins de hauteur qu'aujourd'hui. Elle tient toujours en une
     ligne, et c'est maintenant le seul moyen d'avoir les deux. */
  .fb-pied__bande {
    padding-top: clamp(0.4rem, 2.2vw, 0.7rem);
    --fb-bande-air: clamp(0.55rem, 2.6vw, 0.85rem);
  }

  .fb-pied__inner {
    flex-direction: column;
    /* `nowrap` est explicite : en colonne sans hauteur imposée, `wrap` ne se
       déclencherait jamais, mais le laisser là ferait croire à une intention. */
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: clamp(0.5rem, 2.4vw, 0.8rem);
  }
}

.fb-annote__texte {
  color: var(--fb-blanc);
  font-size: var(--fb-petit);
  line-height: 1.35;
  text-align: center;
  text-wrap: pretty;
}

.fb-annote__texte a { color: var(--fb-blanc); }
.fb-annote__texte a:hover,
.fb-annote__texte a:focus-visible { color: var(--fb-vert); }

/* Réseaux sociaux.

   Les deux variables sont déclarées ICI, sur la liste, et pas sur la bulle qui
   les utilise : ce qui doit réserver la place de l'annotation, c'est la liste
   elle-même (voir plus bas). Une variable descend l'arbre, elle ne le remonte
   pas — déclarée sur la bulle, elle serait invisible depuis son parent. */
.fb-reseaux {
  --fb-bulle-l: clamp(38px, 4.2vw, 50px);
  --fb-bulle-h: calc(var(--fb-bulle-l) * 127 / 136); /* rapport du PNG */

  display: flex;
  align-items: center;
  gap: var(--fb-reseau-ecart);
}
/* 26 px, c'est la taille de la maquette. C'est petit, mais au-dessus du minimum
   de 24 px que demandent les WCAG pour une cible cliquable.

   ⚠ L'écart a été resserré (12 → 8 px au plafond) et ça se vérifie plutôt que
   ça ne se suppose : dans le critère WCAG « Target Size », l'espacement autour
   d'une cible peut être compté POUR atteindre les 24 px. Ici il n'a pas à
   l'être — l'icône fait 26 px à elle seule, écart compris ou non. Le resserrage
   est donc gratuit. Il ne le serait plus si on descendait les icônes sous
   24 px : il faudrait alors du padding transparent autour, pas du `gap`. */
.fb-reseaux__item {
  width: var(--fb-reseau-l);
  /* CARRÉES À NOUVEAU depuis le 23 septembre 2026 (dossier `PIED-DE-PAGE/`,
     84 × 83). Elles l'étaient jusqu'au 9 septembre, où Fabian avait relivré les
     quatre en rond (`Reseaux/`, 91 × 91) ; à l'entretien du 23 il est revenu
     dessus — « les icônes des réseaux sociaux carrés comme avant ». Les deux
     jeux sont dans la source, c'est le build qui choisit (tools/build-assets.sh).

     Le rapport n'est PAS 1 : les carrées font 84 × 83, un pixel de moins en
     hauteur. Le dire plutôt que d'arrondir — `aspect-ratio: 1` écraserait le
     dessin d'un pixel sur 83, ce qui ne se voit pas mais s'accumule avec le
     reste. Si on repassait un jour aux rondes, c'est cette ligne qu'il faudrait
     remettre à 1. */
  aspect-ratio: 84 / 83;
}
.fb-reseaux__item--insta::before    { background-image: url("../img/pied/reseau-insta-base.png"); }
.fb-reseaux__item--insta::after     { background-image: url("../img/pied/reseau-insta-survol.png"); }
.fb-reseaux__item--facebook::before { background-image: url("../img/pied/reseau-facebook-base.png"); }
.fb-reseaux__item--facebook::after  { background-image: url("../img/pied/reseau-facebook-survol.png"); }
.fb-reseaux__item--youtube::before  { background-image: url("../img/pied/reseau-youtube-base.png"); }
.fb-reseaux__item--youtube::after   { background-image: url("../img/pied/reseau-youtube-survol.png"); }
.fb-reseaux__item--behance::before  { background-image: url("../img/pied/reseau-behance-base.png"); }
.fb-reseaux__item--behance::after   { background-image: url("../img/pied/reseau-behance-survol.png"); }

/* Réduites dans le même rapport que les icônes (×0.67) : elles étaient calées
   sur des icônes de 42 px, elles en font 28. Sans ça les annotations seraient
   devenues deux fois plus larges que ce qu'elles désignent.

   `left: 50%` SEUL NE CENTRE RIEN, et c'était le défaut : il place le BORD
   GAUCHE de la bulle au milieu de l'icône, si bien qu'elle partait entièrement
   vers la droite — décalée d'une demi-bulle plus une demi-icône, soit une
   trentaine de pixels. Elle désignait le voisin.

   On la ramène de la moitié de sa largeur. `margin-left` plutôt que
   `translateX(-50%)` parce que `transform` est déjà pris : c'est lui qui porte
   le petit glissement d'apparition de `.fb-annote__bulle` (section 6). Les deux
   se seraient écrasés l'un l'autre, et l'apparition aurait disparu.

   D'où la largeur hissée dans une variable : elle sert trois fois maintenant,
   elle ne doit pas pouvoir être modifiée d'un seul côté. */
.fb-reseaux .fb-annote__bulle {
  bottom: 100%;
  left: 50%;
  margin-left: calc(var(--fb-bulle-l) / -2);
  width: var(--fb-bulle-l);
  aspect-ratio: 136 / 127;
}

/* ⚠ CELLES DU PIED DE PAGE ONT MAINTENANT UN FOND. C'est la contrepartie du
   bloc clair devenu transparent : la bulle monte au-dessus des icônes, donc
   au-dessus de la page, et elle n'a plus la gouttière blanche qui la recevait.
   Le dessin est au trait NOIR — posé sur un paragraphe, on obtient deux choses
   noires superposées et on ne lit ni l'une ni l'autre.
   Le blanc est calé sur la forme dessinée par un `radial-gradient` grossier
   plutôt que sur un rectangle : un carré blanc derrière une bulle ronde se
   verrait plus que le problème qu'il corrige. */
.fb-pied__clair .fb-reseaux .fb-annote__bulle::before,
.fb-social-flux .fb-reseaux .fb-annote__bulle::before {
  content: "";
  position: absolute;
  inset: 0 0 18% 0;   /* la queue occupe le bas du dessin : on l'exclut */
  z-index: -1;
  border-radius: 50%;
}

/* RÉSERVER LA PLACE DE L'ANNOTATION.

   Dans le pied de page, la bulle monte dans le blanc au-dessus du bandeau :
   elle ne recouvre rien. Dans le contenu d'une page, il y a du texte juste
   au-dessus des icônes — sur « Me soutenir », le lien vers la boutique — et
   l'annotation lui passait dessus au survol. Deux dessins noirs superposés :
   illisibles tous les deux.

   Une annotation est en `position: absolute` : elle est sortie du flux, donc
   elle n'a aucun moyen de pousser quoi que ce soit. C'est au parent de lui
   garder son espace. Même situation, même remède que la marge haute de
   `.fb-entete`, qui réserve la place de « RETOUR À L'ACCUEIL. ».

   La marge est CALCULÉE depuis la taille réelle de la bulle, pas fixée à une
   valeur choisie à l'œil : les icônes suivent un `clamp()`, la bulle aussi, et
   une constante serait juste à une seule largeur d'écran. */
.fb-page .fb-reseaux {
  margin-top: calc(var(--fb-bulle-h) + 0.5rem);
}

/* Sur écran tactile les annotations sont retirées (voir section 6) : la place
   réservée n'aurait plus rien à contenir, ce serait un trou. On revient à
   l'espacement ordinaire. */
@media (hover: none) {
  .fb-page .fb-reseaux { margin-top: 1.25rem; }
}
.fb-annote__bulle--insta    { background-image: url("../img/annotations/insta.png"); }
.fb-annote__bulle--facebook { background-image: url("../img/annotations/facebook.png"); }
.fb-annote__bulle--youtube  { background-image: url("../img/annotations/youtube.png"); }
.fb-annote__bulle--behance  { background-image: url("../img/annotations/behance.png"); }

/* Bouton « Me soutenir » hors page d'accueil.

   Il porte maintenant `.fb-btn--me-soutenir` et ses deux calques : c'est ce qui
   lui donne l'animation de _sprites.css, la même qu'à l'accueil. Il était le
   seul bouton du site à rester immobile au survol.

   DEUX CLASSES DANS LE SÉLECTEUR, PAS UNE : `.fb-btn--me-soutenir` fixe une
   largeur (section 9) et se trouve PLUS BAS dans le fichier. À spécificité
   égale, c'est la dernière règle qui gagne — écrit `.fb-soutenir-pied` seul,
   celui du pied de page reprendrait la taille de l'accueil.

   Et la largeur est arrondie au pixel, comme tout ce qui est animé par
   spritesheet : voir la note ⚠ de la section 9.

   SA LARGEUR N'EST PLUS LA SIENNE : c'est celle de la rangée d'icônes, au pixel
   près — quatre icônes plus trois écarts (`--fb-reseaux-l`, section 1). Les deux
   éléments sont l'un au-dessus de l'autre dans le bloc clair, et deux blocs
   empilés de largeurs presque égales se lisent comme une erreur de mesure plutôt
   que comme une intention. Alignés, ils forment une seule colonne.

   ⚠ C'est un CALCUL, pas une valeur recopiée. Retoucher la taille des icônes ou
   leur écart déplace ce bouton avec elles ; une constante de 136 px serait juste
   aujourd'hui et fausse au premier réglage suivant. C'est aussi pour ça que les
   trois variables sont dans `:root` et pas sur `.fb-reseaux`.

   ⚠ CE N'EST PAS LA MÊME CHOSE QUE « LA MÊME LARGEUR DE BOÎTE ».
   Le PNG du bouton est une toile de 380 px dont la pilule verte n'occupe que
   350, décalée de 19 px à gauche — mesuré, pas estimé. Une boîte de 136 px
   donnait donc un dessin de 125 px, rentré de 7 px de chaque côté : à l'œil, le
   bouton restait plus étroit que les icônes, et le défaut sautait justement
   parce que les deux valeurs étaient presque égales. On dimensionne donc la
   boîte pour que le DESSIN fasse la largeur voulue (× 380/350), et on la décale
   de son propre bord transparent (19/350) pour que les deux traits partent de
   la même verticale. C'est le dessin qui porte la mesure, pas la boîte.

   Le décalage ne vaut que dans le pied, où les deux blocs sont empilés. Dans la
   copie en flux, ils sont côte à côte aux deux bouts d'une ligne : il n'y a
   aucun alignement vertical à tenir, et la marge négative ne ferait que déborder
   dans la gouttière.

   La largeur reste arrondie au pixel, comme tout ce qui est animé par
   spritesheet : voir la note ⚠ de la section 9. */
.fb-btn--me-soutenir.fb-soutenir-pied {
  width: calc(var(--fb-reseaux-l) * 380 / 350);
  width: round(down, calc(var(--fb-reseaux-l) * 380 / 350), 1px);
}

.fb-pied__clair .fb-btn--me-soutenir.fb-soutenir-pied {
  margin-left: calc(var(--fb-reseaux-l) * -19 / 350);
}

/* --------------------------------------------------------------------------
   LE BLOC SOCIAL EXISTE À DEUX ENDROITS. EXACTEMENT UN DES DEUX EST AFFICHÉ.
   --------------------------------------------------------------------------
   Le même balisage est rendu deux fois par fb_bloc_social() (inc/helpers.php) :
   une fois dans le pied fixe (`.fb-pied__clair`), une fois dans le flux de la
   page juste avant lui (`.fb-social-flux`). La raison est expliquée en détail
   là-bas ; le résumé est qu'un enfant de `position: fixed` ne peut pas revenir
   dans le flux, et qu'on refuse de faire dépendre la présence de vrais liens de
   l'exécution d'un script.

   LE SEUIL. Le bloc du pied n'a plus de fond : il flotte sur la page. Ça marche
   tant que les icônes tombent à GAUCHE et à DROITE de la colonne de texte. La
   colonne fait au plus 1144 px et les deux groupes d'icônes environ 250 px à
   eux deux : en dessous d'à peu près 700 px de fenêtre, il ne reste plus de
   marge et ils se posent sur les mots. C'est le même seuil que la grille
   boutique (section 15), et ce n'est pas un hasard : c'est la largeur à partir
   de laquelle le contenu occupe tout l'écran.
   -------------------------------------------------------------------------- */

/* La copie dans le flux : masquée par défaut, c'est celle du pied qui sert. */
.fb-social-flux { display: none; }

@media (max-width: 1250px) {
  .fb-pied__clair { display: none; }

  /* ⚠ UNE COLONNE CENTRÉE, ET PLUS UNE LIGNE ÉCARTELÉE.
     Elle était en `row` + `space-between` : les réseaux collés au bord gauche,
     « ME SOUTENIR » collé au bord droit, et au milieu un vide d'autant plus
     large que l'écran l'était. Ce vide est le reste du pied fixe du grand
     écran, où les deux blocs sont EMPILÉS dans une colonne étroite à gauche —
     en passant dans le flux, l'empilement s'était déplié en ligne sans que
     personne le demande.
     Fabian, 7 octobre 2026 : « quand l'écran est plus petit, mettre les
     éléments flottants en bas et au centre » et « le bouton soutenir centré en
     dessous des liens ». C'est donc la colonne du pied fixe qu'on retrouve,
     mais centrée : les quatre réseaux sur une ligne, le bouton dessous.
     L'ordre vient du DOM (fb_bloc_social) et n'a pas à être forcé. */
  .fb-social-flux {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    /* Plus large que l'écart d'origine (8 à 16 px) : horizontalement il
       séparait deux blocs déjà éloignés, verticalement il est la seule chose
       qui empêche le bouton de toucher les icônes. */
    gap: clamp(0.75rem, 2.4vw, 1.25rem);

    /* Elle est dans le flux : elle prend les gouttières du corps de page, et
       une vraie marge haute pour ne pas se coller au dernier paragraphe. */
    padding: 0 var(--fb-gouttiere);
    margin-top: clamp(2rem, 8vw, 3.5rem);
  }

  /* Les bulles d'annotation des réseaux s'ouvrent vers le HAUT (`bottom: 100%`)
     et sortiraient sur le contenu. Ici, il n'y a plus la gouttière blanche du
     pied fixe pour les recevoir : on les retire, comme on le fait déjà sur
     écran tactile (section 6). Les liens restent des liens. */
  .fb-social-flux .fb-annote__bulle { display: none; }
}

/* --------------------------------------------------------------------------
   « RETOUR EN HAUT » — fixé en bas à droite (footer.php, interface.js)
   --------------------------------------------------------------------------
   Le dessin de Fabian (9 septembre 2026, 208 × 268 : une flèche et le mot,
   noir puis vert). En bas à droite, parce que c'est le seul coin libre : les
   réseaux et « Me soutenir » sont empilés en bas à gauche, le panier en haut
   à droite. Il se pose au-dessus de la bande noire, dans la marge.

   Caché par défaut, et ce n'est pas `display: none` : on veut un fondu à
   l'apparition, et `visibility` — contrairement à `display` — se laisse
   transitionner en même temps que l'opacité, et retire bien le bouton du
   parcours clavier tant qu'il est invisible. Sans JavaScript, `is-visible`
   n'est jamais posée et le bouton n'existe pas à l'écran (voir footer.php).

   `bottom` compte la partie opaque du pied (mesurée par interface.js), avec
   un repli si le script n'a pas tourné. */
.fb-retour-haut {
  position: fixed;
  right: var(--fb-gouttiere);
  bottom: calc(var(--fb-pied-bande-h, 60px) + 1rem);
  z-index: 21;
  width: clamp(44px, 4.5vw, 62px);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--fb-transition), visibility var(--fb-transition);
}
.fb-retour-haut.is-visible {
  opacity: 1;
  visibility: visible;
}
/* ⚠ LA RÉSERVATION DE COLONNE A ÉTÉ SUPPRIMÉE, ET LE BOUTON AVEC, SUR TÉLÉPHONE.
   Il y avait ici deux `padding-right` sur `.fb-social-flux`, à 1250 et à
   700 px. Ils réglaient une collision : le bloc social passé dans le flux
   poussait « ME SOUTENIR » contre le bord droit, c'est-à-dire pile sous le
   bouton « retour en haut » quand on arrivait au bout de la page (relevé sur
   le panier à 390 px, 17 septembre 2026). On réservait donc la colonne du
   bouton.
   Le bloc social est maintenant une COLONNE CENTRÉE (voir plus haut) :
   « ME SOUTENIR » est au milieu de la page, le bouton reste dans la gouttière
   droite, il n'y a plus rien à réserver. Garder ces marges décentrerait la
   colonne vers la gauche de 52 à 70 px — on aurait remplacé une collision par
   un défaut d'alignement.

   ET SOUS 700 px, IL N'Y A PLUS DE BOUTON DU TOUT. Fabian, 7 octobre 2026 :
   « enlever le bouton retour en haut ». Sur un téléphone, le geste existe déjà
   partout — barre d'état d'iOS, pression sur la barre d'adresse — et le bouton
   coûtait un objet fixe de plus dans un écran qui en porte déjà deux (le panier
   et le pied). `display: none` et non la transparence de `.fb-retour-haut` :
   interface.js continue de lui poser `is-visible` au défilement, et une
   `visibility` ne tiendrait pas contre elle. Caché ainsi, il ne reçoit plus
   rien et sort du parcours clavier ; le script, lui, tourne dans le vide pour
   quelques octets. */
@media (max-width: 700px) {
  .fb-retour-haut { display: none; }
}

/* ==========================================================================
   9. PAGE D'ACCUEIL
   --------------------------------------------------------------------------
   La maquette ACCUEIL.jpg n'a ni navigation ni pied de page complet : c'est une
   page d'atterrissage. Les 6 vignettes SONT la navigation.
   ========================================================================== */

.fb-accueil {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.25rem, 3.5vh, 2.5rem);
  padding: clamp(1.5rem, 6vh, 4rem) var(--fb-gouttiere);
  text-align: center;
}

.fb-accueil__logo {
  display: flex;
  align-items: center;
  justify-content: center;
  /* L'ECART ENTRE « HEY! Moi c'est » ET « FABIAN », RAMENE A UN TIERS.
     Fabian, 20 octobre 2026 : « rapprocher le Fabian du hey c'est moi ». Les
     deux dessins sont une seule phrase manuscrite coupee en deux fichiers :
     l'air qui les separait se lisait comme une respiration qu'il n'a pas
     dessinee. De 14,4 px au plafond a 4,8. */
  gap: clamp(0.1rem, 0.35vw, 0.3rem);
  margin: 0;
}
/* Ramené à 0,64 de sa taille d'origine (300 → 192 px), en deux temps et à la
   demande de Fabian : d'abord 0,75 le 7 octobre 2026 — à 520 px de large, le
   titre écrasait les six vignettes qui sont pourtant le sujet de la page —
   puis 0,64 le 20 octobre, « faire le titre un peu plus petit ». Le bloc
   entier passe de 391 à 327 px de large sur un 1440.

   ⚠ Ce facteur s'applique AUX DEUX éléments du titre — `.fb-accueil__hey` ici
   et `.fb-accueil .fb-fabian` plus bas. Ne réduire que l'un des deux change les
   proportions du dessin de Fabian, pas sa taille. */
.fb-accueil__hey {
  width: clamp(110px, 14vw, 192px);
  aspect-ratio: 434 / 175;
  background: url("../img/accueil/hey.png") center / contain no-repeat;
}
/* ⚠ LARGEURS ENTIÈRES OBLIGATOIRES SUR TOUT CE QUI EST ANIMÉ
   ---------------------------------------------------------
   Une spritesheet avance en décalant son fond de `k × largeur de l'élément`.
   Si cette largeur vaut 156,6094 px, la frame 1 tombe à ,6094 du pixel, la
   frame 2 à ,2188, la frame 3 à ,8281… Le navigateur ré-échantillonne donc le
   dessin à une position sous-pixel DIFFÉRENTE à chaque image, et les traits
   fins de Fabian tremblent d'environ un pixel pendant toute l'animation.

   Avec une largeur entière, tous les décalages sont entiers : le dessin est
   échantillonné exactement pareil d'une frame à l'autre, et l'animation est
   stable. D'où les `round(down, …, 1px)` ci-dessous.

   Chaque règle est écrite en DEUX temps : la version historique d'abord, la
   version arrondie ensuite. Un navigateur qui ne connaîtrait pas `round()`
   ignore la seconde et garde la première — il tremble, mais il s'affiche. */

.fb-accueil .fb-fabian { width: clamp(74px, 9.5vw, 130px); }
.fb-accueil .fb-fabian { width: round(down, clamp(74px, 9.5vw, 130px), 1px); }

/* Bloc des vignettes + « Me soutenir ».
   Sa largeur est celle d'une rangée pleine : autant de vignettes que le
   modèle en affiche, à leur taille nominale, séparées par leurs écarts.
   `--fb-nb-vignettes` est posé par front-page.php ; la valeur ci-dessous
   n'est qu'un repli si la feuille est utilisée sans ce contexte. */
.fb-accueil__nav {
  --fb-nb-vignettes: 5;

  display: flex;
  flex-direction: column;
  gap: var(--fb-ecart);
  width: 100%;
  max-width: min(
    var(--fb-compo),
    calc(var(--fb-bouton) * var(--fb-nb-vignettes)
       + var(--fb-ecart) * (var(--fb-nb-vignettes) - 1))
  );

  /* Fait de ce bloc une « unité de conteneur » : `100cqw` désigne désormais sa
     largeur réelle, celle qui reste après la marge de la page. C'est ce qui
     permet à la grille ci-dessous de calculer sa colonne au pixel près, y
     compris sur mobile où c'est l'écran qui limite, pas le max-width. */
  container-type: inline-size;
}

/* `auto-fit` fait le responsive tout seul : les vignettes se réorganisent en
   autant de colonnes que la largeur permet, sans un seul point de rupture.

   La piste était `1fr`, c'est-à-dire « partage-toi ce qui reste » — et ce qui
   reste ne tombe presque jamais sur un nombre entier. On calcule donc nous-mêmes
   la même largeur, arrondie au pixel inférieur. `max()` garde le plancher que
   `minmax()` assurait avant : en dessous de ce plancher, on passe à moins de
   colonnes plutôt que de réduire les vignettes indéfiniment.

   ⚠ LE DIVISEUR, C'EST LE NOMBRE DE VIGNETTES, et il n'est pas constant :
   NEWS n'apparaît que si Fabian lui a donné une destination, donc la rangée
   en compte 4 ou 5. C'est pour ça que le nombre vient du PHP. Figé à 4 dans
   la feuille (l'état précédent), le calcul donnait des colonnes d'un quart de
   rangée : quatre remplissaient toute la largeur et CONTACT passait seul à la
   ligne suivante — y compris sur un grand écran, où la place ne manquait pas.

   Le `- 0.1px` est un garde-fou. Sans lui, les colonnes et les écarts font
   pile 100cqw, et il suffit d'un cheveu d'arrondi interne (le navigateur
   compte en 1/64 de pixel) pour que la dernière ne rentre plus et retombe.
   Un dixième de pixel suffit à écarter ce cas sans rien coûter ailleurs.

   `space-between` rend à la rangée la largeur complète que `1fr` lui donnait :
   les quelques pixels perdus à l'arrondi repartent dans les écarts, ce qui
   garde « Me soutenir » aligné sur le bord droit de la dernière vignette. */
.fb-vignettes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--fb-vignette-mini), 1fr));
  grid-template-columns: repeat(auto-fit, round(down, max(
    var(--fb-vignette-mini),
    (100cqw - var(--fb-ecart) * (var(--fb-nb-vignettes) - 1) - 0.1px) / var(--fb-nb-vignettes)
  ), 1px));
  justify-content: space-between;
  gap: var(--fb-ecart);
  width: 100%;
}

.fb-btn {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  text-decoration: none;
}

/* Le calque du bas : l'image de repos. */
.fb-btn__base {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/* Le calque du haut : la spritesheet animée. Elle n'apparaît qu'au survol,
   mais elle est déjà chargée — d'où l'absence de clignotement. */
.fb-btn__anim {
  position: absolute;
  inset: 0;
  opacity: 0;
  background-repeat: no-repeat;
  background-position: 0 0;
}
.fb-btn:hover .fb-btn__anim,
.fb-btn:focus-visible .fb-btn__anim { opacity: 1; }

/* ⚠ Le calque de repos doit DISPARAÎTRE pendant le survol.
   Les frames d'animation comportent des zones transparentes (l'enveloppe de
   CONTACT quitte le cadre, il ne reste que le fond vert). Si l'image de repos
   restait dessous, on la verrait au travers, immobile, pendant que l'animation
   se joue par-dessus — deux dessins superposés dont un seul bouge.

   Pas de transition sur ces deux calques : le passage doit être net. Un fondu,
   même court, ferait apparaître les deux images à demi opaques en même temps,
   ce qui produit exactement le fantôme qu'on cherche à supprimer. Et comme les
   deux images sont déjà chargées, une coupure franche ne clignote pas. */
.fb-btn:hover .fb-btn__base,
.fb-btn:focus-visible .fb-btn__base { opacity: 0; }

.fb-btn--news        .fb-btn__base { background-image: url("../img/boutons/news-base.png"); }
.fb-btn--shop        .fb-btn__base { background-image: url("../img/boutons/shop-base.png"); }
.fb-btn--portfolio   .fb-btn__base { background-image: url("../img/boutons/portfolio-base.png"); }
.fb-btn--a-propos    .fb-btn__base { background-image: url("../img/boutons/a-propos-base.png"); }
.fb-btn--contact     .fb-btn__base { background-image: url("../img/boutons/contact-base.png"); }
.fb-btn--me-soutenir .fb-btn__base { background-image: url("../img/boutons/me-soutenir-base.png"); }

/* « Me soutenir » n'est pas carré (380×99) et se place sous la rangée,
   aligné à droite comme sur la maquette. */
.fb-soutenir-ligne { display: flex; justify-content: flex-end; }
.fb-btn--me-soutenir {
  width: var(--fb-bouton);
  width: round(down, var(--fb-bouton), 1px); /* 4 frames animées : voir la note plus haut. */
  aspect-ratio: 380 / 99;
}

/* --------------------------------------------------------------------------
   SUR ÉCRAN TACTILE, LES VIGNETTES JOUENT TOUTES SEULES, L'UNE APRÈS L'AUTRE
   --------------------------------------------------------------------------
   Il n'y a pas de survol sur un téléphone : les animations ne se
   déclencheraient jamais. La version précédente affichait donc, en guise de
   compromis, la PREMIÈRE IMAGE DE LA SPRITESHEET à la place du dessin de repos
   — figée, mais « vivante ».

   ⚠ ET C'ÉTAIT LA MAUVAISE IMAGE, CE QUI NE SE VOYAIT PAS SANS LES OUVRIR.
   Les deux dessins de chaque vignette ne diffèrent pas par la pose mais par le
   FOND : le dessin de repos est sur blanc, toutes les images de la spritesheet
   sont sur vert. La première frame n'est donc pas « le début de l'animation »,
   c'est déjà l'état d'arrivée — le survol, en vert, avant même qu'on ait
   touché quoi que ce soit. Les six vignettes s'affichaient toutes vertes sur
   téléphone, alors que la page d'accueil est blanche. Vérifié image par image
   (shop, portfolio, à propos, contact) avant de toucher à la règle.
   Fabian, 20 octobre 2026 : « les cartes doivent afficher leur état de départ,
   pas leur état final » et « les cartes doivent jouer leur animation les unes
   après les autres ».

   CE QU'ON FAIT À LA PLACE. Le dessin de repos reprend sa place — c'est l'état
   de départ, et c'est celui de la maquette. Puis chaque vignette joue sa
   séquence UNE fois, à son tour, et revient au repos.

   ⚠ DEUX JEUX DE KEYFRAMES POUR CINQ ANIMATIONS, ET PAS UN SEUL.
   `_sprites.css` en déclare une par bouton (`fb-sprite-shop`,
   `fb-sprite-portfolio`…) — mais elles sont TOUTES identiques : de 0 % à 100 %
   de `background-position-x`. Ce qui diffère d'un bouton à l'autre, c'est le
   nombre d'images, et il n'est pas dans les keyframes : il est dans le
   `steps()`, c'est-à-dire dans la fonction de progression. Tant que chaque
   vignette avait SA durée, un seul jeu suffisait donc.

   La boucle a cassé ça, et il faut comprendre pourquoi avant de lire le code.
   Ce bloc n'écrit aucun nom généré, ce qui le met à l'abri du prochain
   `tools/build-assets.sh` — cette propriété-là, elle, est conservée.

   -------------------------------------------------------------------------
   CE QUE LA BOUCLE CHANGE : LE TEMPS N'EST PLUS DÉCOUPÉ DE LA MÊME FAÇON
   -------------------------------------------------------------------------
   Thomas, 20 octobre 2026 : « joue les animations en boucle, donc quand c'est
   fini on recommence depuis le début ».

   La clé est une propriété peu connue : `animation-delay` ne retarde pas
   seulement le DÉBUT d'une animation `infinite`, il DÉCALE SA PHASE POUR
   TOUJOURS. Une animation de 10,5 s avec 4 s de retard est en permanence
   « à 4 s de là où en est » une animation de 10,5 s sans retard ; les deux
   repassent par leur 0 % au même instant toutes les 10,5 s, décalées. C'est
   exactement un enchaînement cyclique — à condition que les cinq animations
   aient la MÊME PÉRIODE. S'en écarter d'un dixième de seconde suffit à faire
   dériver les cinq vignettes les unes par rapport aux autres, lentement, et à
   finir par les superposer : c'est le défaut classique de ce montage.

   ⚠ CONSÉQUENCE : LA DURÉE N'EST PLUS CELLE DE LA SÉQUENCE, C'EST CELLE DU
   CYCLE ENTIER. Les cinq `animation-duration` valent désormais 10,5 s, et le
   moment où une vignette joue n'est plus exprimé en secondes mais EN
   POURCENTAGE À L'INTÉRIEUR DES KEYFRAMES. `--fb-vignette-duree` a donc
   disparu : il n'y a plus de durée par vignette.

   C'est là que le jeu unique se scinde. Il y a deux longueurs de spritesheet
   dans le projet, 1 s et 2 s, donc deux fractions de cycle :

     spritesheets 1 s  →  1 / 10,5  =   9,524 %   (shop, me soutenir)
     spritesheets 2 s  →  2 / 10,5  =  19,048 %   (portfolio, à propos, contact)

   Deux familles, pas cinq : le nombre d'images reste dans le `steps()`, qui
   peut rester au niveau de l'animation (dans le raccourci) parce que tous les
   AUTRES segments de keyframes sont soit constants, soit instantanés — un
   `steps()` d'animation s'applique segment par segment, et découper en six ou
   en trente-trois un segment où rien ne bouge donne le même résultat.

   Les noms d'animation passent donc eux aussi par des variables
   (`--fb-vignette-defile`…), ce qui laisse chaque vignette ne déclarer que
   trois choses : sa famille, son `steps()` et son retard.

   L'ENCHAÎNEMENT, EN SECONDES, SUR UN CYCLE DE 10,5 s. Chaque retard est la
   fin de la précédente plus une demi-seconde de respiration — sans ce silence,
   les cinq séquences se lisent comme une seule animation confuse. Le dernier
   silence est ce qui sépare la fin du cycle de son redémarrage, et c'est
   pourquoi la période est 10,5 et non 10 :

     shop         0,0 s   + 1 s  →   1,0     puis 0,5 s de silence
     portfolio    1,5 s   + 2 s  →   3,5     puis 0,5 s de silence
     à propos     4,0 s   + 2 s  →   6,0     puis 0,5 s de silence
     contact      6,5 s   + 2 s  →   8,5     puis 0,5 s de silence
     me soutenir  9,0 s   + 1 s  →  10,0     puis 0,5 s de silence → 10,5

   NEWS n'y figure pas, et pour deux raisons maintenant : son « animation »
   était une image unique (voir `_sprites.css`), et depuis ce matin ses deux
   calques sont en `display: none` au profit d'un placeholder en pointillés.
   Ses variables ne sont pas déclarées — et une `animation` dont une variable
   manque devient invalide, donc `none`. L'omission suffit, il n'y a pas de cas
   particulier à écrire.

   ⚠ `animation-fill-mode` RESTE À `none`, MÊME EN BOUCLE. Avec `forwards`, une
   vignette resterait figée sur sa DERNIÈRE image — le vert de l'état
   d'arrivée, c'est-à-dire exactement le défaut signalé ce matin. Ici il ne
   protège plus la fin de la séquence (il n'y a plus de fin) mais les 0,5 s de
   silence et, surtout, le tout premier instant avant que la première
   animation ne démarre.

   LES DEUX CALQUES SONT PILOTÉS SÉPARÉMENT, avec le même retard et la même
   période : l'un s'allume pendant que l'autre s'éteint. Les keyframes tiennent
   une valeur CONSTANTE sur chaque segment — ce n'est pas un fondu, c'est un
   interrupteur, ce que demande la note du bloc `.fb-btn__base` plus haut (deux
   dessins à demi opaques feraient un fantôme). Il le fallait d'autant plus que
   les images des cinq spritesheets sont PARTIELLEMENT TRANSPARENTES (vérifié :
   `magick … -alpha extract` donne un minimum à 0 sur les cinq) — le calque
   animé ne peut donc pas se contenter de recouvrir celui de repos, il faut
   bien éteindre l'autre.

   ⚠ CE QUE COÛTE LA BOUCLE, ET CE QUI LA DÉSACTIVE. Une passe unique coûtait
   dix secondes d'animation par chargement ; la boucle en coûte autant toutes
   les dix secondes, tant que la page est ouverte. Trois garde-fous existent
   déjà et aucun n'a eu besoin d'être ajouté : `@media (hover: none)` limite ça
   aux écrans tactiles, `.fb-accueil` à la seule page d'accueil, et le bloc
   `@media (prefers-reduced-motion: reduce)` plus haut force
   `animation-duration: 0.001ms !important` ET `animation-iteration-count:
   1 !important` — la boucle s'annule donc d'elle-même pour qui a demandé moins
   de mouvement, sans une ligne de plus.
   -------------------------------------------------------------------------- */

@media (hover: none) {

  /* L'état de départ, retrouvé : c'est l'ordre normal des deux calques, celui
     qui vaut sur grand écran hors survol. */
  .fb-btn__base { opacity: 1; }
  .fb-btn__anim { opacity: 0; }

  /* LA PÉRIODE, UNE SEULE FOIS. Elle est posée sur l'accueil et pas sur chaque
     vignette, parce que c'est précisément la valeur qui ne doit SURTOUT pas
     varier d'une vignette à l'autre (voir le décalage de phase plus haut) :
     la mettre à un seul endroit, c'est rendre la dérive impossible.
     ⚠ Si on y touche, les pourcentages des deux familles de keyframes
     (9,524 % et 19,048 %) sont faux — ils valent 1/10,5 et 2/10,5. */
  .fb-accueil { --fb-vignette-cycle: 10.5s; }

  /* Seul l'accueil joue la séquence. Le bouton « Me soutenir » du pied de page
     porte les mêmes classes (section 8) : sans ce `.fb-accueil`, il s'animerait
     lui aussi, en bas de toutes les pages du site, indéfiniment. */

  /* Famille 1 s — les deux spritesheets courtes. */
  .fb-accueil .fb-btn--shop,
  .fb-accueil .fb-btn--me-soutenir {
    --fb-vignette-defile: fb-vignette-defile-1s;
    --fb-vignette-allume: fb-vignette-allume-1s;
    --fb-vignette-eteint: fb-vignette-eteint-1s;
  }

  /* Famille 2 s — les trois longues. */
  .fb-accueil .fb-btn--portfolio,
  .fb-accueil .fb-btn--a-propos,
  .fb-accueil .fb-btn--contact {
    --fb-vignette-defile: fb-vignette-defile-2s;
    --fb-vignette-allume: fb-vignette-allume-2s;
    --fb-vignette-eteint: fb-vignette-eteint-2s;
  }

  /* Puis, par vignette, les deux seules choses qui lui soient propres : son
     nombre d'images et sa place dans le cycle. */
  .fb-accueil .fb-btn--shop {
    --fb-vignette-pas: steps(6, jump-none);
    --fb-vignette-retard: 0s;
  }
  .fb-accueil .fb-btn--portfolio {
    --fb-vignette-pas: steps(22, jump-none);
    --fb-vignette-retard: 1.5s;
  }
  .fb-accueil .fb-btn--a-propos {
    --fb-vignette-pas: steps(17, jump-none);
    --fb-vignette-retard: 4s;
  }
  .fb-accueil .fb-btn--contact {
    --fb-vignette-pas: steps(33, jump-none);
    --fb-vignette-retard: 6.5s;
  }
  .fb-accueil .fb-btn--me-soutenir {
    --fb-vignette-pas: steps(4, jump-none);
    --fb-vignette-retard: 9s;
  }

  .fb-accueil .fb-btn__anim {
    animation:
      var(--fb-vignette-defile) var(--fb-vignette-cycle) var(--fb-vignette-pas) var(--fb-vignette-retard) infinite,
      var(--fb-vignette-allume) var(--fb-vignette-cycle) step-end              var(--fb-vignette-retard) infinite;
  }

  .fb-accueil .fb-btn__base {
    animation: var(--fb-vignette-eteint) var(--fb-vignette-cycle) step-end var(--fb-vignette-retard) infinite;
  }

  /* LE DÉFILEMENT. Le travail se fait dans le premier segment ; les deux
     autres ne servent qu'à ramener le fond à 0 % et à l'y tenir pendant les
     9,5 s (ou 8,5 s) restantes. Le retour se fait sur 0,001 % de la période,
     soit un dixième de milliseconde — invisible, et surtout pas de 100 % à 0 %
     en fondu, ce qui ferait défiler la spritesheet à l'envers pendant tout le
     reste du cycle.
     ⚠ LE `steps()` DU RACCOURCI S'APPLIQUE À CHACUN DES TROIS SEGMENTS, pas à
     l'animation entière. C'est ce qui permet de partager ces keyframes entre
     des vignettes de 6 et de 4 images : découper en 6 ou en 4 un segment où
     `background-position-x` ne bouge pas donne le même résultat. */
  @keyframes fb-vignette-defile-1s {
    0%       { background-position-x: 0%; }
    9.524%   { background-position-x: 100%; }
    9.525%   { background-position-x: 0%; }
    100%     { background-position-x: 0%; }
  }
  @keyframes fb-vignette-defile-2s {
    0%       { background-position-x: 0%; }
    19.048%  { background-position-x: 100%; }
    19.049%  { background-position-x: 0%; }
    100%     { background-position-x: 0%; }
  }

  /* LES DEUX INTERRUPTEURS. Deux arrêts par valeur (`0%, 9.524%`) et non un
     seul : c'est ce qui MAINTIENT la valeur sur tout le segment au lieu de
     l'interpoler. Le `step-end` du raccourci rend de toute façon tout fondu
     impossible, mais l'écrire ainsi dit l'intention. */
  @keyframes fb-vignette-allume-1s {
    0%, 9.524%     { opacity: 1; }
    9.525%, 100%   { opacity: 0; }
  }
  @keyframes fb-vignette-eteint-1s {
    0%, 9.524%     { opacity: 0; }
    9.525%, 100%   { opacity: 1; }
  }
  @keyframes fb-vignette-allume-2s {
    0%, 19.048%    { opacity: 1; }
    19.049%, 100%  { opacity: 0; }
  }
  @keyframes fb-vignette-eteint-2s {
    0%, 19.048%    { opacity: 0; }
    19.049%, 100%  { opacity: 1; }
  }
}

/* --------------------------------------------------------------------------
   L'accueil occupe l'écran, le pied de page attend juste en dessous.
   --------------------------------------------------------------------------
   C'est une page d'atterrissage : la composition doit avoir l'écran entier.
   Le contenu mesure donc exactement une hauteur de fenêtre MOINS la déchirure,
   qui vient affleurer le bas de l'écran. Le seul défilement possible fait
   remonter le bandeau noir — environ 80 px de course, un cran de molette.

   POURQUOI LAISSER DÉPASSER LA DÉCHIRURE, ET PAS CACHER TOUT LE PIED DE PAGE.
   Un défilement que rien n'annonce est un pied de page que personne n'ouvre —
   or il porte l'e-mail, le téléphone et l'inscription à la newsletter, et
   l'accueil n'a par ailleurs AUCUNE raison visible de défiler. Le trait
   déchiré est le seul élément du site capable de dire « il y a une suite »
   sans qu'on ajoute une flèche ou le mot « scroll », qui trahiraient la
   sobriété recherchée. Il est dessiné comme quelque chose qu'on ouvre.

   `100svh` — la PLUS PETITE hauteur de fenêtre, barres d'interface déployées.
   Avec `100vh` (la plus grande), la déchirure tomberait sous le bord de
   l'écran tant que la barre d'URL est visible : l'invitation disparaîtrait
   exactement dans le cas où elle sert. `min-width: 768px` ne suffit pas à
   écarter le problème, une tablette en paysage passe la condition.

   Deux garde-fous dans la condition :
   • `min-width: 768px` — sur mobile la composition défile normalement.
   • `min-height: 620px` — sur une fenêtre très basse (portable 13 pouces avec
     la barre d'onglets, écran en paysage), fixer la hauteur couperait le
     contenu au lieu de le centrer. Mieux vaut alors laisser défiler.

   ⚠ `overflow: hidden` sur `body.home` a disparu, et avec lui la suppression
   de la barre de défilement. Sur Windows elle est de nouveau permanente sur
   cette page. C'est le prix assumé du geste ; `scrollbar-gutter` ne le règle
   pas, il réserve la place au lieu de la rendre.
   -------------------------------------------------------------------------- */

@media (min-width: 768px) and (min-height: 620px) {
  /* LE PIED FIXE A SIMPLIFIÉ CETTE PAGE.
     Avant, on visait à laisser dépasser la seule déchirure en bas de l'écran,
     comme invitation à défiler d'un cran. Le pied étant maintenant fixe, la
     bande est visible en permanence : il n'y a plus rien à annoncer, ni aucune
     raison de défiler. La composition prend exactement la place qui reste.

     `padding-bottom: 0` annule la réservation faite sur `body` (section 2) :
     ici c'est `.site-main` qui porte la hauteur, la compter deux fois
     provoquerait un défilement de la hauteur du pied.

     `min-height: 0` est indispensable : sans lui, un élément flex refuse de
     descendre sous la taille de son contenu et déborderait quand même. */
  body.home { padding-bottom: 0; }

  body.home .site-main {
    height: calc(100vh - var(--fb-pied-h, 90px));  /* repli : navigateurs d'avant 2022 */
    height: calc(100svh - var(--fb-pied-h, 90px));
    display: flex;
    min-height: 0;
  }

  body.home .fb-accueil {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* --------------------------------------------------------------------------
   L'ACCUEIL SUR TÉLÉPHONE — 2 par ligne, NEWS en barre, FABIAN dessous
   --------------------------------------------------------------------------
   Tout ce qui suit est dans un `max-width` : au-dessus de 700 px, la page
   d'accueil est celle de la maquette ACCUEIL.jpg, au pixel près, et rien
   ici ne l'atteint. 700 px est la couture du site (la boutique y passe à deux
   colonnes) : la largeur à partir de laquelle le contenu occupe tout l'écran.

   CE QUI NE MARCHAIT PAS. La rangée de vignettes est une grille `auto-fit`
   dont la colonne vaut « un cinquième de la largeur disponible ». Sur un
   téléphone de 390 px, ça fait des vignettes de 65 px : cinq timbres-poste
   alignés, dont les dessins de Fabian ne se lisent plus. Et le titre, posé en
   ligne à côté de FABIAN, se partageait la même largeur avec lui.

   Fabian, 7 octobre 2026 : « 2 tuiles par ligne, avec la bulle News toute
   seule en bas, en bouton allongé comme sur la page liens », « mettre Fabian
   en dessous (comme ça il a plus de place) et l'agrandir ».
   -------------------------------------------------------------------------- */

@media (max-width: 700px) {

  /* ── LE TITRE EN DEUX LIGNES ─────────────────────────────────────────────
     `column` et non plus `row`. Les deux dessins ne se disputent plus la
     largeur : chacun a la sienne, donc chacun grandit.

     Les deux largeurs descendent d'une seule variable, et c'est la condition
     pour que le dessin de Fabian garde ses proportions — la note de la version
     grand écran le dit déjà : ne réduire qu'un des deux change les proportions
     du dessin, pas sa taille.

     ⚠ DEUX CHIFFRES ONT CHANGÉ LE 20 OCTOBRE 2026, ET LE PREMIER EN EST UN
     QU'ON VENAIT DE POSER. Fabian avait demandé le 7 octobre « hey moi c'est
     fabian en grand, prendre toute la largeur max » ; devant le résultat, il
     corrige : « le titre doit prendre 2/3 de la largeur » et « le FABIAN doit
     faire la même taille qu'au-dessus ». Les deux demandes vont ensemble —
     c'est parce que FABIAN passe à pleine largeur que le bloc entier doit
     redescendre, sans quoi le nom mangerait l'écran à lui seul.

     LE FACTEUR 0,85 A DISPARU, ET VOICI CE QU'IL COÛTAIT.
     Les deux dessins font 175 px de haut dans le fichier ; HEY fait 434 de
     large, FABIAN 294. Posés côte à côte, FABIAN reçoit 294/434 = 0,677 de la
     largeur de HEY, et les deux ont alors EXACTEMENT la même hauteur d'encre —
     c'est la composition d'origine, et c'est voulu tant qu'ils sont sur la même
     ligne. Empilés, cette égalité n'a plus de raison d'être, et `0.85` était une
     tentative de moyen terme : FABIAN un quart plus haut que la phrase, mais
     pas tout à fait aussi large. Un moyen terme ne se lit pas comme une
     intention, il se lit comme un alignement raté — c'est ce que Fabian a vu.

     À MÊME LARGEUR, LES DEUX BORDS SONT LES MÊMES, et le rapport des hauteurs
     d'encre devient 434/294 = 1,476 : le nom est une fois et demie plus haut
     que la phrase qui l'introduit. C'est la proportion que donne le dessin
     lui-même, pas un réglage.

     LES CHIFFRES À 390 PX DE LARGE : conteneur 350 px (390 moins deux
     gouttières de 20), titre 199 px, HEY 80 px de haut, FABIAN 118. Le bloc
     entier fait 199 × 167 (après la remontée décrite plus bas) au lieu de
     350 × 318 — plus du tiers d'un écran
     rendu aux vignettes, qui sont le sujet de la page.

     ⚠ LA FRACTION EST PASSÉE DE 2/3 À 0,57 LE 20 OCTOBRE 2026, en même temps
     que le titre du grand écran et pour la même demande de Fabian (« le titre
     un peu plus petit »). Le téléphone n'était pas visé par sa remarque, mais
     laisser les deux tailles diverger ferait deux compositions différentes
     pour un seul dessin : le titre vaut 57 % de la largeur utile ici comme il
     vaut 0,64 de sa taille nominale là-bas.

     Le plafond de 310 px empêche le titre de devenir énorme entre 560 et
     700 px. Il ne mord qu'à partir de 544 px de large (310 / 0,57), là où il
     mordait dès 400 : au-dessous, c'est la fraction qui commande. */
  /* ⚠ `width: 100%` ET `container-type`, ET LES DEUX SONT INDISPENSABLES.
     Relevé à 390 px : sans eux, le titre mesurait 0 × 3 px et les deux dessins
     avaient disparu de la page. Ce n'est pas un défaut de chargement, c'est une
     boucle de calcul.

     LA BOUCLE. `.fb-accueil__logo` est un enfant de flex en colonne avec
     `align-items: center` : sa largeur n'est pas imposée, elle vaut la largeur
     de son contenu. Tant que les deux dessins étaient côte à côte avec des
     largeurs en pixels, cette largeur se calculait. En passant à
     `min(100%, 360px)`, chaque enfant demande « 100 % de mon parent » à un
     parent dont la largeur vaut « ce que demandent mes enfants ».
     Le navigateur ne boucle pas : il tranche. Un pourcentage dont le conteneur
     n'a pas de taille définie se résout à `auto`, donc ici à ZÉRO — un `<span>`
     vide n'a pas de contenu pour le remplir. Rien ne s'affiche et rien ne
     prévient : c'est une largeur légitime, pas une erreur de syntaxe.

     `width: 100%` donne au titre la largeur de la page et coupe la boucle.

     POURQUOI `cqw` ET NON `%` POUR LA SUITE. Il reste `round(down, …, 1px)` sur
     `.fb-fabian`, qui est une spritesheet (fabian-loop.png, 1764 × 175, six
     images de 294) : une largeur fractionnaire décale chaque image et fait
     baver l'animation d'un demi-pixel — c'est la raison d'être de cet arrondi,
     expliquée en section 2. Or `round()` veut des longueurs, et un pourcentage
     n'en est pas une tant que le conteneur n'est pas résolu.
     `container-type: inline-size` transforme le titre en conteneur de requête :
     `100cqw` est alors une LONGUEUR, en pixels, que `round()` sait traiter.
     C'est le même mécanisme que `.fb-accueil__nav` et `.fb-carte__media`.

     ⚠ `container-type: inline-size` applique `contain: layout style
     inline-size` : la largeur de l'élément ne peut plus dépendre de son
     contenu. C'est précisément ce qu'on veut ici — et c'est aussi pourquoi
     `width: 100%` n'est pas facultatif, mais sa condition. */
  .fb-accueil__logo {
    width: 100%;
    container-type: inline-size;
    --fb-accueil-logo-l: min(calc(100cqw * 0.57), 310px);

    flex-direction: column;
    align-items: center;
    /* Resserré comme sur grand écran : en colonne, l'écart sépare deux lignes
       d'une même phrase manuscrite. De 3,1 px à 1 px sur un 390. */
    gap: clamp(0.05rem, 0.25vw, 0.12rem);
  }
  /* ⚠ LES DEUX SONT ARRONDIS, ET PAS SEULEMENT CELUI QUI S'ANIME.
     `round()` n'est là que pour la spritesheet de FABIAN (voir la note des
     largeurs entières, section 9) ; HEY est une image fixe et s'en passerait.
     Mais ils ont maintenant la MÊME largeur, et c'est tout l'objet de la
     demande : si l'un tombait à 233 px et l'autre à 233,33, les deux bords ne
     seraient plus alignés d'un tiers de pixel — assez pour que le navigateur
     rende les deux contours autrement, sur un dessin au trait. On arrondit donc
     les deux à la même valeur entière, et l'alignement est exact.

     Chaque règle est écrite en deux temps, comme partout dans cette feuille :
     un navigateur sans `round()` ignore la seconde et garde la première. */
  .fb-accueil__hey { width: var(--fb-accueil-logo-l); }
  .fb-accueil__hey { width: round(down, var(--fb-accueil-logo-l), 1px); }

  /* ⚠ ET IL REMONTE SUR « HEY! », PARCE QUE LE `gap` NE SUFFIT PAS ICI.
     En colonne, les deux dessins sont séparés de 1 px de CSS et de 42 px de
     VIDE — celui que les deux PNG portent à l'intérieur d'eux-mêmes. Mesuré :
     l'encre de `hey.png` s'arrête à 140 px sur 175 (35 px de transparent
     dessous) et celle de FABIAN commence à 39 px sur 175. Ramenés à 199 px de
     large, cela fait 16 + 26 px d'air que rien dans la feuille ne décrit.

     Sur grand écran le problème ne se pose pas : les deux dessins sont côte à
     côte, et ce sont leurs marges LATÉRALES qui jouent — 3 et 10 px, soit un
     écart optique de 18 px pour 4,8 px de `gap`. C'est le même dessin, mais
     pas les mêmes bords : d'où une correction propre au téléphone.

     ⚠ EN FRACTION DE LA LARGEUR DU TITRE, PAS EN PIXELS. Les deux réserves
     transparentes sont proportionnelles au dessin ; une valeur en dur serait
     juste à 390 px et fausse partout ailleurs. −0,165 laisse ≈ 11 px d'air
     optique à 390, et le bloc du titre passe de 199 × 200 à 199 × 167. */
  .fb-accueil .fb-fabian { width: var(--fb-accueil-logo-l); }
  .fb-accueil .fb-fabian { width: round(down, var(--fb-accueil-logo-l), 1px); }
  .fb-accueil .fb-fabian { margin-top: calc(var(--fb-accueil-logo-l) * -0.165); }

  /* ⚠ L'ÉTIQUETTE NE TOURNE PLUS SUR L'ACCUEIL — SUR TÉLÉPHONE SEULEMENT.
     Fabian, 20 octobre 2026 : « enlever l'anim' de Fabian ». Le 21 octobre il
     précise qu'il regardait le téléphone : sur ordinateur, la boucle reste.
     D'où une règle logée ici et pas dans le bloc de base — au-dessus de
     700 px, rien ne la voit et `_sprites.css` anime comme avant.

     Les six variantes continuent d'exister et de servir — `--fb-fabian-frame`
     en tire une au sort par visite (inc/identite.php), exactement comme pour
     le logo de l'en-tête figé plus haut. Ce n'est donc pas « toujours la
     même » : c'est « une seule à la fois ».

     ⚠ ET CETTE RÈGLE NE DÉPEND PAS DE `.fb-identite-js`, CONTRAIREMENT À
     CELLE DE L'EN-TÊTE. Là-haut, sans JavaScript, la boucle reprend : c'est
     l'ancien comportement, pas une page cassée. Ici, la boucle est précisément
     ce dont Fabian ne veut plus — elle doit donc s'arrêter même si le script
     n'a pas tourné, d'où le repli `0%` écrit dans le `var()`. */
  .fb-accueil .fb-fabian {
    animation: none;
    background-position-x: var(--fb-fabian-frame, 0%);
  }

  /* ── DEUX VIGNETTES PAR LIGNE ────────────────────────────────────────────
     On reprend mot pour mot le calcul de la grande grille, avec 2 au lieu de
     `--fb-nb-vignettes` : la largeur du conteneur, moins l'écart unique qui
     sépare les deux colonnes, divisée par deux, arrondie au pixel inférieur.
     L'arrondi sert ici aussi — sur écran tactile, c'est la spritesheet qui est
     affichée (sa première image), et un fond redimensionné à une largeur
     fractionnaire est rééchantillonné de travers.

     La première déclaration est le repli pour un navigateur sans `round()`,
     comme partout ailleurs dans cette feuille.

     `--fb-nb-vignettes` continue de servir : c'est lui qui plafonne la largeur
     de `.fb-accueil__nav`, et donc la taille des vignettes. On n'y touche pas. */
  .fb-vignettes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-columns: repeat(2, round(down, (100cqw - var(--fb-ecart)) / 2, 1px));

    /* Plus d'air entre les rangées qu'entre les colonnes : deux vignettes
       côte à côte se lisent comme une paire, deux rangées empilées comme une
       liste, et une liste a besoin qu'on voie où elle respire. */
    row-gap: clamp(0.6rem, 2.4vw, 1rem);
  }

  /* ── NEWS, SEULE ET ALLONGÉE, TOUT EN BAS ────────────────────────────────
     NEWS est le PREMIER élément du DOM (front-page.php le place en tête pour
     que l'ordre de lecture et l'ordre de tabulation commencent par lui sur
     grand écran). `order: 1` le renvoie après les quatre autres, qui restent
     à 0 : la grille le place donc en dernier, et `grid-column: 1 / -1` lui
     donne la rangée entière. Aucun balisage dupliqué, aucun élément caché.

     ⚠ L'ORDRE VISUEL ET L'ORDRE DE TABULATION DIVERGENT ICI. Au clavier, NEWS
     reste le premier atteint alors qu'il est affiché en dernier. C'est le
     défaut connu de `order`, et il est assumé : cinq boutons de navigation
     dans une page qui ne contient qu'eux, l'écart est d'un cran et ne perd
     personne. L'alternative — deux balisages, un caché — coûterait un lien
     fantôme pour les lecteurs d'écran, ce qui est pire. */
  .fb-vignettes__item--news {
    order: 1;
    grid-column: 1 / -1;
  }

  /* ⚠ CE BOUTON A SON PROPRE DESSIN, ET C'EST LE SEUL DE L'ACCUEIL DANS CE CAS.
     Les cinq autres vignettes sont carrées : le même fichier sert en grand sur
     l'écran large et en petit sur le téléphone, c'est la même image réduite.
     NEWS, non. Sous 700 px il prend une rangée entière et devient une BARRE de
     832 × 140 ; l'étoile carrée de l'écran large, étirée là-dedans, ne donne
     rien de regardable. Il a donc fallu un second dessin.

     Ce bouton a connu deux provisoires avant celui-ci, et les deux disent
     quelque chose. Le 7 octobre, faute de dessin, on avait emprunté la barre
     « NEWSLETTER » de la page de liens : bon format, bonne couleur — mais elle
     avait l'air finie et disait le mauvais mot, donc on cherchait l'erreur
     ailleurs. Le 20 octobre au matin, elle a été remplacée par un cadre en
     pointillés portant « NEWS — bouton à dessiner — 832 × 140 », précisément
     pour que le trou se voie comme un trou. ⚠ IL A TENU UN APRÈS-MIDI : Fabian
     a livré le dessin le soir même. C'est, à peu de chose près, le seul effet
     qu'on attend d'un placeholder honnête.

     ⚠ CE N'EST PAS UNE SPRITESHEET. Les cinq autres vignettes déroulent une
     séquence d'images au survol (voir `_sprites.css`) ; celle-ci a deux états
     fixes, repos et survol, comme le bouton « retour ». Rien à désamorcer côté
     animation : NEWS n'a jamais eu de `--fb-vignette-*`, donc rien ne se joue
     tout seul sur téléphone non plus. Le `background-size` est réécrit quand
     même, parce que `_sprites.css` pose `100% 100%` sur `.fb-btn--news
     .fb-btn__anim` — exact tant que l'image fait 832 × 140, déformant dès
     qu'on en pose une autre.

     ⚠ ET ON PEUT EN POSER UNE AUTRE : LES DEUX URL SONT DES VARIABLES.
     Thomas, 20 octobre 2026 : « il faut que le bouton mobile news puisse être
     édité dans les paramètres du thème ». Les deux dessins ci-dessous sont le
     DÉFAUT, pas une fatalité — Apparence → Personnaliser → Fabian Branas pose
     les réglages `fb_news_image_mobile` et `fb_news_image_mobile_survol`, que
     `fb_news_css()` (inc/enqueue.php) injecte en redéfinissant ces deux
     variables. C'est prévu pour « MA PAGE WEMAKEIT ! », livré le même jour au
     même format, que Thomas posera lui-même le moment venu.

     Les variables sont déclarées sur l'ÉLÉMENT DE LISTE et pas sur `.fb-btn` :
     les deux calques sont enfants du bouton, une propriété personnalisée
     descend l'arbre, et c'est le point d'accroche le plus haut qui reste
     propre à NEWS. Voir la note sur `--fb-nav-ecart` (section 3) pour la fois
     où on l'a appris à ses dépens. */
  .fb-vignettes__item--news {
    --fb-news-mobile-base: url("../img/boutons/news-mobile-base.png");
    --fb-news-mobile-survol: url("../img/boutons/news-mobile-survol.png");
  }

  .fb-vignettes__item--news .fb-btn { aspect-ratio: 832 / 140; }

  .fb-vignettes__item--news .fb-btn__base {
    background-image: var(--fb-news-mobile-base);
  }
  /* ⚠ `.fb-btn .fb-btn__anim` ET NON `.fb-btn__anim` SEUL : UN SÉLECTEUR EN
     TROP, ET IL EST INDISPENSABLE. `_sprites.css` déclare
     `.fb-btn--news .fb-btn__anim { background-image: …/news.png }` — même
     spécificité que la règle naturelle ici (0,2,0), et ce fichier est mis en
     file APRÈS style.css (inc/enqueue.php), donc il gagne. Sans ce `.fb-btn`
     intermédiaire, le calque de survol du téléphone affichait l'ÉTOILE CARRÉE,
     étirée en barre par le `background-size: 100% 100%` de la spritesheet.
     Constaté à la mesure, pas à l'œil : le survol n'existe pas sur un écran
     tactile, le défaut ne se serait vu que sur une tablette avec souris. */
  .fb-vignettes__item--news .fb-btn .fb-btn__anim {
    background-image: var(--fb-news-mobile-survol);
    background-size: contain;
    background-position: center;
  }

  /* ── « ME SOUTENIR » AU CENTRE ───────────────────────────────────────────
     Il était aligné à droite, sur le bord de la dernière vignette de la
     rangée de cinq. Cette rangée n'existe plus : à deux colonnes, le bord
     droit n'est plus l'alignement de rien. Fabian, 7 octobre 2026 : « le
     bouton soutenir centré en dessous des liens ». Même geste que pour le bloc
     social (section 8), et pour la même raison. */
  /* ⚠ ET IL S'ÉCARTE DE NEWS, QUI EST JUSTE AU-DESSUS DE LUI ICI.
     Sur grand écran, « Me soutenir » suit une rangée de vignettes carrées et
     l'écart de la grille (`--fb-ecart`, ≈ 5,6 px à 390) suffit à le détacher.
     Sous 700 px, NEWS quitte la rangée pour devenir une BARRE pleine largeur
     posée juste au-dessus : deux boutons larges et plats séparés par 5,6 px se
     lisent comme un seul bloc à deux étages. Fabian, 20 octobre 2026 : « le
     bouton me soutenir est trop proche du bouton news ».

     La marge est écrite ICI et pas sur `--fb-ecart` : cet écart-là est celui
     de la grille des vignettes, et l'augmenter rétrécirait les six dessins
     pour corriger un espacement qui ne les concerne pas. */
  .fb-soutenir-ligne {
    justify-content: center;
    margin-top: clamp(0.75rem, 4vw, 1.25rem);
  }

  /* ⚠ ET IL FALLAIT AUSSI L'AGRANDIR, CE QUI N'ÉTAIT PAS DEMANDÉ.
     Relevé à la capture, 20 octobre 2026 : 84 × 22 px sous des vignettes de
     172 × 172. Sa largeur est `--fb-bouton`, c'est-à-dire UN CINQUIÈME de la
     rangée — la taille d'une vignette quand il y en avait cinq par ligne.
     Les vignettes sont passées à deux par ligne le 20 octobre et ont doublé ;
     lui est resté à l'ancienne échelle, parce que rien ne le liait à la
     grille. Personne ne l'a signalé : un bouton trop petit s'affiche
     correctement, il est juste illisible et presque intouchable (22 px de haut
     contre les 44 recommandés).

     On lui donne LA MÊME EXPRESSION que les colonnes de la grille, et pas la
     valeur qu'elle produit : écrire « 172px » serait juste aujourd'hui et faux
     au prochain réglage de `--fb-ecart`. Une colonne exactement, donc 45 px de
     haut (172 × 99/380) — un bouton de la largeur d'une vignette, posé sous
     elles, ce qui est aussi la composition de la maquette en plus petit. */
  .fb-btn--me-soutenir {
    width: calc((100cqw - var(--fb-ecart)) / 2);
    width: round(down, (100cqw - var(--fb-ecart)) / 2, 1px);
  }

  /* ── LE PIED DE PAGE REDESCEND SOUS LA LIGNE DE FLOTTAISON ───────────────
     Fabian, 20 octobre 2026 : « le footer ne doit pas être fixe, il est en bas
     et on le voit seulement si on scroll ». La demande ne visait alors que
     l'accueil ; elle a été étendue à toutes les pages le soir même (Thomas :
     « corrige partout »), et c'est la section 8 qui porte maintenant le
     `position: static`.

     CE QUI AVAIT ÉTÉ ÉCRIT ICI RESTE VRAI, ET EXPLIQUE L'EXTENSION. Un pied
     fixe se paie en hauteur permanente : ~89 px sur un téléphone depuis qu'il
     empile ses quatre lignes, 99 depuis qu'il respire (voir le budget chiffré,
     section 8), soit 12 % de l'écran. On avait justifié l'exception en disant
     que sur une page de contenu c'était le prix de l'accès à l'e-mail et au
     téléphone, et que seule une page d'atterrissage tenant en un écran ne
     pouvait pas le payer. ⚠ L'ARGUMENT NE RÉSISTE PAS À SA PROPRE ARITHMÉTIQUE :
     12 % coûtent la même chose partout, et l'accès au pied s'obtient en bas de
     page, où tout le monde sait aller.

     ⚠ IL NE RESTE QUE DEUX RÈGLES ICI, ET C'EST LE SIGNE QUE LA DEMANDE A
     ÉTÉ GÉNÉRALISÉE. Le `position: static` qui ouvrait ce bloc a déménagé
     dans la section 8 : le pied n'est plus fixe sur AUCUNE page de téléphone
     (Thomas, le soir du même jour). L'accueil garde quand même ses règles
     propres — pas pour sortir le pied du flux, c'est fait, mais pour garantir
     qu'il tombe bien SOUS la ligne de flottaison même quand la composition est
     plus courte que l'écran.

     1. `margin-top: 0` annule l'air que la section 8 laisse au-dessus du pied.
        Ailleurs il sépare le dernier contenu de la déchirure ; ici la
        composition occupe déjà toute la hauteur de l'écran, et cet air ne
        ferait que décaler le pied d'un cran de plus vers le bas — la
        déchirure cesserait d'affleurer exactement la ligne de flottaison.
        ⚠ C'est la règle qu'on oublie : son effet est à l'autre bout de la
        page, loin d'ici.

     2. `min-height: 100svh` sur le contenu est ce qui fait la promesse du
        « seulement si on scroll » : sans elle, une composition plus courte que
        l'écran remonterait le pied dans le champ, et il serait visible sans
        défiler — exactement ce qu'on vient de défaire. `svh` et non `vh` : la
        plus petite hauteur de fenêtre, barres d'interface déployées, pour que
        la mesure vaille aussi quand la barre d'URL est là. C'est le même choix
        qu'au-dessus de 768 px, pour la raison inverse.

     `flex: 1 1 auto` laisse la composition occuper cette hauteur et le
     `justify-content: center` de `.fb-accueil` la centre dedans. */
  body.home .fb-pied { margin-top: 0; }

  body.home .site-main {
    display: flex;
    min-height: 100vh;   /* repli : navigateurs d'avant 2022 */
    min-height: 100svh;
  }

  body.home .fb-accueil { flex: 1 1 auto; }
}

/* ==========================================================================
   10. MASQUES DE DÉCOUPE
   --------------------------------------------------------------------------
   Applique une des 16 formes dessinées par Fabian à n'importe quelle image
   téléversée dans WordPress. La forme est choisie côté PHP à partir de
   l'identifiant du contenu : elle est donc STABLE (la même vignette garde sa
   forme d'un chargement à l'autre) tout en variant d'un produit à l'autre.
   ========================================================================== */

.fb-masque {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

.fb-masque--01 { -webkit-mask-image: url("../img/masques/masque-01.png"); mask-image: url("../img/masques/masque-01.png"); }
.fb-masque--02 { -webkit-mask-image: url("../img/masques/masque-02.png"); mask-image: url("../img/masques/masque-02.png"); }
.fb-masque--03 { -webkit-mask-image: url("../img/masques/masque-03.png"); mask-image: url("../img/masques/masque-03.png"); }
.fb-masque--04 { -webkit-mask-image: url("../img/masques/masque-04.png"); mask-image: url("../img/masques/masque-04.png"); }
.fb-masque--05 { -webkit-mask-image: url("../img/masques/masque-05.png"); mask-image: url("../img/masques/masque-05.png"); }
.fb-masque--06 { -webkit-mask-image: url("../img/masques/masque-06.png"); mask-image: url("../img/masques/masque-06.png"); }
.fb-masque--07 { -webkit-mask-image: url("../img/masques/masque-07.png"); mask-image: url("../img/masques/masque-07.png"); }
.fb-masque--08 { -webkit-mask-image: url("../img/masques/masque-08.png"); mask-image: url("../img/masques/masque-08.png"); }
.fb-masque--09 { -webkit-mask-image: url("../img/masques/masque-09.png"); mask-image: url("../img/masques/masque-09.png"); }
.fb-masque--10 { -webkit-mask-image: url("../img/masques/masque-10.png"); mask-image: url("../img/masques/masque-10.png"); }
.fb-masque--11 { -webkit-mask-image: url("../img/masques/masque-11.png"); mask-image: url("../img/masques/masque-11.png"); }
.fb-masque--12 { -webkit-mask-image: url("../img/masques/masque-12.png"); mask-image: url("../img/masques/masque-12.png"); }
.fb-masque--13 { -webkit-mask-image: url("../img/masques/masque-13.png"); mask-image: url("../img/masques/masque-13.png"); }
.fb-masque--14 { -webkit-mask-image: url("../img/masques/masque-14.png"); mask-image: url("../img/masques/masque-14.png"); }
.fb-masque--15 { -webkit-mask-image: url("../img/masques/masque-15.png"); mask-image: url("../img/masques/masque-15.png"); }
.fb-masque--16 { -webkit-mask-image: url("../img/masques/masque-16.png"); mask-image: url("../img/masques/masque-16.png"); }

/* Aucune image téléversée : on montre la forme pleine, comme sur la maquette
   BASE.jpg où plusieurs tuiles sont des aplats noirs. */
.fb-masque--vide { background: var(--fb-noir); }

/* LA MÊME DÉCOUPE SUR LA FICHE PRODUIT, SANS LE CARRÉ.
   `.fb-masque` force un carré et recadre (`object-fit: cover`) : juste pour une
   grille, pas pour une fiche où l'on veut voir toute l'image. Ici la forme
   s'étire sur la boîte de l'image, quel que soit son rapport — une affiche en
   portrait garde ses bords, dans une forme un peu allongée. La classe est posée
   par fb_masque_image_fiche() (inc/woocommerce.php) sur chaque diapositive de
   la galerie WooCommerce, avec la forme de la vignette du shop. Le zoom (image
   en absolu dans la même boîte) est découpé avec. */
.fb-masque-fiche {
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

/* ⚠ CINQ FORMES ICI, SEIZE PLUS HAUT, ET CE NE SONT PAS LES MÊMES FICHIERS.
   La fiche a repris les seize masques de vignette pendant trois semaines — même
   `fb_classe_masque()`, donc la forme exacte de la tuile cliquée. C'était joli
   comme idée et raté à l'écran : ces seize-là sont dessinés pour des tuiles de
   150 à 200 px, et l'image de la fiche en fait le triple. Un bord irrégulier
   agrandi trois fois n'est plus un bord irrégulier, c'est une entaille.

   Fabian en a livré cinq tracés pour cette taille (30 septembre 2026, 1036 px
   de côté, réduits à 900 par le build contre 600 pour les petits — l'image monte
   à ~450 px dans sa colonne, et au double en zoom ×2).

   Le nombre est écrit à trois endroits, et il faut les trois : ici (cinq
   règles), dans FB_NB_MASQUES_FICHE (inc/helpers.php, pour la répartition) et
   dans la boucle de tools/build-assets.sh (qui, elle, compte les fichiers
   livrés). Le jour où il y en a six, seul le troisième s'en aperçoit tout seul. */
.fb-masque-fiche--01 { -webkit-mask-image: url("../img/masques/fiche-01.png"); mask-image: url("../img/masques/fiche-01.png"); }
.fb-masque-fiche--02 { -webkit-mask-image: url("../img/masques/fiche-02.png"); mask-image: url("../img/masques/fiche-02.png"); }
.fb-masque-fiche--03 { -webkit-mask-image: url("../img/masques/fiche-03.png"); mask-image: url("../img/masques/fiche-03.png"); }
.fb-masque-fiche--04 { -webkit-mask-image: url("../img/masques/fiche-04.png"); mask-image: url("../img/masques/fiche-04.png"); }
.fb-masque-fiche--05 { -webkit-mask-image: url("../img/masques/fiche-05.png"); mask-image: url("../img/masques/fiche-05.png"); }

/* ==========================================================================
   COMPOSANTS PARTAGÉS — barre de filtres et grille
   --------------------------------------------------------------------------
   Fabian a dessiné le MÊME système pour le Portfolio et pour la Boutique :
   des mots en trois états, et une grille de tuiles découpées. Ces deux blocs
   servent donc aux deux pages, sans duplication.

   Les règles propres à chaque filtre (largeurs, images) sont dans _sprites.css,
   générées par le build : « ILLUSTRATION » fait 270 px de large, « BD » 85 —
   chacun a son propre rapport largeur/hauteur.
   ========================================================================== */

.fb-filtres {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(0.5rem, 1.6vw, 1.4rem);
  margin-bottom: clamp(1.5rem, 4vw, 2.75rem);
}

/* Toutes les images de filtre ont la même hauteur : c'est elle qu'on fixe, la
   largeur suit toute seule par `aspect-ratio`. Les pilules s'alignent ainsi
   quelle que soit la longueur du mot. */
.fb-filtre { height: clamp(22px, 2.6vw, 34px); }

/* La croix de remise à zéro est un pictogramme carré, plus petit que les mots. */
.fb-filtre--portfolio-annulation,
.fb-filtre--shop-annulation { height: clamp(15px, 1.8vw, 22px); }

/* --------------------------------------------------------------------------
   La grille
   -------------------------------------------------------------------------- */

.fb-grille {
  display: grid;
  /* 140 px de minimum : en dessous, un téléphone de 390 px n'afficherait qu'une
     seule colonne, et les vignettes deviendraient énormes. */
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: clamp(0.75rem, 2vw, 1.5rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

.fb-tuile { position: relative; }

.fb-tuile__lien {
  display: block;
  text-decoration: none;
}

/* Le conteneur des DEUX calques d'image. `position: relative` sans quoi le
   calque de survol, en `position: absolute`, se calerait sur la fenêtre.
   Voir « L'IMAGE DE SURVOL » plus bas. */
.fb-tuile__media {
  position: relative;
  display: block;
}

/* `.fb-tuile__titre` et `.fb-tuile__type` n'ont plus de règles : ils portent
   maintenant `screen-reader-text` et ne sont plus dessinés. Voir
   archive-portfolio.php pour le pourquoi. */

/* Message quand un filtre ne donne rien, ou que la section est vide. */
.fb-vide {
  padding: clamp(2rem, 6vw, 4rem) 0;
  text-align: center;
  color: var(--fb-gris);
}

/* --------------------------------------------------------------------------
   L'IMAGE DE SURVOL
   --------------------------------------------------------------------------
   Deux images empilées, on passe de l'une à l'autre en opacité. C'est le même
   principe que `.fb-img` (section 5) : les deux calques sont déjà chargés, donc
   la bascule est instantanée et ne clignote pas. Changer le `src` d'une seule
   image aurait provoqué un téléchargement au premier survol — et un trou blanc
   pendant ce téléchargement.

   `:focus-within` autant que `:hover` : la grille se parcourt aussi à la touche
   Tab, et sans ça le dessin qui porte le titre et le prix serait réservé aux
   personnes qui se servent d'une souris.

   Les deux images portent la MÊME classe de masque (voir fb_image_survol) :
   sans quoi la silhouette changerait de forme pendant le fondu.
   -------------------------------------------------------------------------- */

.fb-survol {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--fb-transition);
}

.fb-tuile:hover .fb-survol,
.fb-tuile:focus-within .fb-survol,
.fb-carte:hover .fb-survol,
.fb-carte:focus-within .fb-survol {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   LA PASTILLE DE PRIX — boutique uniquement
   --------------------------------------------------------------------------
   Le rond vert de Fabian (9 septembre 2026, 196 × 196, livré vierge) dans
   l'angle bas-droit de la vignette, avec le prix écrit dedans en Garamond —
   voir fb_prix_pastille() pour l'écriture « 40.– ». Elle apparaît et disparaît
   AVEC l'image de survol : même déclencheur, même transition.

   Un peu moins d'un tiers de la carte, décalée de 4 % vers l'extérieur pour
   mordre sur le bord du masque comme une étiquette collée — pas posée dedans.
   `overflow` de `.fb-carte__media` n'est pas coupé, elle peut dépasser.

   Le prix se dimensionne en `cqw` — la largeur de la vignette, qui est une
   unité de conteneur — pour rester proportionné à la pastille quelle que soit
   la colonne. Le `rem` juste avant est le repli des navigateurs sans unités de
   conteneur. */
.fb-carte__media { container-type: inline-size; }
.fb-carte__pastille {
  position: absolute;
  right: -4%;
  bottom: -4%;
  width: 30%;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity var(--fb-transition);
  /* Une légère rotation, comme une étiquette collée à la main. */
  transform: rotate(-6deg);
}
.fb-carte:hover .fb-carte__pastille,
.fb-carte:focus-within .fb-carte__pastille { opacity: 1; }

/* SUR UN ÉCRAN TACTILE, ELLE EST TOUJOURS LÀ — Fabian, 23 septembre 2026 :
   « idée mobile, shop : toujours la petite pastille de prix ».

   `hover: none` désigne un pointeur qui ne survole pas : un doigt. Sur ces
   appareils, tout ce qui est accroché à `:hover` n'existe tout simplement pas —
   l'image de survol ne s'affiche jamais, et la pastille non plus. Or c'est elle
   qui porte le prix : la grille du shop se retrouvait sans aucun prix visible,
   et il fallait ouvrir chaque fiche pour le connaître.

   `:hover` n'est PAS retiré pour autant : certains appareils ont les deux
   (tablette avec clavier, écran tactile sur un poste fixe), et les deux règles
   se complètent au lieu de se contredire.

   ⚠ Ce n'est pas la même chose qu'un test de largeur. Une fenêtre étroite sur
   un ordinateur garde sa souris et garde donc le survol ; un grand iPad n'en a
   pas. C'est le pointeur qui décide, pas le nombre de pixels. */
/* ⚠ ET AUSSI SOUS 700 PX, MÊME AVEC UNE SOURIS — C'EST LE VRAI DÉFAUT.
   La condition `hover: none` est juste mais elle est incomplète, et l'écart
   s'est vu exactement là où on teste : une fenêtre de navigateur rétrécie sur
   un ordinateur garde sa souris, donc `hover: none` ne s'applique pas, donc ni
   la pastille ni la légende ne s'affichent — la boutique n'y montre AUCUN prix,
   nulle part, tant qu'on ne survole pas. C'est très probablement ce que Fabian
   a vu quand il a redemandé le 7 octobre 2026 « toujours afficher le prix »,
   un an après avoir déjà demandé « toujours la petite pastille de prix »
   (23 septembre).
   Les deux conditions sont donc réunies en une liste : le pointeur OU la
   largeur. Il suffit de l'une des deux. */
@media (hover: none), (max-width: 700px) {
  .fb-carte__pastille { opacity: 1; }
}
/* LA PASTILLE DESSINÉE PAR FABIAN, choisie produit par produit dans la colonne
   de droite de la fiche (champ « Pastille de prix »). Elle remplace la vierge
   et son prix en Garamond. Le test temporaire du 17 septembre — quatre
   pastilles branchées en dur sur les prix 4 et 40 CHF — a servi à trancher ;
   c'est devenu ce champ le 23.

   ⚠ DEUX CLASSES DANS LE SÉLECTEUR, ET C'EST OBLIGATOIRE : `_sprites.css` est
   chargé APRÈS cette feuille et y écrit `.fb-pastille::before`. À une classe
   contre une classe, c'est le dernier qui gagne — la pastille vierge reprenait
   le dessus et celle de Fabian ne s'affichait jamais.

   `visibility: hidden` et non `display: none` sur le prix : le texte reste dans
   la page et garde sa place. S'il fallait le remontrer (image cassée, dessin
   retiré), il n'y a qu'une propriété à reprendre — et les lecteurs d'écran
   n'ont de toute façon jamais lu cette pastille, qui porte `aria-hidden`. */
.fb-pastille.fb-pastille--dessinee::before { background-image: var(--fb-pastille-dessinee); }
.fb-pastille--dessinee .fb-pastille__prix { visibility: hidden; }

/* LE PRIX EST PLUS GRAND DEPUIS LE 6 OCTOBRE 2026 — Fabian : « prix plus grand
   dans la pastille ». Il l'est de deux façons, et la seconde a coûté un essai.

   LA PREMIÈRE EST GRATUITE : le « dès » a quitté la ligne (voir
   `.fb-pastille__des` juste après), et sur un produit variable le montant a donc
   d'un coup toute la largeur du rond au lieu de la moitié.

   LA SECONDE EST UN CALCUL, parce qu'une taille fixe ne pouvait pas marcher. Le
   rond fait 30 cqw de diamètre (`.fb-carte__pastille`) ; le plus large texte
   qu'un disque puisse contenir est celui de son carré inscrit, 30 / √2 ≃ 21 cqw.
   Mesuré à l'écran, un signe de ce lettrage occupe environ 0,55 em — chiffres,
   point et tiret confondus. Le prix tient donc tant que
   0,55 × signes × taille ≤ 21 cqw, soit taille ≤ 38 cqw / signes.

   D'où le nombre de signes, passé par le gabarit en propriété personnalisée :

     « 4.– »    3 signes → 12,7 cqw
     « 45.– »   4 signes →  9,5 cqw   (7,5 avant, soit +27 %)
     « 450.– »  5 signes →  7,6 cqw

   ⚠ ET C'EST POUR ÇA QU'UNE VALEUR UNIQUE NE TENAIT PAS. L'essai précédent
   écrivait 9,5 cqw pour tout le monde : « 450.– » — un vrai prix du catalogue,
   l'encre de Chine originale — débordait du rond de Fabian, les deux bouts posés
   sur le trait vert. Les 7,5 cqw d'origine n'étaient pas un réglage timide : ils
   étaient dimensionnés pour ce prix-là, et tous les autres les subissaient. Le
   calcul rend donc à chaque prix la place que sa longueur lui laisse, ce qui est
   aussi ce que Fabian fait quand il écrit dans le rond à la main.

   Les deux bornes du `clamp()` restent : sur une carte minuscule ou énorme,
   c'est la lisibilité qui tranche, pas la proportion. */
.fb-pastille__prix {
  position: relative;
  color: var(--fb-blanc);
  font-family: var(--fb-serif);
  font-weight: 700;
  font-size: 1.1rem;
  font-size: clamp(0.85rem, calc(38cqw / var(--fb-pastille-signes, 5)), 2.1rem);
  line-height: 1;
  text-align: center;
  white-space: nowrap;
  /* Le dessin n'est pas centré dans sa toile (marges 9 / 12 px sur 196) : on
     recale le texte d'un poil vers le haut-gauche pour tomber au milieu du rond. */
  translate: -1% -2%;
}

/* « DÈS », SUR SA PROPRE LIGNE ET PLUS PETIT — Fabian, 6 octobre 2026 : « le dès
   en plus petit ou remplacer par > ».

   Le mot est gardé plutôt que remplacé par « > » : dans un rond dessiné à la
   main, un chevron se lit comme un signe mathématique — « plus grand que 40 »,
   soit l'inverse de ce qu'on veut dire. Le mot, lui, ne se confond avec rien.

   `display: block` le pousse sur sa ligne, et c'est là que se joue toute la
   correction : à la suite du montant, « dès 40.– » faisait huit signes dans un
   rond qui en loge quatre confortablement, donc un prix écrit petit sur les
   produits variables et grand sur les simples. Séparés, les deux produits
   affichent le même nombre à la même taille.

   `em` et non `cqw` : la proportion entre le mot et le montant ne dépend pas de
   la taille de la carte, elle est fixe. Une seule valeur à régler. */
.fb-pastille__des {
  display: block;
  font-size: 0.5em;
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: 0.03em;
}
/* ⚠ UNE RÈGLE A ÉTÉ SUPPRIMÉE ICI, ET SON HISTOIRE VAUT D'ÊTRE GARDÉE.
   Il y avait à cet endroit :

       @media (hover: none) { .fb-carte__pastille { display: none; } }

   commentée « écran tactile : pas de survol, donc pas de pastille — le prix est
   dans la légende ». Elle annulait en silence le `opacity: 1` posé une
   soixantaine de lignes plus haut, qui répondait pourtant à une demande
   explicite de Fabian du 23 septembre 2026 (« idée mobile, shop : toujours la
   petite pastille de prix »). Les deux règles ont la même spécificité : c'est
   la dernière écrite qui gagnait, et c'était celle-ci.

   Personne ne l'a vu parce que le symptôme n'est pas « la pastille a disparu »
   — c'est « le prix est en Garamond sous la carte au lieu d'être dans le rond
   dessiné », ce qui ressemble à une décision. Fabian a redemandé la même chose
   le 7 octobre 2026 : « toujours afficher le prix ».

   La pastille reprend donc sa place sur la vignette. Le prix de la légende, lui,
   s'efface quand elle est là — voir `.fb-legende` juste en dessous. */

/* LA LÉGENDE : MASQUÉE À L'ŒIL, MAIS PAS PARTOUT.

   Titre et prix sont dans le HTML de chaque carte. Sur un écran avec souris ils
   ne sont pas dessinés — c'est l'image de survol qui les porte, dans le
   lettrage de Fabian.

   Sur écran TACTILE, il n'y a pas de survol : le calque ne se révélerait
   jamais, et la boutique afficherait une grille de vignettes muettes, sans un
   seul prix. Inacceptable pour un site marchand. On rend donc la légende
   visible, sobrement, sous chaque carte.

   C'est pour ça qu'on n'utilise pas `.screen-reader-text` ici : ses règles sont
   en `!important` (elles doivent gagner partout, c'est leur raison d'être), on
   ne peut donc pas les lever proprement dans une media query. `.fb-legende`
   applique exactement la même technique, sur une classe qui nous appartient. */
.fb-legende {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ⚠ LA CONDITION EST UNE LISTE : LE POINTEUR **OU** LA LARGEUR.
   Elle ne regardait que le pointeur. Une fenêtre rétrécie sur un ordinateur
   garde sa souris : la légende y restait donc hors de l'écran, et comme la
   pastille attendait un survol, la boutique n'affichait aucun prix du tout.
   Même correction que pour la pastille plus haut, et pour la même raison. */
@media (hover: none), (max-width: 700px) {
  /* Pas de survol utile ici : le calque porte le titre et le prix dans le
     lettrage de Fabian, mais à deux colonnes sur un téléphone ce lettrage
     n'est plus lisible — c'est justement la raison d'être de la légende. Et il
     coûterait une image de plus par vignette. */
  .fb-survol { display: none; }

  .fb-legende {
    position: static;
    width: auto;
    height: auto;
    margin: 0.6em 0 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    display: block;
    font-size: var(--fb-petit);
    line-height: 1.3;
    text-align: center;
  }

  .fb-legende .fb-carte__titre,
  .fb-legende .fb-carte__prix { display: block; }

  .fb-legende .fb-carte__titre { font-weight: 700; }
  .fb-carte__prix-barre { text-decoration: line-through; color: var(--fb-gris); }

  /* ⚠ LE PRIX NE S'ÉCRIT PAS DEUX FOIS.
     La pastille dessinée est redevenue visible juste au-dessus : laisser le
     prix dans la légende l'afficherait une fois dans le rond de Fabian et une
     fois en Garamond sous la carte, à 20 px d'écart.

     `:has()` ET PAS UNE RÈGLE SÈCHE, PARCE QUE LA PASTILLE EST FACULTATIVE.
     content-product.php ne l'écrit que si le produit a un montant à montrer
     (`! empty( $fb_pastille['montant'] )`) : un produit variable sans prix
     calculable n'en a pas. Masquer le prix de la légende sans condition
     laisserait ces produits-là sans aucun prix visible — soit exactement le
     défaut qu'on vient de corriger, déplacé ailleurs. On ne masque donc le
     texte que sur les cartes qui portent réellement le dessin. */
  .fb-carte:has(.fb-carte__pastille) .fb-legende .fb-carte__prix { display: none; }

  /* ⚠ LE TYPE DE PROJET EST RETIRÉ, IL ÉTAIT EN `display: block` ICI.
     Fabian, 7 octobre 2026 : « enlever la catégorie / le type sur la page
     portfolio ». Il ne s'affiche que dans cette légende — sur grand écran, les
     tuiles du portfolio ne portent aucun texte. Le retirer de la liste
     ci-dessus suffit : `.fb-legende` est en `display: block`, mais
     `.fb-tuile__type` est un `<span>` dans le titre et reste donc en ligne,
     collé au titre. `display: none` est explicite et ne laisse pas la question
     ouverte.
     Le terme reste dans le HTML (archive-portfolio.php) : c'est lui qui donne
     au lien son nom accessible complet, et il sert au référencement. On le
     retire de l'œil, pas de la page. */
  .fb-tuile__type { display: none; }
}

/* --------------------------------------------------------------------------
   Boutique : on réutilise la grille partagée en habillant le balisage que
   WooCommerce génère lui-même (on ne contrôle pas ses classes).
   -------------------------------------------------------------------------- */

/* TROIS COLONNES, ET UN NOMBRE FIXE PLUTÔT QU'UN `auto-fill`.

   La règle disait `repeat(auto-fill, minmax(140px, 1fr))`, comme la grille du
   portfolio : le navigateur mettait donc autant de colonnes qu'il en tenait —
   sept ou huit sur un écran large. Le filtre `loop_shop_columns` annonçait 5, et
   personne ne les a jamais vues : les deux vivaient chacun de leur côté.

   Fabian veut trois cartes par ligne au maximum, plus grosses, parce que le
   titre et le prix sont maintenant dessinés DANS l'image de survol : à sept
   colonnes, son lettrage devenait illisible. Un nombre fixe est donc la bonne
   réponse ici, alors que `auto-fill` reste la bonne réponse pour le portfolio,
   où les tuiles ne portent aucun texte.

   ⚠ `loop_shop_columns` dans inc/woocommerce.php doit dire la même chose. Il ne
   dessine rien, mais certaines extensions s'y fient. */
.woocommerce ul.products {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 2.5vw, 2rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* En dessous de 700 px, trois colonnes donnent des cartes de 200 px : le dessin
   de survol redevient illisible, ce qu'on cherchait justement à éviter. */
@media (max-width: 700px) {
  .woocommerce ul.products { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ⚠ LE PALIER À 430 PX A ÉTÉ SUPPRIMÉ. Il disait :

       @media (max-width: 430px) { .woocommerce ul.products { grid-template-columns: 1fr; } }

   c'est-à-dire UNE colonne sur un téléphone — donc une carte par écran, et une
   boutique qu'on parcourt au défilement sans jamais voir deux produits à la
   fois. La même règle s'appliquait au PANIER, qui réutilise délibérément cette
   grille : un article par ligne, sur toute la largeur.

   Fabian a demandé deux colonnes des deux côtés le 7 octobre 2026 (« Shop :
   2 colonnes », « Panier : 2 colonnes »). La grille à deux colonnes de 700 px
   court donc maintenant jusqu'en bas.

   CE QUE ÇA DONNE, PARCE QUE C'ÉTAIT LA CRAINTE DERRIÈRE CE PALIER : sur un
   téléphone de 390 px, la rangée utile fait 350 px et l'écart vaut 16 px
   (`clamp(1rem, 2.5vw, 2rem)` au plancher) — soit des cartes de 167 px. C'est
   plus large que les vignettes de l'accueil (172 px) à un pixel près, et le
   dessin de survol n'est de toute façon plus affiché à cette largeur : c'est la
   légende sobre et la pastille qui portent le titre et le prix. Il n'y a donc
   plus de lettrage à rendre illisible, et le palier n'avait plus d'objet. */
.woocommerce ul.products::before,
.woocommerce ul.products::after { content: none; } /* neutralise les clearfix de Woo */

.woocommerce ul.products li.product {
  width: auto !important;
  margin: 0 !important;
  float: none !important;
  text-align: center;
}

/* --------------------------------------------------------------------------
   LA CARTE PRODUIT
   --------------------------------------------------------------------------
   Cette section pesait deux fois plus, et pour une bonne raison : chaque
   emplacement devait occuper la même hauteur sur toutes les cartes, plein ou
   vide. Sans ça, un titre sur deux lignes décalait son prix, une promo ajoutait
   une ligne, un produit variable une autre — et les boutons finissaient en
   escalier. D'où une rangée d'étoiles, un titre et deux lignes de prix aux
   hauteurs réservées au pixel.

   Plus rien de tout ça n'est dessiné. Le titre et le prix sont maintenant dans
   l'image de survol, de la main de Fabian ; les avis sont supprimés ; le bouton
   d'ajout au panier aussi. Ne reste que le visuel carré, qui garantit à lui
   seul l'alignement. Les règles de réservation de hauteur ont donc disparu avec
   ce qu'elles alignaient.

   Structure produite par woocommerce/content-product.php :
     .fb-carte > .fb-carte__lien > (media + légende masquée)
   -------------------------------------------------------------------------- */

.fb-carte {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.fb-carte__lien {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
}

/* Le visuel. `aspect-ratio: 1` sur .fb-masque garantit déjà le carré : toutes
   les cartes d'une rangée démarrent donc à la même hauteur. */
/* ⚠ `line-height: 0`, ET C'EST LUI QUI COLLE LA QUANTITÉ À LA PHOTO DANS LE
   PANIER. Mesuré à l'inspecteur le 6 octobre 2026 : la boîte faisait 160,67 px de
   haut pour une image de 142,67 — DIX-HUIT PIXELS DE VIDE À L'INTÉRIEUR, sous
   l'image, que la marge du bloc d'infos ne pouvait pas rattraper puisqu'ils
   étaient dedans.

   D'où ils viennent : `fb_vignette_masquee()` et `fb_image_survol()` sont deux
   fonctions qui `echo` à la suite, et entre leurs deux sorties il reste du blanc
   — un retour à la ligne, des tabulations. Du texte, donc, même s'il ne s'en
   voit rien : le navigateur lui ouvre une ligne, et une ligne vaut sa
   `line-height`, soit 18 px ici. L'image est en `display: block`, elle n'y est
   pour rien ; c'est le nœud de texte qui suit.

   On aurait pu nettoyer le blanc côté PHP. On ne l'a pas fait : il faudrait le
   refaire à chaque endroit qui compose une vignette (la grille de la boutique,
   le panier, les suggestions du panier vide), et un `trim()` oublié ramènerait
   le trou sans rien dire. Une ligne de hauteur nulle le ferme partout et pour
   de bon — cette boîte ne contient jamais de texte à lire, seulement des images
   et la pastille, qui pose sa propre `line-height`. */
.fb-carte__media {
  position: relative;
  display: block;
  line-height: 0;
}

/* Pastille « Promo ». WooCommerce la sort en jaune moutarde, ce qui
   introduirait une deuxième couleur d'accent dans un site qui n'en a qu'une.
   Fabian n'a rien dessiné pour ça : on reprend le vert et la forme de pilule
   de « Me soutenir ». */
.fb-carte__promo {
  position: absolute;
  top: 0.4em;
  right: 0.4em;
  z-index: 2;
  padding: 0.15em 0.7em;
  background: var(--fb-vert);
  color: var(--fb-blanc);
  font-size: var(--fb-petit);
  font-weight: 700;
  line-height: 1.5;
  border-radius: 1em;
}

/* Les étoiles n'ont plus de règles : les avis sont désactivés sur toute la
   boutique (voir fb_avis_actifs et fb_desactiver_avis dans inc/woocommerce.php)
   et le balisage a disparu du gabarit. Les classes `.fb-carte__note`,
   `.fb-etoiles` et `.fb-etoiles__pleines` sont retirées avec.

   Le titre et les deux lignes de prix non plus : ils sont dans `.fb-legende`,
   masquée à l'œil sauf sur écran tactile. */

/* LE BOUTON D'AJOUT AU PANIER A DISPARU DE LA GRILLE, et ses règles avec lui —
   `.fb-carte__action`, l'habillage en pilule du `.button` de WooCommerce et
   l'état `.added_to_cart`. On achète depuis la fiche produit ; le pourquoi est
   écrit dans woocommerce/content-product.php, là où le balisage était.

   Ce qui tombe avec : la carte n'a plus rien à aligner en bas. C'est le
   `aspect-ratio: 1` du visuel qui garantit à lui seul que toutes les cartes
   d'une rangée font la même hauteur, et il suffit. */

/* ==========================================================================
   11. CONTENU SOBRE
   --------------------------------------------------------------------------
   Tout ce que Fabian n'a pas encore dessiné (tunnel de commande, formulaires,
   404, messages d'erreur) est traité ici de façon délibérément neutre : lisible,
   à la bonne palette, sans inventer un style qui entrerait en concurrence avec
   le sien. Ce qui reste visuellement nu dans le site est exactement ce qu'il
   lui reste à dessiner — c'est lisible d'un coup d'œil, et c'est voulu.
   ========================================================================== */

/* L'AIR AU-DESSUS DU CONTENU A ÉTÉ COUPÉ DE MOITIÉ, À LA DEMANDE DE FABIAN.
   Le `padding-block` était symétrique — jusqu'à 3 rem en haut comme en bas —
   et c'était l'erreur : le haut et le bas ne jouent pas le même rôle.

   En bas, cet air sépare le texte du pied de page, il est dû. En haut, il
   S'AJOUTE à la marge basse de `.fb-entete` (jusqu'à 1.4 rem) : le contenu
   flottait donc à plus de 4 rem sous la navigation, et la page paraissait
   commencer deux fois. Fabian a resserré la même marge sur l'en-tête pour la
   même raison ; c'est la suite de ce geste, pas un nouveau réglage.

   Elles sont donc dissociées. Si l'écart se rouvre un jour, c'est ici ET dans
   `.fb-entete` qu'il faut regarder — les deux valeurs s'additionnent, et
   corriger l'une en oubliant l'autre donne l'impression que rien n'a bougé. */
.fb-page {
  padding-top: clamp(0.75rem, 1.8vw, 1.35rem);
  padding-bottom: clamp(1.5rem, 4vw, 3rem);
}
.fb-page__titre { font-size: var(--fb-titre); }
.fb-page__contenu > * + * { margin-top: 1em; }

/* --------------------------------------------------------------------------
   LES MÉDIAS INTÉGRÉS — UNE VIDÉO GARDE SES PROPORTIONS
   --------------------------------------------------------------------------
   Ces règles sont normalement celles du cœur, dans `wp-block-library`. Ce
   site ne charge PAS cette feuille en façade — vérifié : la page d'un projet
   n'a que WooCommerce et les deux feuilles du thème — et il n'y a aucune
   raison de charger 60 Ko de styles de blocs pour cinq règles. On les écrit
   donc ici, mais en `aspect-ratio` plutôt qu'avec le `padding-top` en
   pourcentage du cœur : même résultat, sans le pseudo-élément fantôme.

   LE SYMPTÔME QUE ÇA CORRIGE. WordPress sert l'iframe YouTube avec
   `width="1200" height="675"`, calculés sur $content_width. Quelque chose
   ramenait la largeur à celle de la colonne (~800 px) sans toucher la
   hauteur : 800 × 675, un presque-carré, la vidéo 16:9 au milieu entre deux
   bandes noires. « Un gros carré noir », Fabian, 23 septembre 2026.

   Les classes `wp-embed-aspect-*` sont posées par l'éditeur au moment où on
   colle l'adresse : c'est lui qui interroge le fournisseur et connaît le
   rapport. On les traduit toutes, y compris les formats verticaux — une vidéo
   filmée au téléphone arrive en 9:16 et doit rester debout. */
.wp-block-embed__wrapper { position: relative; }

.wp-embed-responsive .wp-has-aspect-ratio .wp-block-embed__wrapper > iframe,
.wp-block-embed__wrapper > iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.wp-embed-responsive .wp-embed-aspect-21-9  .wp-block-embed__wrapper { aspect-ratio: 21 / 9; }
.wp-embed-responsive .wp-embed-aspect-18-9  .wp-block-embed__wrapper { aspect-ratio: 18 / 9; }
.wp-embed-responsive .wp-embed-aspect-16-9  .wp-block-embed__wrapper { aspect-ratio: 16 / 9; }
.wp-embed-responsive .wp-embed-aspect-4-3   .wp-block-embed__wrapper { aspect-ratio: 4 / 3; }
.wp-embed-responsive .wp-embed-aspect-1-1   .wp-block-embed__wrapper { aspect-ratio: 1; }
.wp-embed-responsive .wp-embed-aspect-9-16  .wp-block-embed__wrapper { aspect-ratio: 9 / 16; }
.wp-embed-responsive .wp-embed-aspect-1-2   .wp-block-embed__wrapper { aspect-ratio: 1 / 2; }

/* FILET DE SÉCURITÉ : une vidéo dont l'éditeur n'a pas su lire le rapport
   (certains fournisseurs ne le renvoient pas) n'a pas de classe
   `wp-embed-aspect-*`. Sans ce repli, elle retombe dans le carré noir. Le
   16:9 est le format de la quasi-totalité des vidéos en ligne ; se tromper ici
   coûte des bandes noires, ne rien mettre coûte la mise en page. */
.wp-block-embed.is-type-video:not([class*="wp-embed-aspect-"]) .wp-block-embed__wrapper {
  aspect-ratio: 16 / 9;
}

/* Vidéo hébergée sur le site (bloc « Vidéo », pas un lien externe). Elle porte
   ses vraies dimensions, il suffit de ne pas les contredire : `height: auto`
   laisse le navigateur appliquer le rapport du fichier. */
.wp-block-video video,
.fb-page__contenu video {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
}

/* Les figures des deux blocs ne sont pas des blocs de texte : elles respirent
   plus que le `1em` de la colonne, et leurs légendes se lisent en petit. */
.fb-page__contenu .wp-block-embed,
.fb-page__contenu .wp-block-video { margin: 1.5em 0; }
.fb-page__contenu .wp-block-embed figcaption,
.fb-page__contenu .wp-block-video figcaption {
  margin-top: 0.5em;
  color: var(--fb-gris);
  font-size: var(--fb-petit);
  text-align: center;
}

/* Tout autre contenu intégré (carte, lecteur audio, formulaire externe) : on ne
   connaît pas son rapport, on garantit seulement qu'il ne déborde pas de la
   colonne. */
.fb-page__contenu iframe,
.fb-page__contenu embed,
.fb-page__contenu object { max-width: 100%; }

/* --------------------------------------------------------------------------
   LES BLOCS DE L'ÉDITEUR (inc/blocs.php)
   -------------------------------------------------------------------------- */

/* LE BOUTON DESSINÉ. Le HTML est un `.fb-img` ordinaire ; les images et la
   largeur arrivent en propriétés personnalisées sur l'élément, écrites par
   PHP. Une règle pour tous les boutons que Fabian fabriquera. */
.fb-bloc-bouton { display: flex; margin: 1.5em 0; }
.fb-bloc-bouton.aligncenter { justify-content: center; }
.fb-bloc-bouton.alignleft   { justify-content: flex-start; }
.fb-bloc-bouton.alignright  { justify-content: flex-end; }
.fb-bloc-bouton__lien {
  display: block;
  width: var(--fb-bouton-l, 220px);
  max-width: 100%;
  aspect-ratio: var(--fb-bouton-ratio, 3 / 1);
}
.fb-bloc-bouton__lien::before { background-image: var(--fb-bouton-base); }
.fb-bloc-bouton__lien::after  { background-image: var(--fb-bouton-survol); }

/* LE BLOC « DÉTAILS » (menu dépliant) PEUT ÊTRE CENTRÉ. L'alignement est
   ajouté au bloc du cœur par fb_bloc_details_alignement() ; l'éditeur pose
   alors `aligncenter`, et c'est ici que ça devient un centrage : le bloc se
   rétracte sur son contenu et se pose au milieu de la colonne. À gauche ou à
   droite, il flotte — c'est ce que WordPress fait des images alignées, et ce
   que Fabian attend de « placé à gauche ». */
.fb-page__contenu .wp-block-details.aligncenter {
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  text-align: center;
}
.fb-page__contenu .wp-block-details.alignleft  { float: left;  max-width: 50%; margin: 0 1.5em 1em 0; }
.fb-page__contenu .wp-block-details.alignright { float: right; max-width: 50%; margin: 0 0 1em 1.5em; }
.fb-page__contenu .wp-block-details > summary { font-weight: 700; }

.fb-esquisse {
  margin: clamp(1.5rem, 4vw, 3rem) 0;
  padding: clamp(1rem, 3vw, 2rem);
  border: 2px dashed var(--fb-gris-clair);
  color: var(--fb-gris);
  font-size: var(--fb-petit);
}
.fb-esquisse__titre {
  margin: 0 0 0.5em;
  color: var(--fb-noir);
  font-size: var(--fb-texte);
}

.fb-page__date {
  margin: 0 0 1em;
  color: var(--fb-gris);
  font-size: var(--fb-petit);
}

.fb-liste-articles {
  list-style: none;
  margin: 0;
  padding: 0;
}
.fb-liste-articles > li + li {
  margin-top: clamp(1.5rem, 4vw, 2.5rem);
  padding-top: clamp(1.5rem, 4vw, 2.5rem);
  border-top: 1px solid var(--fb-gris-clair);
}
.fb-liste-articles__titre { font-size: var(--fb-texte); }

/* Page d'un projet.
   `.fb-projet__visuel` n'a plus de règle : l'image mise en avant n'est plus
   répétée en haut de la fiche (voir single-portfolio.php).

   LE TITRE ET LES CATÉGORIES SONT CENTRÉS, à la demande de Fabian.
   Ils ne sont pas centrés « pour faire joli » : ils forment un bloc de deux
   lignes posé au-dessus du projet, comme le cartel d'une œuvre accrochée. Le
   contenu, lui, reste aligné à gauche — c'est du texte courant, et du texte
   courant centré ne se lit pas.

   Le centrage porte sur les deux éléments et pas sur `.fb-page` entier, pour
   cette raison exactement. */
.fb-projet__titre,
.fb-projet__types { text-align: center; }

/* Les catégories sont en NOIR et en italique, et ne sont plus cliquables. En
   gris, elles se lisaient comme une métadonnée secondaire ; en noir italique,
   comme une mention de l'auteur — ce que Fabian voulait. */
.fb-projet__types {
  /* Remontées sous le titre (−0,5 → −1,1 em) à la demande de Fabian, 9 septembre
     2026 : le cartel doit se lire comme UN bloc, titre et mention collés. Le
     titre garde sa marge de 0,5 em (règle `h1` de la section 2) ; c'est cette
     marge négative, en `em` du texte des catégories, qui vient la manger. */
  margin: -1.1em 0 1em;
  color: var(--fb-noir);
  font-style: italic;
  font-size: var(--fb-petit);
}
/* Le bouton « Retour ».
   Deux rendus possibles selon que l'asset dessiné est livré ou non — voir
   fb_bouton_retour() dans inc/helpers.php. Le repli texte reprend le langage
   des pilules de la navigation, pour ne pas ressembler à un lien oublié. */
.fb-retour--texte {
  display: inline-block;
  padding: 0.4em 1.1em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  color: var(--fb-noir);
  font-size: var(--fb-petit);
  font-weight: 700;
  text-decoration: none;
  transition: background var(--fb-transition), color var(--fb-transition);
}
.fb-retour--texte:hover,
.fb-retour--texte:focus-visible {
  background: var(--fb-vert);
  border-color: var(--fb-vert);
  color: var(--fb-blanc);
}

/* L'ASSET EST ARRIVÉ (11 août 2026, dossier UI/), et le rapport a changé.
   Il était écrit 372 / 97 — une supposition faite avant la livraison, sur la
   foi d'un bouton « RETOUR » de la largeur du mot. Fabian a dessiné autre
   chose : 209 × 97, parce que l'état de repos ne montre PAS le mot, seulement
   une flèche. Le mot n'apparaît qu'au survol, dans la même boîte.

   Un rapport faux ne se voit pas comme une erreur : le dessin s'affiche, mais
   étiré. C'est le genre de valeur qu'il faut relire à chaque livraison. */
.fb-retour:not(.fb-retour--texte) {
  height: var(--fb-nav-h);
  aspect-ratio: 209 / 97;
}
.fb-retour:not(.fb-retour--texte)::before { background-image: url("../img/boutons/retour-base.png"); }
.fb-retour:not(.fb-retour--texte)::after  { background-image: url("../img/boutons/retour-survol.png"); }

/* SA PLACE EST À GAUCHE DE « SHOP », ET IL N'Y PÈSE RIEN.
   Il était rendu deux fois, à deux endroits différents : en bas de la fiche
   projet, en haut de la fiche produit. Il est maintenant unique, dans la barre
   de navigation — c'est là qu'on le cherche, à côté des autres déplacements.

   Même mécanique que l'étoile « Nouveautés » posée juste au-dessus de la barre,
   et pour la même raison : il n'existe que sur les fiches. Dans le flux, la
   barre se décalerait vers la droite sur les fiches et reviendrait au centre
   ailleurs — un menu qui bouge d'une page à l'autre. En `position: absolute`,
   il ne déplace pas un pixel.

   `right: 100%` VISE BIEN LE BORD DE « SHOP », et c'est `width: fit-content`
   sur `.fb-nav` qui le permet (voir le commentaire de la section 6) : sans
   cela, `.fb-nav` occuperait toute la largeur disponible et le bouton se
   collerait au bord de la page.

   `top: 0` et non un centrage vertical : entre 560 et ~700 px, la barre peut
   passer sur deux lignes, et un bouton centré sur le bloc entier tomberait
   alors entre les deux. Le haut de la boîte est le haut de la PREMIÈRE ligne,
   et les deux boutons ont la même hauteur — ils sont donc alignés. */
/* LA BOÎTE QUI TIENT LES DEUX, ET QUI N'EN EST PAS UNE DE PLUS.
   `.fb-nav-zone` entoure le bouton « Retour » et la barre. Elle n'existe que
   pour une raison : donner au bouton une référence qui ait la largeur du menu
   maintenant qu'il n'est plus dedans (voir le commentaire de header.php).

   `width: fit-content` est la condition, exactement comme sur `.fb-nav` : une
   boîte qui se rétracte sur son contenu a pour bord gauche celui de la première
   pilule, et c'est ce bord que vise `right: 100%`. Les enfants absolus ne
   comptent pas dans un `fit-content` — la zone fait donc la largeur du seul
   <nav>, c'est-à-dire la même qu'avant au pixel près.

   ⚠ PAS DE `padding`, PAS DE `border`, ET ÇA COMPTE.
   La marge haute de `.fb-nav` traverse cette boîte (fusion des marges) et
   continue de pousser la barre sous le slogan comme avant. Un padding, même
   de 1 px, bloquerait la fusion et ferait apparaître un cran de blanc sur
   toutes les pages du site — un décalage que personne n'aurait demandé. */
.fb-nav-zone {
  position: relative;
  width: fit-content;
  margin-inline: auto;

  --fb-nav-ecart: clamp(0.25rem, 0.55vw, 0.4rem);
}

.fb-nav-zone .fb-retour {
  position: absolute;
  top: 0;
  right: calc(100% + var(--fb-nav-ecart) * 2);
}

/* Écran étroit : le bouton revient dans le flux, comme l'étoile et pour la
   même raison — la barre y prend toute la largeur, il n'y a plus de blanc à
   sa gauche où se poser. `inset: auto` est indispensable : en `relative`,
   `right` ne place plus, il DÉCALE, et le bouton partirait vers la gauche
   d'une largeur de menu. */
/* ⚠ LE SEUIL EST PASSÉ DE 560 À 700 PX, EXACTEMENT COMME POUR L'ÉTOILE NEWS.
   Il valait 560 : la largeur à laquelle le blanc à gauche de la barre
   disparaissait. Tant que le bouton était DANS le <nav>, les 140 px entre les
   deux seuils ne posaient rien — la barre y est masquée, et un enfant d'un
   élément masqué l'est aussi. Depuis qu'il en est sorti, il reste visible dans
   cette bande, calé en `right: 100%` sur une zone devenue vide, donc de largeur
   nulle, donc centrée : il se serait posé à gauche du centre, en l'air, sur une
   tablette en portrait. Les deux seuils sont liés — c'est la deuxième fois que
   ce couple se signale, voir la note de `.fb-nav__news`. */
@media (max-width: 700px) {
  .fb-nav-zone .fb-retour {
    position: relative;
    inset: auto;
  }
}

/* ⚠ SOUS 700 PX, LE BOUTON EST LA SEULE CHOSE QUI RESTE DANS LA ZONE.
   La barre y est en `display: none` tant que le burger est fermé : la zone ne
   contient plus que le bouton « Retour », revenu dans le flux par la règle du
   dessus. C'est tout l'objet de la correction du 20 octobre — il est visible
   sans ouvrir le menu.

   LA MARGE EST POSÉE SUR LA ZONE, ET SEULEMENT QUAND LE BOUTON EXISTE.
   Sans elle, le bouton se collerait au slogan : la marge haute qui donnait
   cet air était celle de `.fb-nav`, et une marge d'élément masqué ne pousse
   rien. `:has()` et pas une règle sèche, parce que le bouton n'est rendu que
   sur les fiches (fb_retour_destination()) : sur les autres pages, la zone
   est vide, et lui donner une marge creuserait un blanc sous le slogan de
   toute la boutique. Même valeur que la marge de `.fb-nav`, pour que le menu
   ouvert et le bouton seul tombent au même endroit. */
@media (max-width: 700px) {
  .fb-nav-zone:has(.fb-retour) {
    margin-top: clamp(0.55rem, 1.2vw, 1rem);

    /* ⚠ `text-align` ET NON `margin-inline: auto` SUR LE BOUTON.
       Relevé à la capture, menu ouvert : la zone prend alors la largeur des
       pilules (153 px) et le bouton, qui en fait 95, se collait à son bord
       gauche — tout le bloc était centré, sauf lui. La réponse habituelle,
       `display: block; margin-inline: auto`, ne marche PAS ici : un bloc en
       `width: auto` s'étire à son conteneur, ce qui annule le couple
       `height` + `aspect-ratio` qui donne au dessin ses proportions. Le
       bouton est un `inline-block` ; on centre donc la ligne qui le contient,
       et il reste dimensionné par son rapport. */
    text-align: center;
  }

  /* ET IL DOIT ÊTRE TOUCHABLE, PUISQU'IL EST MAINTENANT LE SEUL VISIBLE.
     Sa hauteur est `--fb-nav-h`, dont le plancher vaut 25 px : relevé à la
     capture, le bouton faisait 54 × 25. Dans la barre, entouré de pilules de
     la même hauteur, c'était cohérent ; seul dans l'en-tête d'un téléphone,
     c'est une cible deux fois trop petite. 44 px est le seuil de la règle
     WCAG 2.5.8 (niveau AAA), déjà retenu pour les boutons de quantité et la
     croix du panier le 20 octobre — même seuil, même geste. Le rapport
     209/97 donne alors 95 px de large.

     Il reste plus haut que les pilules du menu ouvert (40 px) : c'est voulu,
     il est le seul moyen de revenir en arrière et il se lit comme tel.

     ⚠ ET C'ÉTAIT TROP. Thomas, 20 octobre 2026 : « bouton retour plus petit,
     je dirais moitié de taille, voire 2/3 ». Le défaut se voit à la
     comparaison : à 44 px, le bouton du TÉLÉPHONE était plus gros que celui
     de l'ordinateur (34 px). Un raisonnement d'accessibilité pris isolément
     avait produit une disproportion — la cible était confortable, le dessin
     dominait la page.

     ⚠ ON PREND 2/3 ET PAS LA MOITIÉ, ET LE CHOIX SE CALCULE. Les deux seuils
     de la règle WCAG 2.5.8 ne disent pas la même chose :

       44 px  niveau AAA  (seuil Apple, celui retenu le matin même)
       24 px  niveau AA   (le minimum réellement exigible)

     Moitié de 44 = 22 px : SOUS le plancher AA, donc une régression
     d'accessibilité, pas un réglage de taille. 2/3 de 44 = 29,33 → 29 px,
     au-dessus des 24, et le rapport 209/97 donne 62 px de large : la cible
     reste conforme dans les deux dimensions. C'est la seule des deux valeurs
     proposées qui tienne, d'où le choix — et il rapproche le bouton des 34 px
     de l'ordinateur au lieu de le faire dépasser. */
  .fb-nav-zone .fb-retour:not(.fb-retour--texte) { height: 29px; }
}

/* LE PRIX EN VERT.
   ⚠ Ceci contredit la règle posée en tête de ce fichier — « une seule couleur
   d'accent, le vert EST le signal d'interaction ». Un prix n'est pas cliquable.
   C'est une demande explicite de Fabian, et elle se défend : sur une fiche
   dépouillée de ses onglets, de ses catégories et de son UGS, il ne reste que
   trois choses à lire, et le prix est celle qu'on cherche. La règle devient
   donc : le vert signale l'interaction ET le prix — deux emplois, pas trois.
   Voir la section 1. */
.woocommerce div.product p.price,
.woocommerce div.product span.price {
  color: var(--fb-vert);
}
.woocommerce div.product p.price del,
.woocommerce div.product span.price del {
  color: var(--fb-gris);
}
/* Produit à variantes : le prix de la variante choisie s'écrit À LA PLACE de
   la fourchette (boutique.js, section 4). La ligne que WooCommerce ajoute sous
   le sélecteur ferait un second prix — masquée. La disponibilité (« en
   stock »), elle, reste. */
.woocommerce div.product .woocommerce-variation-price { display: none; }

/* LE BOUTON DE LA FICHE PRODUIT, ET LE VIOLET DE WOOCOMMERCE.

   Seul le bouton de la GRILLE était repris dans le langage du site. Celui de la
   fiche produit gardait le violet #7f54b3 de WooCommerce et l'olive de
   « en stock ».

   Tant que la fiche portait ses onglets, ses catégories et son UGS, ça passait
   presque inaperçu. Maintenant qu'elle est réduite à quatre éléments, ce violet
   est la première chose qu'on voit sur une page où il n'y a par ailleurs que du
   noir, du blanc et un vert.

   ⚠ C'EST DÉSORMAIS LE SEUL BOUTON D'ACHAT DU SITE : celui de la grille a été
   retiré, on n'ajoute plus au panier que depuis ici. Les règles ci-dessous
   étaient un rattrapage cosmétique, elles sont devenues le chemin d'achat. */
.woocommerce div.product form.cart .button,
.woocommerce div.product .single_add_to_cart_button {
  padding: 0.6em 1.6em;
  background: transparent;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  color: var(--fb-noir);
  font-family: var(--fb-serif);
  font-size: var(--fb-texte);
  font-weight: 700;
  transition: background var(--fb-transition), color var(--fb-transition);
}
.woocommerce div.product form.cart .button:hover,
.woocommerce div.product form.cart .button:focus-visible,
.woocommerce div.product .single_add_to_cart_button:hover,
.woocommerce div.product .single_add_to_cart_button:focus-visible {
  background: var(--fb-vert);
  border-color: var(--fb-vert);
  color: var(--fb-blanc);
}

/* LE TITRE UN CRAN PLUS PETIT, LE PRIX EN GRAS (Fabian, 9 septembre 2026).
   Le titre reste le <h1> de la page ; il descend d'environ 15 % par rapport à
   `--fb-titre`, pour laisser le prix — qui vient maintenant juste au-dessus du
   bouton — porter l'accent de la colonne. */
.woocommerce div.product .product_title {
  font-size: clamp(1.3rem, 1.05rem + 1.25vw, 1.9rem);
}
.woocommerce div.product p.price,
.woocommerce div.product span.price {
  font-weight: 700;
  font-size: var(--fb-texte);
}
/* L'extrait vient avant le prix (voir fb_epurer_fiche_produit) : il n'a plus
   besoin de la marge basse que WooCommerce lui donnait pour se détacher du
   bouton, c'est le prix qui s'en charge. */
.woocommerce div.product .woocommerce-product-details__short-description { margin-bottom: 0.75em; }

/* LA FICHE PRODUIT CONTIENT SES COLONNES.
   -----------------------------------------
   Correction du 30 septembre 2026, et elle passe AVANT tous les réglages de
   blanc ci-dessous — parce que c'est elle qui les rendait faux.

   WooCommerce pose la galerie et la colonne d'achat en `float` (48 % chacune,
   woocommerce-layout.css) et ne referme JAMAIS `div.product` : il compte sur
   `.woocommerce-tabs { clear: both }`, l'élément qui suivait toujours les deux
   colonnes. Les onglets ont été retirés de la fiche (fb_epurer_fiche_produit,
   inc/woocommerce.php, 23 septembre) — et avec eux, sans que rien ne le
   signale, la seule chose qui refermait le bloc.

   `div.product` mesurait donc ZÉRO pixel de haut. Tout le blanc réservé plus
   bas se comptait à partir du HAUT des colonnes au lieu de leur bas : l'image
   débordait de son propre conteneur — 386 px de dessin dans une boîte de 0 —
   et allait se poser sous le bloc clair du pied, qui est fixe. Mesuré sur la
   fiche « La Truite N°2 » privée de sa description, en 1280 × 800 (le MacBook
   Air 13 pouces) : bas de l'image à 667 px, haut des icônes du pied à 646 px.
   Vingt et un pixels de recouvrement.

   `flow-root` ouvre un contexte de formatage de bloc : le conteneur reprend la
   hauteur de ses flottants. C'est le `clearfix` d'autrefois, en une propriété
   et sans pseudo-élément — ::before et ::after restent libres, ce dont ce
   thème se sert partout.

   ⚠ POURQUOI ÇA NE SE VOYAIT PAS AVANT. Une fiche AVEC description longue la
   rend à plat sous les colonnes, et elle, elle ne flotte pas : elle poussait
   la page à la bonne hauteur et bouchait le trou. Le défaut n'apparaissait
   que sur les fiches sans texte — celles qui vont être les plus nombreuses. */
.woocommerce div.product { display: flow-root; }

/* DE L'AIR SOUS LA FICHE PRODUIT — Fabian, 23 septembre 2026 : « ajouter du
   blanc en bas de la page ».

   La fiche est la page la plus dense du site : la description longue est posée
   à plat sous le bloc d'achat, et sa dernière ligne venait mourir à quelques
   dizaines de pixels de la bande noire du pied. Or le pied est FIXE : ce n'est
   pas un bas de page qu'on quitte en faisant défiler, c'est un mur permanent.
   Un texte qui finit contre lui donne l'impression d'être coupé.

   Doublé ici, et ici seulement : `.fb-page` sert à toutes les pages du site et
   son réglage (1,5 à 3 rem) convient partout ailleurs. Les pages de contenu de
   Fabian se terminent sur un titre ou une image, pas sur un paragraphe long. */
/* LES MINIATURES DE LA GALERIE, ALIGNÉES SUR LA GRANDE IMAGE.
   --------------------------------------------------------------------------
   Fabian, 6 octobre 2026 : « aligner miniatures d'images à l'image du haut ».
   Mesuré sur la fiche « Calendrier 2027 » en 1440 : la bande de miniatures allait
   de 319 à 702 px, la grande image de 324 à 697. Quatre à cinq pixels de
   débord de chaque côté — assez peu pour qu'on ne sache pas le nommer, assez
   pour qu'on voie que ça ne tombe pas juste.

   DEUX CAUSES, ET IL FAUT LES DEUX CORRECTIONS.

   1. La disposition. WooCommerce pose `li { width: 25%; float: left }` et laisse
      l'image à sa taille de fichier (100 px) dans une cellule qui n'en fait pas
      autant : les vignettes ne remplissent pas leur colonne, la dernière ne
      touche pas le bord droit, et les écarts entre elles sont ceux que le
      hasard des arrondis veut bien laisser. Une grille de colonnes égales avec
      une gouttière explicite donne au contraire une bande qui occupe très
      exactement la largeur de la colonne, du premier au dernier pixel.

   2. LE RETRAIT DES CADRES DESSINÉS. La grande image porte un masque de Fabian
      (`.fb-masque-fiche`) dont le tracé ne touche pas le bord de sa toile : les
      cinq fichiers laissent entre 0,67 et 1,22 % de vide à gauche et à droite,
      1 % en moyenne (mesuré sur le canal alpha de masques/fiche-0*.png). L'image
      VISIBLE est donc en retrait de 1 % de part et d'autre de sa boîte. Aligner
      sur la boîte, c'est rester décalé de ce 1 % ; on aligne sur l'encre.

   `margin-inline` en POURCENTAGE et non en pixels : il se résout sur la largeur
   de la colonne, donc le retrait suit l'image à toutes les tailles d'écran,
   comme le masque lui-même qui est étiré en `100% 100%`.

   ⚠ LE NOMBRE DE COLONNES EST ÉCRIT À DEUX ENDROITS. Ici, et dans le filtre
   `woocommerce_product_thumbnails_columns` de WooCommerce (4 par défaut, non
   modifié par ce thème). WooCommerce pose la classe correspondante sur la
   galerie — c'est elle qu'on lit, plutôt que de répéter un 4 en dur. */
.woocommerce div.product div.images .flex-control-thumbs {
  --fb-galerie-colonnes: 4;
  display: grid;
  grid-template-columns: repeat(var(--fb-galerie-colonnes), minmax(0, 1fr));
  gap: clamp(0.35rem, 1.5%, 0.7rem);
  margin-inline: 1%;
  /* WooCommerce met `overflow: hidden` pour contenir ses flottants ; il n'y a
     plus de flottant à contenir, et un débordement caché ne rend ici service
     à personne. */
  overflow: visible;
}
.woocommerce-product-gallery--columns-2 .flex-control-thumbs { --fb-galerie-colonnes: 2; }
.woocommerce-product-gallery--columns-3 .flex-control-thumbs { --fb-galerie-colonnes: 3; }
.woocommerce-product-gallery--columns-5 .flex-control-thumbs { --fb-galerie-colonnes: 5; }

.woocommerce div.product div.images .flex-control-thumbs li {
  float: none;
  width: auto;
  min-width: 0;
}
.woocommerce div.product div.images .flex-control-thumbs li img {
  display: block;
  width: 100%;
  height: auto;
}

/* ⚠ RÉDUIT DE MOITIÉ LE 6 OCTOBRE 2026 — Fabian : « trop de blanc sous les
   articles ». La réserve était de 6 rem au plafond ; elle est de 3.

   Ce qui avait été mal évalué : CE PADDING N'EST PAS LE SEUL BLANC. Sous lui
   viennent la réserve que `body` garde pour le pied fixe (`--fb-pied-h`, 156 px
   sur un 1440) puis la marge propre du bloc produit. Les trois s'additionnent —
   le commentaire d'en dessous le dit depuis le 30 septembre, mais les valeurs,
   elles, avaient été choisies avant qu'elles s'additionnent pour de bon (quand
   `div.product` mesurait encore zéro pixel de haut). On les ramène donc à ce
   qu'elles auraient dû être : ici, le réglage ordinaire de `.fb-page`. */
.woocommerce-page.single-product .fb-page { padding-bottom: clamp(1.5rem, 4vw, 3rem); }

/* ET ENCORE PLUS QUAND IL N'Y A PAS DE DESCRIPTION LONGUE — Fabian, 23
   septembre 2026 : « faire un blanc plus grand si pas de description longue ».

   La demande paraît contre-intuitive : moins de contenu, plus de blanc. Elle
   est juste. Avec une description, la page se termine sur un paragraphe, et le
   blanc qui suit se lit comme une fin de texte. Sans elle, elle se termine sur
   le bouton « Ajouter au panier » — un objet court, posé en haut à droite, avec
   une colonne d'image à sa gauche. Le même blanc dessous paraît alors deux fois
   trop maigre : la page a l'air tronquée juste après le bouton.

   La classe est posée sur le <body> par fb_classe_fiche_sans_description()
   (inc/woocommerce.php), qui teste la description APRÈS filtres — un contenu
   qui ne contient qu'un bloc vide compte pour rien, comme à l'affichage. */
.woocommerce-page.single-product.fb-fiche-sans-description .fb-page {
  /* 6 → 4 rem au plancher, 12 → 8 au plafond, le 30 septembre 2026. Ce réglage
     avait lui aussi été choisi à l'œil sur une page dont le bloc produit ne
     mesurait rien : le blanc annoncé n'était pas le blanc obtenu. Depuis que
     `div.product` se referme, les trois réserves s'additionnent pour de bon —
     la marge du bloc, ce padding, puis celui de `body` — et les anciennes
     valeurs faisaient défiler une demi-page de vide. Ce qui reste sous l'image
     couvre la hauteur du pied fixe (`--fb-pied-h`, 156 px sur un 1440) et une
     bande de blanc par-dessus. */
  padding-bottom: clamp(2rem, 5vw, 4rem);
}

/* --------------------------------------------------------------------------
   LE BOUTON « AJOUTER AU PANIER », DESSINÉ.
   --------------------------------------------------------------------------
   Livré le 9 septembre 2026 (492 × 131, vert au repos, noir au survol). C'est
   le <button> de WooCommerce, qu'on ne peut pas réécrire sans surcharger un
   gabarit par type de produit (simple, variable, groupé…) : on l'habille donc
   en CSS, avec ses deux pseudo-éléments comme calques — la mécanique de
   `.fb-img`, recopiée parce que la classe ne peut pas être posée.

   Le mot reste dans le bouton, en `color: transparent` et non en `font-size:
   0` : la boîte garde une hauteur de ligne, le texte reste sélectionnable et
   lu par les lecteurs d'écran, seul l'encre disparaît sous le dessin.

   ⚠ `.button.loading::after` de WooCommerce dessine un sablier sur ::after —
   uniquement en AJAX depuis la grille, qui n'existe plus ici. Rien à gérer. */
.woocommerce div.product form.cart .button,
.woocommerce div.product .single_add_to_cart_button {
  position: relative;
  /* À la hauteur du cadre de quantité (`--fb-qte-h`, 36 à 42 px) : les deux
     sont sur la même ligne et doivent se lire à la même échelle. Il faisait
     190 px de large, soit 51 de haut — « énorme », Fabian, 17 sept. 2026. */
  --fb-ajouter-h: clamp(34px, 3.2vw, 40px);
  height: var(--fb-ajouter-h);
  width: calc(var(--fb-ajouter-h) * 492 / 131);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: transparent;
  font-size: var(--fb-petit);
  line-height: 1;
  overflow: hidden;
  transition: none;
}
.woocommerce div.product form.cart .button::before,
.woocommerce div.product form.cart .button::after {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../img/objets/ajouter-au-panier-base.png") center / contain no-repeat;
  transition: opacity var(--fb-transition);
}
.woocommerce div.product form.cart .button::after {
  background-image: url("../img/objets/ajouter-au-panier-survol.png");
  opacity: 0;
}
.woocommerce div.product form.cart .button:hover,
.woocommerce div.product form.cart .button:focus-visible,
.woocommerce div.product .single_add_to_cart_button:hover,
.woocommerce div.product .single_add_to_cart_button:focus-visible {
  background: transparent;
  color: transparent;
}
/* Pas d'effacement du calque de repos ici non plus — même raison qu'en section
   3 : le fondu croisé faisait passer un quart du blanc de la page entre les
   deux dessins. `ajouter-au-panier-survol.png` recouvre intégralement sa
   version au repos (mesuré), le survol se pose donc simplement dessus. */
.woocommerce div.product form.cart .button:hover::after,
.woocommerce div.product form.cart .button:focus-visible::after { opacity: 1; }
/* Un produit indisponible : le bouton grisé de WooCommerce reste grisé, mais
   sans le dessin — un bouton vert « AJOUTER AU PANIER » qu'on ne peut pas
   cliquer serait une promesse. */
.woocommerce div.product form.cart .button.disabled::before,
.woocommerce div.product form.cart .button.disabled::after { opacity: 0.35; }

/* … ET LE TROISIÈME ÉTAT : « OUPS », APRÈS UN AJOUT REFUSÉ.
   Livré le 30 septembre 2026. Il ne répond pas à la souris mais à ce qui vient
   de se passer : la classe `.fb-ajout-refuse` est posée sur le <body> quand une
   erreur attend d'être affichée sur une fiche produit (fb_classe_ajout_refuse,
   inc/woocommerce.php — le pourquoi du <body> et non du bouton y est expliqué).

   LES DEUX CALQUES REÇOIVENT LE MÊME DESSIN, et c'est le plus simple des trois
   montages possibles. Le survol continue de faire son travail — `::after` monte
   à 1 — mais il monte sur la même image : le bouton ne bouge pas sous la souris,
   sans qu'on ait eu à désactiver quoi que ce soit. Un `:hover` annulé aurait
   demandé de contrer quatre sélecteurs ; une transition coupée aurait laissé un
   à-coup au premier survol suivant.

   ⚠ CE N'EST PAS `.disabled`. Un produit épuisé garde son dessin vert à 35 %
   d'opacité (règle au-dessus) : il dit « pas maintenant ». « Oups » dit « ça
   vient de rater », et ne vit qu'un affichage — au rechargement suivant la
   session d'avis est vide et le bouton redevient vert. Les deux peuvent se
   cumuler, et l'ordre est bon : l'opacité de `.disabled` s'applique au dessin
   qu'on choisit ici. */
.fb-ajout-refuse.woocommerce div.product form.cart .button::before,
.fb-ajout-refuse.woocommerce div.product form.cart .button::after {
  background-image: url("../img/objets/ajouter-au-panier-oups.png");
}

/* Quantité et bouton sur une ligne, sans les `float` de WooCommerce. Le même
   bloc existe deux fois : directement dans `form.cart` (produit simple) et
   dans `.woocommerce-variation-add-to-cart` (produit variable). */
.woocommerce div.product form.cart,
.woocommerce div.product form.cart .woocommerce-variation-add-to-cart {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1rem;
}
.woocommerce div.product form.cart .variations,
.woocommerce div.product form.cart .single_variation_wrap { flex: 1 1 100%; }
.woocommerce div.product form.cart div.quantity { float: none; margin: 0; }

/* --------------------------------------------------------------------------
   LA FICHE PRODUIT SUR TÉLÉPHONE — EMPILÉE, CENTRÉE, ET LE BOUTON PLUS GRAND
   --------------------------------------------------------------------------
   Fabian, 7 octobre 2026 : « prix, sélecteur de quantité et ajout au panier au
   centre et les uns en dessous des autres, et bouton ajouter au panier plus
   grand ».

   LE `wrap` NE SUFFISAIT PAS, ET C'EST TOUT LE PROBLÈME. `form.cart` est déjà
   un flex en `wrap` : à 390 px, le cadre de quantité (~100 px) et le bouton
   (~127 px) tiennent à deux sur une ligne de 350 px avec l'écart de 16 px.
   Rien ne passait donc à la ligne tout seul — les deux objets restaient
   serrés à gauche, petits, et le prix au-dessus restait aligné à gauche
   pendant que le reste de la colonne, lui, était centré. */
@media (max-width: 700px) {
  .woocommerce div.product form.cart,
  .woocommerce div.product form.cart .woocommerce-variation-add-to-cart {
    flex-direction: column;
    align-items: center;
  }

  /* Le prix n'est pas dans le formulaire : il est au-dessus, dans la colonne
     des informations. Il se centre donc séparément — c'est le `<p class="price">`
     du produit simple comme le `<span>` du bloc de variation. */
  .woocommerce div.product p.price,
  .woocommerce div.product span.price { text-align: center; }

  /* « 8 en stock » se glisse entre le prix et le formulaire : laissé à gauche,
     c'est la seule ligne de la colonne à ne pas être centrée, et elle le
     signale en désignant un bord que plus rien d'autre n'occupe. */
  .woocommerce div.product p.stock { text-align: center; }

  /* LE BOUTON. Il fait 34 px de haut sur un téléphone (plancher du `clamp`),
     soit 127 px de large : une cible plus petite que le minimum recommandé de
     44 px, pour l'action la plus importante de la page. On repart de la
     largeur disponible.

     LA HAUTEUR SE CALCULE, LA LARGEUR SUIT (rapport 492 / 131 = 3,756) :
       390 px de fenêtre − 2 × 20 de gouttière = 350 px utiles
       on vise ~60 % de cette largeur, soit 210 px → 210 / 3,756 = 56 px de haut
     D'où `clamp(40px, 14vw, 56px)` : 14vw vaut 54,6 px à 390 et atteint le
     plafond de 56 à 400. En dessous, la pente le fait rétrécir avec l'écran
     (44,8 px à 320, soit 168 px de large) plutôt que de déborder. Le plancher
     de 40 px tient la cible tactile sur les écrans les plus étroits.

     ⚠ LE COMMENTAIRE DU `clamp` D'ORIGINE NE S'APPLIQUE PLUS ICI : il calait
     le bouton sur la hauteur du cadre de quantité « parce que les deux sont
     sur la même ligne et doivent se lire à la même échelle ». Ils ne sont plus
     sur la même ligne. La contrainte tombe avec l'empilement, c'est ce qui
     autorise ce nombre. */
  .woocommerce div.product form.cart .button,
  .woocommerce div.product .single_add_to_cart_button {
    --fb-ajouter-h: clamp(40px, 14vw, 56px);
  }

  /* Le cadre de quantité monte à 44 px — le minimum d'une cible tactile
     confortable (WCAG 2.5.8 niveau AAA, et la valeur d'Apple). Il tombait à
     36 px, et ses deux boutons « − » et « + » ne font que 55 % de cette
     hauteur : 20 px de dessin. Sous un bouton de 56 px, un cadre de 36
     paraissait de surcroît abandonné là.
     Scopé à la fiche produit : le panier a son propre nombre, plus bas. */
  .woocommerce div.product form.cart .fb-qte--dessine { --fb-qte-h: 44px; }

  /* ⚠ ET LE « ME SOUTENIR » DISPARAÎT DE LA FICHE PRODUIT.
     Fabian, 7 octobre 2026 : « enlever le “me soutenir” sur les pages
     produits ». La raison se voit en descendant la page : sous le bouton
     d'ajout au panier qu'on vient d'agrandir, un second gros bouton dessiné
     qui emmène AILLEURS est une sortie de route posée au bout de l'entonnoir.
     C'est la seule page du site qui demande un geste précis ; elle ne doit pas
     en proposer un concurrent.

     LE BOUTON SEUL, PAS LE BLOC. `.fb-social-flux` porte aussi les quatre
     réseaux : eux n'entrent pas en concurrence avec l'achat, ils n'en sont que
     le pied de page habituel. On vise donc `.fb-soutenir-pied`, pas son
     conteneur.
     Inutile de viser la copie du pied fixe (`.fb-pied__clair`) : elle est déjà
     en `display: none` sous 1250 px. */
  body.single-product .fb-social-flux .fb-soutenir-pied { display: none; }
}

/* --------------------------------------------------------------------------
   LE CHAMP DE QUANTITÉ DESSINÉ — « − [ 1 ] + »
   --------------------------------------------------------------------------
   Gabarit : woocommerce/global/quantity-input.php. Le cadre (215 × 134) et les
   deux boutons (47 × 51) viennent de _sprites.css pour le rapport et l'image ;
   ici, les tailles et la composition. Tout est calé sur `--fb-qte-h`, la
   hauteur du cadre : les boutons en font un peu plus de la moitié, pour que la
   croix du « + » se lise à la même échelle que les chiffres. */
.fb-qte--dessine {
  --fb-qte-h: clamp(36px, 3.4vw, 42px);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.fb-qte--dessine .fb-qte__cadre {
  height: var(--fb-qte-h);
  aspect-ratio: 215 / 134;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Le champ lui-même est nu : pas de bordure, pas de fond, pas de flèches
   système — le cadre dessiné fait tout ça. `appearance: textfield` retire les
   flèches dans Firefox, les deux règles ::-webkit-* dans Chrome et Safari. */
/* Quatre classes dans le sélecteur, à dessein : WooCommerce écrit
   `.woocommerce .quantity .qty { width: 3.631em }` (trois classes), et la
   nôtre doit gagner quel que soit l'ordre de chargement des deux feuilles. */
.fb-qte.fb-qte--dessine .fb-qte__cadre .fb-qte__saisie {
  /* `relative` n'est pas décoratif : le cadre est un ::before en `absolute`,
     et un élément positionné se peint PAR-DESSUS un frère qui ne l'est pas.
     Sans cette ligne, le chiffre est dessiné… puis recouvert par son cadre. */
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  padding: 0 0.6em;
  border: 0;
  background: transparent;
  font-family: var(--fb-serif);
  font-size: var(--fb-texte);
  font-weight: 700;
  text-align: center;
  color: var(--fb-noir);
  -moz-appearance: textfield;
  appearance: textfield;
}
.fb-qte.fb-qte--dessine .fb-qte__saisie::-webkit-outer-spin-button,
.fb-qte.fb-qte--dessine .fb-qte__saisie::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* Au focus, pas de contour système : c'est le cadre qui passe au vert — la
   règle `:focus-within` est dans _sprites.css, et l'image verte est fabriquée
   par le build (tools/build-assets.sh, section 9) à partir du cadre noir. Même
   geste que les champs du contact, dont Fabian a dessiné la « sélection ». */
.fb-qte.fb-qte--dessine .fb-qte__cadre .fb-qte__saisie:focus { outline: none; }
.fb-qte__moins,
.fb-qte__plus {
  height: calc(var(--fb-qte-h) * 0.55);
  padding: 0;
  border: 0;
  background: transparent;
}
/* Vendu à l'unité : le cadre seul, grisé, sans les boutons (voir le gabarit). */
.fb-qte--fixe .fb-qte__cadre { opacity: 0.5; }

/* « 1 en stock » / « Rupture de stock » : olive et rouge par défaut, soit deux
   couleurs de plus dans une palette qui en compte trois. */
.woocommerce div.product .stock { color: var(--fb-gris); }
.woocommerce div.product .stock.out-of-stock { color: var(--fb-noir); font-weight: 700; }

/* --------------------------------------------------------------------------
   LA DESCRIPTION LONGUE, ET LE VIDE SOUS LA FICHE.
   --------------------------------------------------------------------------
   Deux problèmes, une seule zone, et ils se répondent.

   La fiche épurée ne contient plus que le visuel, le titre, le prix et le
   bouton. Sur un grand écran, ça s'arrête à mi-hauteur : le reste est du blanc,
   puis le pied de page fixe, collé au bas de la colonne. Rien ne sépare la fin
   du contenu du début du pied.

   La marge basse ci-dessous règle ce cas — c'est le minimum de respiration dû à
   la fiche, description ou pas. Elle est posée sur `div.product` et non sur la
   description, précisément pour qu'un produit sans texte long en profite aussi.
   -------------------------------------------------------------------------- */
/* Cette marge sépare les deux colonnes de la description longue qui les suit.
   Elle avait été relevée le 9 septembre 2026 (5 → 8 rem au plafond) parce que
   « la fiche sans description est encore collée au pied » — et elle n'y pouvait
   RIEN : `div.product` mesurait alors zéro pixel (voir `flow-root` plus haut),
   ses colonnes débordaient par le bas, et une marge posée sous une boîte vide
   s'applique là où la boîte s'arrête, c'est-à-dire AU-DESSUS de l'image. Le
   document gagnait bien 48 px, l'image ne bougeait pas d'un cheveu.
   Le conteneur refermé, la marge agit enfin depuis le bas réel : on la ramène
   donc à ce qu'elle valait avant la compensation. */
.woocommerce div.product { margin-bottom: clamp(3rem, 7vw, 5rem); }

/* La description elle-même (voir fb_description_longue dans inc/woocommerce.php).
   Pleine largeur sous les deux colonnes, en colonne de lecture limitée : le
   visuel et le résumé partagent la largeur de la page, un paragraphe seul ne le
   peut pas — au-delà d'environ 70 caractères, l'œil perd le début de la ligne
   suivante. C'est le seul endroit du site où Fabian écrit au long cours. */
.fb-produit__description {
  clear: both;
  max-width: 68ch;
  margin-top: clamp(2rem, 5vw, 3.5rem);
  padding-top: clamp(1.25rem, 3vw, 2rem);
  border-top: 2px solid var(--fb-noir);
}

.fb-produit__description-titre {
  /* Un cran sous `--fb-titre` : c'est un sous-titre dans la page, pas le titre
     de la page — celui-là, c'est le nom du produit. */
  font-size: clamp(1.15rem, 1rem + 0.7vw, 1.5rem);
  margin-bottom: 0.6em;
}

.fb-produit__description-corps > * + * { margin-top: 1em; }

/* --------------------------------------------------------------------------
   LE PANIER — les articles comme dans la boutique
   --------------------------------------------------------------------------
   Gabarit : woocommerce/cart/cart.php. Le <ul> porte `products columns-3`, il
   reçoit donc la grille de la boutique (`.woocommerce ul.products`, plus haut)
   sans une ligne de plus. Chaque <li> porte `.fb-carte` — la vignette
   découpée, l'image de survol et la pastille viennent avec — et en dessous, un
   bloc d'informations sobre : titre, variante, prix, quantité, sous-total.
   -------------------------------------------------------------------------- */
/* ⚠ Le formulaire s'appelle `.fb-panier-form`, pas `.fb-panier` : ce nom-là est
   déjà pris par l'ICÔNE du panier dans l'en-tête (section 7), qui fixe une
   largeur de 29 px. Posé sur le formulaire, il l'a réduit à 29 px de large —
   trois cartes empilées dans une colonne d'un pouce. Les enfants gardent le
   préfixe `fb-panier__`, qui n'entre en collision avec rien. */
.fb-panier__ligne { position: relative; }

/* LA CROIX DE FABIAN, dans l'angle haut-droit de la vignette. Elle est posée
   par-dessus la carte (`z-index`), au-dessus du lien de la vignette qui occupe
   toute la surface. cart.js (WooCommerce) intercepte le clic et retire
   l'article sans recharger.

   ⚠ CE N'EST PLUS `fermer-noir.png`, ET LE CHANGEMENT N'EST PAS COSMÉTIQUE.
   Le dessin était un X nu, 80 × 71, posé sur la vignette du produit. Un trait
   noir sur une illustration, c'est-à-dire sur n'importe quoi : selon l'article,
   il tombait sur du clair et se lisait, ou sur du foncé et disparaissait. Et
   rien ne le désignait comme un bouton — un X dessiné sur une image, c'est
   une rature.
   Fabian a demandé le 7 octobre 2026 de reprendre à la place SON bouton
   d'annulation des filtres : le même X, mais blanc dans une pastille pleine,
   44 × 48. La pastille lui donne son propre fond, donc une lisibilité qui ne
   dépend plus de l'image en dessous, et surtout le visiteur l'a déjà vu servir
   à annuler dans la barre de filtres — c'est le même geste, c'est le même
   dessin. Le couple est complet et déjà fabriqué : pastille VERTE au repos,
   NOIRE au survol. L'inversion par rapport à l'ancienne croix (noire au repos,
   verte au survol) est celle de la barre de filtres, et c'est la barre qui fait
   référence puisque c'est de là que le dessin vient. */
.fb-panier__retirer {
  position: absolute;
  top: -0.35rem;
  right: -0.35rem;
  z-index: 3;
}
.fb-croix {
  display: block;
  width: clamp(26px, 2.6vw, 32px);
  /* ⚠ 44 / 48 et non 80 / 71 : le dessin d'annulation est plus HAUT que large,
     l'ancienne croix était plus large que haute. Garder l'ancien rapport
     écraserait la pastille en ovale. */
  aspect-ratio: 44 / 48;
}
.fb-croix::before { background-image: url("../img/filtres/shop-annulation-base.png"); }
/* Au survol, la pastille passe du vert au noir — et c'est tout. La première
   version faisait pivoter la croix d'un quart de tour PAR-DESSUS le rond rouge
   que WooCommerce pose sur `a.remove:hover` : Fabian a demandé la prison pour
   cette animation (17 septembre 2026). Les deux `!important` plus bas répondent
   à ceux de WooCommerce sur `a.remove` (couleur rouge, fond rouge au survol). */
.fb-croix:hover::before,
.fb-croix:focus-visible::before { background-image: url("../img/filtres/shop-annulation-survol.png"); }
.woocommerce .fb-panier__retirer a.remove.fb-croix,
.woocommerce .fb-panier__retirer a.remove.fb-croix:hover {
  /* WooCommerce écrit aussi `width: 1em; height: 1em` (en 1.5em) — le dessin
     n'avait jamais son rapport 80 × 71. La largeur est reprise ici, la hauteur
     vient de `aspect-ratio`. */
  width: clamp(26px, 2.6vw, 32px);
  height: auto;
  background: transparent !important;
  color: transparent !important;
  font-size: 0;
}
/* Le « × » de repli, si le dessin manque. */
.fb-panier__retirer .remove:not(.fb-croix) {
  display: block;
  width: 1.6em;
  height: 1.6em;
  border-radius: 50%;
  background: var(--fb-noir);
  color: var(--fb-blanc);
  font-size: var(--fb-texte);
  line-height: 1.6;
  text-align: center;
  text-decoration: none;
}

/* LE BLOC REMONTE CONTRE LA VIGNETTE — « rapprocher nombre d'articles à la
   photo » (Fabian, 6 octobre 2026), repris trois fois parce que l'écart venait de
   trois endroits et qu'aucun des trois n'était cette marge.

     1. DIX-HUIT PIXELS DANS LA BOÎTE DE LA VIGNETTE : la ligne vide laissée par
        le blanc entre les deux fonctions qui composent l'image. Réglé par
        `line-height: 0` sur `.fb-carte__media`, voir sa règle plus haut.

     2. LA LIGNE DE TEXTE SOUS LE CHAMP DE QUANTITÉ, réglée juste en dessous.

     3. CETTE MARGE, qui ne vaut plus rien — elle est même légèrement négative.
        `-1%` n'est pas un réglage à l'œil : les masques dessinés de Fabian
        laissent en moyenne 0,95 % de leur toile en transparent sous le trait
        (mesuré sur le canal alpha des seize fichiers). L'image VISIBLE s'arrête
        donc un peu avant sa boîte, et on rend ce retrait. Une marge en
        pourcentage se calcule sur la LARGEUR du conteneur, qui est ici la
        largeur de la carte, c'est-à-dire aussi sa hauteur — la vignette est
        carrée. Le compte tombe juste à toutes les tailles d'écran.

        Les 0,2 rem qui s'y ajoutent, eux, sont un réglage à l'œil, demandé le
        6 octobre : « rapproche encore de l'image ». Ils sont écrits à part et
        pas fondus dans le pourcentage, parce que les deux nombres ne disent pas
        la même chose — le premier corrige une mesure et ne bougera que si Fabian
        relivre ses masques, le second est un goût et se reprend librement. */
.fb-panier__infos {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  margin-top: calc(-1% - 0.2rem);
  font-size: var(--fb-petit);
  text-align: center;
}
.fb-panier__infos p { margin: 0; }

/* ⚠ `display: flex` SUR L'ENVELOPPE DE LA QUANTITÉ, ET CE N'EST PAS POUR CENTRER.
   `.fb-qte--dessine` est un `inline-flex` : un élément de niveau LIGNE, donc posé
   sur une ligne de texte, avec le vide sous la ligne de base qu'une ligne de
   texte traîne toujours — quatre à cinq pixels sous le cadre, qui s'ajoutaient à
   l'écart avec la photo de l'article suivant. En faisant de l'enveloppe un
   conteneur flex, le cadre devient un élément flex : il n'y a plus de ligne, plus
   de ligne de base, et la boîte fait exactement la hauteur du dessin.

   On ne touche pas à `.fb-qte--dessine` lui-même : il sert aussi sur la fiche
   produit, où il est volontairement en ligne à côté du bouton « Ajouter au
   panier ». */
.fb-panier__quantite {
  display: flex;
  justify-content: center;
  line-height: 0;
}
/* `.fb-panier__titre` NE PORTE PLUS DE TITRE depuis le 23 septembre 2026 — le
   nom du produit et son prix unitaire sont retirés de la carte, le nom ne
   subsistant qu'en texte caché dans le lien de la vignette (voir
   woocommerce/cart/cart.php). Le bloc reste, parce qu'il porte encore ce qui
   distingue deux lignes identiques : la variante choisie, l'avis « disponible
   sur commande », et ce que les extensions accrochent à cet endroit.

   Quand il n'a rien à dire, il n'est PAS rendu du tout — c'est le gabarit qui
   en décide, pas une règle ici. Un <p> vide ne mesure rien, mais le `gap` du
   bloc le compte quand même, et `:empty` ne l'attraperait pas : un élément qui
   ne contient que des espaces n'est pas vide au sens de ce sélecteur. */
/* La variante choisie (WooCommerce la rend en <dl class="variation">). */
.fb-panier__titre .variation {
  display: block;
  margin: 0;
  font-weight: 400;
  font-style: italic;
}
.fb-panier__titre .variation dt,
.fb-panier__titre .variation dd { display: inline; margin: 0; }
.fb-panier__titre .variation dd p { display: inline; }
.fb-panier__sous-total { font-weight: 700; }
/* « Sous-total » ne s'affiche plus (Fabian, 6 octobre 2026) : la classe porte
   `screen-reader-text` dans le gabarit, et c'est elle qui le cache. Les deux
   règles qui étaient ici — un gris plus clair et un « : » en `::after` — ont
   été retirées plutôt que neutralisées : le deux-points est passé dans le texte
   lui-même, où un lecteur d'écran le lira à coup sûr, ce qui n'est pas garanti
   d'un contenu généré. La classe reste dans le HTML : elle nomme la chose. */

/* LE PANIER EN DEUX COLONNES, AVEC UNE BARRE À DROITE QUI SUIT.
   --------------------------------------------------------------------------
   Fabian, 23 septembre 2026 : « modifier l'affichage du panier pour avoir une
   barre à droite, avec le bouton valider la commande TOUJOURS visible, où
   qu'on soit dans le scroll. Donc partie à gauche, ce qu'on a dans le panier,
   la taille divisée par deux minimum pour les cartes d'articles, et à droite
   bah ce que j'ai dit avant. »

   CE QUE ÇA CHANGE PAR RAPPORT AU 17 SEPTEMBRE. La version précédente était
   déjà une grille de deux colonnes, mais seulement dans le BAS de la page :
   les cartes occupaient toute la largeur sur une première rangée, puis un
   trait, puis code promo à gauche et totaux à droite. Le total vivait donc
   sous les articles — sur un panier de six pièces, il fallait faire défiler
   jusqu'en bas pour voir combien on payait, et redéfiler pour vérifier ce
   qu'on achetait.

   Les deux colonnes partent maintenant du HAUT : les articles à gauche, le
   récapitulatif à droite, côte à côte. Et comme la colonne de droite est plus
   courte que celle de gauche, elle peut rester collée en haut de l'écran
   pendant qu'on fait défiler — `position: sticky`. Le bouton « Valider la
   commande » ne quitte plus le champ de vision.

   LA MÉCANIQUE, INCHANGÉE : le formulaire du panier est en `display: contents`
   — il n'a plus de boîte, ses enfants (la grille de cartes, puis les actions)
   se placent dans la grille directement, à côté de `.cart-collaterals` qui est
   pourtant hors du formulaire. Sans ça, les deux ne pourraient pas partager
   une grille : ils ne sont pas frères dans le HTML, et on ne peut pas les y
   rendre — le code promo DOIT être dans le formulaire (c'est lui qui
   l'envoie), le calcul de livraison DOIT être dehors (c'est son propre
   formulaire).

   `display: contents` sur un <form> : le formulaire fonctionne toujours (les
   champs lui appartiennent par le DOM, pas par la boîte), et cart.js le
   remplace en entier après une mise à jour — le nouveau reçoit la même classe,
   donc la même règle.

   Les rangées sont posées explicitement (`grid-row`) : en placement
   automatique, `.cart-collaterals` — dernier dans le DOM — tomberait sous les
   actions au lieu de monter en haut de sa colonne. */
/* ⚠ LA PAGE DU PANIER A LA LARGEUR DE TOUTES LES AUTRES, ET IL N'Y A DONC
   AUCUNE RÈGLE ICI. Ce commentaire remplace un `max-width: 1100px` qui a tenu
   trois semaines. L'argument d'alors : le panier n'est pas une page qu'on lit,
   c'est une page où l'on travaille — on compare, on compte, on ajuste des
   quantités — et la largeur confortable d'un texte n'a rien à y faire.

   Ce qu'il oubliait : personne ne regarde le panier seul. Venant de la
   boutique, l'ouvrir faisait sauter toute la colonne de contenu de 100 px vers
   l'extérieur, en-tête et pied compris. Aucune autre page du site ne fait ça,
   et celle-ci le faisait au moment précis où l'on demande de la confiance.

   Le gabarit commun revient donc (Fabian, 30 septembre 2026 : « la page de
   panier doit faire la même largeur que le reste »), et c'est la grille des
   cartes qui cède — trois par rangée au lieu de quatre, plus petites. Voir la
   règle plus bas : les 200 px rendus au gabarit sont payés là. */

/* ⚠ LA LARGEUR DE LA BARRE ET LA GOUTTIÈRE SONT DES VARIABLES, ET ELLES SONT
   POSÉES SUR LE <body>. Elles ne servent pas qu'ici : le titre de la page doit
   se centrer sur la COLONNE DE GAUCHE (voir `.fb-panier__entete`), et il est
   frère de cette grille, pas son enfant — il ne peut donc pas lire une variable
   qu'elle porterait. Les deux règles lisent la même source : si la barre
   s'élargit un jour, le titre se recentre tout seul. */
.woocommerce-cart {
  --fb-panier-barre: 18rem;
  --fb-panier-goutte: clamp(1.5rem, 4vw, 3rem);
}
.fb-panier-page {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--fb-panier-barre, 18rem));
  column-gap: var(--fb-panier-goutte, clamp(1.5rem, 4vw, 3rem));
  row-gap: clamp(1rem, 2.5vw, 1.5rem);
  align-items: start;
}
.fb-panier-page .fb-panier-form   { display: contents; }
.fb-panier-page .fb-panier__lignes  { grid-column: 1; grid-row: 1; }
.fb-panier-page .fb-panier__actions { grid-column: 1; grid-row: 2; }

/* LE TITRE DE LA PAGE DU PANIER, DESSINÉ (729 × 99).

   ⚠ IL N'EST PAS DANS LA GRILLE, et il ne doit pas y revenir. Il y a été une
   heure : posé dans le formulaire, il devenait une rangée de la COLONNE DE
   GAUCHE — cadré sur les cartes, décalé du bord de la page, et la barre de
   droite lui remontait à hauteur pour l'accompagner. Or ce n'est pas le titre
   des cartes, c'est celui de la page. Il est donc frère de `.fb-panier-page`
   (woocommerce/cart/cart.php), pleine largeur, et la grille a retrouvé ses deux
   rangées d'origine — c'est pour ça qu'on ne lit ici aucun `grid-row`.

   ⚠ LA LARGEUR EST CALÉE SUR CELLE DU SLOGAN DE L'EN-TÊTE, ET CE N'EST PAS UNE
   FORMULE APPROCHÉE. Fabian, 30 septembre 2026 : « même taille de texte que le
   sous-titre dans la navbar ». Deux dessins, deux canevas, deux marges
   transparentes différentes — régler les deux boîtes à la même hauteur aurait
   donné deux tailles de LETTRES différentes. Ce sont donc les lettres qu'on a
   mesurées, pas les fichiers.

   Ce qu'il faut retirer du canevas du titre avant de mesurer quoi que ce soit :
   les 10 px de marge du haut, et surtout le TRAIT DE SOULIGNEMENT, qui occupe à
   lui seul les lignes 71 à 91 et n'est pas de la lettre.

   ⚠ ET IL FAUT MESURER DES CAPITALES ORDINAIRES, ce qui a coûté un premier
   essai. La bande d'encre complète du titre fait 61 px — mais c'est le « D » de
   DANS, la plus haute lettre du lettrage, qui la fixe ; « MON » n'en mesure que
   50. Prendre 61 pour hauteur de capitale revenait à croire le titre plus gros
   qu'il n'est, donc à le réduire d'autant : il est sorti 14 % trop petit, et ça
   se voyait en le comparant au slogan deux lignes plus haut. La valeur retenue
   est la moyenne des deux, 55 px, qui est ce que l'œil lit.

     · le slogan : capitales de 38 px (« DESSINE ») sur un canevas de 64
                   (`.fb-phrases`, § en-tête), boîte large de 315 px au plafond ;
     · ce titre  : capitales de 55 px sur un canevas de 729 × 99.

   D'où : hauteur de lettre du slogan = 38 × (B × 64/742) / 64 = 0,0512 × B, et
   celle du titre = 55 × W / 729 = 0,0754 × W. Les égaler donne W = 0,679 × B.
   Les trois nombres du `clamp()` ci-dessous sont donc exactement ceux de
   `.fb-phrases` multipliés par 0,679 — pente comprise, pour que les deux ne
   divergent jamais en cours de route. Si le slogan change de taille, celui-ci
   doit suivre par le même facteur.

   La marge basse est à nous : hors de la grille, le titre ne profite plus du
   `row-gap`, et la marge par défaut d'un <h2> est en `em` de la police du
   document — sans rapport avec la taille du dessin. Le `auto` à gauche et à
   droite le centre : `.fb-img` est un `inline-block`, il ne se centrerait pas
   sans passer en `block`, et on ne va pas poser un `text-align` sur le bloc
   `.woocommerce` qui l'entoure pour ça. */
/* ⚠ IL EST CENTRÉ SUR LES CARTES, PAS SUR LA PAGE — correction du 6 octobre
   2026. Fabian : « “dans mon panier j'ai mis…” centrer au-dessus des 3 colonnes ».

   Centré sur toute la largeur, il tombait au milieu d'un bloc qui comprend la
   barre des totaux — donc nettement à droite du milieu des trois colonnes de
   cartes qu'il annonce. L'œil, lui, ne compare pas le titre à la page : il le
   compare à ce qui est dessous.

   ⚠ LE DÉCALAGE SE FAIT EN `translate`, ET SURTOUT PAS EN `padding`. Premier
   essai : un `padding-inline-end` de la largeur de la barre, pour que la zone de
   contenu s'arrête au bord de la colonne de gauche et que le dessin s'y centre
   tout seul. Ça ne pouvait pas marcher, parce que `.fb-panier__entete` et
   `.fb-panier__entete--dessine` sont sur LE MÊME ÉLÉMENT : le <h2> n'est pas une
   boîte pleine largeur contenant l'image, il EST l'image, large de 214 px au
   plafond. Avec `box-sizing: border-box`, 336 px de padding dans 214 px de
   largeur donnent une zone de contenu nulle et une boîte de 336 — le dessin,
   peint sur la boîte, s'est simplement élargi et n'a pas bougé d'un pixel.

   `translate` ne touche à aucune mesure : le dessin reste centré sur la page par
   ses marges `auto`, puis on le déplace de la MOITIÉ de ce que la colonne de
   droite occupe. C'est exactement l'écart entre le milieu de la page et le
   milieu des cartes, et la démonstration tient en une ligne : si la page mesure
   C et la colonne de droite G, les cartes vont de 0 à C−G, leur milieu est
   (C−G)/2, celui de la page C/2, et la différence G/2. Aucune des deux largeurs
   n'a besoin d'être connue — seule G l'est, et elle est juste au-dessus. */
/* ⚠ LA MARGE BASSE EST GÉNÉREUSE, ET C'EST UNE DEMANDE EXPLICITE.
   Elle valait clamp(0.75rem, 2vw, 1.25rem). La phrase dessinée touchait
   presque la première rangée de vignettes : Fabian a demandé « plus d'espace
   entre dans mon panier j'ai mis et les articles » (7 octobre 2026).
   Le plafond est passé de 20 à 44 px — le double, parce qu'une phrase
   MANUSCRITE a besoin de plus d'air qu'un titre composé pour se lire comme une
   phrase et non comme une étiquette collée au contenu. Le plancher monte de 12
   à 20 px seulement : sur un téléphone, la hauteur d'écran est la ressource
   rare, et la phrase y est déjà seule sur sa ligne. */
.fb-panier__entete {
  margin: 0 0 clamp(1.25rem, 3.4vw, 2.75rem);
  translate: calc( -0.5 * ( var(--fb-panier-barre, 18rem) + var(--fb-panier-goutte, 3rem) ) );
  font-size: var(--fb-petit);
  text-align: center;
}
.fb-panier__entete--dessine {
  display: block;
  width: clamp(105px, 16.3vw, 214px);
  height: auto;
  margin-inline: auto;
}

/* LES CARTES : TROIS PAR RANGÉE, ET PLUS PETITES QUE DANS LA BOUTIQUE.
   La page ayant rendu ses 200 px (voir plus haut), la colonne de gauche ne fait
   plus 668 px mais 468 : 804 px de contenu, moins la barre de 18 rem, moins la
   gouttière qui les sépare.

   `grid-template-columns` N'EST PLUS ÉCRIT ICI, et c'est le cœur du changement.
   La boutique pose déjà `repeat(3, minmax(0, 1fr))` sur `.woocommerce
   ul.products`, avec ses paliers à 700 et 430 px. Le panier hérite donc des
   trois colonnes demandées sans les redéclarer — et héritera aussi du jour où
   la boutique changera d'avis. La version précédente superposait un `auto-fill`
   à partir de 120 px qui en tirait quatre à cinq par rangée : c'est exactement
   ce qu'on retire.

   Ne reste que la gouttière, plus serrée que celle de la boutique. Dans 468 px,
   trois cartes séparées par 2 × 20 px mesurent 143 px ; avec l'écart de la
   boutique (jusqu'à 32 px) elles tomberaient à 135. Le panier est dense, la
   boutique respire — ce n'est pas la même page.

   ⚠ 120 px RESTE LE PLANCHER même s'il a disparu du code : c'est la largeur du
   champ de quantité dessiné (« − [ 1 ] + » mesure ~92 px à 30 px de haut, cadre
   et boutons compris). 143 px laisse de la marge, mais élargir la barre de
   droite ou rajouter une colonne le franchirait — il faudrait redessiner le
   champ avant de toucher à ces nombres. */
/* ⚠ LES DEUX ÉCARTS SONT RÉGLÉS SÉPARÉMENT DEPUIS LE 6 OCTOBRE 2026. Fabian :
   « rapprocher nombre d'articles à la photo, et faire plus d'espace entre les
   lignes des articles ».

   C'était un seul `gap`, donc le même nombre dans les deux sens — et les deux
   sens n'ont rien à voir. À l'horizontale, l'écart sépare deux articles POSÉS
   CÔTE À CÔTE, et le serrer rapproche trois cartes dans 468 px. À la verticale,
   il sépare la quantité d'un article du bas de la photo du SUIVANT : trop
   serré, les deux rangées se lisent comme un seul pavé et on ne sait plus quel
   chiffre appartient à quelle image. On double donc l'écart vertical et on
   laisse l'horizontal où il est. */
.fb-panier-page .fb-panier__lignes.products {
  column-gap: clamp(0.75rem, 1.8vw, 1.25rem);
  row-gap: clamp(1.75rem, 3.5vw, 2.75rem);
}
/* Le champ de quantité et la croix suivent l'échelle des cartes. */
.fb-panier__ligne .fb-qte--dessine { --fb-qte-h: 30px; }
.fb-panier__ligne .fb-croix,
.woocommerce .fb-panier__ligne .fb-panier__retirer a.remove.fb-croix,
.woocommerce .fb-panier__ligne .fb-panier__retirer a.remove.fb-croix:hover {
  width: clamp(20px, 1.9vw, 24px);
}

/* --------------------------------------------------------------------------
   LE PANIER SUR TÉLÉPHONE — CE QU'ON TOUCHE DOIT ÊTRE TOUCHABLE
   --------------------------------------------------------------------------
   Fabian, 7 octobre 2026 : « agrandir les trucs interactifs (+, −, nombre,
   suppression du panier) » et « agrandir “dans mon panier j'ai mis” ».

   POURQUOI C'ÉTAIT SI PETIT, parce que ce n'était pas une négligence : ces
   deux nombres sont calés sur des cartes de 143 px de large, trois par rangée
   dans la colonne de 468 px du grand écran. Sur un téléphone, la grille est
   passée à DEUX colonnes (voir `.woocommerce ul.products`) et chaque carte
   fait 167 px dans 350 px utiles — il y a donc 24 px de plus par carte, et
   plus aucune raison de tenir l'échelle du grand écran.

   LES CIBLES. « − » et « + » font 55 % de `--fb-qte-h` : à 30 px, ce sont des
   boutons de 16,5 px. On vise 44 px, le minimum d'une cible tactile
   confortable (WCAG 2.5.8 niveau AAA, et la valeur d'Apple) — ce qui donne
   enfin des signes de 24 px. Le champ complet « − [ 1 ] + » passe alors de
   ~92 px à ~135 px de large : il reste 32 px de marge dans une carte de 167,
   le plancher documenté plus haut tient toujours.

   LA CROIX passe de 20 à 32 px. Elle est en `position: absolute` dans le coin
   de la carte, SANS rembourrage : son dessin EST sa cible, à un pixel près —
   il n'y a pas de zone invisible autour pour rattraper la différence. 32 px
   est le plafond qu'elle atteint déjà sur la fiche produit (le `clamp` de
   `.fb-croix`), donc une valeur que le dessin est fait pour tenir ; et sur une
   carte de 167 px, 44 px occuperaient plus du quart de la vignette pour un
   bouton qui ne fait que retirer un article. */
@media (max-width: 700px) {
  .fb-panier__ligne .fb-qte--dessine { --fb-qte-h: 44px; }
  .fb-panier__ligne .fb-croix,
  .woocommerce .fb-panier__ligne .fb-panier__retirer a.remove.fb-croix,
  .woocommerce .fb-panier__ligne .fb-panier__retirer a.remove.fb-croix:hover {
    width: 32px;
  }

  /* « DANS MON PANIER J'AI MIS » (729 × 99). Il tombait à 105 px de large,
     plancher de son `clamp` — soit un lettrage de 14 px, plus petit que le
     texte courant, pour le titre de la page. Le dessin est posé seul sur sa
     ligne et centré : il peut prendre la largeur utile.
     `min(80vw, 300px)` : 300 px donne 41 px de haut, l'échelle du titre de
     grand écran (214 px → 29 px, mais sur une page quatre fois plus large) ;
     et 80vw le fait rétrécir sous 375 px de fenêtre plutôt que de toucher les
     gouttières. */
  .fb-panier__entete--dessine { width: min(80vw, 300px); }
}

/* ⚠ PLUS AUCUN TRAIT ICI, ET LA RÈGLE QUI EN POSAIT UN A ÉTÉ SUPPRIMÉE.
   Il y en a eu trois formes successives : un `::after` de la grille quand les
   cartes occupaient une rangée pleine largeur, puis — la grille passée à deux
   colonnes — une bordure haute de chaque colonne basse, celle des actions et
   celle du panneau des totaux. La seconde était même conditionnée par un
   `:has(.fb-panier__coupon)`, parce que sans code promo elle laissait un trait
   noir flottant sous la dernière vignette, à séparer le vide du vide.

   Tout ça tombe avec le déménagement du code promo dans le panneau des totaux
   (30 septembre 2026, voir woocommerce/cart/cart.php) : il ne reste plus deux
   blocs à séparer sous les cartes, il n'en reste aucun. `.fb-panier__actions` ne
   contient plus que le bouton « Mettre à jour le panier » — masqué dès que
   panier.js prend la main — et le nonce du formulaire.

   Le trait d'en face, sur `.cart-collaterals`, est parti dans le même geste et
   pour la même raison (Fabian : « retire la ligne en haut du total panier »). */

/* LA BARRE DE DROITE, QUI RESTE À L'ÉCRAN.
   `align-self: start` est la condition du `sticky` et pas un détail de
   présentation : par défaut un élément de grille s'étire sur toute la hauteur
   de sa zone (`stretch`). Étiré, il n'a nulle part où coller — il occupe déjà
   toute la colonne, et `sticky` ne fait rien du tout. Rétracté sur son
   contenu, il lui reste de la place pour glisser.

   `grid-row: 1 / span 2` lui donne la hauteur des DEUX rangées de gauche comme
   terrain de glisse : sans ça, il s'arrêterait au bas de la première.

   Le `top` laisse un doigt d'air sous le haut de la fenêtre. Il n'a pas à
   éviter l'en-tête, qui défile avec la page — mais le pied, lui, est FIXE :
   d'où le `max-height`, qui empêche la barre de passer derrière la bande
   noire. `100svh` et non `100vh` : sur mobile, la barre d'adresse du
   navigateur mange la différence. */
.fb-panier-page .cart-collaterals {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: start;
  position: sticky;
  top: clamp(0.75rem, 2vw, 1.5rem);
  max-height: calc(100svh - var(--fb-pied-bande-h, 80px) - 3rem);
  overflow-y: auto;

  /* ⚠ CES DEUX LIGNES NE DÉPLACENT RIEN, ELLES RENDENT UN BORD DE DÉCOUPE.
     `overflow-y: auto` ne reste pas seul : la spec interdit de mélanger
     `visible` avec une valeur qui défile, donc `overflow-x` passe à `auto`
     avec lui et la boîte se met à ROGNER HORIZONTALEMENT — alors qu'on n'a
     jamais demandé qu'à défiler vers le bas.
     Conséquence vue dans le panier : le champ « Code promo » commence
     exactement au bord gauche du panneau (mesuré, 834 px pour les deux), et
     son halo de focus — `outline: 2px` posé à `outline-offset: 2px`, donc
     4 px DEHORS — était coupé net sur toute sa gauche. Le même sort attendait
     les boutons pleine largeur du panneau.
     On ne peut pas s'en sortir avec `overflow-x: clip` + `overflow-clip-margin` :
     cette paire exige `clip` sur LES DEUX axes, et Safari ne connaît de toute
     façon pas la seconde propriété.
     D'où le remplissage de 4 px — exactement la portée de l'outline, pas un
     nombre choisi à l'œil — repris aussitôt en marge négative : la boîte de
     bordure déborde de 4 px dans la gouttière de la grille, le CONTENU ne
     bouge pas, et le halo a sa place.
     La largeur est reprise explicitement parce que l'étirement de la grille,
     ici, ne tient PAS compte des marges négatives : mesuré, la boîte gardait
     ses 288 px et se contentait de glisser de 4 px vers la gauche, le panneau
     perdant 8 px de contenu et son bord droit cessant d'affleurer celui de la
     colonne d'articles (1122 px). Avec `calc(100% + 8px)`, le contenu retrouve
     exactement ses bornes d'avant. */
  padding-inline: 4px;
  margin-inline: -4px;
  width: calc(100% + 8px);
}

/* ⚠ SUR UN ÉCRAN BAS, ON ARRÊTE DE COLLER. Le récapitulatif fait environ
   300 px de haut (trois lignes de totaux, la livraison, le bouton). Sous
   ~600 px de fenêtre — un portable posé à l'horizontale, une fenêtre réduite —
   il ne tient plus dans la hauteur disponible, et le `max-height` ci-dessus le
   ferait défiler DANS lui-même : le bouton « Valider la commande » se
   retrouverait caché au fond d'une boîte, c'est-à-dire exactement l'inverse de
   ce qui est demandé. Rendu au flux de la page, il redevient atteignable en
   faisant défiler normalement. */
@media (max-height: 600px) {
  .fb-panier-page .cart-collaterals {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}

/* Une colonne : la barre de droite passe sous les articles. Le seuil est à
   900 px et non 800 — la colonne de gauche a une voisine de 18 rem à nourrir,
   elle devient trop étroite plus tôt.

   Depuis que la page est rentrée dans le gabarit, ce seuil tombe pile sur
   `--fb-largeur` : en dessous, la page était de toute façon plus étroite que son
   propre plafond. Les deux nombres sont égaux par coïncidence, pas par
   dépendance — si `--fb-largeur` change, celui-ci n'a pas à suivre. */
@media (max-width: 900px) {
  .fb-panier-page { grid-template-columns: minmax(0, 1fr); }
  /* Une seule colonne : le titre se recentre sur toute la largeur. Sans ça il
     garderait le décalage qui le cale sur les cartes en grand écran, et il
     partirait à gauche au-dessus d'une grille qui, elle, est pleine largeur. */
  .fb-panier__entete { translate: none; }
  /* Ce bloc ne contient plus que « Mettre à jour le panier », masqué dès que
     panier.js prend la main : il est donc le plus souvent invisible, et sa
     rangée se referme sur zéro. Il garde quand même sa place explicite, pour le
     cas sans JavaScript où le bouton, lui, doit se trouver sous les articles et
     non sous le panneau de commande. */
  .fb-panier-page .fb-panier__actions { grid-column: 1; grid-row: 2; }
  .fb-panier-page .cart-collaterals {
    grid-column: 1;
    grid-row: 3;
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}

/* Les actions : il n'y reste que « Mettre à jour » — que panier.js masque dès
   qu'il prend la main (`fb-panier--auto`), la quantité se mettant à jour toute
   seule. Le bouton reste dans le HTML : sans JavaScript, c'est lui qui applique
   les quantités tapées. Le code promo, qui était ici, a rejoint le panneau des
   totaux (30 septembre 2026). */
.fb-panier__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}

/* LE CODE PROMO, EN TÊTE DU PANNEAU DE COMMANDE.
   Il se lit AVANT les totaux, et c'est l'ordre du geste : on applique un code,
   puis on lit ce qu'on paie. L'inverse ferait relire le total deux fois. (C'est
   aussi l'ordre que la version à deux colonnes imposait déjà en dessous de
   900 px, où le panneau passait sous les articles.)

   La marge basse fait le seul écart entre lui et « Total panier » — il n'y a
   plus de trait entre les deux, ni au-dessus. */
.fb-panier__coupon {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: clamp(1rem, 2.5vw, 1.5rem);
}
/* Le titre, à la même taille que « Total panier » qui le suit : ce sont les deux
   intitulés du panneau, ils se lisent au même niveau. */
.fb-panier__coupon-titre { font-size: var(--fb-texte); font-weight: 700; }
.fb-panier__coupon-champs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
/* ⚠ `flex: 1 1 0` ET `min-width: 0`, PARCE QUE LA COLONNE FAIT 288 px ET NON 468.
   Un <input type="text"> a une largeur intrinsèque bien à lui — la fameuse
   `size=20`, ~177 px — que `flex-shrink` ne peut PAS réduire en dessous de sa
   taille minimale de contenu tant que `min-width` vaut `auto`. Dans l'ancienne
   colonne, large, ça passait ; dans le panneau, le champ et le bouton
   n'entraient plus ensemble et le bouton tombait à la ligne. Les deux
   déclarations vont par paire : `flex` seul ne suffit pas, `min-width` seul non
   plus. */
.fb-panier__coupon .input-text {
  flex: 1 1 0;
  min-width: 0;
  padding: 0.4em 0.8em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  font: inherit;
  font-size: var(--fb-petit);
}
.fb-panier__coupon .input-text:focus-visible { outline: 2px solid var(--fb-vert); outline-offset: 2px; }
/* Quatre classes : `.woocommerce .fb-lien-pilule.button` (trois) pose un
   `display` plus bas, et le masquage doit passer devant.

   La classe est posée sur `.fb-panier-page` et non sur le formulaire : cart.js
   peut remplacer le formulaire (calculateur de frais de port), et le bouton
   réapparaîtrait alors tout seul au milieu d'un panier automatique. */
.woocommerce .fb-panier--auto .fb-panier__maj.button { display: none; }

/* ==========================================================================
   LE PANIER QUI NE RECHARGE PLUS — les trois états que le script fait vivre.
   ==========================================================================
   Voir assets/js/panier.js et inc/panier-ajax.php. Tout ce qui suit est du
   CONFORT : sans JavaScript, aucune de ces classes n'est jamais posée, et le
   panier se comporte comme un formulaire ordinaire. */

/* 1. LE PANNEAU DES TOTAUX PENDANT QU'IL ATTEND SA RÉPONSE.

   C'est le seul endroit de la page qui ne peut pas répondre à l'instant : le
   port dépend du poids de ce qui reste, les promos ont des paliers, la TVA
   s'arrondit sur le total. Le sous-total d'une ligne, lui, est une
   multiplication — il est déjà à jour quand celui-ci pâlit.

   ON PÂLIT, ON NE VIDE PAS. Remplacer les nombres par un squelette ou un
   sablier ferait sauter la hauteur du bloc et bouger la page sous le curseur :
   c'est exactement le « flash » qu'on est en train de supprimer. Là, les
   anciens montants restent lisibles, simplement en retrait, et sont remplacés
   sans que rien ne se déplace.

   `aria-busy` dit la même chose aux lecteurs d'écran, qui ne voient pas
   l'opacité. */
.cart_totals { transition: opacity var(--fb-transition); }
.cart_totals.is-encours { opacity: 0.4; }

/* 2. LA CARTE RETIRÉE S'EFFACE.

   Elle part à l'instant du clic — sur la croix, ou sur le « − » qui ramène la
   quantité à zéro — sans attendre la réponse du serveur. Sa hauteur tombe à
   zéro plutôt que de disparaître d'un coup : les cartes suivantes remontent au
   lieu de sauter, et c'est précisément ce sursaut qu'on est en train de retirer
   du panier.

   `overflow: hidden` n'est posé qu'au moment de partir. En permanence, il
   rognerait la croix de retrait, qui déborde du coin de la carte. */
.fb-panier__ligne { transition: height 350ms ease, opacity 250ms ease; }

.fb-panier__ligne.is-parti {
  overflow: hidden;
  opacity: 0;
  padding-block: 0;
  margin-block: 0;
  pointer-events: none;
}

/* 3. LES ERREURS, PUISQUE PLUS RIEN NE LES PORTE.

   Les messages de succès sont supprimés, pas les erreurs — mais la page ne
   rechargeant plus, elles n'ont plus de bandeau où s'afficher. Le script écrit
   donc ici : en tête du panier, en rouge, avec `role="alert"` pour que les
   lecteurs d'écran l'annoncent sans qu'on ait à déplacer le focus.

   Le rouge est celui du formulaire de contact — une seule couleur d'erreur sur
   le site. */
.fb-panier__alerte {
  margin: 0 0 1.5rem;
  padding: 0.7em 1em;
  border-left: 3px solid #b3261e;
  background: rgb(179 38 30 / 0.08);
  color: var(--fb-noir);
  font-size: var(--fb-petit);
}

/* LE LIEN-PILULE — le repli texte des boutons, réutilisé partout où Fabian n'a
   pas (encore) dessiné : « Voir toute la boutique », « Appliquer », « Mettre
   à jour ». Même langage que `.fb-retour--texte`. */
.fb-lien-pilule,
.woocommerce .fb-lien-pilule.button,
.woocommerce .shipping-calculator-form button.button {
  display: inline-block;
  padding: 0.4em 1.1em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  background: transparent;
  color: var(--fb-noir);
  font-family: var(--fb-serif);
  font-size: var(--fb-petit);
  font-weight: 700;
  line-height: 1.4;
  text-decoration: none;
  transition: background var(--fb-transition), color var(--fb-transition);
}
.fb-lien-pilule:hover,
.fb-lien-pilule:focus-visible,
.woocommerce .fb-lien-pilule.button:hover,
.woocommerce .fb-lien-pilule.button:focus-visible,
.woocommerce .shipping-calculator-form button.button:hover,
.woocommerce .shipping-calculator-form button.button:focus-visible {
  background: var(--fb-vert);
  border-color: var(--fb-vert);
  color: var(--fb-blanc);
}
.woocommerce .fb-lien-pilule.button:disabled,
.woocommerce .fb-lien-pilule.button[aria-disabled="true"] { opacity: 0.4; }

/* Les totaux : WooCommerce les rend en tableau (cart_totals), dans une boîte
   à coins arrondis et bordure grise. On enlève la boîte : des lignes fines
   entre les postes, le libellé à gauche, le montant à droite, le total en
   gras. Le bouton « Valider la commande » suit, dans le langage des pilules. */
.woocommerce .cart-collaterals .cart_totals {
  float: none;
  width: 100%;
  margin: 0;
}
.woocommerce .cart-collaterals .cart_totals h2 {
  margin: 0 0 0.6rem;
  font-size: var(--fb-texte);
  line-height: 1.4;
}
.woocommerce .cart-collaterals .cart_totals table.shop_table {
  margin: 0 0 1.25rem;
  border: 0;
  border-radius: 0;
  border-collapse: collapse;
  font-size: var(--fb-petit);
}
.woocommerce .cart-collaterals .cart_totals table.shop_table th,
.woocommerce .cart-collaterals .cart_totals table.shop_table td {
  padding: 0.6em 0;
  border: 0;
  border-bottom: 1px solid var(--fb-gris-clair);
  vertical-align: top;
}
.woocommerce .cart-collaterals .cart_totals table.shop_table th { width: 40%; font-weight: 400; color: var(--fb-gris); }
.woocommerce .cart-collaterals .cart_totals table.shop_table td { text-align: right; }
.woocommerce .cart-collaterals .cart_totals table.shop_table tr.order-total th,
.woocommerce .cart-collaterals .cart_totals table.shop_table tr.order-total td {
  padding-top: 0.9em;
  border-bottom: 0;
  font-size: var(--fb-texte);
  font-weight: 700;
  color: var(--fb-noir);
}
/* LA LIGNE « EXPÉDITION » ET CELLE DU CALCULATEUR.
   La première reste une ligne de montant ordinaire — libellé à gauche, prix à
   droite, comme « Sous-total » et « Total ». La seconde est une rangée à part,
   en `colspan="2"`, posée par woocommerce/cart/cart-shipping.php : le
   formulaire y dispose des 288 px entiers au lieu des 173 px de la colonne de
   droite. Le pourquoi et les deux tentatives CSS qui ont échoué avant sont
   racontés dans l'en-tête de ce gabarit — c'est là que vit la décision.

   Un formulaire s'aligne sur son bord de départ, pas sur celui des montants :
   d'où le `text-align: left`, qui défait le `right` posé sur tous les <td>. */
.woocommerce .cart-collaterals .cart_totals ul#shipping_method { margin: 0; }
.woocommerce .cart-collaterals .cart_totals .woocommerce-shipping-destination { margin: 0.4em 0 0; }
.woocommerce .cart-collaterals .cart_totals tr.fb-expedition-calcul > td {
  padding-top: 0;
  text-align: left;
}
/* Le trait passe SOUS le calculateur, sinon il couperait l'expédition en deux.
   `:has()` parce que la rangée du calculateur n'existe pas toujours : elle
   disparaît si Fabian désactive le calcul dans les réglages de WooCommerce, et
   la ligne d'expédition doit alors récupérer son trait. Un navigateur sans
   `:has()` garde les deux traits — un de trop, jamais un de manquant. */
.woocommerce .cart-collaterals .cart_totals tr.shipping:has(+ tr.fb-expedition-calcul) > th,
.woocommerce .cart-collaterals .cart_totals tr.shipping:has(+ tr.fb-expedition-calcul) > td {
  border-bottom: 0;
}
.woocommerce .cart-collaterals .cart_totals .woocommerce-shipping-calculator { margin: 0; }
.woocommerce .cart-collaterals .cart_totals .shipping-calculator-button { color: var(--fb-noir); }
.woocommerce .cart-collaterals .cart_totals .shipping-calculator-button:hover { color: var(--fb-vert); }

/* LE FORMULAIRE DÉPLIÉ — habillé comme le champ « Code promo » d'en face.
   ⚠ PAS DE `display: flex` ICI, ce serait sans effet : le dépliage est un
   `slideToggle` de jQuery, qui écrit `display: block` EN LIGNE sur la section.
   Un attribut de style passe devant n'importe quelle règle de cette feuille.
   On habille donc les rangées une à une — ce qui revient au même, à un `gap`
   près. */
.woocommerce .cart-collaterals .shipping-calculator-form { margin: 0.75rem 0 0; }
.woocommerce .cart-collaterals .shipping-calculator-form .form-row { margin: 0 0 0.7rem; }
.woocommerce .cart-collaterals .shipping-calculator-form label {
  display: block;
  margin-bottom: 0.3em;
  color: var(--fb-gris);
  font-size: var(--fb-petit);
}
.woocommerce .cart-collaterals .shipping-calculator-form .input-text,
.woocommerce .cart-collaterals .shipping-calculator-form select {
  width: 100%;
  padding: 0.4em 0.8em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  background: transparent;
  font: inherit;
  font-size: var(--fb-petit);
  /* Repris tel quel par la boîte de selectWoo plus bas : sans cette valeur
     commune, les champs tapés faisaient 38 px et les listes déroulantes 34. */
  line-height: 1.5;
}
.woocommerce .cart-collaterals .shipping-calculator-form .input-text:focus-visible,
.woocommerce .cart-collaterals .shipping-calculator-form select:focus-visible {
  outline: 2px solid var(--fb-vert);
  outline-offset: 2px;
}
/* Le <select> du pays est masqué et remplacé par un <span> que fabrique
   selectWoo (le select2 de WooCommerce). Sans ces trois règles, le pays serait
   le seul champ nu de la colonne — bordure fine, coins carrés, texte collé au
   bord. `!important` sur la largeur : selectWoo l'écrit en ligne, en pixels,
   calculés sur la largeur qu'avait le <select> À L'INSTANT DE L'OUVERTURE. */
.woocommerce .cart-collaterals .shipping-calculator-form .select2-container { width: 100% !important; }
.woocommerce .cart-collaterals .shipping-calculator-form .select2-container--default .select2-selection--single {
  height: auto;
  padding: 0.4em 0.8em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  background: transparent;
}
/* `min-height` et pas seulement `line-height` : quand le pays choisi n'a pas de
   cantons connus, selectWoo rend une boîte VIDE. Sans plancher, elle tombait à
   12 px de haut — un champ écrasé au milieu de trois champs normaux. */
.woocommerce .cart-collaterals .shipping-calculator-form .select2-container--default .select2-selection--single .select2-selection__rendered {
  padding: 0;
  min-height: 1.5em;
  color: var(--fb-noir);
  font-size: var(--fb-petit);
  line-height: 1.5;
}
.woocommerce .cart-collaterals .shipping-calculator-form .select2-container--default .select2-selection--single .select2-selection__arrow {
  top: 50%;
  right: 0.7em;
  transform: translateY(-50%);
}
/* Le bouton : celui du panier sans JavaScript et celui-ci parlent la même
   langue que « Appliquer » — voir la règle du lien-pilule, qui les réunit. */
.woocommerce .cart-collaterals .shipping-calculator-form p:last-of-type { margin: 0; }
.woocommerce-cart .wc-proceed-to-checkout { padding: 0; }

/* « VALIDER LA COMMANDE » — LE REPLI D'ABORD, LE DESSIN ENSUITE.

   Ce qui suit est la pilule noire qui a tenu ce rôle jusqu'au 19 octobre 2026,
   et qui reste la réponse quand le dessin n'est pas là : le gabarit
   `cart/proceed-to-checkout-button.php` ne pose `.fb-commander` que si le
   fichier existe, et sans elle c'est cette règle-ci qui s'applique, entière.
   On ne la supprime donc pas — un panier sans bouton de commande n'est pas un
   panier. */
.woocommerce .cart-collaterals .checkout-button.button {
  display: block;
  padding: 0.7em 1.4em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  background: var(--fb-noir);
  color: var(--fb-blanc);
  font-family: var(--fb-serif);
  font-size: var(--fb-texte);
  font-weight: 700;
  text-align: center;
  transition: background var(--fb-transition), border-color var(--fb-transition);
}
.woocommerce .cart-collaterals .checkout-button.button:hover,
.woocommerce .cart-collaterals .checkout-button.button:focus-visible {
  background: var(--fb-vert);
  border-color: var(--fb-vert);
  color: var(--fb-blanc);
}

/* LE DESSIN DE FABIAN (716 × 139, vert au repos, noir au survol).

   Il était le dernier bouton de la boutique encore composé en Garamond : le
   panier s'ouvrait sur les vignettes découpées, la quantité dessinée et le
   titre à la main de Fabian, et se refermait sur un objet de gabarit — celui,
   justement, qu'on vient chercher sur cette page.

   TOUTE LA MÉCANIQUE EST DANS `.fb-img`, posée par le gabarit : les deux calques
   empilés, le survol qui se pose SANS effacer le repos (§ 5, l'éclair blanc),
   le préchargement des deux états. Les URL et l'`aspect-ratio` viennent de
   `_sprites.css`. Il ne reste donc ici que deux choses à dire : déshabiller la
   pilule, et donner sa largeur au dessin.

   ⚠ QUATRE SÉLECTEURS CONTRE QUATRE, ET C'EST L'ORDRE QUI TRANCHE. La règle de
   la pilule ci-dessus pèse autant que celle-ci (`.woocommerce`,
   `.cart-collaterals`, `.checkout-button`, une classe) : à égalité, le dernier
   arrivé gagne. D'où sa place ici, après, et non avant.

   LA LARGEUR EST CELLE DU PANNEAU, et la hauteur suit. C'est l'inverse des
   autres boutons dessinés du site — « ENVOYER », « JE M'INSCRIS », « AJOUTER
   AU PANIER » se mesurent en HAUTEUR, parce qu'ils sont posés sur une ligne à
   côté d'un champ dont ils doivent épouser la taille. Celui-ci est seul sur sa
   ligne, au bas d'une colonne de 288 px : c'est elle qui donne la mesure, et
   `aspect-ratio` en déduit 56 px de haut. */
.woocommerce .cart-collaterals .checkout-button.fb-commander {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  transition: none;
}
.woocommerce .cart-collaterals .checkout-button.fb-commander:hover,
.woocommerce .cart-collaterals .checkout-button.fb-commander:focus-visible {
  background: transparent;
  border-color: transparent;
}

/* LES BOUTONS DE PAIEMENT EXPRESS — APPLE PAY ET GOOGLE PAY.

   ⚠ CES BOUTONS NE SONT PAS DESSINÉS, ET ILS NE PEUVENT PAS L'ÊTRE. Fabian en
   a livré deux dessins le 19 octobre 2026 (716 × 139, la proportion exacte de
   « Valider la commande » : il les voyait empilés sous lui). Ils sont restés
   dans `theme_assets/objets/` et ne sont pas construits — les deux chartes
   l'interdisent, et pas de la même façon. Apple tolère un bouton maison, mais
   SANS le logo Apple ni les mots « Apple Pay » : or le dessin porte les deux.
   Google interdit le bouton maison tout court, et interdit explicitement de
   changer la police, la couleur, le rayon ou les marges du sien. Passer outre
   mettrait en jeu l'enregistrement marchand Apple Pay — ce n'est pas un risque
   esthétique, c'est le moyen de paiement qu'on perdrait.

   CE QUI RESTE DE SON INTENTION, ET C'EST BEAUCOUP : les deux boutons officiels
   acceptent un thème NOIR et une hauteur réglable, et Stripe les pose sur toute
   la largeur. Empilés sous le bouton dessiné, au bas de la colonne des totaux,
   on obtient la pile qu'il avait en tête. Seuls le lettrage et le logo restent
   officiels.

   ⚠ LA HAUTEUR N'EST PAS 56 MAIS 55, ET CE N'EST PAS UN ARRONDI. La proportion
   du dessin donne 288 × 139/716 = 55,9 px, donc 56. Mais Stripe REFUSE la
   valeur : `elements.create('expressCheckout')` n'accepte `buttonHeight` que de
   40 à 55 et lève une exception au-delà. Le réglage WooCommerce est donc à 55
   (Stripe → Express Checkout → Button Height), et le pixel manquant ne se voit
   pas. Ne pas « corriger » vers 56 : ça éteindrait les boutons.

   `!important` À CONTRECŒUR, et il n'y a pas d'autre porte : l'extension écrit
   `style="margin-top: 1em"` EN LIGNE sur son conteneur
   (class-wc-stripe-express-checkout-element.php, à la sortie du HTML). Un style
   en ligne ne se bat pas à la spécificité. 1em (16 px) ouvrait un écart plus
   grand au-dessus du premier bouton express qu'entre les deux boutons express
   eux-mêmes : la pile se lisait en deux morceaux au lieu d'un.

   ⚠ CETTE VALEUR DEMANDE UNE PASSE DE CALAGE SUR UN VRAI BOUTON. 12 px vient du
   rythme de la colonne, pas d'une mesure de l'écart que Stripe met entre ses
   propres boutons empilés — cet écart est dans son iframe et n'a pas pu être
   mesuré ici : Chrome sans interface ne déclare ni Apple Pay ni Google Pay, donc
   Stripe n'y rend aucun bouton. À vérifier dans un vrai navigateur.

   Le séparateur « — OR — » de l'extension ne concerne PAS cette page : il n'est
   émis que sur la commande et sur `order-pay`. Le tunnel de commande reste en
   blocs et n'est pas repris (voir inc/installation.php). */
.woocommerce .cart-collaterals #wc-stripe-express-checkout-element {
  margin-top: 12px !important;
}

/* --------------------------------------------------------------------------
   LE PANIER VIDE — la scène de Fabian, puis trois idées
   --------------------------------------------------------------------------
   fb_panier_vide() dans inc/woocommerce.php. Le personnage (604 × 629) centré,
   la ligne « mais j'ai quelques idées… » (1186 × 85) en dessous, puis une
   grille de trois cartes — la grille de la boutique, encore.
   -------------------------------------------------------------------------- */
.fb-panier-vide {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 3vw, 1.75rem);
  text-align: center;
}
/* LES DEUX LARGEURS SE DÉDUISENT DU LETTRAGE VOULU, elles ne sont pas choisies
   à l'œil. Fabian, 23 septembre 2026 : les deux dessins étaient trop grands, et
   il a donné la même référence que pour le pied de page — « que le texte soit à
   peu près la même taille que le texte des filtres du portfolio ou du shop »,
   soit 12,1 à 18,7 px de haut (le calcul est écrit en section 8).

   Le calcul, à refaire si Fabian relivre un des deux dessins :

     panier-vide-base (604 × 629)  la légende « LE PANIER EST VIDE… » occupe
                                   68 px de haut — mesurée au `-trim` sur la
                                   bande basse du fichier. Le lettrage vaut donc
                                   68/604 = 0,1126 × la largeur affichée.
                                   18,7 px de lettrage → 166 px de large.

     panier-vide-suite (1186 × 85) une seule ligne de texte, 70 px de trait sur
                                   85 de toile : 70/1186 = 0,059 × la largeur.
                                   18,7 px de lettrage → 317 px de large.

   Les `vw` sont calés pour que les deux bornes se rejoignent à la même largeur
   de fenêtre (~1170 px) que celles des filtres — sinon les trois lettrages
   seraient égaux aux extrêmes et faux entre les deux. */
/* ⚠ LES TROIS VALEURS SONT MULTIPLIÉES PAR 1,21 DEPUIS LE 6 OCTOBRE 2026 —
   Fabian : « agrandir légèrement dessin et texte “le panier est vide…” ».

   Le calcul ci-dessus reste juste, c'est sa CIBLE qui change : le lettrage ne
   vise plus les 18,7 px des filtres mais 22,6. Les trois nombres de chaque
   `clamp()` sont donc multipliés par le même facteur, pente comprise, pour que
   le dessin, la ligne et le texte grandissent ensemble — les décaler
   séparément casserait l'égalité de lettrage que tout ce bloc cherche à tenir.

   La page du panier vide est la seule du site à n'avoir rien d'autre à montrer
   que ces deux dessins : ce qui est juste au milieu d'une grille de cartes y
   paraît maigre. */
.fb-panier-vide__dessin { width: min(100%, clamp(133px, 17.5vw, 206px)); }
.fb-panier-vide__suite  { width: min(100%, clamp(242px, 32.7vw, 384px)); }

/* ⚠ SUR TÉLÉPHONE, LES DEUX DESSINS PRENNENT TOUT CE QU'ILS PEUVENT.
   Fabian, 7 octobre 2026 : « agrandir au maximum le petit dessin “panier
   vide” ». Les planchers des deux `clamp()` ci-dessus (133 et 242 px) sont
   atteints dès 760 px de fenêtre et ne descendent plus : sur un téléphone, le
   dessin occupait 133 px au milieu de 350 px utiles, donc un lettrage de 15 px
   — plus petit que le texte qui l'accompagne, sur la seule page qui n'a rien
   d'autre à montrer.

   « AU MAXIMUM » A UNE LIMITE, ET CE N'EST PAS LE DESSIN CARRÉ : c'est
   `panier-vide-suite`, large de 1186 px pour 85 de haut. Lui plafonne à 100 %
   de la colonne. Le dessin carré doit alors suivre À LA MÊME ÉCHELLE DE
   LETTRAGE, sinon les deux lignes de la même phrase ne sont plus écrites de la
   même main — c'est l'égalité que tout le bloc ci-dessus cherche à tenir.

   LE CALCUL, avec les deux facteurs mesurés plus haut (lettrage / largeur
   affichée : 0,1126 pour le dessin, 0,059 pour la suite) :

     0,1126 × D = 0,059 × S   →   D = 0,524 × S

   Avec S = 100 %, le dessin vaut donc 52,4 % de la même colonne. À 390 px de
   fenêtre : suite = 350 px (lettrage 20,6 px), dessin = 183 px (lettrage
   20,6 px). Les deux dessins grandissent de 37 % et leur lettrage atterrit
   juste sous la cible de 22,6 px fixée le 6 octobre — il faudrait 383 px de
   colonne pour l'atteindre, et il n'y en a que 350. C'est littéralement le
   maximum demandé.

   Les `%` ET PAS DES `vw` : la colonne porte les gouttières de la page, un
   `100vw` passerait dessous et déborderait. */
@media (max-width: 700px) {
  .fb-panier-vide__dessin { width: 52.4%; }
  .fb-panier-vide__suite  { width: 100%; }
}
.fb-panier-vide__texte  { font-size: var(--fb-texte); font-weight: 700; }
.woocommerce .fb-panier-vide__idees { width: 100%; margin-top: 0; }

/* ⚠ `align-content: start` — CONTRE UN TROU QUI N'APPARAÎT QUE SUR TÉLÉPHONE.
   -----------------------------------------------------------------------
   Fabian, 21 octobre 2026, capture d'un iPhone à 375 px : les deux rangées de
   suggestions sont séparées d'un grand vide, et la troisième carte descend
   jusqu'à chevaucher les icônes de réseaux qui la suivent.

   CE QUE DIT LA MESURE, et c'est elle qui désigne le coupable. En recalant la
   capture sur un rendu Chrome de la même page (facteur 2,53, décalage de
   défilement 343 px), la première rangée tombe au pixel près — image à 503,
   titres à 688 et 705 — et les icônes de réseaux aussi, à 1010. Seule la
   DEUXIÈME rangée est 73 px plus bas. Rien au-dessus n'a bougé, rien en
   dessous non plus : ce n'est donc ni une marge, ni une carte trop haute, ni
   un débordement — **c'est l'écart entre les rangées qui a grandi tout seul.**

   D'où ça vient : `align-content` vaut `normal` par défaut sur une grille,
   c'est-à-dire `stretch`. Si le conteneur se retrouve plus haut que ses
   rangées — et `.fb-panier-vide` est une boîte flexible dont la hauteur
   dépend de deux dessins qui arrivent après le texte — la grille distribue ce
   surplus À SES RANGÉES, pas sous elles. Avec trois cartes sur deux rangées,
   tout le surplus se voit au milieu.

   `start` les tasse en haut : l'écart vaut `gap` et rien d'autre, quelle que
   soit la hauteur que le conteneur finit par prendre.

   ⚠ AUCUN EFFET LÀ OÙ IL N'Y A PAS DE SURPLUS, ce qui est le cas de tous les
   rendus mesurés ici — 320, 360, 375, 390, 414 px et le bureau donnaient déjà
   16 px pile. C'est une garantie, pas un réglage : elle ne déplace rien sur
   les écrans où la page était déjà juste, et elle empêche le trou partout
   ailleurs. La grille de la boutique n'est volontairement pas touchée : elle
   remplit ses rangées et n'a jamais de hauteur libre à répartir. */
.woocommerce .fb-panier-vide__idees { align-content: start; }

/* LES MESSAGES DE WOOCOMMERCE.

   ⚠ DEPUIS LE 30 SEPTEMBRE 2026, `.woocommerce-message` ne s'affiche plus dans
   la boutique ni dans le panier : les avis de succès y sont écartés par
   fb_masquer_avis() (inc/woocommerce.php). Ces règles servent donc encore les
   erreurs et les informations partout, et les trois familles sur « Mon compte »,
   qui les garde. On les tient à jour pour cette raison.

   WooCommerce les livre avec SON vert,
   un olive (#8fae1b) qui n'est pas celui de Fabian : posés au-dessus des
   dessins, les deux verts se voyaient (relevé du 17 septembre 2026). Le bleu
   des « info » (#1e85be) passe au vert lui aussi — une seule couleur d'accent
   sur le site. L'erreur garde le rouge du formulaire de contact
   (.fb-message--erreur). Le bouton « Voir le panier » que WooCommerce glisse
   dans le message prend le langage des pilules ; il était gris-lilas. */
.woocommerce-message,
.woocommerce-info {
  background: rgb(53 155 60 / 0.08);
  border-top-color: var(--fb-vert);
  color: var(--fb-noir);
  font-family: var(--fb-serif);
}
.woocommerce-message::before,
.woocommerce-info::before { color: var(--fb-vert); }
.woocommerce-error {
  background: rgb(179 38 30 / 0.08);
  border-top-color: #b3261e;
  color: var(--fb-noir);
  font-family: var(--fb-serif);
}
.woocommerce-error::before { color: #b3261e; }

/* ⚠ L'ANNEAU BLEU AUTOUR DE L'AVIS — ET POURQUOI ON NE LE RETIRE PAS À LA SOURCE.
   Signalé par Fabian le 30 septembre 2026 : un cadre bleu vif entoure le message
   « pas assez de stock », large comme toute la page. Ce n'est ni une bordure de
   WooCommerce ni une des nôtres, c'est l'ANNEAU DE FOCUS par défaut de Chrome.

   WooCommerce le provoque exprès (assets/js/frontend/woocommerce.js,
   `focus_populate_live_region`) : 500 ms après le chargement, il pose
   `tabindex="-1"` sur le premier avis et lui donne le focus, parce qu'une zone
   live insérée dans la page n'est pas toujours annoncée par les lecteurs
   d'écran. Le focus EST l'annonce. On y touche et la personne qui ne voit pas
   l'écran n'apprend jamais que son ajout a échoué.

   On ne retire donc que le trait, et seulement sur `:focus` — jamais sur
   `:focus-visible`, qui reste vert comme partout ailleurs sur le site. La
   distinction n'est pas décorative : `:focus` attrape le focus DONNÉ PAR UN
   SCRIPT, celui-ci, et `:focus-visible` celui qu'on obtient au clavier. Un
   élément en `tabindex="-1"` ne s'atteint pas à la tabulation, donc la seconde
   règle ne se déclenchera probablement jamais — elle est là pour le jour où
   quelqu'un rendra l'avis focusable pour de bon. */
.woocommerce-error:focus,
.woocommerce-message:focus,
.woocommerce-info:focus,
.woocommerce-error:focus-visible,
.woocommerce-message:focus-visible,
.woocommerce-info:focus-visible { outline: none; }

/* ⚠ ET `:focus-visible` EST ÉTEINT AUSSI, DEPUIS LE 6 OCTOBRE 2026. La version
   précédente n'éteignait que `:focus` et repeignait `:focus-visible` en vert,
   en pariant que Chrome ne le ferait jamais correspondre pour un focus donné
   par un script. Le pari était faux : Fabian a vu l'anneau revenir, vert cette
   fois (« enlever le cadre vert autour de “il n'y a pas assez de stock” »).

   Chrome fait bien correspondre `:focus-visible` quand un script déplace le
   focus et qu'aucun élément n'avait le focus auparavant — ce qui est le cas
   exact ici : la page vient de se charger, et `focus_populate_live_region`
   appelle `.focus()` 500 ms plus tard.

   Rien n'est perdu côté accessibilité. Ces avis portent `tabindex="-1"` : ils
   ne sont PAS atteignables à la tabulation, l'anneau n'a donc jamais servi à
   montrer où l'on est au clavier. Le focus lui-même reste en place, et c'est lui
   — pas son contour — qui déclenche l'annonce du lecteur d'écran. Le jour où
   quelqu'un rendrait un avis focusable pour de bon, il faudrait le rendre ici. */

/* « PAS ASSEZ DE STOCK », DESSINÉ (835 × 90) — ET RIEN D'AUTRE.

   ⚠ LE DESSIN NE S'AJOUTE PAS AU BANDEAU, IL LE REMPLACE. C'est la correction du
   30 septembre 2026 (11 h 40), et elle défait trois choses qui semblaient toutes
   raisonnables une heure plus tôt : le fond rosé, la croix rouge (`::before` de
   WooCommerce) et le nombre restant écrit à côté du dessin. Fabian : « aucune
   autre info, pas d'icône, pas de stock restant, c'est déjà écrit en dessous ».
   Il a raison sur le fond : « 1 en stock » est écrit quelques lignes plus bas sur
   la fiche, et le champ de quantité le dit par son plafond dans le panier — le
   nombre était affiché deux fois et volait la place au dessin. Quant au bandeau,
   c'était un cadre de WooCommerce autour d'un dessin de Fabian, sur une page qui
   n'en a aucun autre.

   Reste donc UNE image, centrée, à l'endroit exact où le bandeau se trouvait.

   ⚠ `:has()` PORTE SUR LE <ul>, ET ÇA A UNE CONSÉQUENCE. `.woocommerce-error` est
   une liste : un <li> par avis (templates/notices/error.php). On déshabille la
   liste entière, donc si une SECONDE erreur arrivait dans le même affichage, elle
   perdrait le bandeau avec la première — elle s'afficherait en texte nu, centré.
   C'est acceptable et c'est mesuré : les deux filtres concernés se déclenchent
   sur un seul produit à la fois, et une erreur nue reste lisible. Un navigateur
   sans `:has()` garde le bandeau autour du dessin — moins beau, jamais muet. */
.woocommerce-error:has(.fb-avis-stock) {
  padding: 0;
  border: 0;
  background: none;
  text-align: center;
}
.woocommerce-error:has(.fb-avis-stock)::before { content: none; }

/* La largeur est plafonnée en `%` ET en `rem` : le même avis sert la fiche
   produit, pleine largeur, et la colonne étroite du panier. `display: block` plus
   `margin-inline: auto` plutôt que le `text-align` hérité au-dessus — `.fb-img`
   est un `inline-block` dont la boîte, elle, ne se centre pas toute seule. */
.fb-avis-stock {
  display: block;
  width: min(100%, 20rem);
  height: auto;
  margin-inline: auto;
}

/* L'AVIS EST PASSÉ SOUS LE BOUTON LE 6 OCTOBRE 2026 — Fabian : « déplacer le
   “il n'y a pas assez de stock” sous le bouton “oups” ». Le déplacement se fait
   côté PHP (fb_avis_sous_le_bouton, inc/woocommerce.php) : WooCommerce imprime
   ses avis tout en haut de la fiche, on les rebranche dans la colonne d'achat.

   Ce qui change ici : l'avis ne traverse plus la page, il tient dans une colonne
   de 48 %. Il lui faut donc son propre blanc au-dessus — sans quoi le dessin
   viendrait coller au bouton, dont il est la conséquence et non la légende. */
.woocommerce div.product .summary .woocommerce-notices-wrapper { margin-top: 1rem; }
.woocommerce div.product .summary .woocommerce-notices-wrapper:empty { margin-top: 0; }
/* `.woocommerce … a.button` : WooCommerce écrit `.woocommerce a.button`
   (deux classes et une balise), il faut au moins ça pour passer devant. */
.woocommerce .woocommerce-message a.button,
.woocommerce .woocommerce-info a.button,
.woocommerce .woocommerce-error a.button {
  padding: 0.3em 1em;
  border: 2px solid var(--fb-noir);
  border-radius: 2em;
  background: transparent;
  color: var(--fb-noir);
  font-family: var(--fb-serif);
  font-size: var(--fb-petit);
  font-weight: 700;
  line-height: 1.4;
  text-decoration: none;
}
.woocommerce .woocommerce-message a.button:hover,
.woocommerce .woocommerce-message a.button:focus-visible {
  background: var(--fb-vert);
  border-color: var(--fb-vert);
  color: var(--fb-blanc);
}

/* Pagination — WordPress génère ce balisage, on l'habille sobrement. */
.pagination, .woocommerce-pagination {
  margin-top: clamp(2rem, 5vw, 3rem);
  text-align: center;
  font-size: var(--fb-petit);
}
.pagination .page-numbers, .woocommerce-pagination .page-numbers {
  display: inline-block;
  padding: 0.3em 0.7em;
  text-decoration: none;
}
.pagination .current, .woocommerce-pagination .current {
  background: var(--fb-noir);
  color: var(--fb-blanc);
  border-radius: 1em;
}

/* ==========================================================================
   12. MENUS DÉROULANTS
   --------------------------------------------------------------------------
   Un <select> nu est dessiné par le système d'exploitation : bordure grise,
   flèche bleue sur macOS, coins carrés sur Windows. Au milieu d'une interface
   entièrement dessinée à la main, c'est le seul élément qui trahit le
   navigateur. On le rhabille une fois, ici, et toutes les listes du site en
   héritent — tri de la boutique, choix de taille, pays de livraison.

   CE QUI EST HABILLÉ ET CE QUI NE L'EST PAS
   Le champ fermé, oui. La liste qui s'ouvre au clic, non : elle est rendue par
   le système, hors de portée du CSS de façon fiable sur Safari, Firefox et
   Windows. La reconstruire en JavaScript demanderait de réimplémenter la
   navigation au clavier et le support des lecteurs d'écran — beaucoup de
   risque pour un gain esthétique sur un menu qu'on ouvre deux secondes.
   ========================================================================== */

:root {
  /* Le chevron : un trait ouvert, aux extrémités arrondies, volontairement
     pas parfaitement symétrique. */
  --fb-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 12"><path d="M1.6 2.1c2.9 2.6 5.6 5.3 8.3 8 2.8-2.8 5.6-5.5 8.5-8.1" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round"/></svg>');

  /* Le soulignement à main levée : épaisseur inégale, ligne qui ondule,
     extrémités qui s'amincissent.

     POURQUOI CE TRACÉ RÉSISTE À TOUTES LES LARGEURS
     `preserveAspectRatio="none"` lui fait épouser exactement la largeur du
     champ. Ce qui s'étire est donc l'axe HORIZONTAL, c'est-à-dire l'écart
     entre les ondulations. Leur amplitude, elle, est verticale : elle ne bouge
     pas. Le trait garde son caractère aussi bien sous un « S » que sous
     « Tri du plus récent au plus ancien ».

     Le trait dessiné par Fabian sous le « HEY » a été essayé d'abord — il ne
     tient pas ici : son irrégularité vient de l'antialiasing de ses bords à sa
     taille native (110 × 11 px), et disparaît dès qu'on l'étire. */
  --fb-souligne: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 12" preserveAspectRatio="none"><path fill="%23359b3c" d="M2,7.2 Q16,5.4 30,4.6 Q45,5.0 60,5.4 Q77,4.4 95,3.8 Q112,4.5 130,4.9 Q147,4.0 165,3.6 Q181,4.2 198,5.2 L198,7.9 Q181,7.5 165,7.4 Q147,8.5 130,9.3 Q112,8.8 95,8.4 Q77,9.5 60,10.2 Q45,9.9 30,9.4 Q16,9.7 2,9.8 Z"/></svg>');
}

/* POURQUOI CETTE LISTE PASSE PAR :is()
   WooCommerce habille déjà ses propres listes, et avec des sélecteurs très
   longs — `.woocommerce div.product form.cart .variations select` en compte
   sept. Une règle plus courte perd, quel que soit l'ordre de chargement : le
   sélecteur de taille gardait le chevron de WooCommerce, et le trait vert ne
   s'affichait pas.

   `:is()` prend la spécificité de son argument le PLUS fort. Tout le groupe
   hérite donc du poids du sélecteur de WooCommerce, et une seule écriture des
   déclarations suffit — plutôt que de dupliquer le bloc pour chaque cas. */
:is(
  .fb-select,
  .woocommerce div.product form.cart .variations select,
  .woocommerce form .form-row select,
  .woocommerce select.country_select,
  .woocommerce select.state_select
) {
  /* Sans ça, aucune règle de fond ou de bordure n'est appliquée : le champ
     reste dessiné par le système d'exploitation. */
  appearance: none;
  -webkit-appearance: none;

  /* WooCommerce impose `min-width: 75%` au sélecteur de variation, ce qui
     étirerait le trait sur toute la fiche pour trois lettres. On rend sa
     largeur au contenu. */
  width: auto;
  min-width: 0;
  max-width: 100%;

  padding: 0.25em 1.5em 0.55em 0;
  border: 0;
  border-radius: 0;
  background-color: transparent;
  background-image: var(--fb-chevron), var(--fb-souligne);
  background-position: right 0.1em center, left bottom;
  background-size: 0.7em, 100% 7px;
  background-repeat: no-repeat;

  font: inherit;
  font-size: var(--fb-petit);
  font-weight: 700;
  color: var(--fb-noir);
  transition: background-size var(--fb-transition);
}

/* Au survol, le trait s'épaissit — comme si on avait appuyé plus fort. */
:is(
  .fb-select,
  .woocommerce div.product form.cart .variations select,
  .woocommerce form .form-row select,
  .woocommerce select.country_select,
  .woocommerce select.state_select
):is(:hover, :focus-visible) {
  background-size: 0.7em, 100% 11px;
}

/* LE TABLEAU DES VARIANTES (« Socle : Avec / Sans »), remis d'aplomb.
   WooCommerce le rend dans un <table class="variations"> à `width: 100%`, avec
   `line-height: 2em` sur les cellules et `vertical-align: top` — le sélecteur
   héritait de ces 36 px de ligne, faisait 48 px de haut, et son trait vert
   (dessiné en bas de la boîte) tombait à deux doigts sous le mot. Le <th> était
   centré, donc le libellé semblait décalé. Fabian, 17 septembre 2026 : « le th
   est bizarre, le soulignement beaucoup trop bas, un énorme bloc ».

   Placé APRÈS le bloc :is() ci-dessus, à dessein : la ligne du sélecteur y est
   écrite avec la même spécificité, et c'est l'ordre qui tranche. */
.woocommerce div.product form.cart .variations {
  width: auto;
  margin: 0 0 0.25em;
  border-collapse: collapse;
}
.woocommerce div.product form.cart .variations th,
.woocommerce div.product form.cart .variations td {
  padding: 0 0 0.5em;
  line-height: 1.4;
  vertical-align: baseline;
  text-align: left;
}
.woocommerce div.product form.cart .variations th.label {
  padding-right: 0.75em;
  font-size: var(--fb-petit);
  white-space: nowrap;
}
.woocommerce div.product form.cart .variations select {
  margin: 0 0.75em 0 0;
  line-height: 1.3;
  vertical-align: baseline;
}
.woocommerce div.product form.cart .variations .reset_variations {
  font-size: var(--fb-petit);
  color: var(--fb-gris);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.woocommerce div.product form.cart .variations .reset_variations:hover,
.woocommerce div.product form.cart .variations .reset_variations:focus-visible { color: var(--fb-vert); }

/* --------------------------------------------------------------------------
   La barre d'outils de la boutique.
   Elle contenait le compteur de résultats à gauche et le menu « Trier par » à
   droite ; le tri a été retiré (voir fb_retirer_tri dans inc/woocommerce.php).
   Elle ne porte donc plus qu'un élément — d'où `flex-start` au lieu de
   `space-between`, qui n'a plus rien à écarter. Les classes `.fb-tri` et
   `.fb-tri__label` sont supprimées avec le gabarit qui les produisait.
   L'enveloppe est posée par fb_ouvrir_barre_outils() dans inc/woocommerce.php.
   -------------------------------------------------------------------------- */

.fb-barre-outils {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: 0.75rem 1.5rem;

  /* Plus rien n'y est visible (voir ci-dessous) : elle ne doit donc plus rien
     écarter. Sa marge basse creusait un trou entre les filtres et la grille. */
  margin-bottom: 0;
}

/* « 5 résultats affichés » — MASQUÉ À L'ŒIL, PAS RETIRÉ.
   ------------------------------------------------------
   Fabian n'en veut pas : sur une boutique de quelques dizaines de pièces, la
   phrase énonce ce que la grille montre déjà, et c'était le dernier texte gris
   d'une page qui n'en a plus.

   MAIS ON NE LE SUPPRIME PAS, et la raison est le filtrage en AJAX. Cocher une
   catégorie remplace la grille sans recharger la page : à l'œil, le changement
   est évident ; au lecteur d'écran, rien ne se produit. Ce compteur est le seul
   endroit du document qui dise combien de produits ont été trouvés. On le garde
   donc dans le HTML, invisible mais lisible — et il reste dans `.fb-resultats`,
   donc réécrit à chaque filtrage.

   Même technique que `.screen-reader-text`, recopiée plutôt qu'héritée : cette
   classe-là est déclarée avec `!important` et ne se laisse pas relever si un
   jour on veut réafficher le compteur sur une seule page. */
.woocommerce-result-count {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   13. MODALES ET FORMULAIRES
   --------------------------------------------------------------------------
   Deux formulaires vivent ici : l'inscription à la newsletter (dans une modale)
   et le contact (dans la page). Ils partagent volontairement leurs classes —
   .fb-champ__saisie, .fb-envoi, .fb-case, .fb-message — parce que Fabian ne
   dessinera qu'un seul jeu d'images pour les deux, et que le tunnel de commande
   de WooCommerce reprendra les mêmes.

   Tout est sobre, sauf la case à cocher. Un carré gris dessiné par le système
   d'exploitation au milieu d'une interface tracée à la main est la seule chose
   qu'on remarque sur l'écran : celle-là, on la redessine tout de suite.
   ========================================================================== */

:root {
  /* La case à cocher, en deux tracés superposés. Même procédé que le chevron de
     la section 12 : un SVG écrit dans la feuille de style, donc pas de requête
     réseau, pas de fichier à générer, et une couleur qu'on peut changer d'un
     caractère.

     Les côtés ne sont pas droits et les coins ne se rejoignent pas exactement :
     c'est ce qui la fait tenir à côté des dessins de Fabian. Un carré parfait
     aurait l'air d'un carré de navigateur repeint.

     ⚠ Provisoire. Q9 du README : Fabian doit livrer les deux états en PNG. */
  --fb-case-carre: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.7,3.5 C7.6,2.7 15.3,2.4 21.2,3.2 C22.1,8.6 22.2,16.3 21.1,21.5 C15.3,22.3 8,22.2 2.7,21.2 C1.9,15.7 2.1,8.7 2.7,3.5 Z" fill="none" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>');
  --fb-case-coche: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4.9,12.4 C7.2,14 9,16.1 10.4,18.5 C13.4,13.1 16.9,8.4 21.1,4.5" fill="none" stroke="%23359b3c" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
}

/* --------------------------------------------------------------------------
   La modale
   --------------------------------------------------------------------------
   C'est un <dialog> ouvert par showModal(). Ce choix n'est pas une préférence
   d'écriture : la méthode native apporte le fond assombri, le piège à focus, la
   fermeture par Échap et l'inertie du reste de la page pour les lecteurs
   d'écran. Réécrits à la main, ces quatre points font cent lignes de JavaScript
   et sont presque toujours à moitié faux.

   Le script (assets/js/modale.js) ne fait donc qu'ouvrir et fermer.
   -------------------------------------------------------------------------- */

.fb-modale {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--fb-noir);
  max-width: none;
  max-height: none;
}

/* Le <dialog> est étiré sur toute la fenêtre, et la boîte blanche est son
   enfant. C'est ce qui permet au script de reconnaître un clic « à côté » :
   tout clic dont la cible est le <dialog> lui-même est, par construction, un
   clic hors de la boîte.

   `overflow: auto` + `margin: auto` sur la boîte plutôt que `align-items:
   center` : centré par `align-items`, un contenu plus haut que la fenêtre se
   fait couper par le HAUT, et la partie coupée devient inatteignable — on ne
   peut pas défiler vers le négatif. Les marges automatiques, elles, cèdent
   avant de couper. */
.fb-modale[open] {
  position: fixed;
  inset: 0;

  /* ⚠ `inset: 0` NE SUFFIT PAS À ÉTIRER UN <dialog>. Le navigateur lui donne
     `width: fit-content; height: fit-content`, qui l'emporte sur les décalages :
     la boîte restait collée en haut à gauche, et ni `justify-content` ni les
     marges automatiques n'avaient prise sur elle — il n'y avait tout simplement
     pas d'espace libre à répartir. */
  width: auto;
  height: auto;

  display: flex;
  justify-content: center;
  padding: var(--fb-gouttiere);
  overflow: auto;
}

.fb-modale::backdrop { background: rgb(0 0 0 / 0.6); }

.fb-modale__boite {
  position: relative;
  margin: auto;
  width: 100%;
  max-width: 32rem;
  padding: clamp(1.5rem, 4vw, 2.5rem);
  background: var(--fb-blanc);
  border: 2px solid var(--fb-noir);
}

/* L'apparition. Elle porte sur la BOÎTE, pas sur le <dialog> : animer le
   <dialog> reviendrait à animer le conteneur qui sert de zone de clic, avec un
   `transform` qui crée un nouveau contexte de positionnement et déplace le
   repère de tout ce qu'il contient.

   Pas d'animation de disparition : elle demanderait de retarder la fermeture en
   JavaScript, donc de gérer le cas où le visiteur reclique pendant l'animation.
   Une fermeture instantanée n'a jamais gêné personne. */
@keyframes fb-modale-fond {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes fb-modale-boite {
  from { opacity: 0; transform: translateY(14px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}

.fb-modale[open]::backdrop        { animation: fb-modale-fond 200ms ease-out; }
.fb-modale[open] .fb-modale__boite { animation: fb-modale-boite 260ms cubic-bezier(0.22, 1, 0.36, 1); }

@media (prefers-reduced-motion: reduce) {
  .fb-modale[open]::backdrop,
  .fb-modale[open] .fb-modale__boite { animation: none; }
}

/* Sans ça, la page continue de défiler derrière la modale quand on utilise la
   molette : showModal() rend le reste inerte au clic et au clavier, mais pas au
   défilement. */
html:has(.fb-modale[open]) { overflow: hidden; }

.fb-modale__fermeture { position: absolute; top: 0.35rem; right: 0.5rem; margin: 0; }

.fb-modale__fermer {
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--fb-noir);
  font-size: 1.6rem;
  line-height: 1;
  transition: color var(--fb-transition);
}
.fb-modale__fermer:hover,
.fb-modale__fermer:focus-visible { color: var(--fb-vert); }

.fb-modale__titre { margin: 0 2rem 0.4em 0; font-size: var(--fb-titre); }
.fb-modale__intro {
  margin: 0;
  max-width: 42ch;
  color: var(--fb-gris);
  font-size: var(--fb-petit);
}

/* REPLI SANS JAVASCRIPT.
   Le bouton du pied de page est un vrai lien vers #fb-newsletter. Si le script
   ne s'exécute pas, le navigateur suit ce lien, `:target` s'applique, et le
   formulaire s'affiche à la suite du pied de page. Ce n'est plus une modale —
   c'est une section de page, et c'est très bien : il n'y a alors rien à
   fermer, d'où la croix retirée. */
.fb-modale:target:not([open]) {
  display: block;
  position: static;
  width: 100%;
  max-width: var(--fb-largeur);
  margin: clamp(1.5rem, 4vw, 3rem) auto;
  padding-inline: var(--fb-gouttiere);

  /* Chrome donne le focus au <dialog> quand il devient la cible du fragment,
     et peint son cerclage bleu par-dessus — soit un rectangle de 1240 px de
     large autour d'une boîte de 512. On ne supprime pas le signal, on le
     déplace sur la boîte (règle suivante) : sans remplacement, un visiteur au
     clavier ne saurait plus où il vient d'atterrir. */
  outline: none;
}
.fb-modale:target:not([open]) .fb-modale__boite { border-color: var(--fb-vert); }

.fb-modale:not([open]) .fb-modale__fermeture { display: none; }

/* --------------------------------------------------------------------------
   Les champs, partagés par les deux formulaires
   -------------------------------------------------------------------------- */

.fb-champ__saisie {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1.5px solid var(--fb-noir);
  border-radius: 0;
  background: var(--fb-blanc);
  color: var(--fb-noir);
  font: inherit;
  font-size: var(--fb-petit);
  transition: border-color var(--fb-transition);
}
.fb-champ__saisie::placeholder { color: var(--fb-gris); }
.fb-champ__saisie:focus-visible {
  border-color: var(--fb-vert);
  outline: 2px solid var(--fb-vert);
  outline-offset: 2px;
}

/* `resize: vertical` et pas `both` : élargi à la souris, le champ sortait de la
   grille et poussait la page en défilement horizontal. */
textarea.fb-champ__saisie { resize: vertical; min-height: 8rem; line-height: 1.5; }

.fb-envoi {
  padding: 0.6rem 1.4rem;
  border: 0;
  background: var(--fb-vert);
  color: var(--fb-blanc);
  font: inherit;
  font-size: var(--fb-petit);
  font-weight: 700;
  transition: background-color var(--fb-transition);
}
.fb-envoi:hover,
.fb-envoi:focus-visible { background: var(--fb-noir); }

/* La case à cocher.

   `appearance: none` efface le dessin du système d'exploitation — sans cette
   ligne, aucune règle de fond n'a d'effet et la case reste celle de macOS ou de
   Windows. On repeint donc TOUT, y compris l'état coché : le navigateur ne
   fournit plus rien.

   Deux calques quand elle est cochée. Ils partagent un seul `background-size` :
   une valeur unique s'applique cycliquement à toutes les couches. */
.fb-case {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 1.35em;
  height: 1.35em;
  margin: 0.05em 0 0;
  border: 0;
  background: var(--fb-case-carre) center / 100% 100% no-repeat;
}
.fb-case:checked { background-image: var(--fb-case-coche), var(--fb-case-carre); }
.fb-case:focus-visible { outline: 2px solid var(--fb-vert); outline-offset: 3px; }

/* `align-items: flex-start` : le texte du consentement fait deux ou trois
   lignes, la case doit rester en face de la PREMIÈRE. */
.fb-consentement {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin-top: 1rem;
  max-width: 56ch;
  font-size: var(--fb-petit);
  line-height: 1.45;
}

/* Messages de résultat. Ils étaient calés sur le bandeau noir (vert clair, rose
   pâle) : dans la modale et dans la page, sur fond blanc, ces deux teintes
   étaient illisibles. */
.fb-message {
  margin: 0 0 1rem;
  padding: 0.6rem 0.85rem;
  border-left: 3px solid currentColor;
  font-size: var(--fb-petit);
  font-weight: 700;
}
.fb-message--ok     { color: var(--fb-vert); background: rgb(53 155 60 / 0.08); }
.fb-message--erreur { color: #b3261e; background: rgb(179 38 30 / 0.08); } /* 6.4:1 sur blanc */

/* --------------------------------------------------------------------------
   Le formulaire de la newsletter
   -------------------------------------------------------------------------- */

.fb-newsletter { margin-top: 1.25rem; }

.fb-newsletter__champs { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* Le champ prend la place restante, le bouton passe dessous quand il n'y en a
   plus assez. `width: auto` annule le `width: 100%` de .fb-champ__saisie, qui
   forcerait le bouton à la ligne à toutes les largeurs. */
.fb-newsletter__email { flex: 1 1 220px; width: auto; }

/* --------------------------------------------------------------------------
   Le formulaire de contact
   -------------------------------------------------------------------------- */

/* PLUS DE LARGEUR MAXIMALE — Fabian, 9 septembre 2026 : « que le formulaire
   prenne la largeur ». Il était plafonné à 44 rem (~700 px) dans une colonne
   de 1240 : la grille des champs s'arrêtait aux deux tiers de la page et le
   texte au-dessus continuait jusqu'au bord. Il prend maintenant la colonne. */
.fb-form { max-width: none; }

/* DEUX COLONNES, EXPLICITES. La grille était en `auto-fit` de 15 rem : tant
   que le formulaire était plafonné à 44 rem, ça donnait deux colonnes. Passé
   en pleine largeur, `auto-fit` en trouvait trois — nom et e-mail en occupaient
   deux, la troisième restait vide, un trou à droite de la première ligne. La
   première ligne a deux champs : elle a deux colonnes, point. Une seule sous
   560 px, où deux cadres dessinés côte à côte n'ont plus la place d'être lus. */
.fb-form__grille {
  display: grid;
  gap: 1rem 1.25rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 560px) {
  .fb-form__grille { grid-template-columns: minmax(0, 1fr); }
}

.fb-champ {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0;
}
.fb-champ--large { grid-column: 1 / -1; }

.fb-champ__label { font-size: var(--fb-petit); font-weight: 700; }
.fb-requis { color: var(--fb-vert); }

/* LES LIBELLÉS DESSINÉS — livraison du 30 septembre 2026.
   ----------------------------------------------------------------------------
   « TON NOM », « TON ADRESSE MAIL », « C'EST À QUEL SUJET ? », « TON MESSAGE ».
   Fabian avait dessiné les cadres en août ; les mots restaient composés en
   Garamond au-dessus. C'était le dernier endroit du formulaire où une
   typographie de traitement de texte côtoyait un trait fait main.

   ⚠ CE SONT DES MOTS, DONC ILS SE MESURENT EN HAUTEUR. La largeur suit
   l'`aspect-ratio` que le build écrit dans _sprites.css depuis le fichier :
   « C'EST À QUEL SUJET ? » est presque deux fois plus long que « TON NOM », et
   les deux doivent avoir des lettres de la même taille. Une largeur fixe aurait
   donné l'inverse exact — deux écritures différentes pour une seule main.

   La hauteur est bornée entre 19 et 24 px (74 px d'origine, réduits à un tiers).
   Elle vaut un peu plus que l'ancien texte en var(--fb-petit) : une majuscule
   dessinée a besoin de plus de place qu'une bas-de-casse composée pour se lire
   à la même aisance, sa hauteur d'œil est toute sa hauteur. */
.fb-champ__label--dessine {
  display: flex;
  line-height: 0;
}

.fb-label {
  height: clamp(1.2rem, 2.4vw, 1.5rem);
  max-width: 100%;
}

/* LA PHRASE DES ÉTOILES, ELLE, SE MESURE EN LARGEUR — et c'est la même règle
   lue dans l'autre sens. Un libellé de trois mots reste court quoi qu'il
   arrive ; « LES CHAMPS MARQUÉS D'UNE ÉTOILE SONT OBLIGATOIRES. » fait 1111 px
   pour 74 de haut, soit quinze fois plus large que haut. Posée à 22 px de haut,
   elle demanderait 330 px de large — elle déborderait d'un téléphone en 390 px
   avant même d'être trop petite pour être lue.

   On plafonne donc la largeur, et la hauteur suit : sur un grand écran la
   phrase fait 26 rem, sur un petit elle prend la colonne et rapetisse
   proportionnellement, sans jamais déborder ni se déformer. */
.fb-note-obligatoire {
  width: min(100%, 26rem);
}

/* ⚠ `width: 100%` sur le conteneur, sinon le `min(100%, …)` ci-dessus tourne en
   rond : sans largeur propre, le <span> se dimensionne sur son contenu, qui se
   dimensionne lui-même sur le <span>. Le navigateur tranche alors en faveur de
   zéro, et la phrase disparaît. */
.fb-form__note--dessinee {
  display: block;
  width: 100%;
  line-height: 0;
  text-align: center;
}

/* LE PIED DU FORMULAIRE EST UNE COLONNE CENTRÉE : la note sur les étoiles, puis
   le bouton. C'était une rangée bouton-puis-note, calée à gauche. Fabian
   (9 septembre 2026) veut le bouton au centre et la note AU-DESSUS, centrée
   elle aussi — l'ordre est celui de la lecture : on apprend ce que veulent
   dire les étoiles avant d'envoyer. L'ordre du HTML a été inversé en même
   temps (inc/contact.php), plutôt que retourné par `flex-direction:
   column-reverse` : un lecteur d'écran suit le HTML, pas le CSS. */
.fb-form__pied {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  margin-top: clamp(1.25rem, 3vw, 1.75rem);
  text-align: center;
}
.fb-form__note { color: var(--fb-gris); font-size: var(--fb-petit); }
/* « Simuler un envoi » — n'existe qu'en mode maintenance (inc/contact.php,
   fb_contact_apercu_possible). Un trait pointillé au-dessus pour dire que ce
   n'est pas le formulaire, mais un outil posé à côté. */
.fb-form__apercu {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin: 2rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px dashed var(--fb-gris-clair);
  text-align: center;
}

/* --------------------------------------------------------------------------
   « MESSAGE ENVOYÉ ! » — l'animation qui remplace le formulaire
   --------------------------------------------------------------------------
   Une fois le message parti (`?contact=ok`), fb_contact_formulaire() ne rend
   plus le formulaire mais ce bloc : la spritesheet de 27 images livrée le
   9 septembre 2026 (voir `.fb-anim--message-envoye` dans _sprites.css, qui
   porte la pellicule et l'animation), jouée une fois à l'affichage, figée sur
   « MESSAGE ENVOYÉ ! ». Le texte équivalent est dans le HTML pour les lecteurs
   d'écran, avec `role="status"` pour être annoncé sans qu'on le cherche.

   La taille : le dessin est carré (358 px). Un tiers de la colonne au plus,
   parce qu'il vient à la place d'un formulaire et pas d'une affiche — et
   jamais moins de 200 px, en dessous le lettrage final n'est plus lisible. */
/* --------------------------------------------------------------------------
   LA CONFIRMATION D'ENVOI, EN POP-UP
   --------------------------------------------------------------------------
   L'animation de Fabian (27 images) se joue dans une petite fenêtre carrée —
   `Fond_popup_carre-petit`, 475 × 478, celle qu'il a dessinée pour ça — sur
   l'écran assombri de la modale. Elle se referme seule au bout de 3,7 s : voir
   `data-fb-fermer-apres` dans inc/contact.php et modale.js.

   La fenêtre est PETITE À DESSEIN : `max-width: 20rem` contre 36 pour la
   newsletter. Celle-ci contient un formulaire, un titre et un paragraphe ;
   celle-là ne contient qu'un dessin carré de 358 px qu'on regarde quatre
   secondes. À 36 rem, le dessin aurait nagé au milieu du papier.

   24 rem au premier jet, 20 depuis l'entretien du 23 septembre 2026 (« faire
   la pop-up d'animation de l'envoi légèrement plus petite »). Un sixième de
   moins : elle reste une fenêtre, elle cesse d'être une affiche. En dessous de
   ~18 rem le lettrage final de l'animation décroche — c'est le plancher, pas
   une marge de manœuvre. */
.fb-modale__boite.fb-popup--carre-petit {
  aspect-ratio: var(--fb-cadre-ratio);
  max-width: 20rem;

  /* Le dessin au milieu du papier, dans les deux sens. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

/* L'animation elle-même. La largeur est en % de la fenêtre et non en `clamp()`
   de la fenêtre du navigateur : c'est la feuille de papier qui commande ici, et
   elle a déjà sa propre échelle. 78 % laisse voir la marge dessinée tout
   autour — coller le dessin au bord ferait perdre le papier. */
.fb-modale--envoye .fb-envoye__anim { width: 78%; }

/* LE RENDU À PLAT, CONSERVÉ EN SECOURS.
   L'animation ne vit plus dans le flux de la page (elle est dans la modale),
   mais ces deux règles restent : `.fb-envoye__anim` est la même classe des deux
   côtés, et si un jour la confirmation redevient un bloc de page — ou si la
   modale ne s'ouvre pas — elle a encore une taille et un centrage. */
.fb-envoye {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-block: clamp(1.5rem, 4vw, 2.5rem);
}
.fb-envoye__anim {
  width: min(100%, clamp(200px, 30vw, 358px));
}

/* `.fb-contact` N'EST PLUS UNE GRILLE À DEUX COLONNES.
   Elle en était une pour poser le formulaire à gauche et l'encart « Ou
   directement » (e-mail + téléphone) à droite. Cet encart était écrit en dur
   dans le gabarit : Fabian ne pouvait ni le déplacer, ni le retirer, ni écrire
   autre chose à sa place. Il est supprimé — les coordonnées sont déjà dans le
   pied de page, et s'il en veut dans la page, il les écrit dans l'éditeur.

   Il ne reste donc qu'une colonne, et cette classe ne sert plus qu'au filet de
   sécurité du gabarit (formulaire affiché quand le shortcode manque). */
.fb-contact { margin-top: clamp(1.5rem, 4vw, 2.5rem); }

/* Le formulaire posé par le shortcode est dans le flux du contenu : il lui faut
   le même écart que les paragraphes autour de lui, des deux côtés. */
.fb-page__contenu .fb-form { margin-block: clamp(1.5rem, 4vw, 2.5rem); }

/* La liste des dessins attendus, dans le bloc « en attente ». */
.fb-esquisse ul { margin: 0.75em 0 0; padding-left: 1.2em; }
.fb-esquisse li + li { margin-top: 0.5em; }
.fb-esquisse strong { color: var(--fb-noir); }

/* ==========================================================================
   14. LES CADRES DESSINÉS
   --------------------------------------------------------------------------
   Livrés le 11 août 2026 : un cadre par champ, en deux états (trait noir au
   repos, trait vert au focus), plus cinq fonds de fenêtre pour les popups.

   POURQUOI `border-image` ET PAS UNE IMAGE DE FOND.
   Un champ ne fait jamais la taille du dessin : `Cadre_nom_base.png` mesure
   723 × 161 px et s'affiche dans 340 × 56. En fond étiré (`background-size:
   100% 100%`), l'image est écrasée de 69 % en largeur mais de 35 % en hauteur —
   les traits verticaux ressortent deux fois plus épais que les horizontaux et
   les coins s'ovalisent. Sur un trait tracé au feutre, ça se voit du premier
   coup d'œil.

   `border-image` découpe l'image en neuf morceaux : les quatre coins sont posés
   tels quels, les quatre bords ne s'étirent que dans un sens, le centre est
   jeté (ou gardé, pour les fonds de popup). L'épaisseur du trait reste donc la
   même sur tout le pourtour, à n'importe quelle taille de champ.

   CE QUI EST ICI ET CE QUI EST GÉNÉRÉ.
   Ici : la mécanique, identique pour tous les cadres. Dans _sprites.css : les
   images et l'épaisseur de découpe, qui dépendent des dimensions des fichiers
   livrés — voir la fonction `cadre()` de tools/build-assets.sh.

   `--fb-cadre-h` est la seule valeur que ce fichier doit fournir : la hauteur à
   laquelle le cadre s'affiche. Tout le reste s'en déduit.
   ========================================================================== */

.fb-cadre {
  /* SANS `border-style`, `border-image` NE PEINT RIEN.
     C'est le piège classique de cette propriété : elle a besoin d'une bordure
     réelle sur laquelle se poser. On en déclare donc une, transparente — c'est
     elle qui réserve la marge entre le trait dessiné et le texte, et c'est
     `border-image-width` (dans _sprites.css) qui décide de l'épaisseur à
     laquelle le DESSIN est peint, indépendamment de celle-ci. */
  border-style: solid;
  border-color: transparent;
  border-width: 0.55rem;
  border-radius: 0;
  border-image-repeat: stretch;

  /* Le cadre EST le fond. Un blanc opaque par-dessus masquerait le tracé sur
     les bords, là où il déborde vers l'intérieur. */
  background: transparent;

  /* L'anneau de focus du navigateur est retiré — Y COMPRIS AU CLAVIER, ce qui
     mérite d'être justifié.

     La règle générale de la section 4 pose un cerclage vert de 3 px sur tout
     champ qui reçoit le focus. Ici il fait doublon : le cadre PASSE DÉJÀ au
     vert, sur toute sa longueur, et c'est un signal plus large et plus visible
     qu'un liseré. Les deux ensemble donnaient un rectangle vert à angles droits
     posé autour d'un tracé à main levée — le seul angle droit de l'écran, et il
     apparaissait précisément au moment où l'on regarde le champ.

     On ne SUPPRIME donc pas l'indicateur de focus, on le remplace par celui de
     Fabian. La condition d'accessibilité est que le changement reste perceptible
     sans compter uniquement sur la couleur : le trait vert et le trait noir
     diffèrent nettement en clarté, et le changement porte sur tout le pourtour
     du champ, pas sur un détail.

     ⚠ Cette règle a besoin de `:focus-visible` pour l'emporter : `.fb-cadre`
     seul (0,1,0) perdait contre `input:focus-visible` (0,2,0) de la section 4,
     et l'anneau restait. */
  outline: none;
}

.fb-cadre:focus,
.fb-cadre:focus-visible { outline: none; }

/* Champs d'une ligne : nom, e-mail, objet, adresse de la newsletter.
   La hauteur est généreuse parce que les cadres de Fabian sont des pilules —
   un champ court les écraserait en un trait épais sans intérieur.

   POURQUOI DEUX NOMS DE VARIABLE POUR UNE SEULE HAUTEUR.
   `--fb-champ-h` est la hauteur d'un champ ; `--fb-cadre-h` est la hauteur à
   laquelle border-image doit dessiner le trait. Elles coïncident ici, mais on
   ne peut pas les confondre : `--fb-cadre-h` est posée sur beaucoup d'éléments
   (dont `.fb-popup`, qui vaut 26rem) et se transmet aux descendants. Un bouton
   voisin du champ qui lirait `--fb-cadre-h` récupérerait donc la valeur de la
   fenêtre qui le contient, pas celle du champ à côté duquel il s'aligne. */
:root { --fb-champ-h: clamp(3rem, 6vw, 3.75rem); }

.fb-champ__saisie.fb-cadre {
  --fb-cadre-h: var(--fb-champ-h);
  height: var(--fb-cadre-h);
  padding: 0 0.55rem;
}

/* La zone de message. `--fb-cadre-h` vaut ici sa hauteur MINIMALE : c'est la
   valeur qui sert au calcul du découpage. Si le visiteur agrandit le champ à la
   poignée, le trait garde son épaisseur — ce qui est le comportement voulu,
   un trait au feutre ne s'épaissit pas parce qu'on écrit plus long. */
textarea.fb-champ__saisie.fb-cadre {
  --fb-cadre-h: clamp(11rem, 26vw, 15rem);
  height: auto;
  min-height: var(--fb-cadre-h);
  padding: 0.35rem 0.55rem;
  line-height: 1.5;
}

/* Sur téléphone, Fabian a redessiné la zone de message en PORTRAIT
   (720 × 844 au lieu de 1473 × 519). _sprites.css échange l'image au même
   seuil ; ici on change la hauteur pour que la boîte suive le dessin, sinon on
   afficherait un cadre vertical dans une case horizontale. */
@media (max-width: 700px) {
  textarea.fb-champ__saisie.fb-cadre { --fb-cadre-h: clamp(15rem, 62vw, 22rem); }
}

/* --------------------------------------------------------------------------
   Les fonds de fenêtre
   --------------------------------------------------------------------------
   Fabian n'a pas dessiné cinq popups : il a dessiné la MÊME feuille de papier
   dans cinq proportions (newsletter, carré grand, carré petit, horizontal,
   vertical), pour qu'on prenne celle qui colle au contenu plutôt que d'étirer
   la seule disponible. Seule `--newsletter` sert aujourd'hui ; les quatre
   autres sont prêtes pour les popups à venir (ajout au panier, confirmation de
   commande). Elles ne coûtent rien tant qu'aucune classe ne les appelle : une
   règle CSS dont le sélecteur n'existe pas dans la page ne déclenche aucun
   téléchargement d'image.

   Ici le découpage est en mode `fill` (posé par le build) : le centre blanc de
   l'image est conservé, c'est lui qui fait le fond de la fenêtre.
   -------------------------------------------------------------------------- */

.fb-popup {
  --fb-cadre-h: 26rem;
  border-style: solid;
  border-color: transparent;
  border-width: 1.25rem;
  border-image-repeat: stretch;
  background: transparent;
}

/* --------------------------------------------------------------------------
   La modale newsletter habillée
   --------------------------------------------------------------------------
   Elle garde toute sa mécanique (section 13) : c'est le même <dialog>, la même
   fermeture native, le même repli `:target` sans JavaScript. Seule la peau
   change. Le rendu sobre reste en place et sert de secours — voir
   fb_newsletter_est_dessinee() dans inc/newsletter.php.
   -------------------------------------------------------------------------- */

/* La bordure noire et le fond blanc du rendu sobre disparaissent : c'est le
   dessin qui fournit les deux. */
.fb-modale__boite.fb-popup {
  border-color: transparent;
  background: transparent;
  padding: clamp(1.25rem, 3.5vw, 2rem);
}

/* LA FENÊTRE PREND LA FORME DE SON DESSIN.
   Sans ça, la boîte faisait la hauteur de son contenu — 512 × 384, soit du
   4/3 — pendant que la feuille dessinée est du 1478 × 974, soit du 3/2. Le
   centre de l'image étant étiré par `fill`, on voyait bien la feuille de
   Fabian, mais dans une forme qui n'était pas la sienne : trop carrée.

   `--fb-cadre-ratio` est écrite par le build à côté de l'image, dans
   _sprites.css. En la reprenant ici, la boîte suit AUTOMATIQUEMENT le dessin
   actif — y compris quand la règle @media du build échange la feuille
   horizontale contre la carrée sur petit écran. Une seule source de vérité :
   le fichier livré.

   `aspect-ratio` ne rogne rien : quand le contenu est plus haut que le rapport
   ne le prévoit, la taille minimale automatique en bloc reprend la main et la
   boîte s'allonge. Le rapport est une forme VOULUE, pas un plafond. */
.fb-modale__boite.fb-popup--newsletter {
  aspect-ratio: var(--fb-cadre-ratio);
  max-width: 36rem;

  /* Le contenu ne remplit pas toujours la feuille : centré verticalement, il
     reste au milieu du papier au lieu de pendre depuis le bord haut. */
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Le mot « NEWSLETTER » dessiné. `.fb-img` fournit les calques ; _sprites.css
   fournit le rapport et l'image. On ne donne ici que la largeur — la hauteur
   suit. Marge droite pour ne pas passer sous la croix de fermeture. */
.fb-modale__titre--dessine {
  display: block;
  width: clamp(9rem, 40%, 13rem);
  margin: 0 2.5rem 0.75em 0;
}

/* La croix. Elle était typographique (un « × » de la police), elle est
   maintenant dessinée : `width` + `aspect-ratio` remplacent la taille de
   police, et le texte du bouton est vide — son nom accessible vient de
   `aria-label`, posé dans le PHP. */
.fb-modale__fermer--dessine {
  width: clamp(1.1rem, 2.6vw, 1.5rem);
  height: auto;
  font-size: 0; /* évite la ligne de base fantôme d'un bouton vide */
}

/* --------------------------------------------------------------------------
   LES AVIS — CE QUI A REMPLACÉ LES `alert()` DU NAVIGATEUR
   --------------------------------------------------------------------------
   Fabian, 30 septembre 2026 : « retire les messages alert() en JS, c'est
   horrible ». WooCommerce s'en sert pour au moins deux phrases, dont « Veuillez
   sélectionner des options du produit avant de l'ajouter à votre panier » —
   une boîte système grise, police de l'OS et nom de domaine en tête, posée au
   milieu des dessins. Le pourquoi et le comment du remplacement sont dans
   assets/js/avis.js ; ici, seulement la peau.

   ELLE NE RÉINVENTE RIEN : c'est la modale de la newsletter, avec une autre
   feuille. Même <dialog>, même fond assombri, même fermeture par Échap, même
   apparition — tout vient de `.fb-modale` et de `.fb-popup` plus haut. Ne
   restent que quatre réglages propres à un message court.

   `fond-horizontal` plutôt que `carre-petit` : un avis tient en une phrase,
   c'est une largeur qu'il lui faut, pas une hauteur. Fabian a dessiné les cinq
   formats pour qu'on choisisse. */
.fb-avis { cursor: pointer; }

/* ⚠ ET PAS D'ANNEAU BLEU AUTOUR DE LA FENÊTRE — même cause que sur les avis de
   WooCommerce (§ « LES MESSAGES DE WOOCOMMERCE »), autre déclencheur.
   `showModal()` doit poser le focus quelque part dans la boîte ; les autres
   modales du site ont leur bouton de fermeture, qui le prend. Celle-ci n'a aucun
   élément focusable — on la ferme d'un clic n'importe où — alors le focus revient
   au <dialog> lui-même, et Chrome l'encadre sur toute la fenêtre.

   Le focus reste, il est ce qui fait marcher Échap et ce qui annonce le message
   aux lecteurs d'écran. Seul le trait part, et seulement sur `:focus` : au
   clavier (`:focus-visible`), il n'y a rien à retirer puisqu'on n'atteint pas
   cette boîte à la tabulation. */
.fb-avis:focus { outline: none; }

/* ⚠ `--fb-cadre-h` EST REDÉCLARÉE ICI, ET SA VALEUR N'EST PAS DÉCORATIVE.
   C'est elle qui donne l'ÉPAISSEUR DU TRAIT dessiné : _sprites.css en dérive le
   `border-image-width` (× 65 / 575 pour cette feuille-là). `.fb-popup` la pose à
   26 rem, taille d'une vraie fenêtre ; à cette valeur, le cadre d'un avis de
   trois lignes aurait un trait deux fois trop épais pour sa boîte — le dessin
   mangerait le message. 12 rem rend au trait la proportion qu'il a sur la
   planche de Fabian.

   La règle doit rester APRÈS `.fb-popup` dans ce fichier : les deux sélecteurs
   ont la même spécificité, c'est l'ordre qui tranche. */
.fb-avis__boite {
  --fb-cadre-h: 12rem;

  /* La feuille garde sa forme, comme la modale newsletter garde la sienne —
     même mécanique, même variable écrite par le build à côté de l'image. Et
     même réserve : le rapport est une forme voulue, pas un plafond. Un message
     long allonge la boîte plutôt que d'être rogné. */
  aspect-ratio: var(--fb-cadre-ratio);
  max-width: 30rem;

  display: flex;
  align-items: center;
  justify-content: center;
}

/* Le message est centré dans la feuille et sa largeur est bridée bien en deçà
   de la boîte : le dessin a des bords irréguliers, une ligne qui court jusqu'au
   cadre passerait par endroits sous le trait. */
.fb-avis__texte {
  margin: 0;
  max-width: 20rem;
  font-size: var(--fb-texte);
  line-height: 1.45;
  text-align: center;
}

/* Les deux boutons dessinés. Il ne reste rien du bouton vert : ni fond, ni
   remplissage, ni couleur de texte — tout est dans les deux images. */
.fb-newsletter__envoi,
.fb-envoi--dessine {
  padding: 0;
  border: 0;
  background: transparent;
}
.fb-newsletter__envoi:hover,
.fb-newsletter__envoi:focus-visible,
.fb-envoi--dessine:hover,
.fb-envoi--dessine:focus-visible { background: transparent; }

/* « JE M'INSCRIS » SE MESURE EN HAUTEUR, PAS EN LARGEUR.
   Il est posé sur la même ligne que le champ e-mail : c'est la hauteur qui doit
   coïncider, la largeur n'a qu'à suivre. En la fixant à la place (l'ancienne
   règle donnait 168 px aux deux boutons), le bouton retombait à 70 px de haut
   contre 60 au champ, et le décalage se voyait d'autant plus que les deux
   dessins ont un contour épais.

   `aspect-ratio` vient de _sprites.css (335 × 140) : à 60 px de haut, le bouton
   fait 143 px de large, tout seul. Si Fabian relivre un dessin d'une autre
   proportion, rien à retoucher ici. */
.fb-newsletter__envoi {
  height: var(--fb-champ-h);
  width: auto;
}

/* « ENVOYER » se mesure en hauteur lui aussi, mais pas sur la même référence.
   Il est seul sur sa ligne, sous le formulaire : il n'a rien à côté de quoi
   s'aligner, donc rien ne l'oblige à faire la hauteur d'un champ.

   LA TAILLE S'EST CHERCHÉE EN DEUX ESSAIS, ET LES DEUX SONT INSTRUCTIFS.
   Au départ il partageait la largeur fixe de « JE M'INSCRIS » : 168 × 72 px,
   soit plus haut que les champs de saisie eux-mêmes — un bouton qui pèse plus
   lourd que le formulaire qu'il valide. Divisé par trois ensuite, il tombait à
   56 × 24 : lisible de justesse, et sous la cible tactile minimale.

   Il fait maintenant les deux tiers de sa taille d'origine, soit 112 × 48 px,
   et la valeur n'est pas choisie à l'œil : `clamp(2.75rem, 5vw, 3rem)` borne
   la hauteur entre 44 et 48 px. 44 px est exactement la cible tactile minimale
   recommandée — le plancher est donc posé par une contrainte, pas par un goût,
   et il tient à toutes les largeurs d'écran. Plus besoin d'élargir la zone de
   clic par une bordure transparente : le dessin EST la cible.

   La largeur suit `aspect-ratio` (307 × 131, écrit par le build). */
.fb-envoi--dessine {
  height: clamp(2.75rem, 5vw, 3rem);
  width: auto;
}

/* Le champ e-mail et le bouton n'ont plus de texte visible : leur ligne de base
   ne veut plus rien dire. On les aligne sur leur milieu. */
.fb-newsletter__champs { align-items: center; }
