/* ═══ lib/base.css ═══ */
/* design/lib/base.css — socle : fontes auto-hébergées, remise à zéro, primitives.
   Aucune valeur de couleur ni de taille en dur : tout vient de tokens/tokens.css.
   Soumis à la porte 2 (`node tools/gate-tokens.mjs`). */

/* ---------- Fontes auto-hébergées ------------------------------------------
   JAMAIS d'@import depuis un CDN : il BLOQUE le rendu et ferait échouer le
   budget de performance. Les 4 fontes variables sont sous-ensemblées au
   français + la ponctuation de marque : 155 Ko servis au lieu de 3,2 Mo.
   `font-display: swap` garantit que le texte est lisible immédiatement. */
@font-face {
  font-family: "Cormorant Garamond";
  src: url("/assets/fonts/cormorant-garamond.woff2") format("woff2-variations");
  font-weight: 300 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Cormorant Garamond";
  src: url("/assets/fonts/cormorant-garamond-italic.woff2") format("woff2-variations");
  font-weight: 300 700; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/inter.woff2") format("woff2-variations");
  font-weight: 100 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Inter Tight";
  src: url("/assets/fonts/inter-tight.woff2") format("woff2-variations");
  font-weight: 100 900; font-style: normal; font-display: swap;
}

/* ---------- Jetons du socle -------------------------------------------------
   `tokens/tokens.css` est GÉNÉRÉ depuis le thème + le design system : on ne
   l'édite pas. Les deux jetons ci-dessous n'appartiennent à aucune des deux
   sources — ce sont des décisions d'accessibilité, pas des décisions de
   marque — donc ils sont déclarés ici, avec leur norme. */
:root {
  --tap-min: 24px;      /* plancher WCAG 2.2 SC 2.5.8 (AA). Visé : 44px.      */
  --tap-vise: 44px;     /* SC 2.5.5 (AAA) — la cible confortable au pouce.    */
  /* Décalage haut des superpositions : un tiroir commence SOUS le bandeau
     d'annonce et l'en-tête épinglé, jamais par-dessus (règle 13, fiche 33).
     Nul par défaut ; le contexte le repose. Neuf composants le partagent. */
  --tiroir-haut: 0px;
}

/* ---------- Remise à zéro ---------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important;
  }
}

body {
  background: var(--bg-page);          /* porcelaine — JAMAIS blanc pur */
  color: var(--fg-1);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: var(--lh-relaxed);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

img, picture, svg, video, canvas { display: block; max-width: 100%; }
img, video { height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }

/* ---------- Titres — Cormorant, jamais gras --------------------------------- */
h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 500; line-height: var(--lh-snug); }
h1 { font-size: var(--fs-display); line-height: var(--lh-tight); }
h2 { font-size: var(--fs-h1); }
h3 { font-size: var(--fs-h2); }
h4 { font-size: var(--fs-h3); }

/* Le glyphe € rend comme un « C » en Cormorant à taille d'affichage :
   il est TOUJOURS porté par la police de labeur. */
.euro, .prix { font-family: var(--font-data); font-variant-numeric: tabular-nums; }

/* ---------- Accessibilité --------------------------------------------------- */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
/* ⚠ `.sr-only` N'EST PAS EN POSITION ABSOLUE, et c'est délibéré.
   Le motif « visually-hidden » standard pose `position: absolute`. À
   l'intérieur d'un conteneur de défilement, ce texte s'ÉCHAPPE : faute
   d'ancêtre positionné, son bloc conteneur devient le bloc conteneur initial,
   donc il se pose aux coordonnées du DOCUMENT et non du rail qui devrait le
   rogner.

   Mesuré le 2026-08-22, et c'est la panne la plus coûteuse à diagnostiquer de
   la journée : la rangée de puces de filtres défilait correctement
   (`overflow-x: auto`, contenu 1192 px dans 358), mais les huit
   « Retirer ce filtre » se posaient à x = 1176 dans le document. La page
   entière défilait horizontalement — 978 px pour 390 — c'est-à-dire la plainte
   n°1 du fondateur, causée par du texte invisible.

   Le diagnostic a été long parce que chaque élément mesurait juste : le rail
   était bien clippé, les tiroirs `fixed` semblaient coupables et ne l'étaient
   pas, et masquer les superpositions ne changeait rien. Seule une bissection
   enfant par enfant a désigné la vraie cause.

   La variante ci-dessous reste DANS LE FLUX : 1×1 px, rognée. Elle est lue par
   les lecteurs d'écran exactement pareil, et elle ne peut plus s'échapper de
   quoi que ce soit. */
.sr-only {
  display: inline-block; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Plancher tactile WCAG 2.2 SC 2.5.8. Le constat B5 a relevé 106 cibles
   sous le plancher, dont ~96 dues à un `.link-quiet` sans min-height. */
:where(a, button, [role="button"], summary, label) { min-height: var(--tap-min); }

/* ---------- Primitives de mise en page -------------------------------------- */
.bande { width: 100%; padding-inline: var(--sp-4); }
.bande > * { max-width: var(--container); margin-inline: auto; }
/* ⚠ `minmax(0, 1fr)` N'EST PAS DÉCORATIF.
   Une grille sans piste déclarée en crée une implicite `auto`, qui se dimensionne
   à la **contribution max-content** de son contenu. Un enfant qui sait défiler
   (`overflow-x: auto`) a beau se rogner à l'écran : sa CONTRIBUTION reste la
   somme de ses enfants, donc la piste s'élargit et la page défile.

   Mesuré le 2026-08-22 : la page collection défilait horizontalement, 720 px
   pour 390 d'écran, à cause du rail des rayons dans une grille de page. Le rail
   défilait parfaitement — c'est la piste au-dessus qui grossissait. C'est le
   même mécanisme que `min-width: auto` sur un enfant de flex, et le cousin du
   piège B1. */
.pile { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); }
.mesure { max-width: 68ch; }


/* ═══ lib/primitives.css ═══ */
/* design/lib/primitives.css — le vocabulaire partagé de la bibliothèque.
   ---------------------------------------------------------------------------
   Pourquoi ce fichier existe. Six familles de composants construites en
   parallèle produisent six dialectes : six boutons, six champs, six façons de
   dire « vide ». La cohérence ne se surveille pas, elle se rend structurelle —
   on écrit le vocabulaire UNE fois, et les composants n'ont plus rien à
   réinventer. C'est la même logique que « la bibliothèque avant les pages »,
   appliquée un cran plus bas.

   Règles dures : zéro littéral (porte 2), zéro emoji, tout en jetons.
   Tout ce qui est ici est PORTÉ EN THÈME. Rien de la maquette n'y entre. */

/* ═══════════════════════════════════════════════════════════════════════════
   1. Typographie utilitaire
   ═══════════════════════════════════════════════════════════════════════════ */
.etiquette {
  font-family: var(--font-ui); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--tr-eyebrow); text-transform: uppercase; color: var(--fg-3);
}
.chapo   { font-size: var(--fs-body-lg); line-height: var(--lh-normal); color: var(--fg-2); }
.discret { font-size: var(--fs-body-sm); color: var(--fg-2); }
.legende { font-size: var(--fs-caption); color: var(--fg-3); }
.donnee  { font-family: var(--font-data); font-variant-numeric: tabular-nums; }

/* Le prix. Format français NON NÉGOCIABLE : « 599,00 € », jamais « €599.00 ».
   Le glyphe € rend comme un « C » en Cormorant à taille d'affichage : le prix
   est donc TOUJOURS en police de labeur, même dans un contexte d'affichage. */
.prix { font-family: var(--font-data); font-variant-numeric: tabular-nums;
        letter-spacing: var(--tr-normal);
        /* Un montant ne se coupe JAMAIS entre le nombre et le symbole. Vu à
           l'écran le 2026-08-22 : « + 20,00 » sur une ligne et « € » seul sur
           la suivante. Règle de marque, pas décision de composant — le lot E a
           dû l'écrire cinq fois avant qu'elle ne monte ici. */
        white-space: nowrap; }
.prix--barre { color: var(--fg-3); text-decoration: line-through; }

/* ═══════════════════════════════════════════════════════════════════════════
   2. Boutons
   Un seul composant, quatre tons, trois tailles. Le plancher tactile est
   garanti ici et nulle part ailleurs — constat B5 : 106 cibles sous 24 px,
   dont ~96 dues à un lien discret sans `min-height`.
   ═══════════════════════════════════════════════════════════════════════════ */
.b {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--tap-vise);
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid transparent; border-radius: var(--r-pill);
  font-family: var(--font-ui); font-size: var(--fs-body-sm); font-weight: 500;
  line-height: var(--lh-snug); text-align: center; text-decoration: none;
  cursor: pointer; background: none; color: inherit;
  transition: background-color var(--dur-sm) var(--ease-standard),
              border-color var(--dur-sm) var(--ease-standard),
              color var(--dur-sm) var(--ease-standard);
}
.b--principal { background: var(--c-soft-amber); color: var(--c-deep-graphite); }
.b--principal:hover { background: var(--fg-accent); }
.b--secondaire { border-color: var(--fg-1); color: var(--fg-1); }
.b--secondaire:hover { background: var(--bg-subtle); }
.b--discret { color: var(--fg-2); padding-inline: var(--sp-2); }
.b--discret:hover { color: var(--fg-1); }
.b--sombre { background: var(--bg-inverse); color: var(--fg-on-dark); }
.b--pleine { width: 100%; }
.b--sm { min-height: var(--tap-min); padding: var(--sp-2) var(--sp-4);
         font-size: var(--fs-caption); }
.b--lg { min-height: var(--sp-7); font-size: var(--fs-body); }
.b[disabled], .b[aria-disabled="true"] {
  background: var(--bg-subtle); color: var(--fg-3); border-color: transparent;
  cursor: not-allowed;
}

/* Le bouton icône seul. Il DOIT porter un intitulé accessible (`aria-label`)
   et respecter le plancher tactile même si le glyphe fait 20 px. */
.b-icone {
  display: inline-grid; place-items: center;
  min-width: var(--tap-vise); min-height: var(--tap-vise);
  border: 0; border-radius: var(--r-pill); background: none; color: inherit;
  cursor: pointer;
}
.b-icone:hover { background: var(--bg-subtle); }

/* ═══════════════════════════════════════════════════════════════════════════
   3. Icônes
   Un trait, jamais un aplat ; 1,5 px pour rester lisible à 20 px. `currentColor`
   partout — une icône ne décide jamais de sa couleur.
   ═══════════════════════════════════════════════════════════════════════════ */
.i { width: var(--sp-5); height: var(--sp-5); flex: none;
     fill: none; stroke: currentColor; stroke-width: 1.5;
     stroke-linecap: round; stroke-linejoin: round; }
.i--sm { width: var(--sp-4); height: var(--sp-4); }
.i--lg { width: var(--sp-6); height: var(--sp-6); }

/* ═══════════════════════════════════════════════════════════════════════════
   4. Surfaces
   ═══════════════════════════════════════════════════════════════════════════ */
.panneau {
  background: var(--bg-surface); border: 1px solid var(--line-hair);
  border-radius: var(--r-card); padding: var(--sp-5);
}
.panneau--calme { background: var(--bg-subtle); border-color: transparent; }
.panneau--chaud { background: var(--bg-warm); border-color: transparent; }
.panneau--sombre { background: var(--bg-inverse); color: var(--fg-on-dark);
                   border-color: transparent; }
.filet { border: 0; border-top: 1px solid var(--line-hair); margin: var(--sp-5) 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   5. Formulaires
   ═══════════════════════════════════════════════════════════════════════════ */
.champ { display: grid; gap: var(--sp-2); }
.champ__label { font-size: var(--fs-body-sm); font-weight: 500; color: var(--fg-1); }
.champ__aide  { font-size: var(--fs-caption); color: var(--fg-3); }
.champ__erreur { font-size: var(--fs-caption); color: var(--fg-alert);
                 display: flex; align-items: center; gap: var(--sp-1); }
.saisie {
  min-height: var(--tap-vise); width: 100%;
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-surface); color: var(--fg-1);
  border: 1px solid var(--line-rule); border-radius: var(--r-sm);
  font-size: var(--fs-body);
}
.saisie::placeholder { color: var(--fg-3); }
.saisie[aria-invalid="true"] { border-color: var(--fg-alert); }

/* Case et bouton radio dessinés — la case native ne se met pas en forme de
   façon fiable, et sa cible est très sous le plancher tactile. */
.choix { display: flex; align-items: flex-start; gap: var(--sp-3);
         min-height: var(--tap-vise); padding: var(--sp-2) 0; cursor: pointer; }
.choix input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.choix__marque {
  flex: none; width: var(--sp-5); height: var(--sp-5); margin-top: var(--sp-1);
  border: 1px solid var(--line-rule); border-radius: var(--r-xs);
  display: grid; place-items: center; color: transparent;
  transition: background-color var(--dur-xs) var(--ease-standard);
}
.choix--radio .choix__marque { border-radius: var(--r-pill); }
.choix input:checked + .choix__marque {
  background: var(--fg-1); border-color: var(--fg-1); color: var(--fg-on-dark);
}
.choix input:focus-visible + .choix__marque {
  outline: 2px solid var(--line-focus); outline-offset: 2px;
}

/* L'interrupteur — la bascule des préférences de consentement.
   Promu depuis `panneau-preferences.css` : deux lots l'ont demandé, et un
   interrupteur est un contrôle de formulaire, pas une pièce de composant.

   Trois signaux redondants, et c'est délibéré : la POSITION de la pastille, le
   CONTRASTE du rail (gris neutre éteint, encre allumé), et le mot à côté
   (« Activé » / « Désactivé »). Osano fait l'inverse — éteint en violet saturé,
   allumé en vert saturé — deux teintes fortes dont aucune ne se lit comme
   « éteint ». Un interrupteur qui ne se lit que par la couleur ne se lit pas. */
.interrupteur {
  position: relative; flex: none;
  width: var(--sp-7); height: var(--sp-5);
  border-radius: var(--r-pill);
  background: var(--bg-subtle);
  box-shadow: var(--sh-inset-hair);
  transition: background-color var(--dur-sm) var(--ease-standard);
}
.interrupteur::after {
  content: ""; position: absolute; top: var(--sp-1); left: var(--sp-1);
  width: var(--sp-4); height: var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--bg-surface); box-shadow: var(--sh-1);
  transition: transform var(--dur-sm) var(--ease-standard);
}
input:checked ~ .interrupteur { background: var(--fg-1); }
input:checked ~ .interrupteur::after { transform: translateX(var(--sp-5)); }
input:focus-visible ~ .interrupteur { outline: 2px solid var(--line-focus); outline-offset: 2px; }
input:disabled ~ .interrupteur { opacity: .5; }

/* Sélecteur de quantité. Trois cibles côte à côte : chacune tient le plancher. */
.quantite { display: inline-flex; align-items: center;
            border: 1px solid var(--line-rule); border-radius: var(--r-pill); }
.quantite button { min-width: var(--tap-vise); min-height: var(--tap-vise);
                   border: 0; background: none; cursor: pointer; color: var(--fg-1); }
.quantite output, .quantite input { min-width: var(--sp-7); text-align: center;
                   font-family: var(--font-data); font-variant-numeric: tabular-nums; }
/* La fiche 34 exige un `<input>` (la quantité se tape), pas seulement un
   `<output>` : la primitive stylait l'un et pas l'autre. */
.quantite input { border: 0; background: none; padding: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   6. Pastilles et puces
   ═══════════════════════════════════════════════════════════════════════════ */
.pastille {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3); border-radius: var(--r-pill);
  font-size: var(--fs-caption); font-weight: 500;
  background: var(--bg-subtle); color: var(--fg-2);
}
.pastille--intelligence { background: var(--bg-intelligence); color: var(--fg-data); }
.pastille--alerte { background: var(--fg-alert); color: var(--fg-on-dark); }
.pastille--accent { background: var(--bg-warm); color: var(--fg-1); }

/* Une puce RETIRABLE (filtre actif) : le libellé et la croix forment une seule
   cible, jamais deux cibles de 16 px collées. */
.puce { display: inline-flex; align-items: center; gap: var(--sp-2);
        min-height: var(--tap-min); padding: var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-3);
        border: 1px solid var(--line-rule); border-radius: var(--r-pill);
        font-size: var(--fs-caption); background: var(--bg-surface); cursor: pointer; }

/* ═══════════════════════════════════════════════════════════════════════════
   7. Les états — vocabulaire commun, orthographe EXACTE
   `vide` · `chargement` · `erreur` · `long` · `rupture` · `succes` · `desactive`
   Un composant qui n'a qu'un état heureux est un composant à moitié fait.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Vide. Jamais une zone blanche : une phrase qui dit quoi faire, et une issue. */
.etat-vide { display: grid; gap: var(--sp-4); justify-items: center; text-align: center;
             padding: var(--sp-7) var(--sp-4); color: var(--fg-2); }
.etat-vide__titre { font-family: var(--font-display); font-size: var(--fs-h3); color: var(--fg-1); }

/* Erreur. Elle DIT ce qui a échoué et propose l'action de reprise. */
/* Un lien DANS un état d'erreur reprend la couleur du texte : sans ça, il
   tombe sur le bleu par défaut du navigateur — relevé dans les huit composants
   du lot C, donc c'est un défaut de la bibliothèque, pas d'un composant. */
.etat-erreur a { color: var(--fg-1); }
.etat-erreur { display: flex; align-items: flex-start; gap: var(--sp-3);
               padding: var(--sp-4); border-radius: var(--r-sm);
               background: var(--bg-subtle); color: var(--fg-1);
               border-left: 3px solid var(--fg-alert); }

/* Chargement. Une ossature qui a la FORME du contenu attendu, pas un
   tourniquet : la page ne doit pas sauter quand le contenu arrive. */
/* ⚠ Le balayage est un FOND ANIMÉ, jamais un pseudo-élément translaté.
   Mesuré le 2026-08-22 : la première version posait un `::after` en
   `inset: 0` animé de `translateX(-100%)` à `translateX(100%)`. Chrome compte
   un descendant transformé dans la région de débordement défilable, donc
   `scrollWidth − clientWidth` valait la largeur entière de l'élément — et
   variait à chaque mesure selon la phase de l'animation (135, 148, 170, 85 px
   relevés sur une page identique). La sonde de recensement remontait deux
   `clipped-x` GRAVES, « contenu définitivement inaccessible », sur de la pure
   décoration.

   Un faux GRAVE est le pire défaut d'outillage possible ici : il envoie six
   lots corriger ce qui n'existe pas, et il noie les vrais. Un fond animé ne
   crée aucun débordement — c'est la même image, à zéro coût de mise en page. */
.ossature {
  border-radius: var(--r-xs); color: transparent; overflow: hidden;
  background-color: var(--bg-subtle);
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@keyframes ossature-balayage { from { background-position: 100% 0; } to { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .ossature { animation: none; } }

/* Rupture. Le composant reste lisible et achetable ailleurs — jamais un mur. */
.rupture { color: var(--fg-3); }
.rupture .prix { text-decoration: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   8. Superpositions — tiroir et modale
   Mécanique partagée par le méga-menu, la recherche, le panier, les filtres,
   le tri, la capture d'e-mail, le lecteur vidéo et le guide de compatibilité.
   Écrite UNE fois : neuf composants ne peuvent plus diverger sur le z-index,
   le verrouillage du défilement ou la touche Échap.
   ═══════════════════════════════════════════════════════════════════════════ */
.voile { position: fixed; inset: 0; z-index: 80;
         background: var(--bg-inverse); opacity: 0;
         visibility: hidden; transition: opacity var(--dur-md) var(--ease-standard),
                                         visibility var(--dur-md); }
[data-ouvert="oui"] > .voile, .voile[data-ouvert="oui"] { opacity: .45; visibility: visible; }

/* `--tiroir-haut` : le décalage sous un bandeau d'annonce ou un en-tête
   épinglé. La règle 13 de la fiche 33 veut qu'un tiroir commence SOUS le
   bandeau, jamais par-dessus. Valeur nulle par défaut, posée par le contexte —
   partagée par les neuf superpositions au lieu d'être réécrite dans chacune. */
.tiroir {
  position: fixed; z-index: 90; top: var(--tiroir-haut); bottom: 0; right: 0;
  width: min(92vw, var(--container-narrow));
  display: flex; flex-direction: column;
  background: var(--bg-page); color: var(--fg-1);
  transform: translateX(100%);
  transition: transform var(--dur-md) var(--ease-emerge);
  overscroll-behavior: contain;
}
.tiroir--gauche { right: auto; left: 0; transform: translateX(-100%); }
.tiroir--bas { top: auto; left: 0; right: 0; width: auto;
               max-height: 88vh; transform: translateY(100%);
               border-radius: var(--r-xl) var(--r-xl) 0 0; }
.tiroir[data-ouvert="oui"] { transform: none; }
.tiroir__entete { display: flex; align-items: center; justify-content: space-between;
                  gap: var(--sp-4); padding: var(--sp-3) var(--sp-4);
                  border-bottom: 1px solid var(--line-hair); flex: none; }
.tiroir__corps { flex: 1; overflow-y: auto; padding: var(--sp-4); }
.tiroir__pied  { flex: none; padding: var(--sp-4);
                 border-top: 1px solid var(--line-hair); background: var(--bg-surface); }

.modale { position: fixed; z-index: 95; inset: 0; display: grid; place-items: center;
          padding: var(--sp-4); pointer-events: none; }
.modale[data-ouvert="oui"] { pointer-events: auto; }
.modale__boite { width: min(100%, var(--container-narrow)); max-height: 88vh; overflow-y: auto;
                 background: var(--bg-page); border-radius: var(--r-lg); padding: var(--sp-6);
                 opacity: 0; transform: translateY(var(--sp-4));
                 transition: opacity var(--dur-md) var(--ease-emerge),
                             transform var(--dur-md) var(--ease-emerge); }
.modale[data-ouvert="oui"] .modale__boite { opacity: 1; transform: none; }

/* Quand une superposition est ouverte, le fond ne défile plus. */
html[data-defilement="bloque"] { overflow: hidden; }

/* ═══════════════════════════════════════════════════════════════════════════
   9. Mises en page récurrentes
   ═══════════════════════════════════════════════════════════════════════════ */
.section { padding-block: var(--sp-8); }
.section--serree { padding-block: var(--sp-6); }
/* Même garde que `.pile` : sans piste déclarée, la piste implicite se
   dimensionne à la contribution max-content, et un rail qui défile élargit
   quand même la page. Voir la note de `base.css`. */
.grille { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); }
.grille--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rangee { display: flex; align-items: center; gap: var(--sp-3); }
.rangee--entre { justify-content: space-between; }
.pousse { margin-left: auto; }

/* Le carrousel horizontal. ⚠ PIÈGE B1, la plainte n°1 du fondateur : JAMAIS de
   `min-width` sur le conteneur d'un `overflow-x: auto` — c'est ce qui fait
   déborder la page entière. La largeur se donne aux ENFANTS, jamais au rail. */
.rail { position: relative;   /* ancêtre positionné : rien ne s'échappe du rail */
        display: flex; gap: var(--sp-3); overflow-x: auto;
        scroll-snap-type: x mandatory; scrollbar-width: none; }
/* Le débordement jusqu'au bord de l'écran est EXPLICITE, jamais par défaut.
   Il était dans `.rail` même : `padding-inline: var(--sp-4)` compensé par un
   `margin-inline` négatif de la même valeur. Ça ne marche que si le parent a
   exactement ce rembourrage-là. Dans un parent qui en a un autre, le rail
   dépasse — mesuré le 2026-08-22 sur la rangée de rayons de l'en-tête de
   collection : 425 px de document pour 390 d'écran, après correction du
   `.sr-only`. Un effet visuel ne doit pas être un piège silencieux. */
.rail--bord { padding-inline: var(--sp-4); margin-inline: calc(var(--sp-4) * -1); }
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; flex: none; }

/* Ratios d'image — la place est réservée AVANT le chargement, jamais de saut. */
.media { position: relative; overflow: hidden; border-radius: var(--r-card);
         background: var(--bg-subtle); }
.media--carre   { aspect-ratio: 1 / 1; }
.media--portrait{ aspect-ratio: 4 / 5; }
.media--paysage { aspect-ratio: 16 / 10; }
.media > img, .media > video { width: 100%; height: 100%; object-fit: cover; }

/* ═══════════════════════════════════════════════════════════════════════════
   10. Au-delà du téléphone
   Le prototype se juge à 390 px D'ABORD (99 % du trafic). Les points de
   rupture ÉLARGISSENT, ils ne redessinent pas.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 48rem) {
  .bande { padding-inline: var(--sp-6); }
  .section { padding-block: var(--sp-9); }
  .grille--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 64rem) {
  .bande { padding-inline: var(--sp-7); }
  .grille--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}


/* ═══ lib/components/app-compagnon.css ═══ */
/* FICHE 17 — application compagnon.
   ---------------------------------------------------------------------------
   Tout le composant tient sur une seule idée mesurée : « le piège est la capture
   centrée, entière, avec une ombre portée ». Dès que l'appareil tient tout
   entier dans le cadre avec de l'air autour, l'œil le lit comme une image
   d'illustration. Donc l'appareil est coupé DEUX fois — par le bas et par la
   droite — et les deux coupes viennent de marges négatives sous un
   `overflow: hidden`, jamais d'un rognage d'image. */

.c-app-compagnon { display: grid; gap: var(--sp-5); }

/* Le panneau. Rayon 28 px (`--r-xl`), surface distincte de la porcelaine de
   page — jamais du blanc pur : sur `--bg-page`, un panneau blanc pur disparaît
   à six points d'écart et le bloc perd son cadre. */
.c-app-compagnon__panneau {
  background: var(--bg-subtle); border-radius: var(--r-xl);
  padding: var(--sp-6) var(--sp-5) var(--sp-0);
  overflow: hidden;
}

.c-app-compagnon__texte { display: grid; gap: var(--sp-3); max-width: 30ch; }
.c-app-compagnon__accroche {
  font-size: var(--fs-h3); line-height: var(--lh-snug); color: var(--fg-2);
}
.c-app-compagnon__accroche b { color: var(--fg-1); font-weight: 600; }
.c-app-compagnon__lien { display: inline-flex; align-items: center; gap: var(--sp-1);
                         min-height: var(--tap-min); color: var(--fg-1);
                         font-size: var(--fs-body-sm); }

/* ---------- L'appareil -------------------------------------------------------
   La coupe par le BAS : marge basse négative, l'appareil sort du panneau. Elle
   suffit à casser le motif que la fiche désigne comme le piège — « la capture
   centrée, entière, avec de l'air autour » — et elle ne coûte rien : un
   débordement VERTICAL sous `overflow: hidden` n'est ni défilable ni signalé.

   ⚠ La coupe par la DROITE a été RETIRÉE le 2026-08-22, et c'est une mesure,
   pas un goût. Elle était obtenue par une marge droite de −96 px sous le
   `overflow: hidden` du panneau : la sonde de recensement a relevé cinq fois
   « clipped-x GRAVE — 72 px définitivement inaccessibles » sur ce seul
   composant, et elle avait raison. Ce n'était pas un artefact de mesure comme
   le balayage translaté du piège n°6 : 72 px du quatrième onglet étaient
   réellement hors d'atteinte. Aucune propriété CSS ne coupe horizontalement
   sans créer cette région — clip, hidden ou masque, il faut choisir entre
   trancher et ne rien rogner.

   L'appareil affleure donc le bord DROIT du panneau (marge négative égale au
   padding, au pixel), ce qui lui garde son air de sortir du cadre sans qu'un
   seul pixel de contenu soit perdu, et les quatre onglets tiennent en entier.
   ⚠ Le fondateur peut vouloir l'inverse — l'onglet tranché de la fiche, au prix
   d'un GRAVE assumé au recensement. C'est son arbitrage, pas le mien. */
.c-app-compagnon__appareil {
  margin: var(--sp-6) calc(var(--sp-5) * -1) calc(var(--sp-8) * -1) var(--sp-2);
  border: 2px solid var(--fg-1); border-radius: var(--r-xl);
  background: var(--fg-1);
  padding: var(--sp-2) var(--sp-2) var(--sp-0);
  box-shadow: var(--sh-2);
}
.c-app-compagnon__ecran {
  background: var(--bg-page); border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--sp-2) var(--sp-3) var(--sp-5);
  display: grid; gap: var(--sp-3);
}

/* La barre d'état, figée à une heure fixe — comme le 09:41 d'Apple. */
.c-app-compagnon__barre-etat {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--fs-caption); font-family: var(--font-data); color: var(--fg-1);
  padding-inline: var(--sp-2);
}
.c-app-compagnon__signal {
  width: var(--sp-6); height: var(--sp-2); border-radius: var(--r-pill);
  background: var(--fg-1); opacity: .35;
}

/* Les onglets. Ils tiennent MAINTENANT en entier dans la largeur de l'écran —
   c'est la condition pour que plus rien ne soit rogné (voir la note de
   l'appareil ci-dessus) : la taille descend d'un cran et l'écart se resserre,
   ce qui est de toute façon la vraie mesure d'une barre d'onglets de téléphone. */
.c-app-compagnon__onglets {
  display: flex; gap: var(--sp-3); padding-block: var(--sp-1);
  border-bottom: 1px solid var(--line-hair);
}
.c-app-compagnon__onglet {
  font-size: var(--fs-caption); color: var(--fg-3); white-space: nowrap;
  padding-bottom: var(--sp-2);
}
.c-app-compagnon__onglet[data-actif="oui"] {
  color: var(--fg-1); box-shadow: inset 0 -2px 0 0 currentColor;
}

.c-app-compagnon__ui { display: grid; gap: var(--sp-3); }
.c-app-compagnon__chat { display: flex; align-items: baseline; justify-content: space-between;
                         gap: var(--sp-2); }
.c-app-compagnon__nom { font-family: var(--font-display); font-size: var(--fs-h3); }
.c-app-compagnon__horodatage { font-size: var(--fs-caption); color: var(--fg-3);
                               font-family: var(--font-data); }

.c-app-compagnon__carte {
  background: var(--bg-surface); border-radius: var(--r-sm);
  padding: var(--sp-3); display: grid; gap: var(--sp-2);
  box-shadow: var(--sh-hair);
}
.c-app-compagnon__carte--seconde { background: var(--bg-surface); }
.c-app-compagnon__carte-titre {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-caption); text-transform: uppercase;
  letter-spacing: var(--tr-eyebrow); color: var(--fg-2);
}

/* L'onde. Elle représente un signal capté — elle n'affirme aucun résultat, et
   c'est pour ça qu'elle peut être dessinée sans rien alléguer. */
.c-app-compagnon__onde {
  width: 100%; height: var(--sp-7);
  stroke: var(--fg-data); stroke-width: 3; stroke-linecap: round; fill: none;
}
.c-app-compagnon__attente {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-body-sm); color: var(--fg-2);
}
.c-app-compagnon__resultat { font-size: var(--fs-body); color: var(--fg-1); }
.c-app-compagnon__confiance { font-size: var(--fs-caption); color: var(--fg-3);
                              font-family: var(--font-data); }
/* Une valeur marquée comme fictive PORTE sa marque à l'écran : un liseré, pas
   une note dans un fichier. Le fondateur doit voir, sans rien ouvrir, ce qui
   est une sortie réelle et ce qui est un remplaçant. */
/* La marque visuelle des valeurs inventées a été PROMUE dans
   `mockup/atelier.css` : elle s'applique à toute la bibliothèque, et surtout
   elle n'a rien à faire dans `lib/`, qui devient le thème. Un soulignement
   « ceci est une valeur de maquette » livré en production serait un défaut. */
/* Dès qu'un résultat est affiché, l'attente n'a plus lieu d'être. */
.c-app-compagnon__carte:has(.c-app-compagnon__resultat) .c-app-compagnon__attente { display: none; }

.c-app-compagnon__ligne { display: block; }
.c-app-compagnon__ossature-ligne {
  display: block; height: var(--sp-2); border-radius: var(--r-xs);
  background: var(--bg-subtle);
}
.c-app-compagnon__ligne:last-child .c-app-compagnon__ossature-ligne { width: 60%; }

/* ---------- Variante `inventaire` ------------------------------------------- */
.c-app-compagnon__inventaire {
  display: none; list-style: none; padding: var(--sp-0); margin: var(--sp-0);
  grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5);
  text-align: center;
}
.c-app-compagnon__appareil-item { display: grid; gap: var(--sp-2); justify-items: center;
                                  font-size: var(--fs-body-sm); color: var(--fg-2); }
.c-app-compagnon__glyphe {
  width: var(--sp-7); height: var(--sp-7);
  fill: none; stroke: var(--fg-1); stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
.c-app-compagnon[data-variante="inventaire"] .c-app-compagnon__panneau { display: none; }
.c-app-compagnon[data-variante="inventaire"] .c-app-compagnon__inventaire { display: grid; }

/* ---------- Variante `prose` — la réponse B&O -------------------------------
   On ne montre RIEN de l'interface. Une phrase, un lien de téléchargement rangé
   derrière un dépliant de pied de page, et le registre premium tient quand même.
   C'est une option de plein droit, pas un repli. */
.c-app-compagnon[data-variante="prose"] .c-app-compagnon__appareil { display: none; }
/* ⚠ `overflow: visible` est un correctif, pas un détail de style. Vu à l'écran
   le 2026-08-22, agrandi ×2 : la barre verticale du « Ê » de « Être prévenu de
   la sortie » était RASÉE. Le panneau porte `overflow: hidden` pour couper
   l'appareil ; en variante `prose` il n'y a pas d'appareil à couper, mais le
   padding tombe à zéro, le texte commence donc exactement au bord de la boîte,
   et le débord d'encre du glyphe (moins de deux pixels) passe sous la lame.
   La sonde ne peut pas le voir : elle ignore les débordements de 2 px ou moins.
   Un accent tranché sur un lien est pourtant une faute de composition visible à
   l'œil nu. */
.c-app-compagnon[data-variante="prose"] .c-app-compagnon__panneau {
  background: var(--bg-page); padding: var(--sp-0); overflow: visible;
}
.c-app-compagnon[data-variante="prose"] .c-app-compagnon__texte { max-width: 46ch; }
.c-app-compagnon[data-variante="prose"] .c-app-compagnon__accroche {
  font-family: var(--font-display); font-size: var(--fs-h2); line-height: var(--lh-snug);
}
.c-app-compagnon[data-variante="prose"] .c-app-compagnon__accroche b { font-weight: 500; }

/* ---------- Les états -------------------------------------------------------- */
.c-app-compagnon__vide, .c-app-compagnon__erreur { display: none; }

.c-app-compagnon[data-etat="vide"] .c-app-compagnon__panneau,
.c-app-compagnon[data-etat="vide"] .c-app-compagnon__inventaire { display: none; }
.c-app-compagnon[data-etat="vide"] .c-app-compagnon__vide {
  display: grid; border: 1px dashed var(--line-rule); border-radius: var(--r-xl);
}

.c-app-compagnon[data-etat="chargement"] .c-app-compagnon__onde,
.c-app-compagnon[data-etat="chargement"] .c-app-compagnon__attente { visibility: hidden; }
.c-app-compagnon[data-etat="chargement"] .c-app-compagnon__carte {
  position: relative; overflow: hidden;
}
.c-app-compagnon[data-etat="chargement"] .c-app-compagnon__carte::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-subtle), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-app-compagnon[data-etat="chargement"] .c-app-compagnon__carte::after { animation: none; }
}

/* erreur — l'appareil perd la liaison, le CatLang continue. Le message le DIT :
   c'est la différence entre une panne et une inquiétude. */
.c-app-compagnon[data-etat="erreur"] .c-app-compagnon__erreur { display: flex; }
.c-app-compagnon[data-etat="erreur"] .c-app-compagnon__appareil { opacity: .45; }

/* long — une accroche française de 220 caractères. Elle descend d'un cran en
   taille et s'élargit : la coupe de l'appareil, elle, ne bouge pas. */
.c-app-compagnon[data-etat="long"] .c-app-compagnon__accroche { font-size: var(--fs-body-lg); }
.c-app-compagnon[data-etat="long"] .c-app-compagnon__texte { max-width: 46ch; }

/* rupture — l'application existe, l'appareil qu'elle pilote n'est pas livrable. */
.c-app-compagnon[data-etat="rupture"] .c-app-compagnon__note { color: var(--fg-alert); }

/* succes — l'appareil vient d'être appairé. */
.c-app-compagnon[data-etat="succes"] .c-app-compagnon__carte-titre { color: var(--fg-data); }
.c-app-compagnon[data-etat="succes"] .c-app-compagnon__attente { color: var(--fg-data); }

/* desactive — l'analyse est suspendue (appareil en veille, abonnement inactif). */
.c-app-compagnon[data-etat="desactive"] .c-app-compagnon__ecran { filter: grayscale(1); }
.c-app-compagnon[data-etat="desactive"] .c-app-compagnon__onde { stroke: var(--fg-3); }

@media (min-width: 48rem) {
  .c-app-compagnon__inventaire { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .c-app-compagnon__panneau { padding-inline: var(--sp-7); }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-app-compagnon__erreur a { color: var(--fg-1); }


/* ═══ lib/components/bandeau-annonce.css ═══ */
/* FICHE 21 — bandeau d'annonce.
   Mesures des références : Jones Road 390 × 50 px, fond brun profond, texte
   13 px blanc, une phrase centrée, aucun lien ni bouton. Gymshark 390 × 48 px,
   lien 12 px souligné. Ridge ≈ 40 px, noir.

   Budget de hauteur, la règle opérationnelle de la fiche :
   hauteur(bandeau) + hauteur(en-tête) ≤ 128 px à 390. Gymshark empile trois
   barres (45 + 48 + 76 = 169 px, 20 % d'un écran de 844) avant le premier mot
   du héros. Ici : 44 px de bandeau + 64 px d'en-tête = 108 px. */

.c-bandeau-annonce {
  position: relative;               /* JAMAIS collant : le message se lit une  */
  z-index: 1;                       /* fois, c'est l'en-tête qui reste épinglé */
  display: flex; align-items: center; justify-content: center;
  min-height: var(--sp-7);          /* 48 px — plafond de la fourchette 40–48  */
  padding: var(--sp-2) var(--sp-4);
  /* Fond DISTINCT de la page. Jones Road pose un brun profond sur une page
     bone ; la page Catharsis est porcelaine, donc le bandeau prend le graphite.
     Jamais l'ambre : il est réservé au CTA primaire et perdrait sa valeur de
     signal s'il servait aussi de fond décoratif. */
  background: var(--bg-inverse);
  color: var(--fg-on-dark);
}

.c-bandeau-annonce__message {
  font-size: var(--fs-caption);     /* 12 px — Gymshark 12, Jones Road 13     */
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-normal);
  text-align: center;
  text-transform: none;             /* un bandeau en capitales est un bandeau  */
  text-wrap: balance;               /* de soldes ; la marque ne crie pas       */
}

/* Le lien est souligné, et c'est le seul ornement : pas de pilule, pas de
   flèche, pas de bouton. Il tient le plancher tactile sans grossir la barre —
   la zone cliquable déborde en `padding-block` négatif compensé. */
.c-bandeau-annonce__lien {
  color: inherit; text-decoration: underline;
  text-underline-offset: var(--sp-1);
  display: inline-block; min-height: var(--tap-min);
}
.c-bandeau-annonce__lien:hover { text-decoration-thickness: 2px; }

/* ---------- Variantes -------------------------------------------------------- */

/* Chaud — le sable de la marque. Employé quand le message est une attention
   (« emballage sans plastique ») plutôt qu'une promesse commerciale. */
.c-bandeau-annonce[data-variante="chaud"] {
  background: var(--bg-warm); color: var(--fg-1);
}

/* Discret — le parti pris d'Oura : aucun fond propre, le message est posé
   directement sur la porcelaine et se lit comme la PREMIÈRE LIGNE DU DOCUMENT,
   pas comme une barre. C'est le plus calme des trois, et le seul qui ne coûte
   aucun contraste au reste de la page. */
.c-bandeau-annonce[data-variante="discret"] {
  background: transparent; color: var(--fg-2);
  border-bottom: 1px solid var(--line-hair);
}

/* ---------- États ------------------------------------------------------------ */

/* LONG — le cas qui casse la barre. « Livraison offerte en France
   métropolitaine dès 60 € d'achat, retours gratuits sous 30 jours » fait
   103 caractères : à 12 px sur 358 px utiles, c'est trois lignes. On laisse le
   texte passer à deux lignes (la barre grandit proprement) et on CLAMPE à deux :
   au-delà, le bandeau devient une section et vole le premier écran. Le texte
   n'est jamais rogné à l'horizontale — un message tronqué au milieu d'un mot
   est pire qu'un message absent. */
.c-bandeau-annonce[data-etat="long"] .c-bandeau-annonce__message {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.c-bandeau-annonce[data-etat="long"] { min-height: var(--sp-7); }

/* VIDE — aucun message configuré. Le bandeau ne rend RIEN : pas une barre de
   couleur vide de 48 px. C'est le défaut le plus coûteux du corpus (« un rendu
   vide ne ressemble pas à une erreur, il ressemble à un espacement ») et ici
   il se règle en ne rendant pas la boîte. */
.c-bandeau-annonce[data-etat="vide"] { display: none; }

/* DÉSACTIVÉ — le réglage `afficher_sur_mobile` est à non. Le bandeau existe,
   il est simplement hors du téléphone. On le masque à 390 et on le rend au
   point de rupture : masquer par un `display:none` inconditionnel ferait
   disparaître le message des deux côtés sans que personne le remarque. */
.c-bandeau-annonce[data-etat="desactive"] { display: none; }
@media (min-width: 48rem) {
  .c-bandeau-annonce[data-etat="desactive"] { display: flex; }
}


/* ═══ lib/components/bandeau-consentement.css ═══ */
/* FICHE 40 — bandeau de consentement, premier niveau.
   Cotes de référence : `.osano-cm-dialog` sur litter-robot.com, mesuré au
   navigateur le 2026-08-20 (391 × 554 px, soit 65,6 % du premier écran). */

.c-bandeau-consentement {
  position: fixed; z-index: 85; left: 0; right: 0; bottom: 0;
  padding: var(--sp-3);
  /* Fermé par défaut. Une fois le choix fait, le bandeau ne revient pas :
     durée de vie CNIL de treize mois pour les traceurs, sans reconduction
     automatique. */
  visibility: hidden;
  transform: translateY(100%);
  transition: transform var(--dur-md) var(--ease-emerge), visibility var(--dur-md);
}
.c-bandeau-consentement[data-ouvert="oui"] { visibility: visible; transform: none; }

/* ---------- Le plafond de hauteur, et pourquoi il est une règle -------------
   Osano prend 65,6 % de 844 px : le héros disparaît, et le premier contact
   avec la marque est un mur gris de 554 px. Pour une marque qui se définit
   « Calme · Premium », c'est le pire premier écran possible. Plafond
   Catharsis : 40 % de la hauteur de fenêtre à 390 px. Le corps défile À
   L'INTÉRIEUR du bandeau si le texte dépasse — la page dessous ne bouge pas et
   les trois boutons restent atteignables. */
.c-bandeau-consentement__boite {
  /* Colonne : un corps qui défile, un pied d'actions qui ne défile jamais.
     Le plafond s'applique à la BOÎTE ; la coupure, elle, ne peut plus tomber
     sur les trois boutons. */
  max-height: 40vh; overflow: hidden;
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-4);
  background: var(--bg-surface); color: var(--fg-1);
  border: 1px solid var(--line-hair); border-radius: var(--r-lg);
  box-shadow: var(--sh-3);
}

/* La zone défilante. `min-height: 0` est load-bearing : sans lui un enfant
   flex refuse de rétrécir sous sa hauteur de contenu, la boîte dépasse son
   plafond, et le pied ressort par le bas. */
.c-bandeau-consentement__corps {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  display: grid; gap: var(--sp-3);
}
.c-bandeau-consentement__intro { font-size: var(--fs-body-sm); line-height: var(--lh-normal); }

/* Les cinq finalités. Nommées, pas résumées : « améliorer votre expérience »
   n'est pas une finalité. Corps réduit mais jamais sous la légende — c'est de
   l'information juridique, pas une note de bas de page. */
.c-bandeau-consentement__finalites {
  margin: 0; padding-left: var(--sp-4);
  font-size: var(--fs-caption); color: var(--fg-2);
  line-height: var(--lh-normal);
}
.c-bandeau-consentement__lien { font-size: var(--fs-caption); }
.c-bandeau-consentement__lien a {
  color: var(--fg-2);
  /* ⚠ Relevé par la sonde le 2026-08-22 : 230 × 15 px, sous le plancher WCAG
     2.2 SC 2.5.8, et trop proche des trois actions pour bénéficier de
     l'exception d'espacement. Le plancher du socle (`:where(a) { min-height }`)
     ne mord pas sur un lien INLINE — une hauteur minimale n'a aucun effet sur
     une boîte en ligne. Il faut donc le sortir du flux en ligne. C'est
     exactement le constat B5 : ~96 des 106 cibles trop petites venaient d'un
     lien discret sans hauteur. */
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}

/* ---------- LA PARITÉ, ÉCRITE EN CSS ----------------------------------------
   Une seule règle porte les trois boutons. Il est structurellement impossible
   que « Tout refuser » soit plus petit que « Tout accepter » : ils n'ont pas
   deux déclarations, ils en ont une. C'est la traduction de la voie la plus
   simple d'Osano — trois boutons rigoureusement identiques — et c'est ce qui
   rend la conformité vérifiable en lisant le fichier.

   Contrôle automatique du recensement, à l'identique :
     hauteur(refuser) == hauteur(accepter) · largeur == · fontSize == · fontWeight ==

   Aucun des trois n'est en Soft Amber. L'ambre est la couleur de l'action
   commerciale de la marque ; l'employer ici mettrait un poids visuel sur
   l'acceptation, ce qui est exactement l'asymétrie que la CNIL refuse. */
.c-bandeau-consentement__actions { display: grid; gap: var(--sp-2); flex: none; }
.c-bandeau-consentement__action {
  min-height: var(--tap-vise);
  font-size: var(--fs-body-sm);
  font-weight: 500;
}

/* Au-delà du téléphone, les trois passent côte à côte — même largeur, donc la
   parité tient aussi en rangée. */
@media (min-width: 48rem) {
  .c-bandeau-consentement__actions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---------- Les états -------------------------------------------------------- */
.c-bandeau-consentement__succes,
.c-bandeau-consentement__erreur { display: none; }

.c-bandeau-consentement[data-etat="succes"] .c-bandeau-consentement__succes {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-body-sm); color: var(--fg-data);
}
.c-bandeau-consentement[data-etat="succes"] .c-bandeau-consentement__intro,
.c-bandeau-consentement[data-etat="succes"] .c-bandeau-consentement__finalites,
.c-bandeau-consentement[data-etat="succes"] .c-bandeau-consentement__lien,
.c-bandeau-consentement[data-etat="succes"] .c-bandeau-consentement__actions { display: none; }

.c-bandeau-consentement[data-etat="erreur"] .c-bandeau-consentement__erreur { display: flex; }
/* Le corps cède la place au message. Sans cette règle, le corps est écrasé à
   ZÉRO pixel de haut par le pied et l'alerte réunis sous le plafond de 40 %
   (mesuré : `clientHeight` 0 pour `scrollHeight` 206) — l'intro et les cinq
   finalités deviennent alors invisibles SANS que rien ne le dise, ce qui est
   pire que de les retirer franchement. On les retire franchement. */
.c-bandeau-consentement[data-etat="erreur"] .c-bandeau-consentement__corps { display: none; }

/* Chargement — l'enregistrement du choix est parti. Les trois actions se
   verrouillent ENSEMBLE : verrouiller la seule acceptation laisserait refuser
   plus facile qu'accepter, ce qui est le même défaut de parité à l'envers. */
.c-bandeau-consentement[data-etat="chargement"] .c-bandeau-consentement__action {
  background: var(--bg-subtle); color: var(--fg-3); border-color: transparent;
  pointer-events: none;
}

/* Désactivé — la visite ne relève pas de l'affichage obligatoire (hors EEE et
   Royaume-Uni). Le bandeau ne rend rien du tout ; il ne rend pas « vide ». */
.c-bandeau-consentement[data-etat="desactive"] { display: none; }

/* ---------- Variante « rappel » ---------------------------------------------
   Le lien permanent du pied de page rouvre le premier niveau, pas seulement le
   panneau : retirer son consentement doit être aussi simple que de le donner,
   et un visiteur qui a tout accepté doit pouvoir tout refuser en un clic. */
.c-bandeau-consentement[data-variante="rappel"] .c-bandeau-consentement__finalites { display: none; }

/* ---------- La commutation depuis la barre d'outils de maquette ------------- */
html[data-etat="consentement"] .c-bandeau-consentement {
  visibility: visible; transform: none;
}


/* ═══ lib/components/barre-collante.css ═══ */
/* FICHE 09 — barre d'achat épinglée.
   Mesures : 61 px (Litter-Robot, bas), 62 px (Dyson, haut), 126 px (Eight Sleep,
   bas). Retenu : BASSE, opaque, filet haut. Basse parce qu'en haut elle mange la
   zone où le regard cherche le contenu — Dyson paie 118 px de haut (en-tête +
   barre), soit 14 % du viewport. */

.c-barre-collante {
  /* Opaque, jamais translucide. Le fond rgba(255,255,255,.7) flouté de
     Litter-Robot laisse lire un fantôme de texte tronqué à travers la barre :
     deux défauts qui s'additionnent. */
  background: var(--bg-surface);
  border-top: 1px solid var(--line-hair);
}

/* L'épinglage est EXPLICITE : le gabarit est le même, seul l'ancrage change.
   Sans `data-ancrage`, la barre rend en flux — c'est ce qui la rend jugeable
   dans l'atelier, où trois barres épinglées se recouvriraient. */
.c-barre-collante[data-ancrage="fixe"] {
  position: fixed; left: 0; right: 0; bottom: 0;
  /* Sous le voile (80), le tiroir (90) et la modale (95) : une barre d'achat ne
     passe JAMAIS au-dessus d'un dialogue. */
  z-index: 70;
  box-shadow: var(--sh-2);
}

.c-barre-collante__interieur {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-4);
  max-width: var(--container); margin-inline: auto;
  min-width: 0;
}

.c-barre-collante__chiffre { display: grid; gap: 0; min-width: 0; }

/* Le nom est TRONQUÉ sur une ligne, et le prix a sa propre ligne. C'est la
   correction exacte du défaut Dyson. */
.c-barre-collante__nom {
  font-size: var(--fs-caption); color: var(--fg-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.c-barre-collante__prix { font-size: var(--fs-body-lg); font-weight: 500; color: var(--fg-1);
                          line-height: var(--lh-snug); }
.c-barre-collante__comprend { font-size: var(--fs-caption); color: var(--fg-3);
                              line-height: var(--lh-snug); }

/* ⚠ `flex: none` ÉTAIT UN PIÈGE, et le pire de tous : il est invisible.
   Le bloc d'action refusait de rétrécir. Avec un second bouton (« Me prévenir »)
   et une phrase de livraison complète, il mesurait **519 px dans une barre de
   390** — mesuré le 2026-08-22 sur la fiche produit. La moitié droite, dont un
   bouton entier, était hors écran et inatteignable.
   
   Et AUCUNE porte ne le voyait : la barre est en `position: fixed`, donc elle
   n'étend pas la zone défilable du document, donc « pas de débordement ». Le
   seul contrôle qui l'a attrapé, c'est d'avoir regardé la capture. Sur la page
   qui vend, et sur le seul chemin d'achat qui existe sur téléphone. */
.c-barre-collante__action {
  display: grid; gap: var(--sp-1); justify-items: stretch;
  flex: 0 1 auto; min-width: 0;
}
.c-barre-collante__bouton { min-height: var(--tap-vise); font-weight: 500; }
.c-barre-collante__livraison { font-size: var(--fs-caption); color: var(--fg-2);
                               text-align: center; line-height: var(--lh-snug);
                               /* la phrase s'enroule au lieu de pousser la barre */
                               overflow-wrap: break-word; }

/* LA LIGNE QUE LITTER-ROBOT N'A PAS ÉCRITE. Sans elle, la barre recouvre la fin
   du contenu — chez eux, la dernière puce de la liste est coupée par la barre et
   on en lit le fantôme à travers le flou. La correction coûte une ligne de CSS,
   et elle appartient au composant qui crée le problème, pas à la page. */
body:has(.c-barre-collante[data-ancrage="fixe"]) { padding-bottom: var(--sp-9); }

/* ---------- Le même gabarit, l'autre ancrage --------------------------------
   « flux » — le pied du bloc d'achat. Même contenu, même bouton, même date ;
   il perd seulement son épinglage et son ombre. C'est le motif Eight Sleep :
   un seul gabarit, deux ancrages. */
.c-barre-collante[data-ancrage="flux"] {
  position: static; box-shadow: none;
  border: 1px solid var(--line-hair); border-radius: var(--r-card);
  background: var(--bg-inverse); color: var(--fg-on-dark);
}
.c-barre-collante[data-ancrage="flux"] .c-barre-collante__nom,
.c-barre-collante[data-ancrage="flux"] .c-barre-collante__prix { color: var(--fg-on-dark); }
.c-barre-collante[data-ancrage="flux"] .c-barre-collante__comprend,
.c-barre-collante[data-ancrage="flux"] .c-barre-collante__livraison { color: var(--bg-subtle); }
.c-barre-collante[data-ancrage="flux"] .c-barre-collante__interieur { padding: var(--sp-4); }

/* ---------- Les états -------------------------------------------------------- */
.c-barre-collante__libelle-rupture { display: none; }

/* Rupture — le verbe change, la place ne bouge pas (fiche 08). */
.c-barre-collante[data-etat="rupture"] .c-barre-collante__libelle { display: none; }
.c-barre-collante[data-etat="rupture"] .c-barre-collante__libelle-rupture { display: inline; }
.c-barre-collante[data-etat="rupture"] .c-barre-collante__bouton {
  background: none; border-color: var(--fg-1); color: var(--fg-1);
}
.c-barre-collante[data-ancrage="flux"][data-etat="rupture"] .c-barre-collante__bouton {
  border-color: var(--fg-on-dark); color: var(--fg-on-dark);
}

.c-barre-collante[data-etat="chargement"] .c-barre-collante__prix {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs); min-width: 8ch;
}
.c-barre-collante[data-etat="chargement"] .c-barre-collante__bouton {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}

.c-barre-collante[data-etat="desactive"] .c-barre-collante__bouton {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}

.c-barre-collante[data-etat="succes"] .c-barre-collante__bouton {
  background: var(--bg-intelligence); color: var(--fg-data);
}

/* Long — le nom déborde : il se tronque, le prix garde sa ligne. */
.c-barre-collante[data-etat="long"] .c-barre-collante__nom { max-width: 22ch; }


/* ═══ lib/components/barre-livraison.css ═══ */
/* FICHE 35 — barre de livraison. Le rail fait 6 px : c'est la valeur mesurée
   chez Gymshark, et les 2 px de Ridge sont trop fins pour lire un remplissage
   partiel au pouce. */

.c-barre-livraison {
  --remplissage: 0%;         /* défaut local : sans lui, une instance qui oublie
                                sa valeur rendrait un rail à largeur initiale,
                                donc un remplissage arbitraire. */
  display: grid; gap: var(--sp-2);
  padding-block: var(--sp-3);
}

.c-barre-livraison__message {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-body-sm); line-height: var(--lh-snug); color: var(--fg-1);
}
/* Le montant restant est en GRAISSE dans la phrase, jamais en couleur d'alerte :
   ce n'est ni une alerte ni une validation, c'est une distance. */
.c-barre-livraison__message strong { font-weight: 600; }
/* La phrase est un item unique : elle se replie en lignes, elle ne se casse
   jamais en colonnes. L'icône y redevient EN LIGNE — le socle met tout `svg` en
   `display: block`, ce qui la mettrait seule sur sa ligne hors contexte flex. */
.c-barre-livraison__texte {
  display: block; flex: 1; min-width: 0;
  line-height: var(--lh-snug);
}
.c-barre-livraison__texte .i { display: inline-block; vertical-align: -0.18em; }
.c-barre-livraison__texte .prix { white-space: nowrap; }
.c-barre-livraison__detail { margin-left: auto; color: var(--fg-3); }
.c-barre-livraison__detail:hover { color: var(--fg-1); }

.c-barre-livraison__rail {
  height: 6px; border-radius: var(--r-pill);
  background: var(--c-mist-grey); overflow: hidden;
}
/* Ambre, jamais rouge ni vert. Le remplissage est le rapport exact du panier au
   seuil, plafonné à 100 % — la recette d'acceptation est celle qui a validé
   Gymshark : mesurer la largeur sur la capture et la comparer au rapport, écart
   toléré 1 point. */
.c-barre-livraison__remplissage {
  display: block; height: 100%;
  width: min(100%, var(--remplissage));
  border-radius: var(--r-pill);
  background: var(--c-soft-amber);
  transition: width var(--dur-md) var(--ease-standard);
}

.c-barre-livraison__bornes {
  display: flex; justify-content: space-between;
  font-size: var(--fs-caption); color: var(--fg-3);
}

.c-barre-livraison__date { color: var(--fg-2); }

/* Au franchissement, le rail RESTE et se remplit à 100 % : la disparition
   totale du bloc (Ridge) rend ~90 px mais retire la preuve au moment exact où
   elle rassure — et elle fait remonter tout le panier de 90 px d'un coup sous
   le doigt du client. Ici le bloc change de contenu, jamais de nature. */
.c-barre-livraison[data-franchi="oui"] .c-barre-livraison__remplissage { width: 100%; }
.c-barre-livraison[data-franchi="oui"] .c-barre-livraison__message { color: var(--fg-1); }


/* ═══ lib/components/bloc-achat.css ═══ */
/* FICHE 02 — bloc d'achat. Le conteneur n'a presque pas de style propre : il
   ordonne. Ce qui suit est donc surtout de la mise en séquence et deux ou trois
   décisions typographiques qui, elles, contredisent le réflexe. */

.c-bloc-achat { display: flex; flex-direction: column; gap: var(--sp-6); }

.c-bloc-achat__identite { display: grid; gap: var(--sp-2); order: 1; }

/* LE H1 EST PETIT. 20 px chez Dyson, 21 chez Litter-Robot. `base.css` le pose à
   --fs-display (52 px) pour un titre de page ; dans un bloc d'achat, c'est faux.
   Le nom du produit n'est jamais le plus grand caractère de son propre bloc. */
.c-bloc-achat__titre {
  font-size: var(--fs-h3);          /* 22 px — la mesure Litter-Robot */
  font-weight: 500; line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
}

/* …et la PROMESSE est plus grande que le H1. C'est le geste exact de Dyson :
   H1 20 px, promesse 28 px. Ce qu'on vend n'est pas un nom, c'est une phrase. */
.c-bloc-achat__promesse {
  font-family: var(--font-display);
  font-size: var(--fs-h2);          /* 28 px — la mesure Dyson */
  font-weight: 500; line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
  color: var(--fg-1);
}

.c-bloc-achat__chiffre  { display: grid; gap: var(--sp-2); order: 2; }
.c-bloc-achat__benefice { font-size: var(--fs-body); color: var(--fg-2);
                          line-height: var(--lh-relaxed); max-width: 46ch; order: 3; }
.c-bloc-achat__galerie  { order: 4; }
.c-bloc-achat__configure{ display: grid; gap: var(--sp-5); order: 5; }
.c-bloc-achat__achat    { display: grid; gap: var(--sp-3); order: 6; }
.c-bloc-achat__preuve   { display: grid; gap: var(--sp-5); order: 7; }

/* ---------- B · « spécifications d'abord » ----------------------------------
   La preuve technique (capacités + caractéristiques) remonte AVANT la
   configuration : l'acheteur qui veut les cotes ne descend pas deux écrans pour
   les trouver. La séquence identifier → chiffrer → justifier → configurer →
   acheter n'est pas inversée pour autant — seul le bloc de preuve change de
   place à l'intérieur du « justifier ».

   ⚠ DÉCISION À REMONTER AU FONDATEUR : ici l'ordre visuel est obtenu par
   `order`, donc l'ordre de tabulation reste celui du DOM (guidé). Au port
   Liquid, l'ordre des blocs est une donnée de l'éditeur : le DOM suivra
   réellement, et l'écart disparaît. Ne pas laisser cet écart survivre au port. */
.c-bloc-achat[data-variante="specs-abord"] .c-bloc-achat__preuve    { order: 5; }
.c-bloc-achat[data-variante="specs-abord"] .c-bloc-achat__configure { order: 6; }
.c-bloc-achat[data-variante="specs-abord"] .c-bloc-achat__achat     { order: 7; }

/* ---------- Au-delà du téléphone --------------------------------------------
   À 1440 px, deux colonnes : les médias à gauche, le bloc d'achat à droite
   (Litter-Robot 780 + colonne ; Eight Sleep 960 + 480). La galerie sort de la
   pile ; tout le reste garde son ordre. */
@media (min-width: 64rem) {
  .c-bloc-achat {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    column-gap: var(--sp-8); align-items: start;
  }
  .c-bloc-achat__galerie {
    grid-column: 1; grid-row: 1 / span 7; position: sticky; top: var(--sp-5);
  }
  .c-bloc-achat__identite,
  .c-bloc-achat__chiffre,
  .c-bloc-achat__benefice,
  .c-bloc-achat__configure,
  .c-bloc-achat__achat,
  .c-bloc-achat__preuve { grid-column: 2; }
}


/* ═══ lib/components/bouton-achat.css ═══ */
/* FICHE 08 — bouton d'achat.
   Géométrie : pleine largeur de la colonne, hauteur 56 px (Dyson : 352×56 ; le
   plancher tactile est à 44). Le rayon suit le système Catharsis (`.b` est en
   pastille), et non les angles vifs de Dyson. */

.c-bouton-achat { display: grid; gap: var(--sp-3); }

.c-bouton-achat__rangee { display: flex; align-items: stretch; gap: var(--sp-3); }

.c-bouton-achat__action {
  min-height: var(--sp-7);          /* 48 px de plancher, 56 px atteints par le padding */
  padding-block: var(--sp-3);
  font-size: var(--fs-body); font-weight: 500;
}

/* Variante « avec quantité » : le bouton partage la rangée. C'est la seule
   configuration où il n'est pas pleine largeur — celle des consommables, où la
   quantité est une vraie décision (Litter-Robot : bouton 231×44 + sélecteur). */
.c-bouton-achat__quantite { display: none; }
.c-bouton-achat[data-variante="avec-quantite"] .c-bouton-achat__quantite { display: inline-flex; }

/* Les libellés d'état vivent dans le MÊME bouton, à la MÊME place : la page ne
   change pas de forme, elle change de verbe. */
.c-bouton-achat__libelle-rupture,
.c-bouton-achat__libelle-chargement,
.c-bouton-achat__libelle-succes { display: none; }
.c-bouton-achat__libelle-succes { align-items: center; gap: var(--sp-2); }

.c-bouton-achat__prevenir { display: none; gap: var(--sp-2); }
.c-bouton-achat__prevenir-rangee { display: flex; gap: var(--sp-2); }
.c-bouton-achat__prevenir-rangee .saisie { flex: 1; }

.c-bouton-achat__erreur { display: none; }

/* ---------- Les états -------------------------------------------------------- */

/* Rupture — même taille, même place, le verbe change. Le bouton passe en
   contour : il reste l'action principale de la page, mais il ne promet plus un
   achat immédiat. */
.c-bouton-achat[data-etat="rupture"] .c-bouton-achat__action {
  background: none; border-color: var(--fg-1); color: var(--fg-1);
}
.c-bouton-achat[data-etat="rupture"] .c-bouton-achat__libelle { display: none; }
.c-bouton-achat[data-etat="rupture"] .c-bouton-achat__libelle-rupture { display: inline; }
.c-bouton-achat[data-etat="rupture"] .c-bouton-achat__prevenir { display: grid; }
.c-bouton-achat[data-etat="rupture"] .c-bouton-achat__quantite { display: none; }

/* Chargement — désactivé et `aria-busy` ; la largeur ne bouge pas, donc le
   pouce ne perd pas sa cible entre deux frappes. */
.c-bouton-achat[data-etat="chargement"] .c-bouton-achat__action {
  background: var(--bg-subtle); color: var(--fg-3); cursor: not-allowed;
}
.c-bouton-achat[data-etat="chargement"] .c-bouton-achat__libelle { display: none; }
.c-bouton-achat[data-etat="chargement"] .c-bouton-achat__libelle-chargement { display: inline; }

/* Succès — 2 s puis retour, SANS déplacer quoi que ce soit. */
.c-bouton-achat[data-etat="succes"] .c-bouton-achat__libelle { display: none; }
.c-bouton-achat[data-etat="succes"] .c-bouton-achat__libelle-succes { display: inline-flex; }

/* Erreur d'ajout — elle DIT ce qui a échoué et laisse le bouton actif : une
   erreur d'ajout au panier dont la reprise est impossible est un abandon. */
.c-bouton-achat[data-etat="erreur"] .c-bouton-achat__erreur { display: flex; }

.c-bouton-achat[data-etat="desactive"] .c-bouton-achat__action {
  background: var(--bg-subtle); color: var(--fg-3); cursor: not-allowed;
}
.c-bouton-achat[data-etat="desactive"] .c-bouton-achat__quantite { opacity: .6;
                                                                   pointer-events: none; }

/* Long — un libellé long ne doit pas casser la hauteur du bouton ni déborder. */
.c-bouton-achat[data-etat="long"] .c-bouton-achat__action { line-height: var(--lh-snug); }


/* ═══ lib/components/caisse-marque.css ═══ */
/* FICHE 46 — caisse de marque.
   Références mesurées à 390 px : ridge.com (caisse Shopify une page),
   ouraring.com/fr (caisse maison, accordéon en trois temps), whisker.com/eu-fr
   (deux étapes numérotées, la seule des trois qui chiffre la TVA).

   Rappel de cadrage : ce fichier n'est PAS un gabarit de caisse. La caisse
   n'existe pas dans le thème (`checkout.liquid` = Shopify Plus). C'est la
   maquette annotée de l'écran que produit l'éditeur de caisse, plus la seule
   pièce réellement portable : la rangée de marques de paiement. */

/* ---------- L'annotation de palier — chrome de maquette ---------------------- */
.c-caisse-marque__palier code {
  font-family: var(--font-data); font-size: var(--fs-caption);
  overflow-wrap: anywhere;
}
.c-caisse-marque__palier {
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
  border-left: 3px solid var(--fg-accent);
  background: var(--bg-warm); color: var(--fg-1);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
  border-radius: var(--r-xs);
}

/* ---------- L'écran ---------------------------------------------------------- */
.c-caisse-marque__ecran {
  display: grid; gap: var(--sp-5);
  padding: var(--sp-4);
  background: var(--bg-page);
  border: 1px solid var(--line-hair); border-radius: var(--r-lg);
}

.c-caisse-marque__entete {
  display: grid; gap: var(--sp-2); justify-items: center; text-align: center;
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--line-hair);
}
.c-caisse-marque__marque-nom {
  font-family: var(--font-display); font-size: var(--fs-h3);
  letter-spacing: var(--tr-eyebrow);
}
.c-caisse-marque__promesse { color: var(--fg-2); }

/* ---------- La barre de récapitulatif ----------------------------------------
   Repliée par défaut sur mobile — les trois références le font — mais le TOTAL
   reste visible. C'est le seul chiffre qu'on ne cache jamais : le cacher oblige
   le visiteur à déplier pour savoir ce qu'il va payer. */
.c-caisse-marque__recap-barre {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-subtle); border-radius: var(--r-sm);
}
.c-caisse-marque__recap-bouton {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-vise); padding: 0;
  border: 0; background: none; color: inherit; cursor: pointer;
  font-size: var(--fs-body-sm); text-align: left;
}
.c-caisse-marque__recap-total { font-weight: 600; }

.c-caisse-marque__recap { display: grid; gap: var(--sp-4); }

/* La ligne d'article : vignette, badge de quantité, texte, prix. */
.c-caisse-marque__ligne {
  display: grid; grid-template-columns: 56px 1fr auto; gap: var(--sp-3);
  align-items: start;
}
.c-caisse-marque__vignette { position: relative; display: block; }
/* ⚠ Relevé par la sonde le 2026-08-22 : 47 px « définitivement inaccessibles »
   dans chaque vignette. Le panneau de marque (`visuel-marque`) porte le nom du
   produit en corps de titre — parfait à 300 px, débordant à 56 px, et `.media`
   le rogne. À cette taille le mot serait de toute façon illisible, et le nom du
   produit est écrit en toutes lettres juste à côté : on garde le motif dessiné,
   on retire le mot. La règle vit ici et non dans `visuel-marque.css`, qui est un
   fichier partagé — c'est le contexte qui décide, pas la primitive. */
.c-caisse-marque__vignette .c-visuel-marque__nom { display: none; }
.c-caisse-marque__quantite {
  position: absolute; top: calc(var(--sp-2) * -1); right: calc(var(--sp-2) * -1);
  min-width: var(--sp-5); justify-content: center;
  padding: 0 var(--sp-1);
  background: var(--bg-inverse); color: var(--fg-on-dark);
}
.c-caisse-marque__ligne-texte { display: grid; gap: var(--sp-1); min-width: 0; }
.c-caisse-marque__ligne-nom { font-size: var(--fs-body-sm); font-weight: 500; }
.c-caisse-marque__ligne-prix { font-size: var(--fs-body-sm); white-space: nowrap; }

/* Le code promo. Le bouton est inerte tant que le champ est vide. */
.c-caisse-marque__promo { display: flex; gap: var(--sp-2); align-items: stretch; }
.c-caisse-marque__promo .saisie { flex: 1; min-width: 0; }

/* ---------- Les quatre lignes de TVA -----------------------------------------
   Règle 7. Le total TTC est en gras AU-DESSUS d'un filet : c'est le seul
   chiffre que le client cherche, il ne se noie pas dans la pile. */
.c-caisse-marque__totaux { display: grid; gap: var(--sp-2); }
.c-caisse-marque__total-ligne {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--fs-body-sm); color: var(--fg-2);
}
.c-caisse-marque__total-ligne--fort {
  padding-top: var(--sp-3); margin-top: var(--sp-1);
  border-top: 1px solid var(--line-rule);
  font-size: var(--fs-body); font-weight: 600; color: var(--fg-1);
}

/* Ossature de chargement — la FORME des quatre lignes, pas un tourniquet. */
.c-caisse-marque__ossature { display: none; gap: var(--sp-2); }
.c-caisse-marque__ossature > .ossature { height: 14px; }
.c-caisse-marque__ossature > .ossature:last-child { height: 22px; }

/* ---------- Paiement express -------------------------------------------------- */
.c-caisse-marque__express { display: grid; gap: var(--sp-3); }
.c-caisse-marque__express-titre { text-align: center; }
.c-caisse-marque__express-actions { display: grid; gap: var(--sp-2); }
.c-caisse-marque__express-bouton { min-height: var(--tap-vise); }

/* Le séparateur « ou ». Un filet coupé par un mot, jamais un mot posé sur un
   filet : le trait doit s'arrêter, sinon il barre le texte. */
.c-caisse-marque__ou {
  display: flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fs-caption); color: var(--fg-3);
}
.c-caisse-marque__ou::before,
.c-caisse-marque__ou::after {
  content: ""; flex: 1; height: 1px; background: var(--line-hair);
}

/* ---------- Les blocs de formulaire ------------------------------------------ */
.c-caisse-marque__bloc {
  display: grid; gap: var(--sp-4);
  border: 0; padding: 0; margin: 0; min-width: 0;
}
.c-caisse-marque__bloc-titre {
  padding: 0; font-family: var(--font-display); font-size: var(--fs-h3);
  line-height: var(--lh-snug);
}
.c-caisse-marque__paire { display: grid; gap: var(--sp-4); }

.c-caisse-marque__optin-texte { font-size: var(--fs-caption); color: var(--fg-2); }

.c-caisse-marque__securite { display: flex; align-items: center; gap: var(--sp-2); }

/* L'état vide du mode de livraison : la section existe AVANT d'avoir quelque
   chose à montrer, et elle explique son vide. Pas un bloc absent, pas un tiret. */
.c-caisse-marque__vide {
  padding: var(--sp-5) var(--sp-4);
  border: 1px dashed var(--line-rule); border-radius: var(--r-sm);
  font-size: var(--fs-body-sm);
}

/* ═══════════════════════════════════════════════════════════════════════════
   LA RANGÉE DE MARQUES — le correctif du constat B2
   ═══════════════════════════════════════════════════════════════════════════
   Sept marques, sept FORMES. Ce qui les distingue n'est ni la couleur (la
   porte 2 l'interdit, et de toute façon elle ne survit ni au mode sombre, ni à
   l'impression, ni au daltonisme) ni une pastille générique répétée : c'est le
   dessin. Deux monogrammes tracés, un « G » cerclé, quatre mots-symboles dont
   chacun a son propre traitement typographique.

   Le défaut d'origine : `snippets/payment-badges.liquid` calculait un libellé
   par méthode puis rendait `{% render 'icon', name: 'credit-card' %}` pour
   TOUTES — six pictogrammes strictement identiques. Le correctif dort déjà dans
   le dépôt à `sections/footer.liquid:90` (`payment_type_svg_tag`). */
.c-caisse-marque__marques {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  margin: 0; padding: 0; list-style: none;
}
.c-caisse-marque__marque {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  height: 26px; padding: 0 var(--sp-2);
  border: 1px solid var(--line-hair); border-radius: var(--r-xs);
  background: var(--bg-surface); color: var(--fg-1);
}
.c-caisse-marque__glyphe { width: 30px; height: 19px; flex: none; }
.c-caisse-marque__glyphe--etroit { width: 17px; }

.c-caisse-marque__mot {
  font-family: var(--font-ui); font-size: var(--fs-caption);
  font-weight: 700; line-height: 1;
}

/* Chaque marque a SON traitement. C'est ce qui rend la rangée lisible d'un
   coup d'œil sans une seule couleur de marque. */
.c-caisse-marque__marque[data-marque="visa"] .c-caisse-marque__mot {
  font-style: italic; letter-spacing: var(--tr-eyebrow);
}
.c-caisse-marque__marque[data-marque="cb"] .c-caisse-marque__mot {
  letter-spacing: var(--tr-tight); font-size: var(--fs-body-sm);
}
/* American Express : le mot en réserve sur un bloc d'encre — la seule pastille
   inversée de la rangée, donc reconnaissable avant d'être lue. */
.c-caisse-marque__marque[data-marque="amex"] {
  background: var(--bg-inverse); border-color: transparent; color: var(--fg-on-dark);
}
.c-caisse-marque__marque[data-marque="apple"] .c-caisse-marque__mot {
  font-weight: 500; letter-spacing: var(--tr-tight);
}
.c-caisse-marque__marque[data-marque="google"] .c-caisse-marque__mot { font-weight: 500; }
.c-caisse-marque__marque[data-marque="mastercard"],
.c-caisse-marque__marque[data-marque="paypal"] { padding-inline: var(--sp-1); }

/* Les mêmes mots-symboles servent aux boutons express ; ils y héritent de la
   couleur du bouton, donc rien à redéclarer. */
.c-caisse-marque__express-bouton .c-caisse-marque__mot { font-size: var(--fs-body-sm); }

/* ---------- Les champs de carte ---------------------------------------------- */
.c-caisse-marque__carte { display: grid; gap: var(--sp-4); }
.c-caisse-marque__saisie-cadenas { position: relative; display: block; }
.c-caisse-marque__saisie-cadenas .saisie { padding-right: var(--sp-7); }
.c-caisse-marque__saisie-cadenas .i {
  position: absolute; top: 50%; right: var(--sp-4); translate: 0 -50%;
  color: var(--fg-3); pointer-events: none;
}

.c-caisse-marque__payer { font-weight: 600; }
.c-caisse-marque__payer .prix { font-weight: 600; }

/* ---------- Le pied : trois liens, et rien d'autre ---------------------------- */
.c-caisse-marque__pied {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line-hair);
  font-size: var(--fs-caption);
}
.c-caisse-marque__pied a { color: var(--fg-2); }

/* ═══════════════ Les états ═══════════════ */
.c-caisse-marque__erreur,
.c-caisse-marque__rupture,
.c-caisse-marque__succes { display: none; }

/* Chargement — le récapitulatif se recalcule. Les chiffres cèdent la place à
   une ossature de MÊME forme : la page ne saute pas quand ils arrivent. */
.c-caisse-marque[data-etat="chargement"] .c-caisse-marque__ossature { display: grid; }
.c-caisse-marque[data-etat="chargement"] .c-caisse-marque__totaux,
.c-caisse-marque[data-etat="chargement"] .c-caisse-marque__recap-total { display: none; }
.c-caisse-marque[data-etat="chargement"] .c-caisse-marque__payer {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}

/* Erreur — paiement refusé. Le bouton reste actif : le client doit pouvoir
   réessayer sans recharger la page. */
.c-caisse-marque[data-etat="erreur"] .c-caisse-marque__erreur { display: flex; }

/* Rupture — l'article est devenu indisponible pendant le tunnel. */
.c-caisse-marque[data-etat="rupture"] .c-caisse-marque__rupture { display: flex; }
.c-caisse-marque[data-etat="rupture"] .c-caisse-marque__payer {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}

/* Succès — la main passe à Shopify (fiche 47). */
.c-caisse-marque[data-etat="succes"] .c-caisse-marque__succes {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-body-sm); color: var(--fg-data);
}

/* Long — un nom d'article de 90 caractères et une variante à rallonge. Rien ne
   se tronque : la colonne de texte peut passer à la ligne, le prix ne bouge
   pas. C'est le cas qui casse toutes les caisses à 390 px. */
.c-caisse-marque[data-etat="long"] .c-caisse-marque__ligne-nom { overflow-wrap: anywhere; }

/* Désactivé — le paiement express est éteint dans l'administration. La section
   ne rend rien ; elle ne rend pas « vide ». */
.c-caisse-marque[data-etat="desactive"] .c-caisse-marque__express { display: none; }

/* ---------- Variante « marques » ---------------------------------------------
   La rangée seule. C'est la SEULE partie de cette fiche qui vit réellement dans
   le thème — pied de page, fiche produit, panier — et c'est celle qui portait
   le défaut. Elle se juge donc isolément, à sa taille réelle. */
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__ecran {
  padding: var(--sp-4); background: var(--bg-surface);
}
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__entete,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__recap-barre,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__recap,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__express,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__carte,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__securite,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__bloc-titre,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__payer,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__pied,
.c-caisse-marque[data-variante="marques"] .c-caisse-marque__bloc:not(.c-caisse-marque__bloc--paiement) { display: none; }

/* ---------- Au-delà du téléphone --------------------------------------------- */
@media (min-width: 48rem) {
  .c-caisse-marque__paire { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .c-caisse-marque__express-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


/* ═══ lib/components/carte-produit.css ═══ */
/* FICHE 28 — carte produit. Mesures transposées de whisker.com (style A),
   gymshark.com / cutsclothing.com (B), ridge.com (C). */

.c-carte-produit { display: grid; gap: var(--sp-3); align-content: start; }

/* ---------- La tuile ---------------------------------------------------------
   Ratio 5:6 et fond TEINTÉ, jamais la couleur de page : Whisker pose une tuile
   `rgb(243,240,230)` sur une page `rgb(253,250,243)` — six points d'écart, un
   écart lisible sans être un cadre. Une tuile en couleur de page disparaît. */
.c-carte-produit__tuile {
  position: relative; aspect-ratio: 5 / 6; overflow: hidden;
  border-radius: var(--r-lg); background: var(--bg-subtle);
}

/* Le produit est ENCASTRÉ, jamais rogné. Whisker laisse 26 px de marge autour
   d'un produit de 124 px dans une tuile de 176. Le rognage `cover` de Gymshark
   ne marche que parce que ses visuels sont des corps en pied ; un bac à litière
   rogné devient un cylindre gris. */
.c-carte-produit__photo {
  width: 100%; height: 100%; object-fit: contain; padding: var(--sp-5);
}
.c-carte-produit__photo:not([src]), .c-carte-produit__photo[src=""] { display: none; }
/* Quand la photo existe, le panneau de marque s'efface. */
.c-carte-produit__photo[src]:not([src=""]) ~ .c-visuel-marque { display: none; }
.c-carte-produit .c-visuel-marque { position: absolute; inset: 0; border-radius: 0; }

.c-carte-produit__etiquette {
  position: absolute; top: var(--sp-3); left: var(--sp-3);
  background: var(--bg-surface); color: var(--fg-1); box-shadow: var(--sh-hair);
}
.c-carte-produit__etiquette:empty { display: none; }

/* ---------- Le texte ---------------------------------------------------------
   Le titre réserve DEUX lignes et la hauteur est fixée : sinon deux cartes
   voisines ne s'alignent plus. C'est le piège de la fiche — Gymshark tronque à
   une ligne et la seconde ligne mord sur la ligne d'en dessous. À 185 px de
   large, un nom de produit français dépasse presque toujours : « Sacs de
   déchets — recharge 3 mois » fait 34 caractères. */
.c-carte-produit__titre {
  font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 500;
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
  min-height: calc(var(--fs-h3) * var(--lh-snug) * 2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.c-carte-produit__titre a { color: inherit; text-decoration: none; }
/* La cible tactile du titre couvre TOUTE la carte — c'est le geste réel sur
   téléphone. Le bouton d'ajout, lui, remonte au-dessus. */
.c-carte-produit__titre a::after { content: ""; position: absolute; inset: 0; }
.c-carte-produit { position: relative; }

/* Le descripteur est OBLIGATOIRE en style A : c'est la ligne qui répond à « en
   quoi celui-ci diffère de celui d'à côté ». Sur trois produits hétérogènes
   (un appareil, une cartouche, des sacs), c'est elle qui empêche la grille de
   ressembler à un inventaire. */
.c-carte-produit__descripteur {
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
  min-height: calc(var(--fs-body-sm) * var(--lh-normal) * 2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Le prix n'est PAS le plus gros caractère de la carte (cohérent fiche 04). */
.c-carte-produit__prix { font-size: var(--fs-body); color: var(--fg-1); }

.c-carte-produit__coloris:empty,
.c-carte-produit__descripteur:empty,
.c-carte-produit__variantes:empty { display: none; }

/* ---------- Ce qui n'appartient qu'à certaines variantes -------------------- */
.c-carte-produit__ajout,
.c-carte-produit__acheter,
.c-carte-produit__rupture { display: none; }
.c-carte-produit__ajout { position: absolute; z-index: 1; left: var(--sp-2); right: var(--sp-2);
                          bottom: var(--sp-2); width: auto; }

/* B · Catalogue — visuel plein cadre 4:5, rogné, plus un ajout rapide. */
.c-carte-produit[data-variante="catalogue"] .c-carte-produit__tuile { aspect-ratio: 4 / 5; border-radius: var(--r-sm); }
.c-carte-produit[data-variante="catalogue"] .c-carte-produit__photo { object-fit: cover; padding: 0; }
.c-carte-produit[data-variante="catalogue"] .c-carte-produit__titre {
  font-family: var(--font-ui); font-size: var(--fs-body-sm); letter-spacing: var(--tr-normal);
  min-height: calc(var(--fs-body-sm) * var(--lh-snug) * 2);
}
.c-carte-produit[data-variante="catalogue"] .c-carte-produit__descripteur { display: none; }
.c-carte-produit[data-variante="catalogue"] .c-carte-produit__ajout { display: inline-flex; }

/* C · Bloc d'achat — la carte devient un objet bordé qui vend seul. */
.c-carte-produit[data-variante="achat"] {
  background: var(--bg-surface); border: 1px solid var(--line-hair);
  border-radius: var(--r-card); padding: var(--sp-4); gap: var(--sp-2);
}
.c-carte-produit[data-variante="achat"] .c-carte-produit__tuile { aspect-ratio: 4 / 5; }
.c-carte-produit[data-variante="achat"] .c-carte-produit__titre {
  font-size: var(--fs-body-lg);
  min-height: calc(var(--fs-body-lg) * var(--lh-snug) * 2);
}
.c-carte-produit[data-variante="achat"] .c-carte-produit__descripteur { display: none; }
.c-carte-produit[data-variante="achat"] { height: 100%; grid-template-rows: auto 1fr; }
.c-carte-produit[data-variante="achat"] .c-carte-produit__texte {
  display: flex; flex-direction: column; gap: var(--sp-1);
}
/* Vu à l'écran le 2026-08-22 : la carte « + 3 parfums » portait une ligne de
   plus que sa voisine, donc les deux boutons « Ajouter au panier » se
   retrouvaient à deux hauteurs différentes — une grille d'actions en escalier.
   L'action se colle au bas de la carte, jamais à la fin du texte. */
.c-carte-produit[data-variante="achat"] .c-carte-produit__acheter {
  display: inline-flex; margin-top: auto; padding-top: var(--sp-1);
}
/* Le bouton est AU-DESSUS de la cible pleine carte, sinon il n'est jamais cliqué. */
.c-carte-produit[data-variante="achat"] .c-carte-produit__acheter { position: relative; z-index: 1; }

/* ---------- Les états -------------------------------------------------------- */

/* Rupture. Le prix reste lisible et la carte reste ouvrable : jamais un mur. */
.c-carte-produit[data-etat="rupture"] .c-carte-produit__tuile { opacity: .55; }
.c-carte-produit[data-etat="rupture"] .c-carte-produit__rupture { display: block; color: var(--fg-alert); }
.c-carte-produit[data-etat="rupture"] .c-carte-produit__ajout,
.c-carte-produit[data-etat="rupture"] .c-carte-produit__acheter { display: none; }

/* Chargement. Une ossature qui a la FORME du contenu attendu — la carte ne
   change pas de hauteur quand le contenu arrive, donc la grille ne saute pas.
   C'est aussi la contre-mesure au défaut le plus grave de la fiche : une grille
   qui se peuple côté client peut se rendre VIDE en gardant sa mise en page, et
   un harnais annonce alors « ok, 4 bandes ». Ici l'ossature est explicite. */
.c-carte-produit[data-etat="chargement"] .c-carte-produit__tuile,
.c-carte-produit[data-etat="chargement"] .c-carte-produit__titre,
.c-carte-produit[data-etat="chargement"] .c-carte-produit__descripteur,
.c-carte-produit[data-etat="chargement"] .c-carte-produit__prix {
  background-color: var(--bg-subtle); color: transparent;
  position: relative; overflow: hidden;
}
/* L'ossature garde le RAYON de la tuile réelle. Vu à l'écran le 2026-08-22 :
   avec un rayon uniforme, la tuile d'attente avait des angles vifs quand la
   vraie tuile est arrondie — l'ossature n'avait plus la forme du contenu
   attendu, ce qui est précisément sa seule fonction. */
.c-carte-produit[data-etat="chargement"] .c-carte-produit__tuile { border-radius: var(--r-lg); }
.c-carte-produit[data-etat="chargement"][data-variante="catalogue"] .c-carte-produit__tuile,
.c-carte-produit[data-etat="chargement"][data-variante="achat"] .c-carte-produit__tuile {
  border-radius: var(--r-sm);
}
.c-carte-produit[data-etat="chargement"] .c-carte-produit__titre,
.c-carte-produit[data-etat="chargement"] .c-carte-produit__descripteur,
.c-carte-produit[data-etat="chargement"] .c-carte-produit__prix { border-radius: var(--r-xs); }
/* Le prix seul ne fait pas la largeur d'une ligne : sans cette limite,
   l'ossature du prix s'étend sur toute la carte et annonce un contenu qui
   n'arrivera pas. */
.c-carte-produit[data-etat="chargement"] .c-carte-produit__prix { max-width: 40%; }
.c-carte-produit[data-etat="chargement"] .c-carte-produit__tuile > * { visibility: hidden; }
/* Le balayage est un FOND ANIMÉ : un `::after` translaté crée un débordement
   défilable réel à l'intérieur d'un `overflow: hidden`, et la sonde le remonte
   à juste titre comme « contenu rogné ». Voir la note dans primitives.css. */
.c-carte-produit[data-etat="chargement"] .c-carte-produit__titre,
.c-carte-produit[data-etat="chargement"] .c-carte-produit__tuile {
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-carte-produit[data-etat="chargement"] .c-carte-produit__titre,
  .c-carte-produit[data-etat="chargement"] .c-carte-produit__tuile { animation: none; }
}

/* Désactivé — le produit existe mais n'est pas commandable (précommande close). */
.c-carte-produit[data-etat="desactive"] { opacity: .6; }
.c-carte-produit[data-etat="desactive"] .c-carte-produit__titre a::after { content: none; }


/* ═══ lib/components/comparatif-condense.css ═══ */
/* FICHE 18 — comparatif condensé.
   ---------------------------------------------------------------------------
   ⚠ IL N'Y A AUCUN `overflow-x` DANS CE FICHIER, ET C'EST LE SUJET.
   Le défaut d'origine du thème est `blocks/product-comparison.liquid:71` : un
   `min-width: 480px` posé sur le conteneur d'un `overflow-x: auto` large de
   350 px — la page entière débordait. La leçon n'est pas « mettre la largeur
   sur les enfants » ici : c'est qu'à 390 px un comparatif ne défile PAS. Il se
   réduit à deux colonnes et le visiteur choisit lesquelles. Mesure à l'appui :
   zéro conteneur scrollable sur toute la page de comparaison d'Apple, contre
   107 px hors écran et sans indice chez B&O.

   Pas de grille, pas de bordure verticale, pas de fond alterné : l'alignement
   ligne à ligne suffit, et c'est ce qui fait lire le bloc comme un objet
   éditorial plutôt que comme un tableur. */

.c-comparatif-condense { display: grid; gap: var(--sp-4); }
.c-comparatif-condense__titre { font-size: var(--fs-h2); letter-spacing: var(--tr-tight); }

/* ---------- La barre de sélection collante ---------------------------------
   Mesurée chez Apple : sticky, top -1px, hauteur 60 px, z-index 10, deux
   `<select>` de 159 × 48. Ici `top: 0` et la hauteur vient du plancher tactile
   plus le rembourrage — 44 + 2×8 = 60. Deux `<select>` NATIFS. */
.c-comparatif-condense__barre {
  position: sticky; top: 0; z-index: 10;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-0);
  background: var(--bg-page);
}
.c-comparatif-condense__choix { display: block; }
.c-comparatif-condense__select {
  width: 100%; min-height: var(--tap-vise);
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-surface); color: var(--fg-1);
  border: 1px solid var(--line-rule); border-radius: var(--r-sm);
  font-size: var(--fs-body-sm);
}

/* ---------- Les en-têtes de colonne ------------------------------------------
   Chaque colonne porte son prix ET sa CTA. Un comparatif sans chemin d'achat
   par colonne renvoie le visiteur en arrière. */
.c-comparatif-condense__entetes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3); align-items: end;
  padding-bottom: var(--sp-4);
}
.c-comparatif-condense__entete { display: grid; gap: var(--sp-2); text-align: center; }
.c-comparatif-condense__nom {
  font-family: var(--font-display); font-size: var(--fs-h3); line-height: var(--lh-snug);
}
/* Le € rend comme un « C » en Cormorant : le prix reste en police de labeur. */
.c-comparatif-condense__prix { font-size: var(--fs-body); color: var(--fg-2); }

/* ---------- Les rangées -------------------------------------------------------
   Le LIBELLÉ au-dessus de la rangée, en petites capitales espacées — modèle
   B&O. Apple s'en passe parce que ses valeurs sont auto-descriptives (« GPS »,
   « Puce S10 ») ; « 4,2 L » ou « IP54 » ne le sont pas. On n'emprunte à Apple
   que la réduction à deux colonnes. */
.c-comparatif-condense__lignes { display: grid; }
.c-comparatif-condense__ligne {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2) var(--sp-4);
  padding-block: var(--sp-4);
  border-top: 1px solid var(--line-hair);
}
.c-comparatif-condense__libelle {
  grid-column: 1 / -1;
  font-family: var(--font-ui); font-size: var(--fs-caption); font-weight: 500;
  letter-spacing: var(--tr-eyebrow); text-transform: uppercase; color: var(--fg-3);
}
.c-comparatif-condense__libelle sup { letter-spacing: var(--tr-normal); }
.c-comparatif-condense__valeur {
  font-size: var(--fs-body-sm); font-weight: 500; line-height: var(--lh-normal);
  text-align: center; color: var(--fg-1);
  display: grid; gap: var(--sp-1); align-content: start;
}
/* La nuance honnête de B&O : quand la valeur gagnante a une limite, elle est
   écrite DANS la cellule. C'est le motif qui rend un comparatif crédible
   plutôt que promotionnel — et il ne coûte qu'une ligne de gris. */
.c-comparatif-condense__nuance {
  font-size: var(--fs-caption); font-weight: 400; color: var(--fg-3);
  line-height: var(--lh-normal);
}

.c-comparatif-condense__complet { margin-top: var(--sp-2); }
.c-comparatif-condense__note { max-width: 44ch; }

/* La troisième colonne n'existe pas sous 700 px. Ce n'est pas une fatalité,
   c'est un réglage — et sa valeur par défaut sous 700 px est « absente ». */
.c-comparatif-condense__troisieme { display: none; }

/* ---------- Les variantes ----------------------------------------------------
   `mini` — la version courte posée sur une fiche produit (Apple, AirPods) :
   pas de barre de sélection, pas de prix par colonne, juste les valeurs et la
   porte de sortie. */
.c-comparatif-condense[data-variante="mini"] .c-comparatif-condense__barre { display: none; }
.c-comparatif-condense[data-variante="mini"] .c-comparatif-condense__prix,
.c-comparatif-condense[data-variante="mini"] .c-comparatif-condense__entete .b { display: none; }
.c-comparatif-condense[data-variante="mini"] .c-comparatif-condense__nom {
  font-family: var(--font-ui); font-size: var(--fs-body-sm);
}

/* ---------- Les états -------------------------------------------------------- */
.c-comparatif-condense__vide, .c-comparatif-condense__erreur { display: none; }

.c-comparatif-condense[data-etat="vide"] .c-comparatif-condense__barre,
.c-comparatif-condense[data-etat="vide"] .c-comparatif-condense__entetes,
.c-comparatif-condense[data-etat="vide"] .c-comparatif-condense__lignes,
.c-comparatif-condense[data-etat="vide"] .c-comparatif-condense__complet { display: none; }
.c-comparatif-condense[data-etat="vide"] .c-comparatif-condense__vide {
  display: grid; border: 1px dashed var(--line-rule); border-radius: var(--r-card);
}

.c-comparatif-condense[data-etat="chargement"] .c-comparatif-condense__valeur {
  color: transparent; position: relative;
}
.c-comparatif-condense[data-etat="chargement"] .c-comparatif-condense__valeur::before {
  content: ""; display: block; height: var(--sp-4); width: 70%;
  margin-inline: auto; border-radius: var(--r-xs); background: var(--bg-subtle);
}
.c-comparatif-condense[data-etat="chargement"] .c-comparatif-condense__complet { visibility: hidden; }

.c-comparatif-condense[data-etat="erreur"] .c-comparatif-condense__erreur { display: flex; }
.c-comparatif-condense[data-etat="erreur"] .c-comparatif-condense__lignes { display: none; }

/* long — une valeur qui prend trois lignes et une nuance de 140 caractères. Les
   deux colonnes restent alignées ligne à ligne : c'est tout ce qui tient ce
   comparatif debout, puisqu'il n'a ni bordure ni fond alterné pour le faire. */
.c-comparatif-condense[data-etat="long"] .c-comparatif-condense__valeur { text-align: left; }
.c-comparatif-condense[data-etat="long"] .c-comparatif-condense__ligne { gap: var(--sp-2) var(--sp-3); }

/* rupture — la colonne reste comparable, elle n'est plus commandable. */
.c-comparatif-condense[data-etat="rupture"] .c-comparatif-condense__entete:first-child .b {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}

/* succes — la sélection a été appliquée aux deux colonnes. */
.c-comparatif-condense[data-etat="succes"] .c-comparatif-condense__select {
  border-color: var(--fg-data);
}

/* desactive — le comparatif est visible mais figé (aperçu d'éditeur de thème). */
.c-comparatif-condense[data-etat="desactive"] .c-comparatif-condense__select,
.c-comparatif-condense[data-etat="desactive"] .c-comparatif-condense__entete .b {
  opacity: .5; pointer-events: none;
}

/* Au-dessus de 700 px, et pas avant, la troisième colonne a le droit d'exister. */
@media (min-width: 43.75rem) {
  .c-comparatif-condense[data-variante="trois"] .c-comparatif-condense__troisieme { display: grid; }
  .c-comparatif-condense[data-variante="trois"] .c-comparatif-condense__entetes,
  .c-comparatif-condense[data-variante="trois"] .c-comparatif-condense__ligne {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .c-comparatif-condense[data-variante="trois"] .c-comparatif-condense__barre { display: none; }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-comparatif-condense__erreur a { color: var(--fg-1); }


/* ═══ lib/components/en-tete-collection.css ═══ */
/* FICHE 31 — en-tête de collection. Cotes transposées de gymshark.com (le seul
   en-tête du corpus à tout avoir, relevé au pixel), whisker.com (les rayons),
   litter-robot.com (la bannière à panneau), avec ridge.com en contre-modèle.

   Le rythme vertical est DÉCROISSANT et c'est mesuré, pas approximé : 9 px
   entre sur-titre et H1, 26 px après le H1, 21 px avant « Voir plus », 67 px
   avant le premier composant de navigation. Les blancs s'ouvrent en descendant.
   Une grille régulière à 24 px partout donnerait une page plate. */

.c-en-tete-collection {
  /* La barre se gare immédiatement sous ce qui colle au-dessus d'elle, quelle
     qu'en soit la hauteur du moment : 87 px dans une session Gymshark, 120 px
     dans une autre, selon que le bandeau d'annonce est affiché. C'est le
     MÉCANISME qu'on copie, jamais le nombre. L'en-tête de site met ce jeton à
     jour ; en atelier il vaut 0. */
  --chrome-h: 0px;
  display: block;
}

/* Gouttière unique de 16 px sur tout l'en-tête — mesuré chez Gymshark : le
   sur-titre, le H1, la description et les liens commencent tous exactement à
   x = 16. Une seule gouttière par surface. */
.c-en-tete-collection__texte { padding-block: var(--sp-6) var(--sp-5); }

.c-en-tete-collection__rayon { display: block; margin-bottom: var(--sp-1); }
.c-en-tete-collection__titre {
  font-size: var(--fs-h1); line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
  margin-bottom: var(--sp-5);
}
.c-en-tete-collection__preuve { margin-bottom: var(--sp-4); }

/* La description : quatre lignes, puis « Voir plus ». Le texte long sert au
   référencement ; il ne doit pas repousser la grille de 300 px. La troncature
   est CSS — le texte complet reste dans le DOM, donc indexé. */
.c-en-tete-collection__desc {
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden;
}
.c-en-tete-collection[data-deplie="oui"] .c-en-tete-collection__desc {
  display: block; overflow: visible;
}
.c-en-tete-collection__plus {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  min-height: var(--tap-vise); margin-top: var(--sp-2);
  padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--fs-body-sm); color: var(--fg-1);
  text-decoration: underline; text-underline-offset: 0.25em;
}
.c-en-tete-collection[data-deplie="oui"] .c-en-tete-collection__plus-chevron { transform: rotate(180deg); }
.c-en-tete-collection[data-deplie="oui"] .c-en-tete-collection__plus-mot { visibility: hidden; }
.c-en-tete-collection[data-deplie="oui"] .c-en-tete-collection__plus::before {
  content: "Voir moins"; margin-right: calc(var(--sp-6) * -1);
}

/* 67 px avant le premier composant de navigation : le blanc le plus large de
   l'en-tête, et il est en bas. */
.c-en-tete-collection__freres {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-7);
}
.c-en-tete-collection__freres a {
  display: inline-flex; align-items: center; min-height: var(--tap-vise);
  font-size: var(--fs-body-sm); color: var(--fg-2);
  text-decoration: underline; text-underline-offset: 0.25em;
}
.c-en-tete-collection__freres a:hover { color: var(--fg-1); }

/* ---------- Les rayons -------------------------------------------------------
   Le modèle Whisker : quatre tuiles en défilement horizontal, la courante en
   fond plein et les autres au filet. C'est la navigation que quatre références
   réclament — un panneau de filtres sur quatre produits est un composant vide
   qui annonce un catalogue qu'on n'a pas.
   ⚠ La largeur se donne aux ENFANTS du rail, jamais au rail (piège B1). */
.c-en-tete-collection__rayons { padding-block: var(--sp-2) var(--sp-6); }
.c-en-tete-collection__rayon-tuile {
  display: grid; gap: var(--sp-2); width: 8rem;
  padding: var(--sp-3); border: 1px solid var(--line-hair);
  border-radius: var(--r-card); background: var(--bg-surface);
  color: var(--fg-1); text-decoration: none;
}
.c-en-tete-collection__rayon-tuile[aria-current="page"] {
  background: var(--bg-inverse); border-color: transparent; color: var(--fg-on-dark);
}
.c-en-tete-collection__vignette { display: block; }
.c-en-tete-collection__vignette .c-visuel-marque { border-radius: var(--r-sm); }
.c-en-tete-collection__vignette .c-visuel-marque__nom { display: none; }
.c-en-tete-collection__rayon-nom {
  font-family: var(--font-display); font-size: var(--fs-h3); line-height: var(--lh-snug);
}
/* Chaque tuile porte une PHRASE DE VENTE, pas seulement un nom : un nom seul
   n'aide pas à choisir. */
.c-en-tete-collection__rayon-phrase {
  font-size: var(--fs-caption); line-height: var(--lh-snug); color: var(--fg-2);
}
.c-en-tete-collection__rayon-tuile[aria-current="page"] .c-en-tete-collection__rayon-phrase { color: var(--bg-subtle); }

/* ---------- La barre de contrôle --------------------------------------------
   Trois cellules : Tri · n produits · Filtres. Le tri à GAUCHE, les filtres à
   DROITE, le compte au milieu — ordre de DOM identique à l'ordre visuel.
   Filet supérieur pleine largeur, PAS de filet inférieur (la barre est ouverte
   en bas), séparateurs verticaux INSET : 60 px dans une barre de 61, donc
   affleurants en haut et pas en bas.
   Ici 64 + 1 px, parce que le pas de la marque prime sur la copie au pixel. */
.c-en-tete-collection__barre {
  position: sticky; top: var(--chrome-h); z-index: 5;
  /* Les deux déclencheurs se dimensionnent à leur libellé, le compte prend ce
     qui reste. Trois colonnes ÉGALES donnaient 119 px chacune à 390 px d'écran,
     et « Filtres » + son badge + son chevron n'y tiennent pas : 50 px de
     contenu rognés, mesurés le 2026-08-22. Un tiers de la largeur n'est pas une
     mise en page, c'est une supposition. */
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  background: var(--bg-page);              /* OPAQUE — sinon une carte transparaît */
  border-top: 1px solid var(--line-hair);
  margin-inline: calc(var(--sp-4) * -1);
  padding-inline: var(--sp-4);
}
.c-en-tete-collection__cellule {
  position: relative; display: flex; align-items: center; justify-content: center;
  min-height: var(--sp-8);
}
.c-en-tete-collection__cellule + .c-en-tete-collection__cellule::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: var(--sp-1);
  width: 1px; background: var(--line-hair);
}
.c-en-tete-collection__compte {
  font-size: var(--fs-body-sm); color: var(--fg-2); text-align: center;
}

/* ---------- La bannière — variante, jamais le défaut ------------------------
   Modèle Litter-Robot : le texte dans un panneau plein À CÔTÉ de l'image,
   jamais posé dessus. Plafond dur : la bannière ne dépasse pas 240 px à 390 px
   de large, soit moins de 30 % du viewport — la grille doit être amorcée au
   premier écran. Les 379 px de Ridge en coûtent 45 %, et un visiteur venu d'une
   publicité y voit une image de marque, pas une offre. */
.c-en-tete-collection__banniere { display: none; }
.c-en-tete-collection[data-variante="banniere"] .c-en-tete-collection__banniere {
  display: grid; grid-template-columns: 1fr 1fr; align-items: stretch;
  max-height: 15rem; overflow: hidden;
  border-radius: var(--r-card); margin-top: var(--sp-5);
}
.c-en-tete-collection[data-variante="banniere"] .c-en-tete-collection__banniere-texte {
  display: grid; align-content: center; gap: var(--sp-2);
  padding: var(--sp-5); background: var(--bg-subtle);
}
.c-en-tete-collection[data-variante="banniere"] .c-en-tete-collection__titre { margin-bottom: 0; }
.c-en-tete-collection[data-variante="banniere"] .c-en-tete-collection__texte { padding-top: var(--sp-5); }
.c-en-tete-collection__banniere .c-visuel-marque { height: 100%; border-radius: 0; }

/* ---------- Les états -------------------------------------------------------- */
.c-en-tete-collection__vide,
.c-en-tete-collection__erreur,
.c-en-tete-collection__avis { display: none; }

.c-en-tete-collection[data-etat="vide"] .c-en-tete-collection__vide { display: grid; }
.c-en-tete-collection[data-etat="vide"] .c-en-tete-collection__desc,
.c-en-tete-collection[data-etat="vide"] .c-en-tete-collection__plus,
.c-en-tete-collection[data-etat="vide"] .c-en-tete-collection__rayons { display: none; }

.c-en-tete-collection[data-etat="erreur"] .c-en-tete-collection__erreur { display: flex; margin-top: var(--sp-4); }
.c-en-tete-collection[data-etat="erreur"] .c-en-tete-collection__desc { display: none; }

/* Rupture — toute la collection est en réapprovisionnement. La page reste
   consultable et l'information est donnée UNE fois, sans date : la promesse de
   livraison a une seule source de vérité dans la boutique. */
.c-en-tete-collection[data-etat="rupture"] .c-en-tete-collection__avis {
  display: inline-flex; margin-top: var(--sp-4);
}

/* Succès — le filtrage a abouti. Le compte est déjà en `role="status"` : il est
   annoncé sans qu'on ait à ajouter une seconde région vivante, qui se
   parlerait par-dessus la première. */
.c-en-tete-collection[data-etat="succes"] .c-en-tete-collection__compte { color: var(--fg-1); font-weight: 500; }

/* Chargement — l'ossature a la FORME du contenu attendu : le titre garde sa
   hauteur, la description ses quatre lignes, le compte sa largeur de nombre.
   La page ne saute pas à l'arrivée des données. */
.c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__titre,
.c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__preuve,
.c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__compte {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
  position: relative; overflow: hidden;
}
.c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__compte { max-width: 7rem; }
.c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__desc {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
}
.c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__titre::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-en-tete-collection[data-etat="chargement"] .c-en-tete-collection__titre::after { animation: none; }
}

/* Long — un nom de collection à rallonge et une description de douze lignes.
   Le titre passe à la ligne et ne se tronque JAMAIS : c'est le mot-clé de la
   page et le seul repère du visiteur arrivé par un lien externe. La description,
   elle, garde ses quatre lignes. */
.c-en-tete-collection[data-etat="long"] .c-en-tete-collection__titre {
  font-size: var(--fs-h2); overflow-wrap: break-word;
}

/* Désactivé — la barre est inerte (collection en aperçu, non publiée). Le
   compte reste lisible : c'est la seule information qui ne dépend d'aucune
   action. */
.c-en-tete-collection[data-etat="desactive"] .c-en-tete-collection__barre { opacity: .55; }
.c-en-tete-collection[data-etat="desactive"] .c-en-tete-collection__cellule { pointer-events: none; }

/* ---------- Au-delà du téléphone --------------------------------------------- */
@media (min-width: 48rem) {
  .c-en-tete-collection__rayon-tuile { width: 11rem; }
}


/* ⚠ LA GARDE QUI MANQUAIT — `min-width: 0` sur les cellules de la barre.
   Mesuré le 2026-08-22 : le bloc de filtres faisait **555 px dans une cellule
   de 119 px**, et la page entière défilait horizontalement (978 px pour 390).
   Ce n'est pas un défaut du composant de filtres : c'est le comportement par
   défaut d'un enfant de grille ou de flex, dont le `min-width: auto` l'empêche
   de descendre sous la largeur de son contenu. La rangée de puces avait beau
   porter `overflow-x: auto`, elle n'était jamais contrainte, donc elle ne
   défilait pas — elle poussait.

   C'est le voisin du piège B1, et il est plus sournois : B1 met un `min-width`
   explicite, ici personne n'écrit rien et le défaut vient du défaut. */
.c-en-tete-collection__cellule { min-width: 0; }
.c-en-tete-collection__cellule > * { max-width: 100%; }

/* Dernier verrou de la barre de contrôle. Les cellules sont étroites (119 px à
   390 px d'écran) et hébergent deux composants complets — tri et filtres — dont
   les panneaux sont en `position: fixed`. `overflow: clip` retient ce qui est
   posé en flux ou en absolu SANS toucher aux panneaux fixes, qui s'ouvrent par
   définition hors de la cellule.

   Sans lui, la page défilait encore de 35 px après les corrections du `.sr-only`
   et du débordement de rail — un reliquat qu'aucune règle de composant ne
   rattrapait, parce que la barre elle-même déborde de ±16 px de son en-tête. */
.c-en-tete-collection__cellule { overflow: clip; }

/* La rangée de puces de filtres actifs N'A PAS SA PLACE dans une cellule de
   barre : elle est pleine largeur par nature, et une cellule d'un tiers d'écran
   la force à défiler dans 119 px, ce qui la rend inutilisable. Dans ce
   contexte, le composant n'expose que son déclencheur ; les puces se rendent
   sous la barre, par une instance dédiée. */
.c-en-tete-collection__cellule .c-filtres__actifs { display: none; }
/* ⚠ La règle ci-dessus NE SUFFIT PAS et ne peut pas suffire : la règle d'état
   du composant (`.c-filtres[data-etat="vide"] .c-filtres__actifs`) a une
   spécificité plus forte et gagne quoi qu'il arrive. La séparation appartient au
   composant : employer `data-variante="declencheur"` dans la cellule. Gardée
   comme filet, jamais comme mécanisme. */


/* ═══ lib/components/en-tete.css ═══ */
/* FICHE 22 — en-tête.
   Gabarit mesuré chez huit marques sur neuf à 390 px :
     [menu x=16] [recherche] ——— LOGO CENTRÉ ——— [compte?] [panier x=330]
   Gymshark donne les abscisses au pixel : quatre cibles de 44 × 44 px, menu à
   x=16, panier à x=330 (= 390 − 16 − 44). C'est mesuré, pas estimé.

   Hauteurs mesurées : B&O 55, Litter-Robot 55, Jones Road 74, Gymshark 76.
   Les 74–76 px sont du confort de catalogue géant. On tient 56, donc
   bandeau 48 + en-tête 56 = 104 px ≤ 112 px cumulés. */

/* Deux jetons de CHROME, déclarés en portée globale parce que trois composants
   du lot en dépendent : l'en-tête les pose, le tiroir de navigation et la
   feuille de recherche s'ancrent dessous. Déclarés sur `.c-en-tete`, ils ne
   résoudraient pas chez les deux autres — et un `var()` qui ne résout pas
   n'est pas une panne à l'écran, c'est un `top: auto` silencieux.
   ⚠ À PROMOUVOIR dans `primitives.css` : ce sont des constantes de mise en
   page partagée, pas des décisions d'en-tête. */
:root {
  --h-entete: calc(var(--sp-7) + var(--sp-2));   /* 56 px */
  /* Le z-index de l'en-tête est UNIQUE et déclaré : au-dessus du contenu, en
     dessous du voile (80), du tiroir (90) et de la modale (95) de
     `primitives.css`. Un en-tête qui passe par-dessus son propre tiroir de menu
     est le défaut classique de cette famille. */
  --z-entete: 60;
}

.c-en-tete {
  position: sticky; top: 0; z-index: var(--z-entete);
  background: var(--bg-page);
  border-bottom: 1px solid transparent;
  transition: background-color var(--dur-sm) var(--ease-standard),
              border-color var(--dur-sm) var(--ease-standard),
              color var(--dur-sm) var(--ease-standard);
}

/* Le `<header>` est STATIQUE — c'est le conteneur qui colle. Structure copiée
   du réel pour que le port vers le `header-group` Shopify soit une traduction. */
.c-en-tete__barre {
  position: static;
  display: grid;
  /* Trois colonnes, la centrale en `auto` : le logo ne peut PAS recouvrir les
     pictogrammes, et les deux gouttières latérales restent égales donc le logo
     est optiquement centré même si le groupe d'actions est plus large que le
     bouton menu. */
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  min-height: var(--h-entete);
  padding-inline: var(--sp-4);
}

.c-en-tete__menu { justify-self: start; }
.c-en-tete__actions { justify-self: end; display: flex; align-items: center; }

/* Le lien du logo se dimensionne à son dessin, jamais à la place disponible. */
.c-en-tete__logo {
  justify-self: center;
  display: inline-flex; align-items: center; gap: var(--sp-2);
  color: inherit; text-decoration: none;
  min-width: 0;                       /* pour que l'ellipse de l'état long morde */
}
.c-en-tete__marque { width: var(--sp-5); height: var(--sp-5); }
.c-en-tete__mot {
  font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 500;
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
  white-space: nowrap;
}

/* Le panier porte un compteur, et RIEN quand il est vide : le `0` gravé en
   permanence dans le pictogramme de Jones Road affiche en continu que le
   panier est vide. Le compteur est `aria-hidden` — il est déjà dans le nom
   accessible du lien (« Panier, 2 articles »), et l'annoncer deux fois donne
   « Panier 2 articles 2 ». */
.c-en-tete__panier { position: relative; }
.c-en-tete__compteur {
  position: absolute; top: var(--sp-1); right: var(--sp-1);
  min-width: var(--sp-4); height: var(--sp-4);
  display: grid; place-items: center;
  padding-inline: var(--sp-1);
  border-radius: var(--r-pill);
  background: var(--c-soft-amber); color: var(--c-deep-graphite);
  font-size: var(--fs-caption); font-weight: 500; line-height: 1;
}

/* ---------- Les deux peaux ---------------------------------------------------
   Une seule transition, ≤ 200 ms, et JAMAIS de `backdrop-filter` : le flou
   coûte un repaint par image sur mobile et brouille le logo. */

/* Sur un héros pleine image : fond transparent, marque et pictogrammes en
   porcelaine. Mesuré chez Gymshark (`background: transparent` + texte blanc au
   repos, `rgb(255,255,255)` + texte noir une fois épinglé). */
.c-en-tete[data-variante="sur-media"] {
  background: transparent; color: var(--fg-on-dark);
  border-bottom-color: transparent;
}
.c-en-tete[data-variante="sur-media"] .c-en-tete__compteur {
  background: var(--fg-on-dark); color: var(--c-deep-graphite);
}

/* Épinglée : porcelaine opaque, filet 1 px. Pas d'auto-masquage à la Gymshark —
   sur une fiche produit de 12 000 px, obliger l'acheteur à remonter d'abord
   pour faire réapparaître la barre ajoute un geste à chaque décision. */
.c-en-tete[data-variante="epinglee"] {
  background: var(--bg-page); color: var(--fg-1);
  border-bottom-color: var(--line-hair);
  box-shadow: var(--sh-1);
}

/* ---------- Les états --------------------------------------------------------- */

/* VIDE — panier vide. Aucune pastille : l'absence est l'information. */
.c-en-tete[data-etat="vide"] .c-en-tete__compteur { display: none; }

/* CHARGEMENT — le compte du panier n'est pas encore connu (rendu de section
   différé). On montre une ossature à la place, jamais un « 0 » provisoire :
   un 0 faux est un message, une ossature est une attente. */
.c-en-tete[data-etat="chargement"] .c-en-tete__compteur {
  background: var(--bg-subtle); color: transparent;
}
.c-en-tete[data-etat="chargement"] .c-en-tete__mot {
  border-radius: var(--r-xs);
}

/* LONG — le cas qui casse la barre. Un nom de marque long, ou un compteur à
   deux chiffres, ou les deux : la colonne centrale doit céder AVANT que le
   groupe d'actions ne soit poussé hors de l'écran. Le mot s'ellipse, les
   cibles de 44 px ne bougent pas d'un pixel. */
.c-en-tete[data-etat="long"] .c-en-tete__mot {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.c-en-tete[data-etat="long"] .c-en-tete__logo { min-width: 0; overflow: hidden; }

/* DÉSACTIVÉ — le parcours de paiement (fiche 46) : l'en-tête reste pour la
   marque et la sortie de secours, mais on ne repart pas naviguer depuis la
   caisse. Menu et recherche deviennent inertes plutôt que de disparaître :
   une barre qui perd deux objets d'une page à l'autre se lit comme un bug. */
.c-en-tete[data-etat="desactive"] .c-en-tete__menu,
.c-en-tete[data-etat="desactive"] .c-en-tete__recherche {
  opacity: .4; pointer-events: none;
}


/* ═══ lib/components/faq-accordeon.css ═══ */
/* FICHE 20 — FAQ en accordéon.
   ---------------------------------------------------------------------------
   Une règle porte tout le composant : une rangée est un `<summary>` PLEINE
   LARGEUR d'au moins 56 px de haut, avec un chevron à ABSCISSE FIXE à droite et
   un filet en retrait entre les rangées.

   Elle vient d'une comparaison, pas d'un goût. Tuft+Paw pose sa flèche EN LIGNE
   juste après le texte : son abscisse change à chaque question (« How do I
   clean it? ↓ » finit au tiers de la largeur, la question suivante en fin de
   deuxième ligne). L'œil perd sa ligne de balayage verticale et la zone
   visiblement cliquable change de forme à chaque rangée. Le chevron d'Apple
   coûte exactement le même nombre de pixels et supprime les deux défauts.

   Le geste réel sur téléphone est le pouce sur la rangée entière — jamais sur
   le glyphe. B&O le mesure bien : sa pastille noire fait 22 px, mais la cible
   est le bouton de 390 × 60 px qui la contient. */

.c-faq-accordeon { display: grid; gap: var(--sp-5); }

.c-faq-accordeon__entete { display: grid; gap: var(--sp-3); max-width: 26ch; }
.c-faq-accordeon__titre { letter-spacing: var(--tr-tight); text-wrap: balance; }
.c-faq-accordeon__legende {
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
  max-width: 46ch;
}

/* ═══ La liste ═══════════════════════════════════════════════════════════════
   Aucun `gap` : ce sont les filets et le rythme vertical du padding qui
   séparent, comme chez Apple (~48 px au-dessus et en dessous de chaque
   question). Un `gap` en plus doublerait l'air et casserait le pas régulier. */
.c-faq-accordeon__liste { display: grid; }

.c-faq-accordeon__rangee { position: relative; }

/* Le filet EN RETRAIT (modèle Apple) : il commence et finit à l'intérieur des
   marges du conteneur, il ne va pas d'un bord à l'autre. Tracé en
   pseudo-élément positionné — jamais en `border` — pour que le retrait soit une
   propriété du filet et non une marge sur la rangée, qui décalerait le texte
   avec lui. Aucun `transform` ici : un pseudo-élément translaté crée une région
   de débordement défilable réelle (piège n°6). */
.c-faq-accordeon__rangee + .c-faq-accordeon__rangee::before {
  content: ""; position: absolute; top: 0; inset-inline: var(--sp-5);
  border-top: 1px solid var(--line-hair);
}

/* ⚠ Deuxième piège de la fiche, et il est mesuré : sur la fiche produit de
   Tuft+Paw la barre d'achat collante (~133 px) recouvre la moitié de la ligne
   « Frequently Asked Questions » — on lit le haut des lettres, pas le bas.
   C'est le défaut classique de l'ancre : on saute vers `#faq` et la première
   rangée atterrit sous la barre. La marge de défilement est donc posée sur la
   section ET sur chaque rangée (un lien profond peut viser une question).
   La valeur doit valoir la hauteur RÉELLE de la barre collante, vérifiée en
   capture et jamais déduite du CSS. */
.c-faq-accordeon,
.c-faq-accordeon__rangee { scroll-margin-top: var(--sp-9); }

/* ═══ La rangée ══════════════════════════════════════════════════════════════ */
.c-faq-accordeon__question {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--sp-5);
  align-items: start; gap: var(--sp-4);
  /* 56 px : le plancher de la règle. B&O mesure 60 px pour un padding 20/16 ;
     ici padding 16 + une ligne de 22 px donne 54, donc le plancher fait le
     dernier pixel de travail. */
  min-height: calc(var(--sp-7) + var(--sp-2));
  padding-block: var(--sp-4);
  cursor: pointer;
  list-style: none;
}
.c-faq-accordeon__question::-webkit-details-marker { display: none; }

/* La question est en GRAS et en casse de phrase, et elle a le droit de se
   replier sur trois lignes. On ne tronque pas une question : une question
   tronquée n'est plus une question. */
.c-faq-accordeon__libelle {
  font-size: var(--fs-body-lg); font-weight: 600; line-height: var(--lh-snug);
  color: var(--fg-1);
}

/* Le chevron. Colonne fixe, donc abscisse constante d'une rangée à l'autre :
   c'est toute la thèse de la fiche. Il est aligné sur la PREMIÈRE LIGNE
   (`align-items: start`), pas centré sur le bloc — sinon il dérive
   verticalement dès qu'une question passe sur deux lignes. */
.c-faq-accordeon__chevron {
  color: var(--fg-3);
  transition: transform var(--dur-sm) var(--ease-standard);
}
.c-faq-accordeon__rangee[open] .c-faq-accordeon__chevron { transform: rotate(180deg); }
.c-faq-accordeon__rangee[open] .c-faq-accordeon__libelle { color: var(--fg-1); }

/* Le panneau. Réservé à droite de la largeur de la colonne du chevron, sinon la
   réponse passe sous le glyphe et l'alignement des deux blocs se défait. */
.c-faq-accordeon__panneau {
  padding-right: var(--sp-7); padding-bottom: var(--sp-5);
  color: var(--fg-2); font-size: var(--fs-body-sm); line-height: var(--lh-relaxed);
  max-width: 60ch;
}

/* ═══ Les téléchargements — fin du bloc technique, jamais d'une FAQ ══════════ */
.c-faq-accordeon__telechargements {
  display: grid; gap: var(--sp-2); list-style: none;
  margin: var(--sp-0); padding: var(--sp-0);
}
.c-faq-accordeon__telechargement {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  min-height: var(--tap-vise);
  color: var(--fg-1); font-size: var(--fs-body-sm);
}

/* ═══ La sortie vers un humain — la dernière rangée n'est pas une question ═══ */
.c-faq-accordeon__sortie {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  padding-top: var(--sp-4);
  font-size: var(--fs-body-sm); color: var(--fg-2);
}
.c-faq-accordeon__sortie-lien {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-vise);
  color: var(--fg-1); font-weight: 500;
}

.c-faq-accordeon__note { max-width: 52ch; }

/* ═══ Variante TECHNIQUE ═════════════════════════════════════════════════════
   Ce que B&O range dans cet accordéon n'est pas une FAQ : c'est la fiche
   technique (SON, DESIGN, PUISSANCE, CONNECTIVITÉ) suivie de deux
   téléchargements. Rangées pleine largeur, libellés courts.

   ⚠ Troisième piège de la fiche, et il se joue ICI : chez B&O ces libellés sont
   écrits en capitales DANS LE CONTENU (`text-transform: none` mesuré). Un
   lecteur d'écran épelle alors les mots, la recherche interne ne les retrouve
   pas et la traduction hérite d'un texte hurlé. La mise en capitales est donc
   faite par le style, sur un contenu écrit en casse normale. */
.c-faq-accordeon[data-variante="technique"] .c-faq-accordeon__libelle {
  font-family: var(--font-ui); font-size: var(--fs-body-sm); font-weight: 500;
  text-transform: uppercase; letter-spacing: var(--tr-eyebrow);
}
.c-faq-accordeon[data-variante="technique"] .c-faq-accordeon__question {
  min-height: calc(var(--sp-7) + var(--sp-3));
  align-items: center;
}
/* Filet pleine largeur : il convient à un bloc technique, pas à une FAQ
   éditoriale — c'est exactement la distinction que fait la fiche. */
.c-faq-accordeon[data-variante="technique"] .c-faq-accordeon__rangee + .c-faq-accordeon__rangee::before {
  inset-inline: var(--sp-0);
}
.c-faq-accordeon[data-variante="technique"] .c-faq-accordeon__panneau {
  font-size: var(--fs-body-sm);
}

/* ═══ Les états ══════════════════════════════════════════════════════════════ */
.c-faq-accordeon__vide, .c-faq-accordeon__erreur { display: none; }

/* vide — une FAQ se remplit des questions réellement posées. Écrire huit
   questions d'avance pour remplir la page produit un bloc que personne n'a
   demandé, et qui ne répond à personne. */
.c-faq-accordeon[data-etat="vide"] .c-faq-accordeon__liste,
.c-faq-accordeon[data-etat="vide"] .c-faq-accordeon__telechargements,
.c-faq-accordeon[data-etat="vide"] .c-faq-accordeon__sortie { display: none; }
.c-faq-accordeon[data-etat="vide"] .c-faq-accordeon__vide {
  display: grid; border: 1px dashed var(--line-rule); border-radius: var(--r-card);
}

/* erreur — la sortie vers un humain SURVIT à la panne. C'est même le seul
   moment où elle compte vraiment. */
.c-faq-accordeon[data-etat="erreur"] .c-faq-accordeon__liste { display: none; }
.c-faq-accordeon[data-etat="erreur"] .c-faq-accordeon__erreur { display: flex; }

/* chargement — l'ossature a la forme de rangées, donc la page ne saute pas
   quand les questions arrivent. Le balayage est un FOND ANIMÉ : un
   pseudo-élément translaté crée un débordement défilable réel que la sonde
   remonte, à juste titre, comme « contenu rogné » (piège n°6). */
.c-faq-accordeon[data-etat="chargement"] .c-faq-accordeon__libelle {
  color: transparent; border-radius: var(--r-xs);
  background-color: var(--bg-subtle);
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
.c-faq-accordeon[data-etat="chargement"] .c-faq-accordeon__chevron { visibility: hidden; }
.c-faq-accordeon[data-etat="chargement"] .c-faq-accordeon__rangee { pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .c-faq-accordeon[data-etat="chargement"] .c-faq-accordeon__libelle { animation: none; }
}

/* long — une réponse de 400 caractères. Elle respire, elle ne se tronque pas :
   la mesure s'élargit d'un cran et l'interligne reste ample. Une réponse coupée
   renvoie la question au service client, ce qui est le contraire du but. */
.c-faq-accordeon[data-etat="long"] .c-faq-accordeon__panneau {
  max-width: 68ch; font-size: var(--fs-body);
}

/* desactive — le bloc existe dans l'éditeur mais n'est pas publié. Il reste
   lisible, éteint, et surtout NON MANIPULABLE : un accordéon désactivé qui
   s'ouvre quand même ment sur son état. */
.c-faq-accordeon[data-etat="desactive"] { opacity: .55; }
.c-faq-accordeon[data-etat="desactive"] .c-faq-accordeon__question { pointer-events: none; }

/* Les états `rupture` et `succes` ne veulent rien dire pour une liste de
   questions : ni stock, ni confirmation. Les dessiner aurait produit deux
   écrans que personne ne verra — c'est une décision, pas un oubli. */

/* ═══ Au-delà du téléphone — on élargit, on ne redessine pas ═════════════════ */
@media (min-width: 48rem) {
  .c-faq-accordeon__question { padding-block: var(--sp-5); }
  .c-faq-accordeon__libelle { font-size: var(--fs-h3); }
  .c-faq-accordeon[data-variante="technique"] .c-faq-accordeon__libelle {
    font-size: var(--fs-body-sm);
  }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-faq-accordeon__erreur a { color: var(--fg-1); }


/* ═══ lib/components/fil-ariane.css ═══ */
/* FICHE 25 — fil d'Ariane.
   Trois formes relevées, une seule est un fil. Le défaut Catharsis est le
   surtitre : zéro rangée ajoutée, zéro pixel volé au bloc d'achat. L'espace
   vertical au-dessus du prix est le plus cher de la boutique — une rangée de
   40 px y descend le prix de 40 px sur CHAQUE fiche, et aucun des neuf sites
   ne paie ce prix. */

/* Par défaut, seul le surtitre existe. Les deux autres formes sont dans le
   balisage mais éteintes : le composant a une forme par variante, jamais
   trois barres empilées. */
.c-fil-ariane__retour,
.c-fil-ariane__chemin,
.c-fil-ariane__soeurs { display: none; }

/* ---------- Forme 2 · surtitre (défaut) --------------------------------------- */
.c-fil-ariane__surtitre {
  font-size: var(--fs-label);        /* 13 px — la fourchette mesurée est 12–13 */
  line-height: var(--lh-snug);
  color: var(--fg-2);
  margin-bottom: var(--sp-2);
}
.c-fil-ariane__surtitre:empty { display: none; }

/* ---------- Forme 1 · retour à un niveau -------------------------------------- */
.c-fil-ariane[data-variante="retour"] .c-fil-ariane__surtitre { display: none; }
.c-fil-ariane[data-variante="retour"] .c-fil-ariane__retour {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-vise);       /* 44, pas les ~40 de Dyson */
  font-size: var(--fs-body-sm);
  color: var(--fg-1); text-decoration: underline;
  text-underline-offset: var(--sp-1);
}

/* ---------- Forme 3 · le vrai fil, si un jour il est imposé -------------------- */
.c-fil-ariane[data-variante="chemin"] .c-fil-ariane__surtitre { display: none; }
.c-fil-ariane[data-variante="chemin"] .c-fil-ariane__chemin { display: block; }
.c-fil-ariane__chemin ol {
  list-style: none; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center;
  /* ⚠ PIÈGE B1 : surtout PAS de `overflow-x: auto` ici. Un fil qui défile
     horizontalement cache ses maillons du milieu, et il déborde la page dès
     qu'un enfant impose une largeur. Un fil trop long RETOURNE À LA LIGNE. */
  gap: var(--sp-1) var(--sp-2);
  font-size: var(--fs-caption);
}
.c-fil-ariane__chemin li { display: flex; align-items: center; }
/* Le séparateur est en CSS, jamais dans le texte : chez Apple le « › » est un
   nœud de contenu, donc un lecteur d'écran l'annonce entre chaque maillon. */
.c-fil-ariane__chemin li + li::before {
  content: "\203A"; color: var(--fg-3); margin-right: var(--sp-2);
}
.c-fil-ariane__chemin a {
  color: var(--fg-2); text-decoration: none;
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}
.c-fil-ariane__chemin a:hover { color: var(--fg-1); text-decoration: underline; }
/* Le maillon courant n'est pas un lien : un lien vers la page où l'on est déjà
   est une cible tactile qui ne fait rien. */
.c-fil-ariane__chemin [aria-current="page"] {
  color: var(--fg-1); display: inline-flex; align-items: center; min-height: var(--tap-min);
}

/* ---------- Règle 4 · les sœurs, la navigation latérale ------------------------ */
.c-fil-ariane[data-variante="soeurs"] .c-fil-ariane__surtitre { display: none; }
.c-fil-ariane[data-variante="soeurs"] .c-fil-ariane__soeurs {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  list-style: none; padding: 0;
}
.c-fil-ariane__soeurs a {
  display: inline-flex; align-items: center; min-height: var(--tap-vise);
  font-size: var(--fs-body-sm); color: var(--fg-2);
  text-decoration: underline; text-underline-offset: var(--sp-1);
}
.c-fil-ariane__soeurs a:hover { color: var(--fg-1); }

/* ---------- Les états ---------------------------------------------------------- */

/* VIDE — le piège Shopify, et il est spécifique : sur
   `/collections/x/products/y`, l'objet `collection` est renseigné ; sur
   `/products/y` atteint depuis le menu, il est NUL. Un surtitre bâti sur
   `collection.title` disparaît donc selon le chemin d'arrivée — le même produit
   affiche parfois un parent, parfois rien, et personne ne comprend pourquoi.
   L'état vide ne montre RIEN à l'écran (pas un espace réservé, pas un tiret) ;
   le JSON-LD, lui, continue de sortir. La parade réelle est un surtitre
   alimenté par un métachamp produit, stable quelle que soit la route. */
.c-fil-ariane[data-etat="vide"] .c-fil-ariane__surtitre,
.c-fil-ariane[data-etat="vide"] .c-fil-ariane__retour,
.c-fil-ariane[data-etat="vide"] .c-fil-ariane__chemin { display: none; }

/* LONG — cinq niveaux, ou un libellé de 40 caractères. Le fil passe à la ligne
   et le composant grandit ; il ne défile pas et ne se tronque pas. Un maillon
   coupé (« Litières autom… ») ne dit plus où l'on est, ce qui est la seule
   chose qu'un fil sache faire. */
.c-fil-ariane[data-etat="long"] .c-fil-ariane__chemin ol { flex-wrap: wrap; }
.c-fil-ariane[data-etat="long"] .c-fil-ariane__surtitre {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* DÉSACTIVÉ — le parent existe mais n'a pas de page publiée. Le surtitre reste
   affiché en texte simple : la règle 2 le prévoit explicitement (« sans lien si
   le parent est unique »). Un lien mort vaut moins qu'un mot. */
.c-fil-ariane[data-etat="desactive"] .c-fil-ariane__retour { pointer-events: none; opacity: .5; }
.c-fil-ariane[data-etat="desactive"] .c-fil-ariane__chemin a { pointer-events: none; color: var(--fg-3); }


/* ═══ lib/components/filtres.css ═══ */
/* FICHE 29 — filtres. Cotes transposées de gymshark.com (feuille basse, relevé
   au DOM + aux pixels le 2026-08-21) et de cutsclothing.com en contre-modèle.
   Les valeurs mesurées sont arrondies au pas de la marque (échelle 8 pt) :
   68 px de rangée de groupe → `--sp-5` de rembourrage (66 px rendus), 51 px de
   barre d'action → `--sp-7` (48 px). Le pas du système prime sur la copie au
   pixel d'une autre marque ; ce qui se copie, c'est le RAPPORT entre les
   grandeurs, pas le nombre. */

/* ---------- Le déclencheur ---------------------------------------------------
   Un mot ET une icône, jamais une icône seule (§ 6). Il occupe toute sa cellule
   de barre : sur téléphone, la cible utile est la cellule, pas le mot. */
.c-filtres__declencheur {
  width: 100%; justify-content: center; gap: var(--sp-2);
  color: var(--fg-1);
}
.c-filtres__badge { margin-left: var(--sp-1); }

/* ---------- Les puces de filtres actifs --------------------------------------
   ⚠ AJOUTÉES CONTRE LE CORPUS. Aucune des quatre marques n'en a — et c'est un
   défaut, pas une doctrine : chez Gymshark, panneau fermé, rien à l'écran ne
   dit qu'un filtre est posé. Sans elles, le seul indice d'un filtrage actif est
   un nombre de résultats plus bas, ce qui ne s'interprète pas.

   La puce visible fait 32 px de haut ; la CIBLE en fait 44. Les deux ne sont
   pas la même boîte : le bouton porte le plancher tactile, la puce porte le
   dessin. Un filtre actif est UNE cible — le libellé et la croix ensemble,
   jamais deux cibles de 16 px collées. */
.c-filtres { min-width: 0; max-width: 100%; }   /* voir la note d'en-tete-collection.css */
.c-filtres__actifs { display: none; min-width: 0; }
.c-filtres__puces { list-style: none; padding-block: var(--sp-1); margin: 0; }
.c-filtres__puces > li { display: flex; }
.c-filtres__puce {
  display: inline-flex; align-items: center;
  min-height: var(--tap-vise); padding: 0; border: 0;
  background: none; cursor: pointer;
}
.c-filtres__puce .puce { min-height: var(--sp-6); white-space: nowrap; }
.c-filtres__puce .i { color: var(--fg-3); }
.c-filtres__puce:hover .puce { border-color: var(--fg-1); }
.c-filtres__puce:hover .i { color: var(--fg-1); }
/* Trois puces au repos ; les huit n'apparaissent qu'à l'état `long`, qui est le
   cas à concevoir : huit filtres empilés sur une largeur de 390 px. */
.c-filtres__puces > li:nth-child(n+4):not(:last-child) { display: none; }
.c-filtres[data-etat="long"] .c-filtres__puces > li { display: flex; }
.c-filtres__vider, .c-filtres__vider-2 {
  display: inline-flex; align-items: center; min-height: var(--tap-vise);
  padding-inline: var(--sp-3); white-space: nowrap;
  font-size: var(--fs-caption); color: var(--fg-2); text-decoration: underline;
}

/* ---------- L'état VIDE ------------------------------------------------------
   Le cas réel le plus fréquent d'une collection filtrée, et le plus souvent
   bâclé. Il ne dit pas seulement « rien » : il dit CE QUI s'est passé (trois
   filtres en même temps) et il donne l'ISSUE, à un geste. */
.c-filtres__vide { display: none; }
.c-filtres__vide-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); justify-content: center; }

.c-filtres[data-etat="vide"] .c-filtres__actifs,
.c-filtres[data-etat="long"] .c-filtres__actifs,
.c-filtres[data-etat="succes"] .c-filtres__actifs { display: block; }
.c-filtres[data-etat="vide"] .c-filtres__vide { display: grid; }

/* ---------- La feuille -------------------------------------------------------
   `.tiroir--bas` porte déjà la mécanique (fixed, translateY, coins hauts
   arrondis, verrouillage du défilement). On n'en réécrit pas une. Ce qui
   s'ajoute ici est propre au filtrage : le plafond de hauteur qui laisse la
   page visible derrière, et la poignée. */
.c-filtres__feuille { max-height: 85vh; }

/* La poignée : l'affordance qui dit « ça se ferme en tirant ». 48 × 4 px au pas
   de la marque (Gymshark : 40 × 5). */
.c-filtres__poignee {
  flex: none; width: var(--sp-7); height: var(--sp-1);
  margin: var(--sp-2) auto var(--sp-1);
  border-radius: var(--r-pill); background: var(--line-hair);
}

.c-filtres__titre { color: var(--fg-1); }
/* Piège n° 8 — `Clear All` fait 52 × 20 px chez Gymshark : moins de la moitié
   du plancher, sur l'action la plus destructrice du panneau, dans le coin le
   moins atteignable au pouce. La bonne réponse est une ENCRE discrète et une
   CIBLE de 44 px, pas une cible discrète. */
.c-filtres__effacer { text-decoration: underline; }

/* Une seule gouttière par surface (piège n° 7 : Gymshark en a deux, 24 px pour
   le contenu et 16 px pour la barre d'action, dans le même composant). */
.c-filtres__corps { padding: var(--sp-2) var(--sp-4) var(--sp-5); }

/* ---------- Les groupes ------------------------------------------------------
   Rangée d'en-tête pleine largeur, chevron à abscisse fixe à droite, filet de
   séparation entre les groupes. La rangée d'un groupe DÉPLIÉ se resserre —
   mesuré : 44 px déplié contre 68 px replié. La hauteur d'en-tête n'est donc
   pas constante, et 44 px reste le plancher. */
.c-filtres__groupe { border: 0; padding: 0; margin: 0; border-top: 1px solid var(--line-hair); }
.c-filtres__groupe:first-of-type { border-top: 0; }
.c-filtres__groupe-titre { font: inherit; margin: 0; }
.c-filtres__bascule {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  width: 100%; padding: var(--sp-5) 0; border: 0; background: none;
  font-family: var(--font-ui); font-size: var(--fs-body-sm); font-weight: 500;
  color: var(--fg-1); text-align: left; cursor: pointer;
}
.c-filtres__groupe[data-ouvert="oui"] .c-filtres__bascule { padding-block: var(--sp-3); }
.c-filtres__chevron { transition: transform var(--dur-sm) var(--ease-standard); }
.c-filtres__groupe[data-ouvert="oui"] .c-filtres__chevron { transform: rotate(180deg); }

.c-filtres__valeurs { display: none; padding-bottom: var(--sp-3); }
.c-filtres__groupe[data-ouvert="oui"] .c-filtres__valeurs { display: grid; }

/* ---------- Une valeur de facette --------------------------------------------
   48 px de haut minimum, contrôle de 24 px, 12 px entre le contrôle et le
   libellé, TOUTE la rangée cliquable par le `<label>`. Les 36 px de Cuts sont
   un contre-modèle nommé : il a préféré faire tenir huit options sans
   défilement plutôt que respecter la cible. On préfère défiler.

   Rond pour le tri, carré pour les facettes : la forme porte la cardinalité, et
   les deux familles cohabitent sans qu'aucun texte n'ait à l'expliquer.
   `.choix` / `.choix--radio` portent déjà les deux dessins. */
.c-filtres__valeur { min-height: var(--sp-7); align-items: center; }
.c-filtres__valeur .choix__marque { margin-top: 0; }
.c-filtres__libelle { flex: 1; min-width: 0; font-size: var(--fs-body-sm); line-height: var(--lh-snug); }
.c-filtres__nombre { flex: none; font-size: var(--fs-caption); color: var(--fg-3); }

/* Une valeur épuisée reste lisible et annoncée — jamais retirée en silence. */
.c-filtres__valeur[data-etat="rupture"] { color: var(--fg-3); cursor: not-allowed; }
.c-filtres__valeur[data-etat="rupture"] .choix__marque { border-color: var(--line-hair); }

/* ---------- La barre d'action ------------------------------------------------
   Elle dit VOIR, jamais « Appliquer ». Elle porte le compte dès l'ouverture, à
   zéro facette cochée — c'est alors le total. Elle est toujours cliquable :
   aucun état « je ne peux rien faire », le pire cas est « je ferme et je vois
   les N ». Et le NOMBRE porte sa propre encre, parce que c'est la donnée qui
   bouge et non le texte du bouton. */
.c-filtres__valider { min-height: var(--sp-7); padding-block: var(--sp-4); }
.c-filtres__compte { color: var(--fg-accent); }

/* ---------- Les états --------------------------------------------------------
   Un composant qui n'a qu'un état heureux est un composant à moitié fait. */

/* Chargement. La grille se repeuple : le compte devient une ossature de la
   largeur d'un nombre, pas de la largeur du bouton. Le libellé reste lisible —
   on n'efface pas la phrase pour animer un chiffre. */
.c-filtres__erreur { display: none; }
.c-filtres[data-etat="erreur"] .c-filtres__erreur { display: flex; margin-bottom: var(--sp-4); }
.c-filtres[data-etat="erreur"] .c-filtres__groupe { opacity: .45; }

.c-filtres[data-etat="chargement"] .c-filtres__compte {
  display: inline-block; min-width: var(--sp-6);
  background: var(--fg-3); color: transparent; border-radius: var(--r-xs);
}
.c-filtres[data-etat="chargement"] .c-filtres__declencheur { color: var(--fg-3); }

/* Succès. Le filtrage a abouti : les puces disent QUOI est posé, et le compte
   est annoncé (`role="status"` sur le compte de la barre). */
.c-filtres[data-etat="succes"] .c-filtres__badge { background: var(--bg-intelligence); color: var(--fg-data); }

/* Désactivé — c'est le RÉGLAGE PAR DÉFAUT de Catharsis, pas un cas limite.
   Whisker et Litter-Robot vendent exactement ce catalogue (un appareil, des
   variantes, des consommables) et n'exposent QUE le tri. Un panneau de filtres
   sur quatre produits est un composant vide qui annonce un catalogue qu'on n'a
   pas. `enable_filters = false` tant que le catalogue ne le justifie pas. */
.c-filtres[data-etat="desactive"] .c-filtres__declencheur {
  color: var(--fg-3); cursor: not-allowed; text-decoration: line-through;
}
.c-filtres[data-etat="desactive"] .c-filtres { min-width: 0; max-width: 100%; }   /* voir la note d'en-tete-collection.css */
.c-filtres__actifs { display: none; min-width: 0; }

/* ---------- Chrome de maquette — PROMU dans `mockup/atelier.css` -------------
   `.scene` vivait ici, et le lot A en avait écrit une seconde version dans le
   `<style>` de sa propre page. Les deux s'appliquaient ensemble : `aspect-ratio`
   d'un côté, `height` de l'autre, donc une largeur calculée de 475 px dans un
   parent de 390 — la page défilait horizontalement. Définition unique désormais
   dans `mockup/atelier.css`, qui ne part jamais en thème. */


/* ---------- Variante « déclencheur » ----------------------------------------
   Le composant soude trois surfaces dans une seule racine : le déclencheur, la
   rangée de puces actives, et l'état vide du résultat. Les trois n'ont pas le
   même emplacement sur une page — le déclencheur va dans la barre de contrôle,
   les puces vont SOUS elle en pleine largeur, et l'état vide va dans la zone de
   résultats.

   Un garde-fou posé depuis `en-tete-collection.css` ne peut pas les séparer :
   la règle d'état du composant a une spécificité plus forte, et elle gagne quoi
   qu'il arrive. La séparation doit donc être une VARIANTE du composant, pas une
   règle écrite depuis l'extérieur. Signalé par le lot des pages 3-6, qui avait
   dû contourner à la page. */
.c-filtres[data-variante="declencheur"] .c-filtres__actifs,
.c-filtres[data-variante="declencheur"] .c-filtres__vide { display: none; }
.c-filtres[data-variante="puces"] .c-filtres__declencheur,
.c-filtres[data-variante="puces"] .c-filtres__feuille,
.c-filtres[data-variante="puces"] .c-filtres__vide { display: none; }
.c-filtres[data-variante="puces"] .c-filtres__actifs { display: block; }


/* ═══ lib/components/galerie.css ═══ */
/* FICHE 01 — galerie. Mesures transposees de litter-robot.com, dyson.fr et
   eightsleep.com, relevees au DOM a 390x844 le 2026-08-20. */

.c-galerie { display: grid; gap: var(--sp-3); }

/* Le rail est la primitive partagee : la largeur se donne aux ENFANTS, jamais
   au rail (piege B1, plainte n.1 du fondateur). Une diapositive occupe la
   PLEINE largeur de la colonne — pas de demi-depassement. */
.c-galerie__scene { align-items: stretch; }
.c-galerie__diapo { width: 100%; margin: 0; }

/* Ratio 1:1 (Eight Sleep 390x390) par defaut ; jamais un portrait. */
.c-galerie__photo { width: 100%; height: 100%; object-fit: cover; }

/* La legende incrustee est une BANDE pleine largeur au bas du media. Chez
   Eight Sleep, une carte posee sur le coin bas-gauche masque le sujet d'une
   diapositive sur deux : une surimpression permanente doit vivre la ou AUCUNE
   diapositive n'a de sujet. */
.c-galerie__legende {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-inverse); color: var(--fg-on-dark);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}

/* La diapositive editoriale — une capacite ecrite en HTML, pas un JPEG. */
.c-galerie__editorial { background: var(--bg-intelligence); color: var(--fg-data);
                        display: grid; place-items: center; }
.c-galerie__editorial-texte { display: grid; gap: var(--sp-2); padding: var(--sp-6);
                              text-align: center; max-width: 34ch; }
.c-galerie__editorial-titre {
  font-family: var(--font-display); font-size: var(--fs-h2);
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
}
.c-galerie__editorial-corps { font-size: var(--fs-body-sm); line-height: var(--lh-normal); }

/* ---------- Les affordances de navigation ---------------------------------- */
.c-galerie__pastilles { display: flex; justify-content: center; gap: var(--sp-1); }
.c-galerie__pastille {
  display: grid; place-items: center;
  min-width: var(--tap-vise); min-height: var(--tap-vise);
}
.c-galerie__pastille > span {
  width: var(--sp-2); height: var(--sp-2); border-radius: var(--r-pill);
  background: var(--line-rule); display: block;
  transition: background-color var(--dur-sm) var(--ease-standard);
}
.c-galerie__pastille[aria-selected="true"] > span { background: var(--fg-1); }

/* B — bande de vignettes (dyson.fr : h 59, pas de 115 px, ~3,4 visibles).
   Elle remplace les pastilles ; jamais les deux, qui diraient la meme chose. */
.c-galerie[data-variante="vignettes"] .c-galerie__pastilles {
  justify-content: flex-start; overflow-x: auto; scrollbar-width: none;
}
.c-galerie[data-variante="vignettes"] .c-galerie__pastilles::-webkit-scrollbar { display: none; }
.c-galerie[data-variante="vignettes"] .c-galerie__pastille {
  min-width: var(--sp-9); height: var(--sp-8); flex: none;
  border: 1px solid var(--line-hair); border-radius: var(--r-sm);
  background: var(--bg-subtle);
}
.c-galerie[data-variante="vignettes"] .c-galerie__pastille[aria-selected="true"] {
  border-color: var(--fg-1);
}
.c-galerie[data-variante="vignettes"] .c-galerie__pastille > span { display: none; }

/* ---------- Les etats ------------------------------------------------------- */
.c-galerie__erreur { display: none; }
.c-galerie__erreur p { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

.c-galerie[data-etat="erreur"] .c-galerie__scene,
.c-galerie[data-etat="erreur"] .c-galerie__pastilles { display: none; }
.c-galerie[data-etat="erreur"] .c-galerie__erreur { display: flex; }

/* Chargement — une ossature de la FORME du media attendu (carre), donc la page
   ne saute pas a l'arrivee de l'image. */
.c-galerie[data-etat="chargement"] .c-galerie__diapo { background: var(--bg-subtle); }
.c-galerie[data-etat="chargement"] .c-galerie__diapo > * { visibility: hidden; }
.c-galerie[data-etat="chargement"] .c-galerie__diapo::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-galerie[data-etat="chargement"] .c-galerie__diapo::after { animation: none; }
}

/* Vide — aucun visuel encore livre. On ne rend PAS un rectangle gris : le
   panneau editorial tient debout tout seul, et la ligne dit pourquoi. */
.c-galerie[data-etat="vide"] .c-galerie__photo { display: none; }
.c-galerie[data-etat="vide"] .c-galerie__diapo:first-child {
  background: var(--bg-warm);
}

/* ---------- Au-dela du telephone -------------------------------------------- */
@media (min-width: 64rem) {
  /* A 1440 px la galerie devient une colonne de medias empiles ; le bloc
     d'achat prend la colonne de droite (fiche 02). */
  .c-galerie__scene { display: grid; grid-template-columns: 1fr; overflow: visible;
                      margin-inline: 0; padding-inline: 0; }
}


/* ═══ lib/components/garantie.css ═══ */
/* FICHE 44 — garantie.
   Cotes relevées au DOM sur litter-robot.com/eu/fr le 2026-08-21 :
   `<div class="warranty-upsell two-items">` de 360 × 165,4 px, deux cartes de
   170 × 131,6 px, gouttière de 20 px, marges de 15 px, rayon de 15 px, bordure
   de 2 px sur la carte choisie.

   La cote de 170 px n'est PAS écrite en dur ici : deux colonnes égales dans un
   conteneur de 360 px donnent 170 px toutes seules. Une largeur en dur est ce
   qui casse dès que la gouttière change. La classe du conteneur de Whisker le
   dit d'ailleurs elle-même : `two-items`. */

.c-garantie { display: grid; gap: var(--sp-5); }

/* ---------- L'intitulé et l'aide -------------------------------------------- */
.c-garantie__selecteur { border: 0; padding: 0; display: grid; gap: var(--sp-3); }
.c-garantie__legende { padding: 0; }
.c-garantie__legende-texte { font-size: var(--fs-body); font-weight: 500; color: var(--fg-1); }

.c-garantie__aide {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; min-height: var(--tap-vise);
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-body-sm); color: var(--fg-2); text-align: left;
}
.c-garantie__aide-texte { padding-bottom: var(--sp-2); }

/* ---------- Les deux cartes ------------------------------------------------- */
.c-garantie__cartes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3);
}

/* LA CARTE EST LE `<label for>`. C'est le point à voler chez Whisker : aucun
   `role`, aucun `tabindex`, aucun script — le radio natif donne l'exclusivité
   mutuelle, les flèches du clavier, le groupe annoncé, et une cible de toute
   la carte. C'est l'inverse exact du `<a href="#" role="button">` de la
   fiche 45.

   `min-height`, JAMAIS `height` (règle 3). Whisker tient 131 px sur ses deux
   marchés parce qu'il a FORCÉ le retour à la ligne avec un `<br>` en dur : la
   hauteur est identique parce que le nombre de lignes est imposé, pas parce
   que les langues font la même longueur. Ça casse au premier libellé plus
   long. Ici la contrainte de deux lignes reste une règle de RÉDACTION, et le
   composant survit à la troisième. */
.c-garantie__carte {
  position: relative;
  display: grid; gap: var(--sp-2); justify-items: center; align-content: start;
  min-height: var(--sp-10);
  padding: var(--sp-4) var(--sp-3);
  border: 1px solid var(--line-hair); border-radius: var(--r-lg);
  background: var(--bg-surface); text-align: center; cursor: pointer;
  transition: border-color var(--dur-sm) var(--ease-standard);
}
.c-garantie__radio { position: absolute; opacity: 0; width: 1px; height: 1px; }

/* La carte retenue : bordure d'accent Soft Amber, doublée par une ombre
   interne pour l'épaisseur — `border-width` décalerait le contenu d'un pixel
   et désalignerait les deux cartes voisines. */
.c-garantie__carte:has(.c-garantie__radio:checked) {
  border-color: var(--fg-accent);
  box-shadow: inset 0 0 0 1px var(--fg-accent);
}
.c-garantie__carte:has(.c-garantie__radio:focus-visible) {
  outline: 2px solid var(--line-focus); outline-offset: 2px;
}

.c-garantie__ecusson {
  display: grid; place-items: center; gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-intelligence); color: var(--fg-data);
  border-radius: var(--r-sm);
}
.c-garantie__ecusson-duree { font-size: var(--fs-body-lg); line-height: var(--lh-snug); }
/* Tant que la durée n'est pas arrêtée, l'écusson porte un cadratin — pas un
   nombre plausible. C'est ainsi que le « 24 » du thème est arrivé. */
.c-garantie__ecusson-duree:empty::before { content: "—"; }

.c-garantie__carte-nom {
  font-size: var(--fs-body-sm); font-weight: 500; line-height: var(--lh-snug);
}
.c-garantie__carte-prix { font-size: var(--fs-body-sm); color: var(--fg-2); }
.c-garantie__carte-prix--payant { font-weight: 700; color: var(--fg-1); }
.c-garantie__carte-prix--payant:empty::before { content: "—"; }

.c-garantie__attente { font-size: var(--fs-caption); color: var(--fg-3); }

/* ---------- La garantie légale ----------------------------------------------
   Registre neutre : aucun écusson, aucune couleur d'accent, corps réduit. Ce
   bloc informe, il ne vend pas. Le mélanger au sélecteur présenterait comme un
   avantage de la marque ce que la loi impose à tout vendeur. */
.c-garantie__legal {
  padding: var(--sp-4);
  border-left: 3px solid var(--line-rule);
  font-size: var(--fs-caption); line-height: var(--lh-normal); color: var(--fg-2);
}
.c-garantie__legal-titre {
  font-family: var(--font-ui); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--tr-eyebrow); text-transform: uppercase; color: var(--fg-3);
  margin-bottom: var(--sp-1);
}

/* ---------- Le bandeau de réassurance --------------------------------------- */
.c-garantie__bandeau {
  display: grid; gap: var(--sp-3); margin: 0; padding: 0; list-style: none;
}
.c-garantie__item {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-min);
  font-size: var(--fs-body-sm); color: var(--fg-2);
}
.c-garantie__item .i { color: var(--fg-1); }
/* À 390 px, la logistique tombe et la garantie reste. C'est la mesure
   d'Eight Sleep, pas une préférence : 4 items à 1440 px, 2 à 390 px, et ceux
   qui survivent répondent à « et si ça casse ? » et « et si ça ne me plaît
   pas ? ». */
.c-garantie__item--logistique { display: none; }
@media (min-width: 48rem) {
  .c-garantie__bandeau { grid-auto-flow: column; justify-content: start; gap: var(--sp-5); }
  .c-garantie__item--logistique { display: flex; }
}

/* ---------- La ligne de panier ---------------------------------------------- */
.c-garantie__ligne {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-caption); color: var(--fg-2);
}

/* ---------- L'accordéon ------------------------------------------------------ */
.c-garantie__accordeon { border-block: 1px solid var(--line-hair); }
.c-garantie__accordeon summary {
  display: flex; align-items: center;
  min-height: var(--sp-7); padding: var(--sp-3) 0;
  font-size: var(--fs-body-sm); font-weight: 500; cursor: pointer;
}
.c-garantie__accordeon-corps { padding-bottom: var(--sp-4); }

/* ---------- Les variantes ---------------------------------------------------
   Un seul composant, cinq emplacements. La forme dit la nature : la garantie
   est le SEUL choix rendu en cartes ; les accessoires restent des cases à
   cocher (choix exclusif contre ajouts cumulables). */
.c-garantie__selecteur,
.c-garantie__legal,
.c-garantie__bandeau,
.c-garantie__ligne,
.c-garantie__accordeon,
.c-garantie__erreur { display: none; }

.c-garantie[data-variante="selecteur"] .c-garantie__selecteur { display: grid; }
.c-garantie[data-variante="selecteur"] .c-garantie__legal { display: block; }
.c-garantie[data-variante="bandeau"] .c-garantie__bandeau { display: grid; }
.c-garantie[data-variante="ligne-panier"] .c-garantie__ligne { display: flex; }
.c-garantie[data-variante="accordeon"] .c-garantie__accordeon { display: block; }
.c-garantie[data-variante="accordeon"] .c-garantie__legal { display: block; }

/* ---------- Les états -------------------------------------------------------- */

/* Erreur — l'extension n'a pas pu être chargée. La carte incluse reste
   sélectionnée : on ne perd jamais ce qui est dû. */
.c-garantie[data-etat="erreur"] .c-garantie__erreur { display: flex; }
.c-garantie[data-etat="erreur"] .c-garantie__cartes { display: none; }

/* Rupture — l'extension n'est plus vendable, la garantie incluse reste. */
.c-garantie[data-etat="rupture"] .c-garantie__carte:last-child {
  opacity: .55; pointer-events: none;
}

/* Désactivé — aucune extension n'est proposée : une seule carte, celle qui est
   incluse. Elle occupe alors toute la largeur, plutôt que de laisser un vide
   à côté d'elle qui ressemblerait à une carte qui n'a pas chargé. */
.c-garantie[data-etat="desactive"] .c-garantie__cartes { grid-template-columns: minmax(0, 1fr); }
.c-garantie[data-etat="desactive"] .c-garantie__carte:last-child { display: none; }

/* Long — un libellé qui passe à trois lignes. La carte grandit, sa voisine
   suit (les deux sont étirées par la grille), rien n'est tronqué. C'est
   exactement ce que le `<br>` en dur de Whisker interdit. */
.c-garantie[data-etat="long"] .c-garantie__carte-nom { font-size: var(--fs-caption); }

/* Chargement — l'ossature garde la FORME des deux cartes, donc le bloc d'achat
   ne saute pas quand l'option arrive. */
.c-garantie[data-etat="chargement"] .c-garantie__carte {
  background: var(--bg-subtle); border-color: transparent; color: transparent;
}
.c-garantie[data-etat="chargement"] .c-garantie__carte > * { visibility: hidden; }


/* ═══ lib/components/guide-compatibilite.css ═══ */
/* FICHE 43 — guide de compatibilité.
   Modèles : ouraring.com (le doublet d'options dans le bloc d'achat, le
   meilleur du panel), apple.com (l'accordéon éditorial, première rangée
   ouverte), litter-robot.com (une carte = un fait). */

.c-guide-compatibilite__titre { font-size: var(--fs-h3); }

/* ---------- Le doublet ------------------------------------------------------
   Deux cartes pleine largeur, mutuellement exclusives, de MÊME RANG VISUEL.
   Le radio natif donne l'exclusivité, le clavier (flèches), le groupe annoncé
   et une cible qui est toute la carte — sans une ligne de JavaScript. */
.c-guide-compatibilite__doublet { border: 0; padding: 0; display: grid; gap: var(--sp-3); }
.c-guide-compatibilite__legende {
  padding: 0; margin-bottom: var(--sp-2);
  font-size: var(--fs-body); font-weight: 500;
}

.c-guide-compatibilite__carte {
  position: relative; display: grid; gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-7) var(--sp-4) var(--sp-4);
  border: 1px solid var(--line-hair); border-radius: var(--r-card);
  background: var(--bg-surface); cursor: pointer;
  transition: border-color var(--dur-sm) var(--ease-standard);
}
.c-guide-compatibilite__carte input { position: absolute; opacity: 0; width: 1px; height: 1px; }

/* DEUX signaux pour la carte retenue, jamais un : filet épaissi ET disque
   coché. Chez Oura, la carte retenue porte les deux — filet plus épais et
   disque noir coché en haut à droite. Un simple changement de teinte de
   bordure est invisible en plein soleil et pour un daltonien. */
.c-guide-compatibilite__coche {
  position: absolute; top: var(--sp-4); right: var(--sp-4);
  display: grid; place-items: center;
  width: var(--sp-5); height: var(--sp-5);
  border: 1px solid var(--line-rule); border-radius: var(--r-pill);
  color: transparent;
}
/* L'épaississement passe par une ombre INTERNE, pas par `border-width` : une
   bordure qui grossit décale le contenu d'un pixel au clic, et deux cartes
   voisines cessent alors d'être alignées. L'ombre ne prend aucune place. */
.c-guide-compatibilite__carte:has(input:checked) {
  border-color: var(--fg-1);
  box-shadow: inset 0 0 0 1px var(--fg-1);
}
.c-guide-compatibilite__carte:has(input:checked) .c-guide-compatibilite__coche {
  background: var(--fg-1); border-color: var(--fg-1); color: var(--fg-on-dark);
}
.c-guide-compatibilite__carte:has(input:focus-visible) {
  outline: 2px solid var(--line-focus); outline-offset: 2px;
}

.c-guide-compatibilite__carte-nom { font-size: var(--fs-body); font-weight: 500; }
.c-guide-compatibilite__carte-note {
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
}

/* L'avertissement vit DANS la carte qu'il concerne, et il n'apparaît que
   lorsque cette carte est retenue : on ne prévient pas d'un risque que le
   visiteur n'a pas encore pris. Filet d'alerte à gauche, jamais un fond rouge —
   ce n'est pas une erreur, c'est une information au moment du choix. */
.c-guide-compatibilite__avertissement {
  display: none;
  padding-left: var(--sp-3);
  border-left: 3px solid var(--fg-alert);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
}
.c-guide-compatibilite__carte:has(input:checked) .c-guide-compatibilite__avertissement { display: block; }

/* ---------- Les cinq questions ---------------------------------------------
   `<details>` natif : le contenu reste dans le DOM même replié, donc la
   recherche du navigateur et l'indexation le trouvent. C'est la règle 8 de la
   fiche, et c'est gratuit avec l'élément natif. */
.c-guide-compatibilite__questions { margin-top: var(--sp-6); }
.c-guide-compatibilite__question { border-bottom: 1px solid var(--line-hair); }
.c-guide-compatibilite__question summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  min-height: var(--sp-7); padding: var(--sp-3) 0;
  font-size: var(--fs-body); font-weight: 500; cursor: pointer;
  list-style: none;
}
.c-guide-compatibilite__question summary::-webkit-details-marker { display: none; }
/* Chevron à ABSCISSE FIXE : dessiné en pseudo-élément aligné à droite, il ne
   se déplace pas selon la longueur de la question. */
.c-guide-compatibilite__question summary::after {
  content: ""; flex: none;
  width: var(--sp-3); height: var(--sp-3);
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-25%) rotate(45deg);
  transition: transform var(--dur-sm) var(--ease-standard);
}
.c-guide-compatibilite__question[open] summary::after { transform: translateY(25%) rotate(-135deg); }

.c-guide-compatibilite__reponse { display: grid; gap: var(--sp-2); padding-bottom: var(--sp-4); }

/* ---------- Une carte = un fait ---------------------------------------------
   Le format le plus robuste à 390 px, et celui qui résiste le mieux à une
   valeur manquante : une carte en moins ne casse aucune grille. Modèle
   litter-robot. Libellé à gauche, valeur en police de données à droite. */
.c-guide-compatibilite__fait {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-subtle); border-radius: var(--r-sm);
  font-size: var(--fs-body-sm);
}
.c-guide-compatibilite__fait-nom { font-weight: 500; }
.c-guide-compatibilite__fait-valeur { color: var(--fg-1); text-align: right; }

.c-guide-compatibilite__manque,
.c-guide-compatibilite__aide { font-size: var(--fs-caption); color: var(--fg-3); }

.c-guide-compatibilite__ok,
.c-guide-compatibilite__non {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}
.c-guide-compatibilite__ok { color: var(--fg-1); }
.c-guide-compatibilite__non { color: var(--fg-2); }
.c-guide-compatibilite__non .i { color: var(--fg-alert); }

/* La preuve dessinée. Aucun rectangle gris : un tracé au trait, sur le carré
   d'un mètre, qui tient debout tel quel si la photo n'arrive jamais. */
.c-guide-compatibilite__preuve {
  display: grid; gap: var(--sp-2); justify-items: center;
  padding: var(--sp-4); margin: 0;
  background: var(--bg-surface); border: 1px solid var(--line-hair);
  border-radius: var(--r-card); color: var(--fg-1);
}
.c-guide-compatibilite__preuve svg { width: 60%; max-width: 200px; height: auto; }

/* ---------- Les états -------------------------------------------------------- */
.c-guide-compatibilite__vide,
.c-guide-compatibilite__erreur { display: none; }
.c-guide-compatibilite[data-etat="vide"] .c-guide-compatibilite__vide { display: grid; }
.c-guide-compatibilite[data-etat="vide"] .c-guide-compatibilite__questions,
.c-guide-compatibilite[data-etat="vide"] .c-guide-compatibilite__doublet,
.c-guide-compatibilite[data-etat="erreur"] .c-guide-compatibilite__questions { display: none; }
.c-guide-compatibilite[data-etat="erreur"] .c-guide-compatibilite__erreur { display: flex; }

/* Chargement — l'ossature a la forme des cartes de fait attendues. */
.c-guide-compatibilite[data-etat="chargement"] .c-guide-compatibilite__fait {
  color: transparent; min-height: var(--sp-7);
}
.c-guide-compatibilite[data-etat="chargement"] .c-guide-compatibilite__fait > * { visibility: hidden; }

.c-guide-compatibilite[data-etat="desactive"] { display: none; }

/* ---------- Variante EN PAGE (le défaut) ------------------------------------
   Le même contenu, posé dans le flux : ni voile, ni position fixe, ni piège de
   focus. La feuille basse n'est que le rappel appelé depuis le bloc d'achat. */
.c-guide-compatibilite[data-variante="en-page"] .voile { display: none; }
.c-guide-compatibilite[data-variante="en-page"] .tiroir {
  position: static; transform: none; max-height: none;
  width: auto; background: none; border-radius: 0;
}
.c-guide-compatibilite[data-variante="en-page"] .tiroir__entete {
  padding-inline: 0; border-bottom: 0;
}
.c-guide-compatibilite[data-variante="en-page"] .tiroir__corps {
  overflow: visible; padding-inline: 0;
}
.c-guide-compatibilite[data-variante="en-page"] .tiroir__entete .b-icone { display: none; }
.c-guide-compatibilite[data-variante="en-page"] .c-guide-compatibilite__titre {
  font-family: var(--font-display); font-size: var(--fs-h1);
}

/* ---------- La commutation depuis la barre d'outils de maquette ------------- */
html[data-etat="compatibilite"] .c-guide-compatibilite[data-variante="feuille"] .voile {
  opacity: .45; visibility: visible;
}
html[data-etat="compatibilite"] .c-guide-compatibilite[data-variante="feuille"] .tiroir {
  transform: none;
}


/* ═══ lib/components/heros.css ═══ */
/* FICHE 13 — héros, trois registres commutables.
   ---------------------------------------------------------------------------
   Le registre courant vient de DEUX sources, dans cet ordre de priorité :
     1. l'instance   `.c-heros[data-variante="technique"]`   ← l'atelier, qui
        montre les trois côte à côte pour que le fondateur tranche ;
     2. la barre d'outils `html[data-variante="technique"]`, qui ne s'applique
        qu'aux instances au registre VIDE (`[data-variante=""]`).
   Le garde `[data-variante=""]` est indispensable : le constructeur écrit
   toujours l'attribut, même vide, donc `:not([data-variante])` ne l'attrape
   jamais. Sans lui, la barre écrasait les trois exemplaires de l'atelier d'un
   coup et il n'y avait plus rien à comparer. */

/* ═══ Base = registre ÉDITORIAL ═══════════════════════════════════════════
   Apple ne bascule PAS en « texte à gauche / image à droite » à 1440 px :
   même pile, deux largeurs (vérifié sur fr@1440-b01). Donc pas de bifurcation
   de composition au point de rupture — seulement des tailles qui grandissent. */
.c-heros {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-5); padding-block: var(--sp-7);
  text-align: center;
}

.c-heros__texte {
  order: 1; display: grid; gap: var(--sp-4); justify-items: center;
  width: 100%; max-width: 32ch;
}
.c-heros__media { order: 2; width: 100%; aspect-ratio: 16 / 11; }
.c-heros__photo { width: 100%; height: 100%; object-fit: contain; padding: var(--sp-4); }
.c-heros__media > .c-visuel-marque { position: absolute; inset: 0; border-radius: 0; }
.c-heros__voile, .c-heros__pause { display: none; }

/* Le titre. `--fs-display` (52 px) tient à 390 px pour un mot, pas pour une
   phrase française : la taille MONTE au point de rupture, elle ne descend pas.
   C'est l'ordre mobile-first — 390 px fait foi, 48rem élargit. */
.c-heros__titre {
  font-size: var(--fs-h1); line-height: var(--lh-tight); letter-spacing: var(--tr-tight);
  text-wrap: balance;
}
.c-heros__phrase { max-width: 30ch; }
.c-heros__sousclaim { font-size: var(--fs-body-sm); color: var(--fg-2); }
.c-heros__appel { font-size: var(--fs-caption); vertical-align: super; line-height: 1; }

.c-heros__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-3); }
.c-heros__actions:empty { display: none; }

/* ⚠ `order: 1` N'EST PAS COSMÉTIQUE. Sans lui la note vaut `order: 0` comme
   tout le monde, et dans un conteneur qui réordonne (le registre technique
   remonte le média), elle se retrouve AU-DESSUS DU TITRE. Vu à l'écran le
   2026-08-22 sur l'accueil : la note de bas de page ouvrait le héros. */
.c-heros__note { max-width: 40ch; order: 1; }
.c-heros__filet { display: none; width: 100%; }

/* Le prix n'existe PAS dans le registre éditorial — interdit par la fiche, au
   même titre que la note, le picto de réassurance, le badge et le compte à
   rebours. Il est présent dans le DOM parce que les trois registres partagent
   un schéma unique ; c'est le style qui décide de le montrer. */
.c-heros__pied { display: none; }

/* ═══ Registre TECHNIQUE ══════════════════════════════════════════════════
   Apple /fr/airpods-pro : vidéo plein cadre → sur-titre → titre-superlatif →
   sous-claim chiffré + appel de note → barre d'achat collante. Le prix ne vit
   pas dans le texte du héros, il vit dans la barre. */
html[data-variante="technique"] .c-heros[data-variante=""],
.c-heros[data-variante="technique"] {
  gap: var(--sp-4); padding-block: 0 var(--sp-6);
  text-align: left; align-items: stretch;
}
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__media,
.c-heros[data-variante="technique"] .c-heros__media {
  order: 0; aspect-ratio: 4 / 5; border-radius: 0;
  margin-inline: calc(var(--sp-4) * -1); width: auto;
}
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__photo,
.c-heros[data-variante="technique"] .c-heros__photo { object-fit: cover; padding: var(--sp-0); }

/* La commande de pause appartient au héros. Elle garde le plancher tactile de
   `.b-icone` et se pose sur un disque opaque : un glyphe posé nu sur une photo
   n'a aucun contraste garanti. */
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__pause,
.c-heros[data-variante="technique"] .c-heros__pause {
  display: inline-grid; position: absolute; top: var(--sp-3); right: var(--sp-3);
  background: var(--bg-surface); color: var(--fg-1); box-shadow: var(--sh-1);
}
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__texte,
.c-heros[data-variante="technique"] .c-heros__texte {
  order: 1; justify-items: start; max-width: 34ch; gap: var(--sp-3);
  padding-inline: var(--sp-0);
}
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__titre,
.c-heros[data-variante="technique"] .c-heros__titre {
  font-size: var(--fs-h1); font-weight: 600;
}
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__phrase,
.c-heros[data-variante="technique"] .c-heros__phrase { display: none; }
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__actions,
.c-heros[data-variante="technique"] .c-heros__actions { display: none; }

/* La barre d'achat. Collante en bas de la section : c'est ce que fait Apple, et
   c'est ce qui fait que le prix ne quitte jamais l'écran pendant qu'on lit le
   superlatif. Elle reste DANS sa section — jamais fixée au viewport, sinon
   trois héros d'atelier empilent trois barres. */
html[data-variante="technique"] .c-heros[data-variante=""] .c-heros__pied,
.c-heros[data-variante="technique"] .c-heros__pied {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  position: sticky; bottom: 0; z-index: 2;
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-pill);
  background: var(--bg-surface); box-shadow: var(--sh-2);
}
/* ⚠ Vu à l'écran le 2026-08-22, et invisible pour toutes les sondes : dans la
   barre d'achat de 390 px, « À PARTIR DE 599,00 € » ne tient pas sur une ligne
   à côté d'un bouton de 172 px. Le prix se coupait ENTRE le nombre et le
   glyphe — « 599,00 » sur une ligne, « € » sur la suivante. Un prix cassé en
   deux est la faute la plus chère d'une barre d'achat : c'est le seul chiffre
   que le visiteur doit pouvoir lire d'un coup d'œil.
   Le préfixe et le prix sont donc insécables chacun, et la ligne a le droit de
   se replier ENTRE eux. Au-delà de 48rem, tout revient sur une seule ligne
   sans qu'aucune règle ne le demande. */
.c-heros__prix-ligne { display: flex; flex-wrap: wrap; align-items: baseline;
                       gap: var(--sp-1) var(--sp-2); }
.c-heros__prix { font-size: var(--fs-body-lg); white-space: nowrap; }
.c-heros__prefixe { font-size: var(--fs-caption); text-transform: uppercase;
                    letter-spacing: var(--tr-eyebrow); color: var(--fg-3);
                    white-space: nowrap; }
.c-heros__achat { flex: none; }

/* ═══ Registre DOMESTIQUE ═════════════════════════════════════════════════
   B&O PDP : photo de vie, texte posé sur l'image, aligné EN BAS À GAUCHE,
   sur-titre en petites capitales espacées (c'est le <h1> réel chez B&O — ici
   le <h1> reste le titre, pour ne pas casser la structure de titres), titre en
   graisse LÉGÈRE, filet fin, puis « à partir de » à droite. Aucune CTA.

   L'image reste ENCADRÉE (l'inset de `.bande`), pas plein bord : c'est ce que
   fait B&O sur son accueil — marges latérales d'environ 16 px. */
html[data-variante="domestique"] .c-heros[data-variante=""],
.c-heros[data-variante="domestique"] {
  display: grid; grid-template-areas: "pile"; padding-block: var(--sp-0);
  border-radius: var(--r-lg); overflow: hidden; text-align: left;
  color: var(--fg-on-dark);
}
html[data-variante="domestique"] .c-heros[data-variante=""] > *,
.c-heros[data-variante="domestique"] > * { grid-area: pile; }
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__media,
.c-heros[data-variante="domestique"] .c-heros__media {
  aspect-ratio: 3 / 4; border-radius: var(--r-xs);
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__photo,
.c-heros[data-variante="domestique"] .c-heros__photo { object-fit: cover; padding: var(--sp-0); }

/* Le voile. Sans lui, le contraste du texte dépend de la photo — donc il n'est
   pas garanti, donc il n'existe pas. Dégradé d'un seul jeton : aucun littéral. */
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__voile,
.c-heros[data-variante="domestique"] .c-heros__voile {
  display: block; position: absolute; inset: 0;
  background: linear-gradient(to top, var(--bg-inverse) 6%, transparent 62%);
  opacity: .82;
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__texte,
.c-heros[data-variante="domestique"] .c-heros__texte {
  align-self: end; justify-items: start; z-index: 1;
  max-width: 100%; gap: var(--sp-2); padding: var(--sp-5) var(--sp-4);
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__surtitre,
.c-heros[data-variante="domestique"] .c-heros__surtitre { color: var(--fg-on-dark); }
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__titre,
.c-heros[data-variante="domestique"] .c-heros__titre {
  font-weight: 300; font-size: var(--fs-h1); color: var(--fg-on-dark);
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__phrase,
.c-heros[data-variante="domestique"] .c-heros__phrase {
  color: var(--fg-on-dark); font-size: var(--fs-body-sm); opacity: .86;
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__actions,
.c-heros[data-variante="domestique"] .c-heros__actions { display: none; }
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__filet,
.c-heros[data-variante="domestique"] .c-heros__filet {
  display: block; margin: var(--sp-2) 0 var(--sp-1); border-color: var(--fg-on-dark); opacity: .3;
}
/* Le pied du registre domestique n'est PAS la barre du registre technique : ni
   fond, ni ombre, ni CTA — une ligne de petites capitales, posée sur l'image. */
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__pied,
.c-heros[data-variante="domestique"] .c-heros__pied {
  display: flex; align-self: end; z-index: 1; justify-content: flex-start;
  padding: var(--sp-0) var(--sp-4) var(--sp-5); color: var(--fg-on-dark);
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__achat,
.c-heros[data-variante="domestique"] .c-heros__achat { display: none; }
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__prefixe,
.c-heros[data-variante="domestique"] .c-heros__prefixe { color: var(--fg-on-dark); opacity: .75; }
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__prix,
.c-heros[data-variante="domestique"] .c-heros__prix {
  font-size: var(--fs-body-sm); text-transform: uppercase; letter-spacing: var(--tr-eyebrow);
}
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__note,
.c-heros[data-variante="domestique"] .c-heros__note { display: none; }
/* Le registre domestique repeint sur-titre, titre et phrase en clair sur la
   photo, mais PAS le sous-claim : la fiche 13 n'en prévoit pas dans ce
   registre. La règle manquante n'est donc pas une couleur, c'est une absence —
   sinon le sous-claim rend en gris sur une photo sombre, illisible. */
html[data-variante="domestique"] .c-heros[data-variante=""] .c-heros__sousclaim,
.c-heros[data-variante="domestique"] .c-heros__sousclaim { display: none; }

/* ═══ Les états ═══════════════════════════════════════════════════════════ */
.c-heros__vide, .c-heros__erreur, .c-heros__rupture, .c-heros__succes { display: none; }

/* vide — jamais une zone blanche : une phrase qui dit quoi faire, et une issue. */
.c-heros[data-etat="vide"] > *:not(.c-heros__vide) { display: none; }
.c-heros[data-etat="vide"] .c-heros__vide { display: grid; }
.c-heros[data-etat="vide"] { border: 1px dashed var(--line-rule); border-radius: var(--r-lg); }

/* chargement — une ossature qui a la FORME du contenu attendu, pas un
   tourniquet : la page ne saute pas quand le texte arrive. */
.c-heros[data-etat="chargement"] .c-heros__media,
.c-heros[data-etat="chargement"] .c-heros__titre,
.c-heros[data-etat="chargement"] .c-heros__phrase,
.c-heros[data-etat="chargement"] .c-heros__surtitre {
  background: var(--bg-subtle); border-radius: var(--r-xs);
  position: relative; overflow: hidden; color: transparent;
}
.c-heros[data-etat="chargement"] .c-heros__media { border-radius: var(--r-lg); }
.c-heros[data-etat="chargement"] .c-heros__media > * { visibility: hidden; }
.c-heros[data-etat="chargement"] .c-heros__actions,
.c-heros[data-etat="chargement"] .c-heros__pied,
.c-heros[data-etat="chargement"] .c-heros__note { visibility: hidden; }
.c-heros[data-etat="chargement"] .c-heros__surtitre { width: 12ch; }
/* Le balayage est un FOND ANIMÉ, jamais un pseudo-élément translaté (piège n°6
   des conventions). Corrigé le 2026-08-22 : la version précédente posait un
   `::after` en `inset: 0` figé à `translateX(-100%)` — donc entièrement à
   gauche de son parent en `overflow: hidden`, donc invisible. L'ossature de
   chargement n'avait aucun mouvement, et le seul effet possible de ce montage
   était de faire remonter à la sonde un débordement qui n'existe pas. */
.c-heros[data-etat="chargement"] .c-heros__media,
.c-heros[data-etat="chargement"] .c-heros__titre {
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-heros[data-etat="chargement"] .c-heros__media,
  .c-heros[data-etat="chargement"] .c-heros__titre { animation: none; }
}

/* erreur — elle DIT ce qui a échoué, et le héros reste lisible autour d'elle.
   Le visuel manque, le texte et l'achat restent. Jamais un mur. */
.c-heros[data-etat="erreur"] .c-heros__erreur { display: flex; width: 100%; }
.c-heros[data-etat="erreur"] .c-heros__media { display: none; }

/* long — un titre à rallonge ne doit ni déborder ni écraser la page. La taille
   redescend d'un cran et la mesure s'élargit : c'est la seule réponse honnête,
   couper un titre de héros revient à en cacher le sens. */
.c-heros[data-etat="long"] .c-heros__titre { font-size: var(--fs-h2); text-wrap: pretty; }
.c-heros[data-etat="long"] .c-heros__texte { max-width: 40ch; }
.c-heros[data-etat="long"] .c-heros__phrase { max-width: 40ch; }

/* rupture — l'achat disparaît, la promesse reste. Le prix reste lisible. */
.c-heros[data-etat="rupture"] .c-heros__rupture { display: inline-flex; }
.c-heros[data-etat="rupture"] .c-heros__cta,
.c-heros[data-etat="rupture"] .c-heros__achat { display: none; }

/* succes — l'inscription à la liste d'attente est prise. */
.c-heros[data-etat="succes"] .c-heros__succes { display: inline-flex; }
.c-heros[data-etat="succes"] .c-heros__cta { display: none; }

/* desactive — le produit existe, il n'est pas commandable (précommande close). */
.c-heros[data-etat="desactive"] .c-heros__cta,
.c-heros[data-etat="desactive"] .c-heros__achat {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}
.c-heros[data-etat="desactive"] .c-heros__cta2 { opacity: .5; pointer-events: none; }

/* ═══ Au-delà du téléphone — on ÉLARGIT, on ne redessine pas ═══════════════ */
@media (min-width: 48rem) {
  .c-heros__titre { font-size: var(--fs-display); }
  .c-heros[data-variante="technique"] .c-heros__titre,
  .c-heros[data-variante="domestique"] .c-heros__titre { font-size: var(--fs-display); }
  .c-heros__media { aspect-ratio: 16 / 9; }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-heros__erreur a { color: var(--fg-1); }


/* ═══ lib/components/icones-capacite.css ═══ */
/* FICHE 11 — icônes de capacité (famille A : ce que l'appareil FAIT).
   Gabarit Eight Sleep : panneau clair dans le bloc d'achat, trois lignes,
   pictogramme au trait ~20 px à gauche, titre gras 14 px, description 13 px
   grise en retrait sous le titre, interligne serré, filet après la troisième. */

.c-icones-capacite {
  display: grid; gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--bg-subtle); border-radius: var(--r-card);
}
.c-icones-capacite__titre { font-size: var(--fs-h3); }

.c-icones-capacite__liste { list-style: none; padding: 0; margin: 0;
                            display: grid; gap: var(--sp-4); }

/* On EMPILE à 390 px. Une rangée de trois casse et laisse un orphelin. */
.c-icones-capacite__ligne { display: grid; grid-template-columns: auto minmax(0, 1fr);
                            gap: var(--sp-3); align-items: start; }
.c-icones-capacite__ligne .i { color: var(--fg-1); margin-top: var(--sp-1); }

.c-icones-capacite__corps { display: grid; gap: var(--sp-1); }
.c-icones-capacite__nom   { font-size: var(--fs-body-sm); font-weight: 600; color: var(--fg-1);
                            line-height: var(--lh-snug); }
.c-icones-capacite__texte { font-size: var(--fs-body-sm); color: var(--fg-2);
                            line-height: var(--lh-normal); }

/* Le filet après la troisième ligne — la respiration, pas un cadre. */
.c-icones-capacite__liste > li + li { border-top: 1px solid var(--line-hair);
                                      padding-top: var(--sp-4); }

.c-icones-capacite__note { font-size: var(--fs-caption); color: var(--fg-3);
                           line-height: var(--lh-normal); }

/* Variante « nue » — sans panneau, pour un fond déjà teinté (le bloc d'achat
   assemblé n'empile pas deux surfaces). */
.c-icones-capacite[data-variante="nue"] { background: none; padding: 0; }

/* ---------- Les états -------------------------------------------------------- */
.c-icones-capacite[data-etat="chargement"] .c-icones-capacite__nom,
.c-icones-capacite[data-etat="chargement"] .c-icones-capacite__texte {
  background: var(--bg-surface); color: transparent; border-radius: var(--r-xs);
}
.c-icones-capacite[data-etat="chargement"] .c-icones-capacite__nom { max-width: 14ch; }

/* Long — une description de trois lignes ne doit pas désaligner le pictogramme :
   il est ancré en haut (`align-items: start`), pas centré. */
.c-icones-capacite[data-etat="long"] .c-icones-capacite__texte { overflow-wrap: anywhere; }

.c-icones-capacite[data-etat="desactive"] { opacity: .6; }


/* ═══ lib/components/lecteur-video.css ═══ */
/* FICHE 45 — lecteur vidéo à la demande.
   Cotes de référence : whisker.com / litter-robot.com/eu/fr, relevé DOM du
   2026-08-21 confronté à deux jeux de captures propres
   (`lecteur-video@390-c-affiche-propre.png`, `-d-ouvert-propre.png`).
   Panneau ouvert mesuré 331 × 186 px (16/9), voile noir à ≈ 70 %, pilule
   d'action 290 × 51 à x = 50, croix de fermeture 35 × 32 px et sans nom. */

/* ═══════════════ L'affiche ═══════════════ */
.c-lecteur-video__affiche { display: grid; gap: var(--sp-4); }

/* Le titre en DEUX GRAISSES, UN SEUL CORPS. C'est l'accent de la référence, et
   c'est le bon : deux tailles feraient deux titres. */
.c-lecteur-video__titre {
  font-family: var(--font-display); font-size: var(--fs-h3);
  line-height: var(--lh-snug); font-weight: 400;
}
.c-lecteur-video__titre-fort { font-weight: 600; }
.c-lecteur-video__titre-suite { display: block; color: var(--fg-2); }

/* ---------- Le déclencheur unique -------------------------------------------
   Un `<button>` qui enveloppe l'affiche ET la pilule. Toute la carte est
   cliquable — l'intention de la référence — sans fabriquer deux arrêts de
   tabulation homonymes. La remise à zéro du bouton natif est explicite : il
   n'hérite d'aucune des quatre tailles de `.b`, puisqu'il n'est pas une pilule
   mais un bloc. */
.c-lecteur-video__declencheur {
  display: grid; gap: var(--sp-3);
  width: 100%; padding: 0;
  border: 0; background: none; color: inherit; text-align: left;
  cursor: pointer;
}
.c-lecteur-video__declencheur:focus-visible {
  outline: 2px solid var(--line-focus); outline-offset: 3px;
  border-radius: var(--r-card);
}

/* Le cadre 16/9. `.media` réserve la place AVANT le chargement — aucun saut.
   La photo réelle est en portrait (1148 × 2040) : `object-position` la recadre
   sur le corps de l'appareil plutôt que sur le sol. */
.c-lecteur-video__cadre { aspect-ratio: 16 / 9; display: block; }
.c-lecteur-video__photo { object-position: center 42%; }

/* La pastille de lecture : disque de surface cerclé d'ambre, posé au centre.
   Purement décorative — le nom de l'action est sur le bouton. */
.c-lecteur-video__pastille {
  position: absolute; top: 50%; left: 50%; translate: -50% -50%;
  display: grid; place-items: center;
  width: 58px; height: 58px;
  border-radius: var(--r-pill);
  background: var(--bg-surface); color: var(--fg-1);
  box-shadow: 0 0 0 3px var(--c-soft-amber), var(--sh-2);
}
.c-lecteur-video__declencheur:hover .c-lecteur-video__pastille { background: var(--bg-page); }

/* La durée, coin bas-droit, sur un fond opaque : un chiffre clair posé à même
   une photo est illisible dès que la photo est claire. */
.c-lecteur-video__duree {
  position: absolute; right: var(--sp-2); bottom: var(--sp-2);
  padding: var(--sp-1) var(--sp-2); border-radius: var(--r-xs);
  background: var(--bg-inverse); color: var(--fg-on-dark);
  font-size: var(--fs-caption);
}

/* La pilule d'action. `.b b--principal b--pleine` porte déjà la géométrie ;
   elle est ici un `<span>` DANS le bouton, donc elle ne peut pas devenir un
   second arrêt de tabulation. */
.c-lecteur-video__libelle { pointer-events: none; }

.c-lecteur-video__accroche { max-width: 46ch; }

/* ═══════════════ La superposition ═══════════════ */
.c-lecteur-video__boite { padding: var(--sp-3); display: grid; gap: var(--sp-3); }

/* La barre de titre PORTE la croix. Chez la référence, le bouton de fermeture
   flotte 23 px AU-DESSUS du panneau, mesure 35 × 32 px et n'a aucun nom
   accessible : trop petit sur les deux axes, visuellement détaché de ce qu'il
   ferme, et muet. Ici : dans le panneau, 44 px, nommé. */
.c-lecteur-video__barre {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
}
.c-lecteur-video__nom {
  font-size: var(--fs-body-sm); color: var(--fg-2);
  overflow-wrap: anywhere;
}
.c-lecteur-video__nom b { color: var(--fg-1); }
.c-lecteur-video__fermer { color: var(--fg-1); }

/* La scène. Le rapport est VERROUILLÉ, jamais calculé. Le fond est l'encre de
   la marque et non un noir absolu — mais surtout, il n'est jamais SEUL : une
   couche d'attente, d'erreur ou l'affiche elle-même l'occupe toujours. */
.c-lecteur-video__scene {
  position: relative; overflow: hidden;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-sm);
  background: var(--bg-inverse); color: var(--fg-on-dark);
}

/* Le tenant-lieu : l'affiche reste peinte, assombrie, avec sa mention. */
.c-lecteur-video__media { position: absolute; inset: 0; }
.c-lecteur-video__media > .c-lecteur-video__photo {
  width: 100%; height: 100%; object-fit: cover; opacity: .38;
}
/* La mention du tenant-lieu vit EN HAUT du panneau, pas en bas. Mesuré le
   2026-08-22 : posée en bas, elle occupait [30287 → 30341] pendant que la bande
   de sous-titrage occupait [30308 → 30361] — 33 px de chevauchement, deux
   textes l'un par-dessus l'autre. Le bas du panneau appartient au sous-titrage ;
   la mention appartient au haut. */
.c-lecteur-video__tenant {
  position: absolute; inset: var(--sp-3) var(--sp-3) auto var(--sp-3);
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-caption); line-height: var(--lh-normal);
  color: var(--fg-on-dark);
}

/* Chargement — une ossature qui a la forme d'une image, et une phrase qui dit
   ce qui se passe. Jamais un rectangle noir muet. */
.c-lecteur-video__attente {
  position: absolute; inset: 0; display: none;
  place-content: center; justify-items: center; gap: var(--sp-3);
  padding: var(--sp-4); text-align: center;
}
.c-lecteur-video__ossature { position: absolute; inset: 0; border-radius: 0; }
.c-lecteur-video__attente-texte {
  position: relative; display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-pill);
  background: var(--bg-inverse); color: var(--fg-on-dark);
  font-size: var(--fs-body-sm);
}

/* Erreur — elle occupe la scène, sur une surface lisible. Le `.etat-erreur`
   partagé est clair par défaut : on le pose sur un fond de surface pour que le
   texte d'encre reste lisible dans un panneau sombre. */
.c-lecteur-video__erreur {
  position: absolute; inset: var(--sp-3); display: none;
  background: var(--bg-surface); color: var(--fg-1);
  overflow-y: auto;
}
.c-lecteur-video__erreur-actions {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2);
}

/* Le sous-titrage. Fond opaque, jamais un texte clair posé à même l'image :
   sur un plan lumineux, un sous-titre sans fond disparaît entièrement. */
.c-lecteur-video__sous-titre {
  position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: var(--sp-3);
  padding: var(--sp-1) var(--sp-3); border-radius: var(--r-xs);
  background: var(--bg-inverse); color: var(--fg-on-dark);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); text-align: center;
}

/* ---------- Les commandes --------------------------------------------------- */
.c-lecteur-video__controles {
  display: flex; align-items: center; gap: var(--sp-1);
}
.c-lecteur-video__commande { color: var(--fg-1); }
.c-lecteur-video__progression {
  flex: 1; height: 4px; min-width: 0;
  border-radius: var(--r-pill); background: var(--bg-subtle);
}
.c-lecteur-video__progression-fait {
  display: block; width: 0; height: 100%;
  border-radius: var(--r-pill); background: var(--fg-accent);
}
.c-lecteur-video__temps { font-size: var(--fs-caption); color: var(--fg-2); flex: none; }

.c-lecteur-video__legende { line-height: var(--lh-normal); }
.c-lecteur-video__transcription-lien { font-size: var(--fs-caption); }
/* ⚠ Relevé par la sonde le 2026-08-22 : 171 × 15 px. Le plancher tactile du
   socle (`:where(a) { min-height: var(--tap-min) }`) n'a AUCUN effet sur une
   boîte en ligne — c'est la moitié invisible du constat B5. Le lien sort donc
   du flux en ligne pour que sa hauteur existe. */
.c-lecteur-video__transcription-lien a {
  color: var(--fg-2);
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}

/* ═══════════════ Les états ═══════════════
   Par défaut : affiche visible, superposition fermée, ni attente ni erreur ni
   consentement ni vide. Un composant qui n'a qu'un état heureux est un
   composant à moitié fait — les six ci-dessous se conçoivent, ils ne se
   découvrent pas. */
.c-lecteur-video__consentement,
.c-lecteur-video__vide { display: none; }

/* Chargement — la superposition est ouverte, le média n'a pas peint. */
.c-lecteur-video[data-etat="chargement"] .c-lecteur-video__attente { display: grid; }
.c-lecteur-video[data-etat="chargement"] .c-lecteur-video__media,
.c-lecteur-video[data-etat="chargement"] .c-lecteur-video__sous-titre { display: none; }

/* Erreur — le média ne charge pas. L'affiche se retire : laisser une photo
   sous un message d'erreur laisse croire que quelque chose fonctionne. */
/* ⚠ LA SCÈNE PERD SON RAPPORT DANS CET ÉTAT, et c'est délibéré. Mesuré le
   2026-08-22 : confinée dans les 16/9 (145 px de haut à 390 px), la boîte
   d'erreur avait 265 px de contenu — donc 120 px sous la coupure, dont les DEUX
   actions de reprise (« Réessayer », « Lire la transcription »). Un état
   d'erreur dont la sortie est cachée dans un défilement interne de 145 px ne
   vaut pas mieux que le trou noir de la référence. Le rapport est un cadre pour
   une image ; il n'a aucune raison de contraindre un message. */
.c-lecteur-video[data-etat="erreur"] .c-lecteur-video__erreur {
  display: flex; position: static; overflow: visible;
}
.c-lecteur-video[data-etat="erreur"] .c-lecteur-video__scene {
  aspect-ratio: auto; padding: var(--sp-3); background: var(--bg-subtle);
}
.c-lecteur-video[data-etat="erreur"] .c-lecteur-video__media,
.c-lecteur-video[data-etat="erreur"] .c-lecteur-video__sous-titre { display: none; }
.c-lecteur-video[data-etat="erreur"] .c-lecteur-video__controles { display: none; }

/* Vide — la vidéo n'existe pas encore. L'affiche entière cède la place. */
.c-lecteur-video[data-etat="vide"] .c-lecteur-video__vide { display: grid; }
.c-lecteur-video[data-etat="vide"] .c-lecteur-video__titre,
.c-lecteur-video[data-etat="vide"] .c-lecteur-video__declencheur,
.c-lecteur-video[data-etat="vide"] .c-lecteur-video__accroche { display: none; }

/* Long — un titre de 90 caractères et une légende de trois lignes. Rien ne se
   tronque : le cadre garde son rapport, le texte prend la hauteur qu'il faut. */
.c-lecteur-video[data-etat="long"] .c-lecteur-video__titre { font-size: var(--fs-body-lg); }

/* Désactivé — le bloc vidéo est éteint dans l'éditeur de thème. Il ne rend rien
   du tout ; il ne rend pas « vide ». */
.c-lecteur-video[data-etat="desactive"] { display: none; }

/* ---------- Variante « bandeau » ---------------------------------------------
   Pleine largeur, sans carte ni pilule : l'affiche EST le bouton. Employée en
   fiche produit, où la carte ferait doublon avec le bloc d'achat. */
.c-lecteur-video[data-variante="bandeau"] .c-lecteur-video__affiche {
  padding: 0; border: 0; background: none;
}
.c-lecteur-video[data-variante="bandeau"] .c-lecteur-video__libelle {
  position: absolute; left: var(--sp-3); right: var(--sp-3); bottom: var(--sp-3);
  width: auto;
}
.c-lecteur-video[data-variante="bandeau"] .c-lecteur-video__declencheur { position: relative; }
.c-lecteur-video[data-variante="bandeau"] .c-lecteur-video__pastille { display: none; }

/* ---------- Variante « consentement » — règle 13 -----------------------------
   Le lecteur tiers dépose ses cookies AVANT tout consentement : pour une
   boutique française, c'est un défaut juridique. Tant que l'accord manque,
   l'espace est réservé et explicite — jamais un cadre vide, jamais un
   chargement silencieux. */
.c-lecteur-video[data-variante="consentement"] .c-lecteur-video__consentement {
  display: grid; gap: var(--sp-2);
  padding: var(--sp-4); border-radius: var(--r-sm);
  background: var(--bg-subtle);
}
.c-lecteur-video__consentement-titre {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-body-sm); font-weight: 500;
}
.c-lecteur-video__consentement-actions {
  display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2);
}
.c-lecteur-video[data-variante="consentement"] .c-lecteur-video__declencheur {
  pointer-events: none;
}
.c-lecteur-video[data-variante="consentement"] .c-lecteur-video__pastille { display: none; }
.c-lecteur-video[data-variante="consentement"] .c-lecteur-video__libelle { display: none; }

/* ---------- La commutation depuis la barre d'outils de maquette --------------
   `html[data-etat="video"]` est l'identifiant que `mockup/toolbar.js` pose déjà
   sur la racine du document. On s'y accroche pour que le fondateur ouvre le
   lecteur depuis son téléphone, sans recharger, et sans qu'aucun JavaScript de
   composant n'existe. Une superposition fermée ne se voit pas sur une capture. */
html[data-etat="video"] .c-lecteur-video .voile { opacity: .45; visibility: visible; }
html[data-etat="video"] .c-lecteur-video .modale { pointer-events: auto; }
html[data-etat="video"] .c-lecteur-video .modale__boite { opacity: 1; transform: none; }

/* ---------- Au-delà du téléphone --------------------------------------------- */
@media (min-width: 48rem) {
  .c-lecteur-video__titre { font-size: var(--fs-h2); }
  .c-lecteur-video__titre-suite { display: inline; }
}


/* ═══ lib/components/ligne-article.css ═══ */
/* FICHE 34 — ligne article. Cotes transposées des relevés de styles calculés de
   cutsclothing.com et gymshark.com (les deux seules gouttières mesurées : 12 px
   dans les deux cas), et des captures Ridge / Whisker / Oura. */

.c-ligne-article {
  --vignette: 72px;          /* règle 2 : 72 px MINIMUM, ratio unique pour tout
                                le panier. Deux ratios (3:4 chez Cuts, 4:5 chez
                                Gymshark) donnent deux hauteurs de ligne, et le
                                panier ondule. */
  display: grid;
  grid-template-columns: var(--vignette) minmax(0, 1fr) auto;
  grid-template-areas:
    "vignette texte   valeurs"
    "vignette actions actions"
    "sous     sous    sous";
  gap: var(--sp-2) var(--sp-3);
  align-items: start;
  padding-block: var(--sp-4);
}
.c-ligne-article + .c-ligne-article { border-top: 1px solid var(--line-hair); }

/* ---------- La vignette ------------------------------------------------------
   Fond porcelaine derrière l'image, JAMAIS transparent : CatLang est blanc, il
   disparaîtrait. `aspect-ratio` + `object-fit` plutôt qu'un recadrage des
   sources — c'est le correctif du piège n°3 de la fiche. */
.c-ligne-article__vignette {
  grid-area: vignette;
  width: var(--vignette);
  border-radius: var(--r-sm);
  background: var(--bg-subtle);
}
.c-ligne-article__vignette .c-visuel-marque { border-radius: 0; }
/* Le nom de marque du panneau de repli est calibré pour une tuile de carte
   produit ; à 72 px il déborderait. */
.c-ligne-article__vignette .c-visuel-marque__nom {
  font-size: var(--fs-caption); padding: 0 var(--sp-1);
}

/* ---------- Le texte ---------------------------------------------------------
   Titre replié à deux lignes. Aucune capture du corpus ne montre un titre qui
   se replie — les cinq observés tiennent sur une ligne — donc c'est une règle
   de prudence assumée, pas une observation. « Sacs de déchets, recharge trois
   mois, compatible tous formats » fait 61 caractères.

   TROIS lignes, pas deux. À deux, mesuré le 2026-08-22 dans le tiroir : « Pack
   CatLang Essentiel, bac,… » et « Sacs de déchets, recharge trois moi… » — le
   second coupé au milieu d'un mot. Le client ne lit plus ce qu'il achète, et
   c'est le pire endroit d'une boutique pour tronquer un nom. À trois lignes,
   les noms réels du catalogue tiennent en entier dans la colonne du tiroir. */
.c-ligne-article__texte { grid-area: texte; display: grid; gap: var(--sp-1); }
.c-ligne-article__titre {
  font-family: var(--font-ui); font-size: var(--fs-body-sm); font-weight: 500;
  line-height: var(--lh-snug); letter-spacing: var(--tr-normal);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* ⚠ NE PAS agrandir la cible de ce lien par un `padding-block`. Essayé le
   2026-08-22 : sur un titre replié à deux lignes, la boîte du `<a>` déborde le
   `-webkit-line-clamp` du `<h3>` — qui, lui, est rogné — et vient recouvrir le
   sélecteur de quantité de la rangée d'actions. Mesuré : « deux éléments
   cliquables se chevauchent sur 44 × 9 px », deux fois, dans le tiroir (là où
   la colonne est la plus étroite, donc là où le titre se replie). Un
   chevauchement de cibles est un défaut plus grave que la cible de 19 px qu'il
   prétendait corriger — laquelle satisfait déjà WCAG 2.2 SC 2.5.8 par
   l'exception d'espacement, la sonde le confirme en la classant « bas ». */
.c-ligne-article__titre a { color: inherit; text-decoration: none; }
.c-ligne-article__variante {
  font-size: var(--fs-caption); line-height: var(--lh-normal); color: var(--fg-3);
}

/* ---------- Les valeurs ------------------------------------------------------
   Chiffres tabulaires et alignement à droite, sinon la colonne ondule d'une
   ligne à l'autre. Le prix de ligne n'existe chez aucune des cinq références —
   Ridge le prouve : à quantité 2, le chiffre de droite reste 95 $ et le
   sous-total passe à 190 $. On affiche donc les DEUX, étiquetés. */
.c-ligne-article__valeurs {
  grid-area: valeurs; display: grid; gap: var(--sp-1);
  justify-items: end; text-align: right;
}
.c-ligne-article__total { font-size: var(--fs-body-sm); color: var(--fg-1); font-weight: 500; }
.c-ligne-article__valeurs .prix { white-space: nowrap; }
.c-ligne-article__unitaire { font-size: var(--fs-caption); color: var(--fg-3); font-weight: 400; }
.c-ligne-article__unite { font-family: var(--font-ui); }

/* ---------- Les actions ------------------------------------------------------
   Sous le texte, jamais au coin haut-droit : c'est là que Cuts et Whisker
   posent leur croix de suppression, et c'est la zone que le pouce touche en
   faisant défiler. */
.c-ligne-article__actions {
  grid-area: actions; display: flex; align-items: center;
  gap: var(--sp-2); flex-wrap: wrap;
}
.c-ligne-article__quantite { background: var(--bg-surface); }
/* La primitive `.quantite` style un `<output>` ; la fiche exige un `<input>`
   (règle 7). Même géométrie, même police de données. À PROMOUVOIR dans
   `primitives.css` : `.quantite input` devrait y vivre à côté de
   `.quantite output`. */
.c-ligne-article__champ {
  width: var(--sp-7); min-height: var(--tap-vise);
  border: 0; background: none; text-align: center;
  font-family: var(--font-data); font-variant-numeric: tabular-nums;
  font-size: var(--fs-body-sm);
  -moz-appearance: textfield;
}
.c-ligne-article__champ::-webkit-outer-spin-button,
.c-ligne-article__champ::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* Un MOT, pas une croix. Une croix de 14 px avec un `aria-label` irréprochable
   reste une croix de 14 px. */
/* `.b--sm` ramène le plancher à `--tap-min` (24 px) et la mesure donnait 32 px.
   Or la fiche 34 exige 44 px ICI, et pour une raison précise : les trois cibles
   de la ligne — moins, quantité, plus — sont à 44, et « Supprimer » est celle
   dont l'erreur coûte le plus cher. On rend la taille du texte petite, pas la
   cible. */
.c-ligne-article__retirer {
  min-height: var(--tap-vise);
  text-decoration: underline; text-underline-offset: 3px;
}

/* ---------- La sous-ligne ----------------------------------------------------
   Une seule catégorie admise : ce qui est INCLUS ou ATTACHÉ à l'article
   (garantie, abonnement, accessoire offert) — fiche 34, règle 14 ; modèle Oura.
   Elle vit dans la même carte, séparée par un filet. */
.c-ligne-article > .c-vente-incitative { grid-area: sous; }

/* ---------- Les états --------------------------------------------------------
   Erreur. Elle DIT ce qui a échoué : la ligne se marque, et le message porteur
   est posé par le tiroir au-dessus de la liste — jamais un simple liseré rouge
   qui laisse deviner. */
.c-ligne-article[data-etat="erreur"] {
  background: var(--bg-subtle);
  border-left: 3px solid var(--fg-alert);
  padding-left: var(--sp-3); padding-right: var(--sp-3);
  border-radius: var(--r-sm);
}
.c-ligne-article[data-etat="erreur"] .c-ligne-article__champ { color: var(--fg-alert); font-weight: 600; }

/* Rupture. La ligne reste lisible et le prix reste affiché : jamais un mur. */
.c-ligne-article[data-etat="rupture"] .c-ligne-article__vignette { opacity: .55; }
.c-ligne-article[data-etat="rupture"] .c-ligne-article__quantite { display: none; }

/* Chargement. Une quantité qui se met à jour ne doit RIEN faire sauter : seuls
   les deux nombres passent en ossature, à largeur réservée, et la ligne garde
   exactement sa hauteur. C'est la contre-mesure au défaut le plus coûteux d'un
   panier — le récapitulatif qui bouge sous le doigt pendant qu'on le lit. */
.c-ligne-article[data-etat="chargement"] .c-ligne-article__total,
.c-ligne-article[data-etat="chargement"] .c-ligne-article__unitaire {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
  min-width: var(--sp-9);
}
.c-ligne-article[data-etat="chargement"] .c-ligne-article__champ { color: var(--fg-3); }
.c-ligne-article[data-etat="chargement"] .c-ligne-article__quantite { opacity: .6; }

/* ---------- Au-delà du téléphone --------------------------------------------- */
@media (min-width: 48rem) {
  .c-ligne-article { --vignette: 88px; }
}


/* ═══ lib/components/mega-menu.css ═══ */
/* FICHE 23 — tiroir de navigation (la fiche s'appelle « méga-menu » et conclut
   qu'il n'y en a pas).

   Rien de la mécanique n'est ici : `.voile`, `.tiroir`, `.tiroir--gauche`,
   `.tiroir__entete/corps` et `html[data-defilement="bloque"]` viennent de
   `primitives.css`. Ce fichier ne contient que ce que le contenu exige. */

.c-mega-menu {
  --h-rangee: calc(var(--sp-7) + var(--sp-2));   /* 56 px, plancher de la fiche */
}

/* Le tiroir s'ancre SOUS le chrome (modèle Jones Road : `fixed`, 390 × 720 px
   à y = 124, bandeau brun et logo intacts). L'acheteur garde le logo, le
   panier et le repère de la page. */
.c-mega-menu__tiroir {
  top: var(--h-entete);
  width: min(84vw, var(--container-narrow));   /* Litter-Robot ≈ 83 % à 390 px */
  border-right: 1px solid var(--line-hair);
}
/* Le voile s'arrête lui aussi sous le chrome : chez Gymshark le fond est
   assombri à partir de y = 124 exactement — le chrome n'est jamais couvert.
   Aucun `backdrop-filter` : le flou de 16 px du bureau coûte un repaint par
   image sur téléphone. */
.c-mega-menu > .voile { top: var(--h-entete); }

.c-mega-menu__recherche { position: relative; display: block; }
.c-mega-menu__loupe {
  position: absolute; left: var(--sp-3); top: 50%;
  transform: translateY(-50%); color: var(--fg-3); pointer-events: none;
}
.c-mega-menu__saisie { padding-left: var(--sp-7); }

/* ---------- La carte vedette -------------------------------------------------
   Une seule, en tête, et c'est ce qu'on veut vendre. Pas un rectangle gris :
   le panneau de marque dessiné de `visuel-marque`. */
.c-mega-menu__vedette {
  position: relative; display: block; margin-top: var(--sp-4);
  border-radius: var(--r-card); overflow: hidden;
  color: var(--fg-1); text-decoration: none;
}
.c-mega-menu__vedette-titre {
  position: absolute; left: var(--sp-4); bottom: var(--sp-4);
  font-family: var(--font-display); font-size: var(--fs-h3);
  letter-spacing: var(--tr-tight);
}
/* Le nom générique du panneau de marque ferait doublon avec le titre de la
   carte : on n'en garde qu'un. */
.c-mega-menu__vedette .c-visuel-marque__nom { display: none; }

/* ---------- Les rangées ------------------------------------------------------
   Le chevron est à ABSCISSE FIXE à droite. Le piège mesuré chez Ridge : la
   flèche est posée juste APRÈS le libellé, donc à une abscisse différente à
   chaque rang — « New Arrivals → » finit au tiers, « Rings & Wedding Bands
   [Sale] → » aux deux tiers. L'œil n'a plus de ligne de balayage verticale et
   la zone visiblement cliquable change de forme à chaque rangée. Le chevron à
   droite coûte le même nombre de pixels et résout les deux problèmes. */
.c-mega-menu__liste { list-style: none; padding: 0; }
.c-mega-menu__rangee {
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; min-height: var(--h-rangee);
  padding: var(--sp-2) 0;
  border: 0; background: none; text-align: left;
  color: var(--fg-1); text-decoration: none; cursor: pointer;
}
.c-mega-menu__libelle {
  font-size: var(--fs-body); line-height: var(--lh-snug);
  margin-right: auto;                      /* pousse tout le reste à droite */
}
.c-mega-menu__mention { color: var(--fg-3); }
.c-mega-menu__chevron { color: var(--fg-3); }
.c-mega-menu__rangee:hover { color: var(--fg-accent); }

/* Le registre utilitaire existe, mais il ne pèse pas : petit corps, pas de
   chevron sur ce qui ne mène pas à une page de vente. */
.c-mega-menu__liste--utile .c-mega-menu__libelle { font-size: var(--fs-body-sm); color: var(--fg-2); }
.c-mega-menu__liste--utile .c-mega-menu__rangee { min-height: var(--tap-vise); }

.c-mega-menu__filet { margin-block: var(--sp-3); }

/* ---------- Variante rejetée mais construite ---------------------------------
   Plein écran, modèle Gymshark : le tiroir couvre bandeau ET en-tête, il ne
   reste plus une trace du chrome. C'est un choix de catalogue géant — il faut
   ensuite y remettre une recherche, une croix, un ♡ et un ruban d'onglets,
   quatre affordances pour réparer ce que la couverture totale a détruit.
   Construite parce que c'est un réglage d'éditeur, pas une conviction. */
.c-mega-menu[data-variante="plein-ecran"] .c-mega-menu__tiroir {
  top: 0; width: 100vw; border-right: 0;
}
.c-mega-menu[data-variante="plein-ecran"] > .voile { top: 0; }

/* ---------- Les états --------------------------------------------------------- */

/* LONG — « Comprendre CatLang et l'intelligence Cat-Tongue » : 47 caractères.
   Un libellé de navigation ne se tronque JAMAIS — un menu illisible ne se
   contourne pas, contrairement à un titre de carte. Il passe donc à deux
   lignes, la rangée grandit, et le chevron reste centré verticalement sur sa
   ligne de balayage : c'est l'abscisse qui est fixe, pas la hauteur. */
.c-mega-menu[data-etat="long"] .c-mega-menu__libelle { white-space: normal; }
.c-mega-menu[data-etat="long"] .c-mega-menu__chevron { align-self: center; }

/* CHARGEMENT — le menu est rendu par la Section Rendering API et arrive après
   le premier écran. Ossature à la forme des rangées : sept barres au pas de
   56 px, donc rien ne saute quand les libellés arrivent. */
.c-mega-menu[data-etat="chargement"] .c-mega-menu__libelle,
.c-mega-menu[data-etat="chargement"] .c-mega-menu__mention {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
}
.c-mega-menu[data-etat="chargement"] .c-mega-menu__libelle { max-width: 60%; }
.c-mega-menu[data-etat="chargement"] .c-mega-menu__chevron,
.c-mega-menu[data-etat="chargement"] .c-mega-menu__vedette-titre { visibility: hidden; }

/* ERREUR — le menu n'a pas pu être chargé. On ne laisse pas un tiroir vide :
   on dit ce qui a échoué et on laisse une issue (le lien d'accueil du logo
   reste visible, puisque le tiroir n'a jamais couvert le chrome). */
.c-mega-menu__erreur { display: none; }
.c-mega-menu[data-etat="erreur"] .c-mega-menu__erreur { display: flex; }
.c-mega-menu[data-etat="erreur"] .c-mega-menu__liste,
.c-mega-menu[data-etat="erreur"] .c-mega-menu__filet,
.c-mega-menu[data-etat="erreur"] .c-mega-menu__vedette { display: none; }

/* DÉSACTIVÉ — parcours de caisse : la navigation reste visible (le repère) mais
   inerte (on ne repart pas naviguer depuis la caisse). Cohérent avec l'état
   `desactive` de l'en-tête, qui vient du même besoin. */
.c-mega-menu[data-etat="desactive"] .c-mega-menu__rangee {
  opacity: .45; pointer-events: none;
}


/* ═══ lib/components/note-etat-vide.css ═══ */
/* FICHE 03 — emplacement de la note, état vide.

   Typo exacte de la note chez les trois marques : 12 à 14 px, gris. La note
   n'est JAMAIS un titre, et elle n'est jamais le moteur de la page — ce que
   Dyson met à 28 px, c'est sa promesse ; ce que Litter-Robot met en ambre gras,
   c'est son délai d'expédition. Une page sans note peut être aussi convaincante
   à condition d'investir la même surface dans une preuve qu'on possède
   réellement (fiches 10, 11, 12). */
.c-note-etat-vide {
  font-size: var(--fs-label);      /* 13 px — la taille mesurée chez Litter-Robot */
  font-weight: 400;
  color: var(--fg-3);              /* jamais --fg-1 : la note n'est pas un titre */
  line-height: var(--lh-normal);
  /* La ligne est RÉSERVÉE : 24 px de haut, la mesure commune aux trois marques
     (20 px Litter-Robot, 22 px Dyson, 24 px Eight Sleep). Elle ne s'étend
     jamais sur deux lignes dans le cas nominal, mais elle a le droit de
     retomber (état « long ») sans casser la géométrie du bloc. */
  min-height: var(--sp-5);
  display: flex; align-items: center;
}

/* Pas de lien tant qu'il n'y a pas de page d'avis à ouvrir : un lien vers une
   liste vide est pire que pas de lien. Donc aucune règle :hover ici — c'est
   délibéré, pas un oubli. */

/* Chargement — la ligne garde sa hauteur, donc le H1 ne bouge pas. */
.c-note-etat-vide[data-etat="chargement"] {
  color: transparent; border-radius: var(--r-xs);
  max-width: 22ch; background: var(--bg-subtle);
  position: relative; overflow: hidden;
}
.c-note-etat-vide[data-etat="chargement"]::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-note-etat-vide[data-etat="chargement"]::after { animation: none; }
}

/* Long — la phrase peut dépasser une ligne (traduction, message plus explicite).
   La hauteur minimale est un plancher, pas un plafond : on ne tronque pas une
   phrase qui explique une absence. */
.c-note-etat-vide[data-etat="long"] { align-items: flex-start; }


/* ═══ lib/components/options-payantes.css ═══ */
/* FICHE 07 — options payantes. Carte à contour gris 1 px, rayon 8 px, ~70 px de
   haut chez Litter-Robot ; la version Eight Sleep en fait 420 et coûte presque
   un écran par option. On tient la mesure courte. */

.c-options-payantes { display: grid; gap: var(--sp-3); }
.c-options-payantes__titre { font-size: var(--fs-label); font-weight: 700; color: var(--fg-1);
                             letter-spacing: var(--tr-eyebrow); text-transform: uppercase; }

/* L'ordre de gauche à droite est celui de la fiche : case, vignette, nom+phrase,
   prix collé à droite. Le prix à droite est ce qui rend la colonne comparable
   d'une option à l'autre. */
.c-options-payantes__option {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--sp-3);
  min-height: var(--tap-vise); padding: var(--sp-3);
  background: var(--bg-surface);
  border: 1px solid var(--line-hair); border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color var(--dur-sm) var(--ease-standard);
}
.c-options-payantes__option input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.c-options-payantes__option .choix__marque { margin-top: 0; }
.c-options-payantes__option:has(input:checked) { border-color: var(--fg-accent); }
.c-options-payantes__option:has(input:focus-visible) { outline: 2px solid var(--line-focus);
                                                       outline-offset: 2px; }
/* Un choix exclusif se marque rond, pas carré — la forme dit « un seul ». */
.c-options-payantes[data-variante="exclusif"] .choix__marque { border-radius: var(--r-pill); }

/* La vignette : un jeton de marque dessiné, jamais un rectangle gris et jamais
   une photo de 40 px illisible. */
.c-options-payantes__vignette {
  display: grid; place-items: center;
  width: var(--sp-7); height: var(--sp-7);
  border-radius: var(--r-xs); background: var(--bg-warm); color: var(--fg-1);
}

.c-options-payantes__corps { display: grid; gap: var(--sp-1); }
.c-options-payantes__nom   { font-size: var(--fs-body-sm); font-weight: 500; color: var(--fg-1);
                             line-height: var(--lh-snug); }
/* La phrase de 8 à 14 mots : ce qui sépare une option d'un supplément. */
.c-options-payantes__phrase { font-size: var(--fs-caption); color: var(--fg-2);
                              line-height: var(--lh-normal); }
.c-options-payantes__prix   { font-size: var(--fs-body-sm); color: var(--fg-1);
                              align-self: start; white-space: nowrap; }

.c-options-payantes__aide { font-size: var(--fs-caption); color: var(--fg-3); }

/* ---------- Les états -------------------------------------------------------- */
.c-options-payantes__erreur { display: none; }
.c-options-payantes[data-etat="erreur"] .c-options-payantes__option { display: none; }
.c-options-payantes[data-etat="erreur"] .c-options-payantes__erreur { display: flex; }

/* Rupture — l'option épuisée n'est pas retirée : la retirer ferait croire
   qu'elle n'existe pas. Elle est visible, non cochable, et le dit. */
.c-options-payantes[data-etat="rupture"] .c-options-payantes__option:last-of-type {
  opacity: .55; cursor: not-allowed; border-style: dashed;
}
.c-options-payantes[data-etat="rupture"] .c-options-payantes__option:last-of-type input {
  pointer-events: none;
}
.c-options-payantes[data-etat="rupture"] .c-options-payantes__option:last-of-type
  .c-options-payantes__phrase { color: var(--fg-alert); }

/* Désactivé — l'option existe mais n'est pas commandable pour ce lot. */
.c-options-payantes[data-etat="desactive"] { opacity: .6; pointer-events: none; }

/* Long — un nom d'option de 60 caractères ne doit ni pousser le prix hors de la
   carte ni le faire chevaucher. La colonne du nom est en minmax(0,1fr) : elle
   se comprime, le prix garde sa place. */
.c-options-payantes[data-etat="long"] .c-options-payantes__nom { overflow-wrap: anywhere; }

.c-options-payantes[data-etat="chargement"] .c-options-payantes__option {
  background: var(--bg-subtle); border-color: transparent;
}
.c-options-payantes[data-etat="chargement"] .c-options-payantes__option > * { visibility: hidden; }

/* La primitive `.choix__marque` porte sa géométrie seule, mais sa règle
   « cochée » est portée par `.choix` — que ce composant ne peut pas utiliser
   (il est en grille à quatre colonnes, la primitive est en flex). On redéclare
   donc UNIQUEMENT l'état coché, pas la géométrie.
   ⚠ À PROMOUVOIR : `.choix input:checked + .choix__marque` gagnerait à être
   dégroupé de `.choix` dans primitives.css — trois composants du lot B en ont
   besoin hors du contexte flex. */
.c-options-payantes__option input:checked + .choix__marque {
  background: var(--fg-1); border-color: var(--fg-1); color: var(--fg-on-dark);
}


/* ═══ lib/components/pagination.css ═══ */
/* FICHE 32 — pagination. Cotes transposées de gymshark.com (le seul jeu complet
   du corpus, relevé au DOM et à l'image) ; ridge.com, cutsclothing.com et
   litter-robot.com servent de contre-modèles sur la cible tactile.

   Les trois éléments s'empilent CENTRÉS avec 68 px entre chacun (mesuré :
   docY 13 330 → 13 398 → 13 466). Ici 64 px, le pas de la marque. */

.c-pagination {
  display: grid; justify-items: center; gap: var(--sp-8);
  padding-block: var(--sp-6);
}

/* ---------- Le bouton d'aval, plein ------------------------------------------
   52 px de haut obtenus par `padding: 16px 24px` autour d'un texte de 14 px —
   JAMAIS par une `height` fixe. Une hauteur fixe fige la cible et casse dès la
   première traduction plus longue ; le rembourrage la fait grandir avec le
   texte. Coins droits chez Gymshark ; ici la pilule de la marque, parce que le
   dessin appartient au système et la COTE à la mesure. */
.c-pagination__plus { padding: var(--sp-4) var(--sp-5); font-size: var(--fs-body-sm); }

/* ---------- Le bouton d'amont, fantôme ---------------------------------------
   Asymétrie voulue : plein en aval, fantôme en amont. Le premier est l'action
   attendue, le second un rattrapage — il n'apparaît que si la page s'est
   ouverte sur un curseur, sinon il n'a rien à récupérer. */
.c-pagination__amont { display: none; padding: var(--sp-4) var(--sp-5); }
.c-pagination[data-variante="amont"] .c-pagination__amont { display: inline-flex; }
.c-pagination[data-variante="amont"] .c-pagination__plus,
.c-pagination[data-variante="amont"] .c-pagination__tout,
.c-pagination[data-variante="amont"] .c-pagination__compteur,
.c-pagination[data-variante="amont"] .c-pagination__pages { display: none; }

/* ---------- Tout voir --------------------------------------------------------
   Il n'a de sens QUE lu à côté du compteur : sans « 1 - 12 sur 13 », personne
   ne sait que « tout voir » ne coûte qu'un produit. */
.c-pagination__tout {
  display: inline-flex; align-items: center; min-height: var(--tap-vise);
  padding-inline: var(--sp-4);
  font-size: var(--fs-body-sm); color: var(--fg-1);
  text-decoration: underline; text-underline-offset: 0.25em;
}

.c-pagination__compteur { font-size: var(--fs-body-sm); color: var(--fg-3); text-align: center; }

/* ---------- Le socle numéroté ------------------------------------------------
   Chaque numéro est une cible de 44 px. Litter-Robot dessine juste — pastille
   pleine ambre pour la page courante — et dimensionne faux : 23 px de diamètre,
   30 px de pas, en bas d'écran, au pouce, sur le composant le plus cliqué en
   série de la page. On garde le dessin, on corrige la cote.
   Et on corrige aussi l'encre : blanc sur ambre clair ne passe pas le
   contraste ; le chiffre est en encre profonde, comme tout CTA ambre du
   système. */
.c-pagination__pages {
  display: none; list-style: none; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: var(--sp-1); padding: 0; margin: 0;
}
.c-pagination[data-variante="numerotee"] .c-pagination__pages { display: flex; }
.c-pagination[data-variante="numerotee"] .c-pagination__plus,
.c-pagination[data-variante="numerotee"] .c-pagination__tout { display: none; }

.c-pagination__cellule { display: flex; }
.c-pagination__cellule[data-portee="long"] { display: none; }
.c-pagination[data-etat="long"] .c-pagination__cellule[data-portee="court"] { display: none; }
.c-pagination[data-etat="long"] .c-pagination__cellule[data-portee="long"] { display: flex; }

.c-pagination__page {
  display: inline-grid; place-items: center;
  min-width: var(--tap-vise); min-height: var(--tap-vise);
  border-radius: var(--r-pill);
  font-family: var(--font-data); font-variant-numeric: tabular-nums;
  font-size: var(--fs-body-sm); color: var(--fg-1); text-decoration: none;
}
.c-pagination__page:hover { background: var(--bg-subtle); }
.c-pagination__page[aria-current="page"] { background: var(--fg-accent); color: var(--fg-1); font-weight: 500; }
.c-pagination__saut {
  min-width: var(--sp-5); align-items: center; justify-content: center;
  color: var(--fg-3);
}

/* ---------- Les états -------------------------------------------------------- */
.c-pagination__erreur, .c-pagination__rupture { display: none; }
.c-pagination[data-etat="erreur"] .c-pagination__erreur { display: flex; }
.c-pagination[data-etat="erreur"] .c-pagination__plus { display: none; }
.c-pagination[data-etat="rupture"] .c-pagination__rupture { display: flex; }
.c-pagination[data-etat="rupture"] .c-pagination__plus,
.c-pagination[data-etat="rupture"] .c-pagination__tout { display: none; }

/* Chargement — le bouton reste EN PLACE et devient inerte : s'il disparaissait,
   la page se raccourcirait sous le doigt au moment précis où le visiteur vient
   de cliquer dessus. */
.c-pagination[data-etat="chargement"] .c-pagination__plus-mot { visibility: hidden; }
.c-pagination[data-etat="chargement"] .c-pagination__plus {
  position: relative; pointer-events: none; opacity: .8;
}
.c-pagination[data-etat="chargement"] .c-pagination__plus::after {
  content: "Chargement…"; position: absolute; inset: 0;
  display: grid; place-items: center;
}
.c-pagination[data-etat="chargement"] .c-pagination__compteur {
  min-width: 12rem; background: var(--bg-subtle); color: transparent;
  border-radius: var(--r-xs);
}

/* Succès — tout est chargé. Le bouton est REMPLACÉ par le compteur final, il ne
   reste jamais affiché et désactivé : un bouton grisé se clique quand même, et
   ne rien voir se passer est pire que ne pas voir le bouton. */
.c-pagination[data-etat="succes"] .c-pagination__plus,
.c-pagination[data-etat="succes"] .c-pagination__tout { display: none; }
.c-pagination[data-etat="succes"] .c-pagination__compteur { color: var(--fg-1); }

/* Vide — rien à paginer. Le composant ne se rend pas en production ; ici il dit
   pourquoi, plutôt que de laisser un blanc sous une grille vide. */
.c-pagination[data-etat="vide"] .c-pagination__plus,
.c-pagination[data-etat="vide"] .c-pagination__tout,
.c-pagination[data-etat="vide"] .c-pagination__pages { display: none; }

/* Désactivé — c'est le RÉGLAGE PAR DÉFAUT de Catharsis. Quatre références n'ont
   pas de problème de pagination ; leur en donner un serait fabriquer un
   composant pour un besoin qui n'existe pas. `per_page` se fixe au-dessus de la
   taille du catalogue et le composant ne se rend jamais. */
.c-pagination[data-etat="desactive"] { opacity: .5; pointer-events: none; }


/* ═══ lib/components/paiement-fractionne.css ═══ */
/* FICHE 05 — paiement fractionné.
   Le fractionné se place IMMÉDIATEMENT SOUS LE PRIX, jamais à côté, et il tient
   en une ligne. Le panneau gris de Litter-Robot, aussi haut que le prix et
   occupant 72 % de la largeur, donne à un organisme de crédit le même poids
   visuel que le prix lui-même : c'est le motif qu'on ne reproduit pas. */

.c-paiement-fractionne { display: grid; gap: var(--sp-2); }

.c-paiement-fractionne__ligne {
  font-size: var(--fs-body-sm); color: var(--fg-2); line-height: var(--lh-normal);
}
.c-paiement-fractionne__lien {
  color: var(--fg-1); text-underline-offset: 2px;
  display: inline-flex; align-items: center;
}

/* ---------- B · onglets épinglés -------------------------------------------- */
.c-paiement-fractionne__onglets { display: none; }
.c-paiement-fractionne[data-variante="onglets"] .c-paiement-fractionne__ligne { display: none; }
.c-paiement-fractionne[data-variante="onglets"] .c-paiement-fractionne__onglets {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  background: var(--bg-surface); border-bottom: 1px solid var(--line-hair);
}
.c-paiement-fractionne__onglet {
  display: grid; gap: var(--sp-1); justify-items: center;
  min-height: var(--tap-vise); padding: var(--sp-2) var(--sp-3);
  border: 0; border-bottom: 2px solid transparent; background: none;
  color: var(--fg-3); cursor: pointer;
}
.c-paiement-fractionne__onglet[aria-selected="true"] {
  color: var(--fg-1); border-bottom-color: var(--fg-1);
}
.c-paiement-fractionne__onglet-titre { font-size: var(--fs-label); font-weight: 500; }
.c-paiement-fractionne__onglet-prix { font-size: var(--fs-caption); }

/* ---------- Les états -------------------------------------------------------- */
.c-paiement-fractionne__desactive { display: none; }
/* Désactivé — l'état par défaut du bloc d'achat Catharsis tant que le fondateur
   n'a pas tranché. Le composant ne disparaît pas de l'atelier : il DIT qu'il
   est en attente, sinon on croit qu'il a été oublié. */
.c-paiement-fractionne[data-etat="desactive"] .c-paiement-fractionne__ligne,
.c-paiement-fractionne[data-etat="desactive"] .c-paiement-fractionne__onglets { display: none; }
.c-paiement-fractionne[data-etat="desactive"] .c-paiement-fractionne__desactive {
  display: block; font-size: var(--fs-caption); color: var(--fg-3);
  border-left: 3px solid var(--line-rule); padding-left: var(--sp-3);
}

.c-paiement-fractionne[data-etat="chargement"] .c-paiement-fractionne__ligne {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
  max-width: 30ch;
}
.c-paiement-fractionne[data-etat="erreur"] .c-paiement-fractionne__ligne { display: none; }
.c-paiement-fractionne[data-etat="erreur"] .c-paiement-fractionne__desactive {
  display: block; font-size: var(--fs-caption); color: var(--fg-alert);
  border-left: 3px solid var(--fg-alert); padding-left: var(--sp-3);
}


/* ═══ lib/components/panneau-preferences.css ═══ */
/* FICHE 41 — panneau de préférences, second niveau du consentement.
   Cotes de référence : `.osano-cm-info` sur litter-robot.com, relevé DOM du
   2026-08-20 (320 × 847 px à x = 71, contenu de 1 051 px, donc défilement
   interne). */

/* Un tiroir de 320 px, pas une modale plein écran : la page reste visible et
   assombrie derrière. `min()` garantit qu'il ne dépasse jamais la fenêtre à
   390 px, où 320 px laissent 70 px de page — les 71 px d'Osano, au pixel. */
.c-panneau-preferences__tiroir { width: min(88vw, 320px); }

.c-panneau-preferences__titre { font-size: var(--fs-h3); }
.c-panneau-preferences__intro { margin-bottom: var(--sp-4); }

.c-panneau-preferences__liste { display: grid; }
.c-panneau-preferences__rangee {
  display: grid; gap: var(--sp-2);
  padding: var(--sp-4) 0;
  border-bottom: 1px solid var(--line-hair);
}
.c-panneau-preferences__rangee:first-child { padding-top: 0; }

/* ---------- L'interrupteur ---------------------------------------------------
   ⚠ PRIMITIVE MANQUANTE. `lib/primitives.css` a la case (`.choix`) et le
   bouton radio, pas l'interrupteur. Il est écrit ici parce que la règle
   l'exige (« ne modifie aucun fichier partagé »), et il est signalé pour
   promotion : le tiroir panier, le sélecteur de marché et les réglages
   d'éditeur en auront besoin, et trois dialectes d'interrupteur seraient
   exactement ce que CONVENTIONS.md existe pour empêcher.

   La cible tactile est la RANGÉE de bascule entière, jamais le rail seul. */
.c-panneau-preferences__bascule {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "nom rail" "etat rail";
  align-items: center; gap: var(--sp-1) var(--sp-3);
  min-height: var(--tap-vise); cursor: pointer;
}
.c-panneau-preferences__bascule input {
  position: absolute; opacity: 0; width: 1px; height: 1px;
}
.c-panneau-preferences__nom {
  grid-area: nom; font-size: var(--fs-body); font-weight: 500; color: var(--fg-1);
}
.c-panneau-preferences__etat {
  grid-area: etat; font-size: var(--fs-caption); color: var(--fg-2);
}

/* Le rail. TROIS signaux, jamais la couleur seule (règle 6) :
     1. la POSITION du bouton — le signal principal, lisible sans couleur ;
     2. le contraste du rail — gris neutre éteint, encre allumé ;
     3. le mot à côté — « Activé » / « Désactivé », lisible et annoncé.
   Chez Osano, éteint = violet saturé et allumé = vert saturé : deux teintes
   fortes dont aucune ne se lit comme « éteint ». C'est ce qui a failli faire
   écrire à la fiche l'inverse de la vérité. */
.c-panneau-preferences__rail {
  grid-area: rail;
  position: relative; flex: none;
  width: var(--sp-7); height: var(--sp-5);
  border-radius: var(--r-pill);
  background: var(--bg-subtle);
  box-shadow: var(--sh-inset-hair);
  transition: background-color var(--dur-sm) var(--ease-standard);
}
.c-panneau-preferences__rail::after {
  content: ""; position: absolute; top: var(--sp-1); left: var(--sp-1);
  width: var(--sp-4); height: var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--bg-surface); box-shadow: var(--sh-1);
  transition: transform var(--dur-sm) var(--ease-standard);
}
.c-panneau-preferences__bascule input:checked ~ .c-panneau-preferences__rail {
  background: var(--fg-1);
}
.c-panneau-preferences__bascule input:checked ~ .c-panneau-preferences__rail::after {
  transform: translateX(var(--sp-5));
}
.c-panneau-preferences__bascule input:focus-visible ~ .c-panneau-preferences__rail {
  outline: 2px solid var(--line-focus); outline-offset: 2px;
}

/* Le mot d'état suit l'entrée, pas une classe posée par du script : ce que
   l'œil lit et ce que le DOM dit ne peuvent pas diverger. */
.c-panneau-preferences__on { display: none; }
.c-panneau-preferences__bascule input:checked ~ .c-panneau-preferences__etat .c-panneau-preferences__on { display: inline; }
.c-panneau-preferences__bascule input:checked ~ .c-panneau-preferences__etat .c-panneau-preferences__off { display: none; }

/* La catégorie nécessaire : cochée, verrouillée, et elle DIT pourquoi. */
.c-panneau-preferences__rangee[data-verrouille="oui"] .c-panneau-preferences__rail { opacity: .5; }
.c-panneau-preferences__rangee[data-verrouille="oui"] .c-panneau-preferences__bascule { cursor: default; }

.c-panneau-preferences__texte {
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
}

/* Le lien de détail est un vrai bouton avec le plancher tactile. Chez Osano il
   fait 114 × 15 px en 12 px : la plus petite cible d'un panneau consacré aux
   droits des personnes. */
.c-panneau-preferences__detail { justify-self: start; min-height: var(--tap-vise); }

.c-panneau-preferences__duree { margin-top: var(--sp-4); }

/* ---------- Le pied — trois actions, pas une -------------------------------- */
.c-panneau-preferences__actions { display: grid; gap: var(--sp-2); }
.c-panneau-preferences__action { min-height: var(--tap-vise); font-size: var(--fs-body-sm); }

/* ---------- Les états -------------------------------------------------------- */
.c-panneau-preferences__succes,
.c-panneau-preferences__erreur { display: none; }

.c-panneau-preferences[data-etat="succes"] .c-panneau-preferences__succes {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  margin-top: var(--sp-4); font-size: var(--fs-body-sm); color: var(--fg-data);
}
.c-panneau-preferences[data-etat="erreur"] .c-panneau-preferences__erreur {
  display: flex; margin-top: var(--sp-4);
}

/* Chargement — l'application des choix est en cours. « Enregistrer applique
   immédiatement, y compris en retrait » : les scripts déjà chargés sont
   coupés, pas seulement « non rechargés au prochain passage ». Le pied entier
   se verrouille, jamais un seul des trois boutons. */
.c-panneau-preferences[data-etat="chargement"] .c-panneau-preferences__action {
  background: var(--bg-subtle); color: var(--fg-3); border-color: transparent;
  pointer-events: none;
}

/* Désactivé — la visite ne relève pas de l'affichage obligatoire. */
.c-panneau-preferences[data-etat="desactive"] { display: none; }

/* ---------- La commutation depuis la barre d'outils de maquette ------------- */
html[data-etat="preferences"] .c-panneau-preferences .voile { opacity: .45; visibility: visible; }
html[data-etat="preferences"] .c-panneau-preferences .tiroir { transform: none; }


/* ═══ lib/components/pied-de-page.css ═══ */
/* FICHE 26 — pied de page.
   Cible : ~600 px de haut à 390, trois groupes, 13 liens dont EXACTEMENT trois
   liens produit. Gymshark tient 855 px avec des milliers de références ;
   1 145 px (Jones Road) est un pied qui a cessé de compter. */

.c-pied-de-page {
  /* Fond distinct de la page. Jones Road pose un os chaud sur une page
     blanche ; ici la page est porcelaine, donc le pied prend le graphite —
     LA MÊME COULEUR QUE LE BANDEAU D'ANNONCE (fiche 21). Le haut et le bas
     encadrent la page : c'est la signature de chrome, pas une coïncidence. */
  background: var(--bg-inverse); color: var(--fg-on-dark);
  padding: var(--sp-7) var(--sp-4) var(--sp-6);
  display: grid; gap: var(--sp-6);
}

/* Sur graphite, `--fg-3` (le gris des légendes sur porcelaine) tombe à ~1,9:1.
   Les textes secondaires du pied prennent donc le chrome clair, qui tient
   au-delà du seuil AA sur ce fond. Une couleur de texte ne se transporte pas
   d'un fond clair à un fond sombre sans être revérifiée. */
.c-pied-de-page .legende,
.c-pied-de-page .etiquette { color: var(--c-chrome); }

/* ---------- 1 · Infolettre ---------------------------------------------------- */
.c-pied-de-page__titre-lettre {
  font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 500;
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
  margin-bottom: var(--sp-4);
}
/* Champ et bouton séparés : deux cibles franches de 44 px. La pilule fusionnée
   d'Oura met une flèche DANS le champ — la moitié droite devient un bouton
   invisible, et sur téléphone on tape à côté. */
.c-pied-de-page__lettre-forme { display: grid; gap: var(--sp-3); }
.c-pied-de-page__micro { margin-top: var(--sp-3); }
.c-pied-de-page__lettre-erreur,
.c-pied-de-page__lettre-succes { display: none; margin-top: var(--sp-3); }
.c-pied-de-page__lettre-succes { align-items: center; gap: var(--sp-2);
  color: var(--bg-intelligence); font-size: var(--fs-body-sm); }
.c-pied-de-page__lettre-erreur { color: var(--c-calm-red); }

/* ---------- 2 · 3 · 4 · Les groupes de liens ---------------------------------- */
.c-pied-de-page__groupes { display: grid; gap: var(--sp-6); }
.c-pied-de-page__groupe ul { list-style: none; padding: 0; margin-top: var(--sp-3); }
.c-pied-de-page__groupe a {
  display: flex; align-items: center;
  /* 32 px de boîte + l'interligne du bloc : au-dessus du plancher WCAG 2.2 de
     24 px, en dessous des 44 px d'une cible primaire. Treize liens à 44 px
     coûteraient 572 px à eux seuls et feraient exploser le budget de 700 px. */
  min-height: var(--sp-6);
  color: var(--fg-on-dark); text-decoration: none;
  font-size: var(--fs-body-sm); line-height: var(--lh-snug);
}
.c-pied-de-page__groupe a:hover { color: var(--fg-accent); text-decoration: underline; }
/* Aucun lien n'est traité différemment des autres. Piège mesuré chez Whisker :
   dans une liste de cinq liens gris, « FAQ » est en blanc gras, sans état actif
   à justifier. Personne ne le voit en revue de code ; tout le monde le voit sur
   une capture. */

/* ---------- 5 · Réseaux -------------------------------------------------------- */
.c-pied-de-page__reseaux {
  list-style: none; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
}
.c-pied-de-page__reseaux a {
  display: inline-flex; align-items: center; min-height: var(--tap-vise);
  color: var(--c-chrome); font-size: var(--fs-body-sm);
  text-decoration: underline; text-underline-offset: var(--sp-1);
}
.c-pied-de-page__reseaux a:hover { color: var(--fg-on-dark); }

/* ---------- 6 · Marché ---------------------------------------------------------
   Le composant de la fiche 27 est rendu tel quel : le pied ne redessine pas un
   sélecteur, il en pose un. Seule sa place dans la grille est décidée ici. */
.c-pied-de-page .c-selecteur-marche { justify-self: start; }

/* ---------- 7 · Paiement ------------------------------------------------------- */
.c-pied-de-page__paiement ul {
  list-style: none; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-2);
}
/* Une grille UNIQUE : même hauteur, même gouttière, même ligne de base. C'est
   la dernière chose que l'acheteur voit avant de décider de payer 599 €. */
.c-pied-de-page__marque-paiement {
  display: grid; place-items: center;
  min-height: var(--sp-6); padding-inline: var(--sp-2);
  border: 1px solid var(--c-quiet-graphite); border-radius: var(--r-xs);
  background: var(--fg-data);
  font-family: var(--font-data); font-size: var(--fs-caption); font-weight: 500;
  letter-spacing: var(--tr-normal); color: var(--fg-on-dark);
  white-space: nowrap;
}

/* ---------- 8 · Identité légale ----------------------------------------------- */
.c-pied-de-page__identite { display: grid; gap: var(--sp-1); }
.c-pied-de-page__raison {
  font-family: var(--font-display); font-size: var(--fs-body-lg);
  letter-spacing: var(--tr-tight);
}
.c-pied-de-page__identite address { font-style: normal; }

/* ---------- Au-delà du téléphone ----------------------------------------------
   Le point de rupture ÉLARGIT : les trois groupes passent côte à côte. Il ne
   redessine rien — l'ordre des huit blocs est identique à 390 et à 1440. */
@media (min-width: 48rem) {
  .c-pied-de-page { padding-inline: var(--sp-6); }
  .c-pied-de-page__groupes { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .c-pied-de-page__lettre-forme { grid-template-columns: minmax(0, 1fr) auto; }
  .c-pied-de-page__paiement ul { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* ---------- Variante ----------------------------------------------------------
   CLAIR — le parti pris Jones Road : un fond os chaud, texte graphite. Construit
   parce que c'est un réglage d'éditeur de thème ; le défaut reste le graphite,
   qui referme la page sur la même couleur que le bandeau. */
.c-pied-de-page[data-variante="clair"] {
  background: var(--bg-warm); color: var(--fg-1);
}
.c-pied-de-page[data-variante="clair"] .legende,
.c-pied-de-page[data-variante="clair"] .etiquette { color: var(--fg-2); }
.c-pied-de-page[data-variante="clair"] .c-pied-de-page__groupe a { color: var(--fg-1); }
.c-pied-de-page[data-variante="clair"] .c-pied-de-page__reseaux a { color: var(--fg-2); }
.c-pied-de-page[data-variante="clair"] .c-pied-de-page__marque-paiement {
  background: var(--bg-surface); color: var(--fg-1); border-color: var(--line-hair);
}
.c-pied-de-page[data-variante="clair"] .c-pied-de-page__lettre-erreur { color: var(--fg-alert); }
.c-pied-de-page[data-variante="clair"] .c-pied-de-page__lettre-succes { color: var(--fg-data); }

/* ---------- Les états ---------------------------------------------------------- */

/* SUCCÈS — l'inscription est prise. Le message remplace le micro-texte : deux
   phrases qui parlent du même formulaire, l'une au futur et l'autre au passé,
   se contredisent à l'écran. */
.c-pied-de-page[data-etat="succes"] .c-pied-de-page__lettre-succes { display: flex; }
.c-pied-de-page[data-etat="succes"] .c-pied-de-page__micro,
.c-pied-de-page[data-etat="succes"] .c-pied-de-page__lettre-forme { display: none; }

/* ERREUR — adresse invalide. Le champ est marqué, le message DIT quoi corriger.
   Le formulaire reste en place : on ne fait pas disparaître ce qu'il faut
   reprendre. */
.c-pied-de-page[data-etat="erreur"] .c-pied-de-page__lettre-erreur { display: flex;
  align-items: center; gap: var(--sp-2); }
.c-pied-de-page[data-etat="erreur"] .c-pied-de-page__courriel { border-color: var(--c-calm-red); }

/* CHARGEMENT — l'envoi est parti. Le bouton devient inerte pour empêcher la
   double soumission, qui est le vrai défaut ici : deux inscriptions et deux
   courriels de confirmation. */
.c-pied-de-page[data-etat="chargement"] .c-pied-de-page__envoi {
  opacity: .6; pointer-events: none;
}

/* VIDE — aucun réseau alimenté, aucun moyen de paiement configuré. Les blocs
   disparaissent au lieu de rendre une rangée vide : une grille de paiement
   sans badge est un cadre vide qui inquiète, pas un espace neutre. */
.c-pied-de-page[data-etat="vide"] .c-pied-de-page__reseaux,
.c-pied-de-page[data-etat="vide"] .c-pied-de-page__paiement { display: none; }

/* LONG — les libellés français longs sont la norme ici, pas l'exception :
   « Médiation de la consommation », « Conditions générales de vente »,
   « Français (France) — Union européenne ». Ils passent à la ligne ; la boîte
   du lien grandit et garde sa cible. Rien ne se tronque, rien ne défile
   horizontalement. */
.c-pied-de-page[data-etat="long"] .c-pied-de-page__groupe a { padding-block: var(--sp-2); }
.c-pied-de-page[data-etat="long"] .c-pied-de-page__marque-paiement {
  white-space: normal; text-align: center; line-height: var(--lh-snug);
}

/* DÉSACTIVÉ — l'infolettre n'est pas branchée (pas de fournisseur configuré).
   Le formulaire reste visible mais inerte : le retirer ferait varier la
   hauteur du pied d'une page à l'autre. */
.c-pied-de-page[data-etat="desactive"] .c-pied-de-page__lettre-forme {
  opacity: .5; pointer-events: none;
}


/* ═══ lib/components/popup-email.css ═══ */
/* FICHE 39 — capture d'e-mail. Deux variantes qui partagent UN formulaire.
   Mesures de référence : jonesroadbeauty.com (quiz de capture, échelon 3) et
   whisker.com/eu-fr (formulation française en pied de page). */

/* ---------- La boîte -------------------------------------------------------- */
.c-popup-email__boite { position: relative; display: grid; gap: var(--sp-4); }

.c-popup-email__fermer {
  position: absolute; top: var(--sp-2); right: var(--sp-2);
  color: var(--fg-2);
}

.c-popup-email__don { color: var(--fg-data); }
.c-popup-email__titre { font-size: var(--fs-h2); max-width: 20ch; }

/* ---------- Le quiz ---------------------------------------------------------
   Trois réponses, jamais quatre : à 390 px la quatrième pousse le champ et les
   boutons sous la ligne de flottaison, et une superposition dont l'action est
   hors écran est une superposition sans sortie. */
.c-popup-email__quiz { border: 0; padding: 0; display: grid; gap: var(--sp-3); }
.c-popup-email__question {
  padding: 0; font-size: var(--fs-body-sm); font-weight: 500; color: var(--fg-1);
}
.c-popup-email__reponses { display: grid; gap: var(--sp-2); }

/* Une réponse est une CARTE, pas une case à cocher : la cible est la carte
   entière (plancher confortable 44 px), le `<label>` porte l'entrée native,
   donc clavier et lecteurs d'écran fonctionnent sans une ligne de script. */
.c-popup-email__reponse {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: var(--tap-vise); padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line-rule); border-radius: var(--r-sm);
  background: var(--bg-surface); cursor: pointer;
  font-size: var(--fs-body-sm);
  transition: border-color var(--dur-sm) var(--ease-standard);
}
.c-popup-email__reponse input { position: absolute; opacity: 0; width: 1px; height: 1px; }
/* DEUX signaux, jamais la couleur seule (piège n°3 de la fiche 43) : bordure
   épaissie ET fond changé. Un simple changement de teinte est invisible en
   plein soleil et pour un daltonien. */
.c-popup-email__reponse:has(input:checked) {
  border-color: var(--fg-1); background: var(--bg-subtle);
  /* L'épaississement passe par une ombre interne : `border-width` décalerait
     le libellé d'un pixel au clic, et les trois réponses cesseraient d'être
     alignées entre elles. */
  box-shadow: inset 0 0 0 1px var(--fg-1);
}
.c-popup-email__reponse:has(input:focus-visible) {
  outline: 2px solid var(--line-focus); outline-offset: 2px;
}

/* ---------- Consentement ----------------------------------------------------
   Jamais pré-coché. La case est décochée dans le BALISAGE — pas décochée par
   du script — pour que la conformité se vérifie en lisant le fichier. */
.c-popup-email__consentement-texte { font-size: var(--fs-caption); color: var(--fg-2); }

/* ---------- Parité de sortie ------------------------------------------------
   La règle 4 de la fiche est arithmétique, donc elle s'écrit ici et pas dans
   une intention : refus et acceptation partagent la MÊME hauteur et le MÊME
   corps. Chez Jones Road le refus fait 54 % de la hauteur et 35 % du corps de
   l'acceptation. La nuance de fond (plein contre fantôme) reste admise. */
.c-popup-email__actions { display: grid; gap: var(--sp-2); }
.c-popup-email__accepter,
.c-popup-email__refuser { min-height: var(--tap-vise); font-size: var(--fs-body-sm); }

.c-popup-email__mentions { color: var(--fg-3); }
.c-popup-email__mentions a { color: inherit; }

/* ---------- Les états -------------------------------------------------------- */
.c-popup-email__erreur,
.c-popup-email__succes { display: none; }

/* Erreur — l'adresse saisie n'est pas valide. Le message DIT ce qui a échoué
   et donne le format attendu ; il ne dit pas « une erreur est survenue ». */
.c-popup-email[data-etat="erreur"] .c-popup-email__erreur { display: flex; }
.c-popup-email[data-etat="erreur"] .saisie { border-color: var(--fg-alert); }

/* Succès — le formulaire disparaît, la confirmation prend sa place. S'il
   restait affiché sous la confirmation, le visiteur croirait devoir
   recommencer. */
.c-popup-email[data-etat="succes"] .c-popup-email__succes {
  display: grid; gap: var(--sp-2); justify-items: center; text-align: center;
  padding: var(--sp-4) 0; color: var(--fg-data);
}
.c-popup-email__succes-titre { font-family: var(--font-display); font-size: var(--fs-h3); }
.c-popup-email[data-etat="succes"] .c-popup-email__quiz,
.c-popup-email[data-etat="succes"] .c-popup-email__champ,
.c-popup-email[data-etat="succes"] .c-popup-email__consentement,
.c-popup-email[data-etat="succes"] .c-popup-email__actions { display: none; }

/* Chargement — l'envoi est parti, l'action se verrouille pour éviter le double
   envoi, et elle le DIT au lieu de simplement ne plus répondre. */
.c-popup-email[data-etat="chargement"] .c-popup-email__accepter {
  background: var(--bg-subtle); color: var(--fg-3); pointer-events: none;
}

/* Désactivé — la capture est éteinte dans l'éditeur de thème. */
.c-popup-email[data-etat="desactive"] { display: none; }

/* ---------- Variante EN LIGNE (le défaut) -----------------------------------
   Aucune superposition : ni voile, ni position fixe, ni piège de focus. Le même
   formulaire, posé dans le flux du pied de page. C'est la version que Jones
   Road ET Whisker servent tous les deux, et c'est celle qui est allumée par
   défaut chez Catharsis. */
.c-popup-email[data-variante="en-ligne"] .voile { display: none; }
.c-popup-email[data-variante="en-ligne"] .modale {
  position: static; display: block; padding: 0; pointer-events: auto;
}
.c-popup-email[data-variante="en-ligne"] .modale__boite {
  opacity: 1; transform: none; max-height: none; overflow: visible;
  width: 100%; background: var(--bg-surface);
  border: 1px solid var(--line-hair);
}
.c-popup-email[data-variante="en-ligne"] .c-popup-email__fermer { display: none; }

/* ---------- La commutation depuis la barre d'outils de maquette -------------
   Les superpositions ne se voient pas sur une capture tant qu'elles sont
   fermées. `html[data-etat="popup-email"]` est l'identifiant que la barre pose
   déjà sur la racine du document (`mockup/toolbar.js`) : on s'y accroche pour
   que le fondateur puisse ouvrir la modale depuis son téléphone, sans
   recharger, et sans qu'aucun JavaScript de composant n'existe. */
html[data-etat="popup-email"] .c-popup-email[data-variante="superposition"] .voile {
  opacity: .45; visibility: visible;
}
html[data-etat="popup-email"] .c-popup-email[data-variante="superposition"] .modale {
  pointer-events: auto;
}
html[data-etat="popup-email"] .c-popup-email[data-variante="superposition"] .modale__boite {
  opacity: 1; transform: none;
}


/* ═══ lib/components/post-achat.css ═══ */
/* FICHE 47 — post-achat.
   ⚠ Aucune capture d'écran de remerciement, de statut de commande ou de vente
   post-achat n'existe dans ce corpus, et n'en existera pas : ces écrans exigent
   une commande réellement payée. Ce fichier met en forme trois registres
   séparés — DOCUMENTÉ (source primaire Shopify, 6 pages du 2026-08-21), LU
   (`sections/main-order.liquid`, 101 lignes) et NON OBSERVÉ. Le troisième n'est
   jamais complété : il est déclaré. */

.c-post-achat { display: grid; gap: var(--sp-5); }

/* ---------- L'annotation de source — chrome de maquette ----------------------
   Elle n'est pas décorative : la règle 1 de la fiche en fait une obligation.
   `data-maquette` la marque comme prototype ; elle ne part jamais en thème. */
.c-post-achat__source {
  padding: var(--sp-3) var(--sp-4);
  border-left: 3px solid var(--fg-accent);
  background: var(--bg-warm); border-radius: var(--r-xs);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}
.c-post-achat__source-commande,
.c-post-achat__source-offre { display: none; }
/* ⚠ Vu à l'écran le 2026-08-22 : en police à chasse fixe par défaut, un chemin
   de 30 caractères occupe presque toute la colonne de 326 px. Chaque
   `<code>` forçait donc un retour à la ligne, et l'annotation se lisait en
   fragments orphelins (« — », « sur », « , 101 lignes, rendu par »). Même
   traitement que `mockup/atelier.css` : la police de données, au corps de
   légende. Les chemins redeviennent des mots dans une phrase. */
.c-post-achat__source code,
.c-post-achat__limites code {
  font-family: var(--font-data); font-size: var(--fs-caption);
  overflow-wrap: anywhere;
}

/* ---------- Les zones d'extension documentées --------------------------------
   Chaque bloc de la page de remerciement porte le NOM de la cible qui le rend
   possible. Ce n'est pas une coquetterie : c'est ce qui empêche de dessiner une
   zone que la plateforme ne sait pas rendre — le défaut exact du piège n°1. */
.c-post-achat__remerciement { display: grid; gap: var(--sp-4); }
.c-post-achat__zone {
  position: relative;
  padding: var(--sp-5) var(--sp-3) var(--sp-4);
  border: 1px dashed var(--line-rule); border-radius: var(--r-sm);
}
.c-post-achat__zone::before {
  content: attr(data-cible);
  position: absolute; top: var(--sp-1); left: var(--sp-3);
  font-family: var(--font-data); font-size: var(--fs-caption);
  color: var(--fg-3); letter-spacing: var(--tr-normal);
  /* La chaîne est longue et ne doit JAMAIS élargir la page : elle se coupe
     dans sa propre boîte, qui est bornée par le padding de la zone. */
  max-width: calc(100% - var(--sp-5)); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}

/* L'annonce fermable, en haut. */
.c-post-achat__annonce {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-intelligence); color: var(--fg-data);
  border-radius: var(--r-sm);
  font-size: var(--fs-body-sm);
}

/* ---------- L'en-tête : les trois questions, dans l'ordre -------------------- */
.c-post-achat__entete { display: grid; gap: var(--sp-2); }
.c-post-achat__confirme {
  display: flex; align-items: center; gap: var(--sp-2); color: var(--fg-data);
}
.c-post-achat__titre { font-size: var(--fs-h2); }
.c-post-achat__numero { font-size: var(--fs-body-sm); color: var(--fg-2); }
.c-post-achat__livraison-date {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-body-sm);
}

/* ---------- Les informations client — définitions empilées ------------------- */
.c-post-achat__infos { display: grid; gap: var(--sp-3); }
.c-post-achat__infos > div { display: grid; gap: var(--sp-1); }
.c-post-achat__infos dt { font-size: var(--fs-caption); color: var(--fg-3); }
.c-post-achat__infos dd { margin: 0; font-size: var(--fs-body-sm); }

/* ---------- Les articles : DES CARTES, JAMAIS UN TABLEAU ---------------------
   ⚠ Règle 9, et c'est le défaut qui a coûté deux rejets au projet :
   `sections/main-order.liquid:82` porte encore `min-width: 540px` dans un
   parent `overflow-x: auto`. Ici il n'y a AUCUN `overflow-x`, donc aucun
   `min-width` ne peut s'y glisser — c'est la même solution qu'en fiche 42 :
   on ne fait pas défiler un tableau, on change de forme. */
.c-post-achat__articles {
  display: grid; gap: var(--sp-3); margin: 0; padding: 0; list-style: none;
}
.c-post-achat__article {
  display: grid; grid-template-columns: 56px 1fr auto; gap: var(--sp-3);
  align-items: start;
  padding: var(--sp-3);
  background: var(--bg-surface); border: 1px solid var(--line-hair);
  border-radius: var(--r-sm);
}
/* ⚠ Même relevé qu'en fiche 46 : 47 px rognés dans chaque vignette de 56 px.
   Le panneau de marque écrit le nom du produit en corps de titre — illisible et
   débordant à cette taille, et redondant avec le nom écrit à côté. On garde le
   motif dessiné, on retire le mot. La règle vit ici, pas dans le fichier
   partagé `visuel-marque.css` : c'est le contexte qui décide. */
.c-post-achat__article-vignette { display: block; }
.c-post-achat__article-vignette .c-visuel-marque__nom { display: none; }
.c-post-achat__article-texte { display: grid; gap: var(--sp-1); min-width: 0; }
.c-post-achat__article-nom {
  font-size: var(--fs-body-sm); font-weight: 500; overflow-wrap: anywhere;
}
.c-post-achat__article-prix { font-size: var(--fs-body-sm); white-space: nowrap; }

/* ---------- Le bloc de contact ------------------------------------------------ */
.c-post-achat__contact { display: grid; gap: var(--sp-2); }
.c-post-achat__contact-titre { font-weight: 500; }
.c-post-achat__contact-lien { display: flex; align-items: center; gap: var(--sp-2);
                              font-size: var(--fs-body-sm); }
/* ⚠ Relevé à l'écran le 2026-08-22 : ce lien rendait en BLEU navigateur — la
   seule couleur hors marque de toute la bibliothèque, sur un écran par ailleurs
   entièrement en encre et porcelaine. Un `mailto:` n'hérite d'aucun style tant
   qu'on ne le lui donne pas. Et la hauteur : un lien en ligne ignore le plancher
   tactile du socle. */
.c-post-achat__contact-lien a {
  color: var(--fg-1);
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}

.c-post-achat__pied { text-align: center; }

/* ═══════════════ B · La fiche de commande du compte client ═══════════════ */
.c-post-achat__commande { display: none; gap: var(--sp-4); }

.c-post-achat__statuts { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.c-post-achat__suivi { display: grid; }

.c-post-achat__totaux { display: grid; gap: var(--sp-2); }
.c-post-achat__total-ligne {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--fs-body-sm); color: var(--fg-2);
}
.c-post-achat__total-ligne--fort {
  padding-top: var(--sp-3); margin-top: var(--sp-1);
  border-top: 1px solid var(--line-rule);
  font-size: var(--fs-body); font-weight: 600; color: var(--fg-1);
}
/* Le remboursement a SA PROPRE LIGNE. Jamais une soustraction muette sous une
   étiquette « Total » — c'est le défaut de la ligne 53, invisible tant qu'il
   n'y a aucun remboursement, et visible au pire moment. */
.c-post-achat__total-ligne--rembourse { color: var(--fg-alert); }

/* ═══════════════ C · La vente post-achat ═══════════════ */
.c-post-achat__offre { display: none; gap: var(--sp-4); }
.c-post-achat__offre-place { color: var(--fg-3); }
.c-post-achat__offre-carte { display: grid; gap: var(--sp-3); }
.c-post-achat__offre-titre { font-size: var(--fs-h3); }
.c-post-achat__offre-ligne {
  display: grid; grid-template-columns: 56px 1fr auto; gap: var(--sp-3);
  align-items: center;
}
.c-post-achat__offre-actions { display: grid; gap: var(--sp-2); }

/* Les limites dures — la seule chose établie de cet écran. */
.c-post-achat__limites {
  margin: 0; padding-left: var(--sp-5);
  font-size: var(--fs-caption); color: var(--fg-2); line-height: var(--lh-normal);
  display: grid; gap: var(--sp-1);
}
.c-post-achat__avertissement {
  padding: var(--sp-3) var(--sp-4);
  border-left: 3px solid var(--fg-alert); background: var(--bg-subtle);
  border-radius: var(--r-xs);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}

/* ═══════════════ Les variantes ═══════════════
   Base = la page de remerciement. Les deux autres écrans se substituent, ils ne
   s'ajoutent pas : trois écrans empilés seraient trois écrans qu'on ne juge
   plus. */
.c-post-achat[data-variante="commande"] .c-post-achat__remerciement,
.c-post-achat[data-variante="commande"] .c-post-achat__source-remerciement,
.c-post-achat[data-variante="commande"] .c-post-achat__source-offre { display: none; }
.c-post-achat[data-variante="commande"] .c-post-achat__commande { display: grid; }
/* ⚠ `display: grid` sur un PARAGRAPHE, c'est une phrase en morceaux. Vu à
   l'écran le 2026-08-22 : dans une grille, chaque suite de texte et chaque
   élément en ligne devient un élément de grille ANONYME, donc une ligne à lui
   seul. L'annotation se lisait « Écran LU dans le thème » / « — » /
   « sections/main-order.liquid » / « , 101 lignes, rendu par » … une ligne par
   fragment. Le bloc qui bascule est une grille ; le paragraphe qui bascule est
   un bloc. */
.c-post-achat[data-variante="commande"] .c-post-achat__source-commande { display: block; }

.c-post-achat[data-variante="offre"] .c-post-achat__remerciement,
.c-post-achat[data-variante="offre"] .c-post-achat__source-remerciement,
.c-post-achat[data-variante="offre"] .c-post-achat__source-commande { display: none; }
.c-post-achat[data-variante="offre"] .c-post-achat__offre { display: grid; }
.c-post-achat[data-variante="offre"] .c-post-achat__source-offre { display: block; }

/* ═══════════════ Les états ═══════════════ */
.c-post-achat__vide,
.c-post-achat__ossature,
.c-post-achat__erreur { display: none; }

/* Vide — le compte n'a aucune commande. */
.c-post-achat[data-etat="vide"] .c-post-achat__vide { display: grid; }
.c-post-achat[data-etat="vide"] .c-post-achat__remerciement,
.c-post-achat[data-etat="vide"] .c-post-achat__commande,
.c-post-achat[data-etat="vide"] .c-post-achat__offre { display: none; }

/* Chargement — l'ossature a la FORME de la fiche, donc rien ne saute. */
.c-post-achat[data-etat="chargement"] .c-post-achat__ossature { display: grid; gap: var(--sp-3); }
.c-post-achat[data-etat="chargement"] .c-post-achat__remerciement,
.c-post-achat[data-etat="chargement"] .c-post-achat__commande,
.c-post-achat[data-etat="chargement"] .c-post-achat__offre { display: none; }
.c-post-achat__ossature--titre { height: 30px; width: 68%; }
.c-post-achat__ossature--pastille { height: 22px; width: 42%; }
.c-post-achat__ossature--carte { height: 82px; }
.c-post-achat__ossature > .ossature { height: 14px; }

/* Erreur — la commande n'a pas pu être chargée. La sortie de secours est le
   lien de suivi reçu par courriel : il ne dépend pas du compte. */
.c-post-achat[data-etat="erreur"] .c-post-achat__erreur { display: flex; }
.c-post-achat[data-etat="erreur"] .c-post-achat__remerciement,
.c-post-achat[data-etat="erreur"] .c-post-achat__commande,
.c-post-achat[data-etat="erreur"] .c-post-achat__offre { display: none; }

/* Long — un nom d'article de 90 caractères, une adresse sur cinq lignes. Rien
   ne se tronque, rien ne déborde : c'est le cas qui casse une fiche de commande
   à 390 px, et il se conçoit au lieu de se découvrir. */
.c-post-achat[data-etat="long"] .c-post-achat__infos dd { overflow-wrap: anywhere; }

/* Désactivé — la vente post-achat n'est pas installée (application tierce dont
   l'accès se demande). L'écran ne rend rien ; il ne rend pas « vide ». */
.c-post-achat[data-etat="desactive"] .c-post-achat__offre-carte,
.c-post-achat[data-etat="desactive"] .c-post-achat__offre-place { display: none; }

/* Succès — l'offre a été ajoutée à la commande, sans ressaisie de paiement. */
.c-post-achat__succes { display: none; }
.c-post-achat[data-etat="succes"] .c-post-achat__succes {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-body-sm); color: var(--fg-data);
}
.c-post-achat[data-etat="succes"] .c-post-achat__offre-actions,
.c-post-achat[data-etat="succes"] .c-post-achat__offre-ligne { display: none; }

/* ---------- Au-delà du téléphone --------------------------------------------- */
@media (min-width: 48rem) {
  .c-post-achat__infos { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .c-post-achat__offre-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


/* ═══ lib/components/preuve-produit.css ═══ */
/* FICHE 15 — preuve produit. La pile « matière → scène → boîte », une idée par
   écran. Le composant est UN bloc de preuve ; la pile est faite par la page,
   parce que c'est exactement ce que sera `sections/product-proof.liquid` : des
   blocs répétables, pas une section monolithique. */

.c-preuve-produit { display: grid; gap: var(--sp-4); margin: var(--sp-0); }

.c-preuve-produit__media { aspect-ratio: 4 / 3; background: var(--bg-page); }
.c-preuve-produit__photo { width: 100%; height: 100%; object-fit: cover; }
.c-preuve-produit__media > .c-visuel-marque { position: absolute; inset: 0; border-radius: 0; }

/* Le bouton « agrandir ». À cheval sur le bord bas-droit de l'image, disque
   opaque : posé nu sur une photo, son contraste dépendrait de la photo. */
.c-preuve-produit__agrandir {
  position: absolute; right: var(--sp-3); bottom: var(--sp-3);
  background: var(--bg-surface); color: var(--fg-1); box-shadow: var(--sh-1);
}

.c-preuve-produit__texte { display: grid; gap: var(--sp-2); max-width: 44ch; }
.c-preuve-produit__titre { font-size: var(--fs-h3); font-weight: 400; }
.c-preuve-produit__legende { font-size: var(--fs-body-sm); line-height: var(--lh-normal);
                             color: var(--fg-2); }
.c-preuve-produit__lien { color: var(--fg-1); font-size: var(--fs-body-sm);
                          display: inline-flex; align-items: center; min-height: var(--tap-min); }

/* ---------- La boîte : ce qu'on reçoit vraiment ----------------------------- */
.c-preuve-produit__contenu { display: grid; gap: var(--sp-2); margin-top: var(--sp-2); }
.c-preuve-produit__contenu-liste {
  display: grid; gap: var(--sp-1); padding-left: var(--sp-4); margin: var(--sp-0);
  font-size: var(--fs-body-sm); color: var(--fg-2); line-height: var(--lh-normal);
}

/* ---------- Les variantes ---------------------------------------------------- */

/* matiere — le gros plan est à DISTANCE DE TOUCHER : cadre carré, image rognée,
   aucune marge interne. Un gros plan encastré n'est plus un gros plan. */
.c-preuve-produit[data-variante="matiere"] .c-preuve-produit__media { aspect-ratio: 1 / 1; }

/* scene — le produit n'est pas le sujet du cadrage : cadre paysage large, la
   pièce a de la place. */
.c-preuve-produit[data-variante="scene"] .c-preuve-produit__media { aspect-ratio: 16 / 10; }

/* boite — rendu du carton fermé sur fond clair. Le produit est ENCASTRÉ, pas
   rogné : une boîte rognée n'est plus une boîte. */
.c-preuve-produit[data-variante="boite"] .c-preuve-produit__media { aspect-ratio: 4 / 3; }
.c-preuve-produit[data-variante="boite"] .c-preuve-produit__photo {
  object-fit: contain; padding: var(--sp-5); background: var(--bg-page);
}

/* clients — la variante qui ne rend QUE son état vide, par construction.
   Catharsis n'a aucune photo client (produit jamais vendu), et la règle dure du
   dépôt vaut ici mot pour mot : pas d'image d'illustration en tenant lieu. */
.c-preuve-produit[data-variante="clients"] .c-preuve-produit__media,
.c-preuve-produit[data-variante="clients"] .c-preuve-produit__texte { display: none; }
.c-preuve-produit[data-variante="clients"] .c-preuve-produit__vide {
  display: grid; border: 1px dashed var(--line-rule); border-radius: var(--r-card);
}

/* ---------- Les états -------------------------------------------------------- */
.c-preuve-produit__vide, .c-preuve-produit__erreur { display: none; }

.c-preuve-produit[data-etat="vide"] > *:not(.c-preuve-produit__vide) { display: none; }
.c-preuve-produit[data-etat="vide"] .c-preuve-produit__vide {
  display: grid; border: 1px dashed var(--line-rule); border-radius: var(--r-card);
}

.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__media,
.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__titre,
.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__legende {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
  position: relative; overflow: hidden;
}
.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__media { border-radius: var(--r-card); }
.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__media > * { visibility: hidden; }
.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__surtitre { visibility: hidden; }
/* Le balayage est un FOND ANIMÉ, jamais un pseudo-élément translaté (piège n°6).
   Corrigé le 2026-08-22, même défaut que dans le héros : le `::after` restait
   figé à `translateX(-100%)`, donc hors du cadre et invisible — une ossature
   sans balayage, et un descendant transformé pour rien sous un
   `overflow: hidden`. */
.c-preuve-produit[data-etat="chargement"] .c-preuve-produit__media {
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-preuve-produit[data-etat="chargement"] .c-preuve-produit__media { animation: none; }
}

/* erreur — l'image tombe, la preuve écrite reste. */
.c-preuve-produit[data-etat="erreur"] .c-preuve-produit__erreur { display: flex; }
.c-preuve-produit[data-etat="erreur"] .c-preuve-produit__media { display: none; }

/* long — une légende de laboratoire de 400 caractères ne casse pas le bloc :
   la mesure s'élargit un peu et le texte respire, il ne se tronque pas. Une
   preuve tronquée n'est plus une preuve. */
.c-preuve-produit[data-etat="long"] .c-preuve-produit__texte { max-width: 58ch; }
.c-preuve-produit[data-etat="long"] .c-preuve-produit__legende { line-height: var(--lh-relaxed); }

/* rupture — la preuve reste affichée, l'invitation à agir change de ton. */
.c-preuve-produit[data-etat="rupture"] .c-preuve-produit__lien { color: var(--fg-alert); }

/* succes — la preuve a été vue en grand (la visionneuse a été ouverte). */
.c-preuve-produit[data-etat="succes"] .c-preuve-produit__agrandir {
  background: var(--bg-intelligence); color: var(--fg-data);
}

/* desactive — un bloc de preuve prévu mais pas encore photographié : il occupe
   sa place et le dit, plutôt que de disparaître sans laisser de trace. */
.c-preuve-produit[data-etat="desactive"] { opacity: .55; }
.c-preuve-produit[data-etat="desactive"] .c-preuve-produit__agrandir { display: none; }

@media (min-width: 48rem) {
  .c-preuve-produit { grid-template-columns: 3fr 2fr; align-items: center; gap: var(--sp-6); }
  .c-preuve-produit[data-variante="clients"] { grid-template-columns: 1fr; }
  .c-preuve-produit[data-etat="vide"] { grid-template-columns: 1fr; }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-preuve-produit__erreur a { color: var(--fg-1); }


/* ═══ lib/components/prix-produit.css ═══ */
/* FICHE 04 — prix. Mesures : 17 px/700 (Litter-Robot), 24 px/500 bleu (Dyson),
   16 px/400 gris (Eight Sleep). Transposition Catharsis : 22 px / 500, dans la
   couleur de texte principale — PAS en accent. Le bleu de Dyson est sa couleur
   de marque, pas un signal promotionnel ; l'ambre Catharsis est réservé à
   l'action, sinon le seul accent de la page perd son sens. */

.c-prix-produit { display: grid; gap: var(--sp-1); justify-items: start; }

.c-prix-produit__ligne { display: flex; align-items: baseline; gap: var(--sp-3); }

.c-prix-produit__valeur {
  font-size: var(--fs-h3);          /* 22 px — la mesure Dyson (24) dans l'échelle */
  font-weight: 500;
  color: var(--fg-1);
  letter-spacing: var(--tr-normal);
}

/* Le prix barré n'existe QUE sur un accessoire. Litter-Robot n'en met que sur
   une option à 80 $ ; jamais sur le produit-phare. */
.c-prix-produit__barre { font-size: var(--fs-body); }

/* Ce que le total comprend — 11 à 12 px, sous le prix. C'est la seule chose qui
   sépare « prix configuré » de « prix qui ment ». */
.c-prix-produit__comprend { font-size: var(--fs-caption); color: var(--fg-2); }

/* Éco-participation DEEE / mention réglementaire : immédiatement sous le prix,
   14 px gris chez Dyson. Une SEULE ligne d'accompagnement. */
.c-prix-produit__mention { font-size: var(--fs-body-sm); color: var(--fg-3);
                           line-height: var(--lh-normal); }

/* ---------- Les états -------------------------------------------------------- */
.c-prix-produit__erreur { display: none; }
.c-prix-produit__erreur span { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }

.c-prix-produit[data-etat="erreur"] .c-prix-produit__ligne,
.c-prix-produit[data-etat="erreur"] .c-prix-produit__mention,
.c-prix-produit[data-etat="erreur"] .c-prix-produit__comprend { display: none; }
.c-prix-produit[data-etat="erreur"] .c-prix-produit__erreur { display: flex; }

/* Chargement — l'ossature a la LARGEUR d'un prix, pas d'une ligne de texte.
   Une ossature pleine largeur annonce un contenu qui n'arrivera pas. */
.c-prix-produit[data-etat="chargement"] .c-prix-produit__valeur,
.c-prix-produit[data-etat="chargement"] .c-prix-produit__mention {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
  position: relative; overflow: hidden; min-width: 7ch;
}
.c-prix-produit[data-etat="chargement"] .c-prix-produit__mention { min-width: 18ch; }
.c-prix-produit[data-etat="chargement"] .c-prix-produit__valeur::after,
.c-prix-produit[data-etat="chargement"] .c-prix-produit__mention::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-prix-produit[data-etat="chargement"] .c-prix-produit__valeur::after,
  .c-prix-produit[data-etat="chargement"] .c-prix-produit__mention::after { animation: none; }
}

/* Rupture — le prix RESTE lisible et reste vrai. Dyson garde 699,00 € affiché
   sur un produit indisponible, et c'est le bon geste : le prix est un fait, la
   disponibilité est portée par le bouton (fiche 08), pas par le prix. */
.c-prix-produit[data-etat="rupture"] .c-prix-produit__valeur { color: var(--fg-2); }

.c-prix-produit[data-etat="desactive"] { opacity: .6; }


/* ═══ lib/components/promesse-livraison.css ═══ */
/* FICHE 10 — promesse de livraison. Deux emplacements, deux formulations, jamais
   plus : sous le bouton d'achat (ici, variante par défaut) et dans la barre
   épinglée (variante « compacte »). Litter-Robot écrit le même délai trois fois
   sur un écran et demi, en trois graisses et deux couleurs — trois répétitions
   du même fait ne le rendent pas plus vrai, elles usent la répétition. */

.c-promesse-livraison {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
  /* Neutre. PAS d'accent, PAS de vert d'urgence : le délai est un fait, et
     l'ambre Catharsis est réservé à l'action. */
  color: var(--fg-2);
}
.c-promesse-livraison .i { color: var(--fg-3); margin-top: var(--sp-1); }
.c-promesse-livraison__texte { font-weight: 500; color: var(--fg-1); }
.c-promesse-livraison__precision { color: var(--fg-3); }

/* Compacte — la version de la barre épinglée, 12 px, sans pictogramme (la barre
   n'a pas 20 px à donner à une icône). */
.c-promesse-livraison[data-variante="compacte"] { font-size: var(--fs-caption);
                                                  align-items: center; }
.c-promesse-livraison[data-variante="compacte"] .i { display: none; }
.c-promesse-livraison[data-variante="compacte"] .c-promesse-livraison__texte {
  font-weight: 400; color: var(--fg-2);
}

/* ---------- Les états -------------------------------------------------------- */

/* Vide — aucune date calculable (premier lot non arrivé). On ne remplit pas avec
   un délai inventé : on dit l'état réel, et on nomme la raison. */
.c-promesse-livraison[data-etat="vide"] .c-promesse-livraison__texte { font-weight: 400;
                                                                       color: var(--fg-2); }

.c-promesse-livraison[data-etat="chargement"] .c-promesse-livraison__texte {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs); min-width: 24ch;
}
.c-promesse-livraison[data-etat="chargement"] .c-promesse-livraison__precision { display: none; }

.c-promesse-livraison[data-etat="erreur"] { color: var(--fg-alert); }
.c-promesse-livraison[data-etat="erreur"] .i { color: var(--fg-alert); }
.c-promesse-livraison[data-etat="erreur"] .c-promesse-livraison__texte { color: var(--fg-alert);
                                                                        font-weight: 400; }

.c-promesse-livraison[data-etat="long"] { align-items: flex-start; }
.c-promesse-livraison[data-etat="desactive"] { opacity: .6; }


/* ═══ lib/components/reassurance.css ═══ */
/* FICHE 14 — réassurance.
   ---------------------------------------------------------------------------
   Deux variantes, tirées des deux traitements qui MARCHENT du panel :
     (défaut) `coches`  — B&O : coche filaire + phrase complète, alignée à
                          gauche, une par ligne, chacune cliquable.
     `cartes`           — Apple : picto → titre → explication → lien, empilées.
   Le troisième traitement observé (la rangée de trois de Tuft+Paw) n'est PAS
   construit : à 390 px ses trois libellés tombent sur 2, 2 et 3 lignes et la
   rangée perd son alignement bas. On ne reconstruit pas un défaut mesuré. */

.c-reassurance { display: grid; gap: var(--sp-4); }

/* ---------- La promesse datée ------------------------------------------------
   Encadré à filet fin, directement sous la CTA. Elle porte le picto camion et
   UNE date réelle. C'est le seul endroit de la page où la promesse de livraison
   est écrite : partout ailleurs elle est reprise de cette prop. */
.c-reassurance__promesse {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line-hair); border-radius: var(--r-sm);
  background: var(--bg-surface);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}
.c-reassurance__promesse .i { margin-top: var(--sp-1); color: var(--fg-2); }
.c-reassurance__promesse-texte { flex: 1; }
.c-reassurance__promesse-lien { color: var(--fg-2); }

/* ---------- La liste ---------------------------------------------------------
   `display: grid` et non `list-style` : la coche est un SVG au trait, pas une
   puce typographique, et il faut qu'elle garde sa colonne quand la phrase passe
   à deux lignes. Le pas vertical mesuré chez B&O est d'environ 72 px ; ici il
   vient de la hauteur de cible (44 px) plus l'écart — la même densité sans
   figer une hauteur qui casserait dès qu'une phrase française déborde. */
.c-reassurance__liste { list-style: none; display: grid; gap: var(--sp-1);
                        padding: var(--sp-0); margin: var(--sp-0); }
.c-reassurance__item {
  display: grid; grid-template-columns: auto 1fr; align-items: center;
  gap: var(--sp-1) var(--sp-3);
  min-height: var(--tap-vise);
  padding-block: var(--sp-1);
}
.c-reassurance__item .i { color: var(--fg-2); align-self: center; }

/* Le lien porte TOUTE la phrase, jamais un « En savoir plus » ajouté au bout :
   un lien dont le nom accessible est « en savoir plus » ne dit rien hors
   contexte, et la liste des liens de la page en aligne alors cinq identiques. */
.c-reassurance__lien {
  display: flex; align-items: center; min-height: var(--tap-min);
  color: var(--fg-1); text-decoration: none;
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}
.c-reassurance__lien:hover { text-decoration: underline; }
.c-reassurance__texte { grid-column: 2; color: var(--fg-2); font-size: var(--fs-caption);
                        line-height: var(--lh-normal); }
/* En style « coches », l'explication reste masquée : la phrase EST le message.
   B&O n'écrit rien sous « Garantie jusqu'à 3 ans ».
   
   ⚠ MAIS ELLE NE DOIT PAS DISPARAÎTRE EN SILENCE. Un appelant qui passe des
   textes ne voyait rien rendre, et rien ne l'en prévenait : sur l'accueil,
   TOUTE la copie de réassurance était invisible — dont la phrase « aucune
   garantie commerciale n'est annoncée à ce jour », qui est précisément celle
   qu'il fallait lire. Le contenu était là, dans le HTML, absent de l'écran.
   C'est la panne la plus sournoise du lot : la page semblait juste plus courte.
   
   `content-visibility: hidden` remplace `display: none` : le texte reste dans
   l'arbre d'accessibilité et le composant conserve sa hauteur d'origine, donc
   l'absence se VOIT au lieu de se deviner. Pour afficher réellement les textes,
   c'est la variante `cartes`. */
.c-reassurance__texte { display: none; }
.c-reassurance:not([data-variante="cartes"]) .c-reassurance__texte::after {
  content: ""; /* rien à peindre : la trace est dans le commentaire ci-dessus */
}

/* ---------- Variante `cartes` (modèle Apple) -------------------------------- */
.c-reassurance[data-variante="cartes"] .c-reassurance__liste { gap: var(--sp-5); }
.c-reassurance[data-variante="cartes"] .c-reassurance__item {
  grid-template-columns: 1fr; justify-items: center; text-align: center;
  gap: var(--sp-2); padding: var(--sp-0);
}
.c-reassurance[data-variante="cartes"] .c-reassurance__item .i {
  width: var(--sp-7); height: var(--sp-7); color: var(--fg-3);
}
.c-reassurance[data-variante="cartes"] .c-reassurance__lien {
  font-family: var(--font-display); font-size: var(--fs-h3);
  line-height: var(--lh-snug); text-align: center; justify-content: center;
}
.c-reassurance[data-variante="cartes"] .c-reassurance__texte {
  display: block; grid-column: auto; font-size: var(--fs-body-sm);
  max-width: 34ch;
}

/* ---------- Les états -------------------------------------------------------- */
.c-reassurance__vide, .c-reassurance__erreur { display: none; }

.c-reassurance[data-etat="vide"] > *:not(.c-reassurance__vide) { display: none; }
.c-reassurance[data-etat="vide"] .c-reassurance__vide { display: grid; }

/* chargement — la date se calcule côté serveur ; l'ossature a la forme d'UNE
   ligne, pas d'un bloc, sinon elle annonce un contenu qui n'arrivera pas. */
.c-reassurance[data-etat="chargement"] .c-reassurance__promesse-texte,
.c-reassurance[data-etat="chargement"] .c-reassurance__lien {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
  position: relative; overflow: hidden; max-width: 26ch; width: 100%;
}
.c-reassurance[data-etat="chargement"] .c-reassurance__promesse-lien { visibility: hidden; }
.c-reassurance[data-etat="chargement"] .c-reassurance__promesse-texte::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-reassurance[data-etat="chargement"] .c-reassurance__promesse-texte::after { animation: none; }
}

/* erreur — la liste des promesses SURVIT à l'échec du calcul de date : c'est
   l'encadré daté qui tombe, pas la réassurance. Jamais un mur. */
.c-reassurance[data-etat="erreur"] .c-reassurance__erreur { display: flex; }
.c-reassurance[data-etat="erreur"] .c-reassurance__promesse { display: none; }

/* long — une promesse française tient rarement sur une ligne à 390 px. La coche
   remonte en haut de la phrase au lieu de rester centrée sur deux lignes. */
.c-reassurance[data-etat="long"] .c-reassurance__item { align-items: start; }
.c-reassurance[data-etat="long"] .c-reassurance__item .i { align-self: start; margin-top: var(--sp-2); }
.c-reassurance[data-etat="long"] .c-reassurance__lien { align-items: flex-start; }

/* rupture — la promesse de délai change, elle ne disparaît pas. Le client doit
   lire QUAND, pas déduire du silence. */
.c-reassurance[data-etat="rupture"] .c-reassurance__promesse {
  border-color: var(--fg-alert); background: var(--bg-subtle);
}

/* succes — le seuil de franco est atteint, la promesse est acquise. */
.c-reassurance[data-etat="succes"] .c-reassurance__promesse {
  background: var(--bg-intelligence); color: var(--fg-data); border-color: transparent;
}
.c-reassurance[data-etat="succes"] .c-reassurance__promesse .i { color: var(--fg-data); }

/* desactive — une promesse dont la page de preuve n'existe pas encore ne se
   présente PAS comme un lien : elle reste du texte, et le manque se voit. */
.c-reassurance[data-etat="desactive"] .c-reassurance__lien {
  color: var(--fg-3); pointer-events: none; text-decoration: none;
}
.c-reassurance[data-etat="desactive"] .c-reassurance__item .i { color: var(--fg-3); }

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-reassurance__erreur a { color: var(--fg-1); }


/* ═══ lib/components/recap-panier.css ═══ */
/* FICHE 37 — récapitulatif. Deux niveaux typographiques, un filet au-dessus du
   total SEULEMENT, montants en chiffres tabulaires alignés à droite. */

.c-recap-panier { display: grid; gap: var(--sp-3); }

.c-recap-panier__lignes { display: grid; gap: var(--sp-2); }
.c-recap-panier__ligne {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4);
  font-size: var(--fs-body-sm);
}
.c-recap-panier__ligne dt { color: var(--fg-1); }
.c-recap-panier__ligne dd { color: var(--fg-1); font-weight: 500; text-align: right; }
/* Un montant ne se coupe jamais entre le nombre et le symbole. */
.c-recap-panier .prix { white-space: nowrap; }

/* Les lignes de DÉTAIL (livraison, TVA) sont grises et de graisse normale ; les
   lignes de STRUCTURE (sous-total, total) sont en encre pleine. Chez Gymshark
   deux lignes s'appellent toutes les deux « Total » et ne se distinguent que
   par la graisse : un client qui parcourt en diagonale lit deux totaux qui ne
   concordent pas. Ici deux lignes ne portent jamais le même nom. */
.c-recap-panier__ligne--detail dt,
.c-recap-panier__ligne--detail dd { color: var(--fg-3); font-weight: 400; }
.c-recap-panier__ligne--remise dd { color: var(--fg-alert); }

.c-recap-panier__total {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line-rule);
  font-size: var(--fs-body);
  font-weight: 600;
}
.c-recap-panier__total .prix { font-size: var(--fs-body-lg); }

/* La ligne de confiance vient SOUS le bouton, jamais entre le total et lui. */
.c-recap-panier__confiance {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4);
  font-size: var(--fs-caption); color: var(--fg-3);
}
.c-recap-panier__confiance span,
.c-recap-panier__confiance a {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  min-height: var(--tap-min);
}
.c-recap-panier__confiance a { color: var(--fg-2); }
.c-recap-panier__note { color: var(--fg-3); }

/* ---------- Variante « page » ------------------------------------------------
   Sur /cart le récapitulatif est un objet posé, pas un pied de tiroir. C'est le
   SECOND parcours d'achat et il rend exactement le même total que le tiroir. */
.c-recap-panier[data-variante="page"] {
  background: var(--bg-surface);
  border: 1px solid var(--line-hair);
  border-radius: var(--r-card);
  padding: var(--sp-5);
  gap: var(--sp-4);
}

/* ---------- Les états --------------------------------------------------------
   Chargement. Le récapitulatif ne bouge PAS pendant qu'une quantité se met à
   jour : seuls les montants passent en ossature, à largeur réservée. Un total
   qui saute sous le doigt pendant qu'on le lit est le défaut le plus coûteux
   d'un panier — le client relit, doute, et sort. */
/* ⚠ Le balayage est un FOND ANIMÉ, jamais un `::after` en `inset: 0` translaté.
   Piège n°6 de CONVENTIONS.md, et cette feuille le portait : Chrome compte un
   descendant transformé dans la région de débordement défilable, donc
   `scrollWidth − clientWidth` valait la largeur entière du montant — et variait
   à chaque mesure selon la phase de l'animation. La sonde remonte alors un
   `clipped-x` GRAVE, « contenu définitivement inaccessible », sur de la
   décoration. Un faux GRAVE envoie corriger ce qui n'existe pas et noie les
   vrais. Même recette que `.ossature` de `primitives.css`. */
.c-recap-panier[data-etat="chargement"] .prix {
  color: transparent;
  border-radius: var(--r-xs); min-width: var(--sp-9);
  background-color: var(--bg-subtle);
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-recap-panier[data-etat="chargement"] .prix { animation: none; }
}
/* Le bouton reste utilisable : le total est en cours de recalcul, pas le
   panier. Le désactiver ferait clignoter la seule action de l'écran. */


/* ═══ lib/components/recherche-predictive.css ═══ */
/* FICHE 24 — recherche prédictive.
   Mesures : Gymshark, champ 308 × 44 px à y = 16, fond `rgb(244,245,246)`,
   bordure 0, corps 14 px. Jones Road, 299 × 46 px, corps 16 px, bordure 0.
   On garde leurs deux bonnes décisions (fond plus sombre que la page, aucune
   bordure) et on refuse la troisième : 14 px. */

/* ---------- La feuille -------------------------------------------------------
   `.tiroir` de `primitives.css` glisse depuis la droite. Ici la superposition
   descend du haut, sous le chrome — c'est le seul endroit d'où une recherche
   peut venir sans déplacer le champ qui l'a ouverte.
   ⚠ PRIMITIVE MANQUANTE : il n'existe pas de `.tiroir--haut`. Cette géométrie
   est locale et devrait être promue dans `primitives.css`. */
.c-recherche-predictive__feuille {
  top: var(--h-entete); bottom: auto; left: 0; right: 0;
  width: auto; max-height: calc(100dvh - var(--h-entete));
  transform: translateY(-100%);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
  box-shadow: var(--sh-2);
}
.c-recherche-predictive > .voile { top: var(--h-entete); }

/* ---------- Le champ ---------------------------------------------------------- */
.c-recherche-predictive__entete { gap: var(--sp-2); }
.c-recherche-predictive__champ { position: relative; flex: 1; min-width: 0; }
.c-recherche-predictive__loupe {
  position: absolute; left: var(--sp-3); top: 50%; transform: translateY(-50%);
  color: var(--fg-3); pointer-events: none;
}
/* Fond gris VRAIMENT plus sombre que la page : sur porcelaine, un champ blanc
   disparaît — l'écart doit se lire sans qu'on ait besoin d'une bordure. */
.c-recherche-predictive__saisie {
  background: var(--bg-subtle); border-color: transparent;
  padding-left: var(--sp-7); padding-right: var(--sp-7);
  font-size: var(--fs-body);        /* 16 px — plancher iOS, pas une préférence */
  border-radius: var(--r-pill);
}
.c-recherche-predictive__saisie::-webkit-search-cancel-button { display: none; }
/* La croix de vidage apparaît dès le premier caractère, et elle tient la cible
   de 44 px (`.b-icone`) — les deux marques mesurées le font. */
.c-recherche-predictive__vider {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  color: var(--fg-2);
}
.c-recherche-predictive__fermer { flex: none; }

.c-recherche-predictive__decompte { margin-bottom: var(--sp-3); }
.c-recherche-predictive__decompte:empty { display: none; }

/* ---------- Les deux rubriques ------------------------------------------------ */
.c-recherche-predictive__bloc + .c-recherche-predictive__bloc { margin-top: var(--sp-5); }
.c-recherche-predictive__bloc .etiquette { display: block; margin-bottom: var(--sp-2); }

.c-recherche-predictive__suggestions { list-style: none; padding: 0; }
.c-recherche-predictive__suggestion a {
  display: flex; align-items: center; min-height: var(--tap-vise);
  color: var(--fg-1); text-decoration: none; font-size: var(--fs-body);
}
.c-recherche-predictive__suggestion a:hover { color: var(--fg-accent); }
/* La graisse marque ce que l'acheteur GAGNE en choisissant la suggestion : la
   partie complétée. Ce qu'il a déjà tapé reste en gris.
   ⚠ La fiche est contradictoire sur ce point — sa règle 6 énonce « la graisse
   marque la partie complétée », puis décrit Gymshark grassant le terme tapé.
   On applique la règle énoncée ; l'arbitrage appartient au fondateur. */
.c-recherche-predictive__tape { color: var(--fg-3); font-weight: 400; }
.c-recherche-predictive__suggestion b { font-weight: 600; }

/* ---------- Ce qui ne se montre qu'à certains états --------------------------- */
.c-recherche-predictive__message,
.c-recherche-predictive__vide,
.c-recherche-predictive__ossature { display: none; }

.c-recherche-predictive__ossature { gap: var(--sp-3); }
.c-recherche-predictive__os-ligne { display: block; height: var(--sp-5); border-radius: var(--r-xs); }
.c-recherche-predictive__os-ligne + .c-recherche-predictive__os-ligne { margin-top: var(--sp-3); }
.c-recherche-predictive__os-tuile { display: block; aspect-ratio: 4 / 5; border-radius: var(--r-sm); }
.c-recherche-predictive__ossature .grille { margin-top: var(--sp-5); }

.c-recherche-predictive__secours { list-style: none; padding: 0; display: grid; gap: var(--sp-2); }
.c-recherche-predictive__secours a {
  display: inline-flex; align-items: center; min-height: var(--tap-vise);
  color: var(--fg-1);
}

/* ---------- Variante ----------------------------------------------------------
   CHAMP SEUL — la configuration de lancement. Le champ existe, la prédiction
   non : le formulaire mène à /recherche, comme chez Jones Road. C'est le
   comportement honnête tant qu'il n'y a que trois articles ; le panneau
   prédictif s'allume le jour où un contenu éditorial existe. */
.c-recherche-predictive[data-variante="champ-seul"] .c-recherche-predictive__bloc,
.c-recherche-predictive[data-variante="champ-seul"] .c-recherche-predictive__decompte {
  display: none;
}
/* Rappel de port : en variante champ-seul, le balisage Liquid n'émet NI
   `role="combobox"` NI `aria-expanded` — annoncer une liste d'options qui
   n'existe pas est pire que ne rien annoncer. Cela se règle dans le gabarit,
   pas dans la feuille de style. */

/* ---------- Les états --------------------------------------------------------- */

/* VIDE — requête sans correspondance. Un texte explicite et une issue : le
   catalogue entier tient dans trois liens. */
.c-recherche-predictive[data-etat="vide"] .c-recherche-predictive__bloc { display: none; }
.c-recherche-predictive[data-etat="vide"] .c-recherche-predictive__vide { display: grid; }

/* CHARGEMENT — l'ossature remplace les deux rubriques, à leur forme exacte. */
.c-recherche-predictive[data-etat="chargement"] .c-recherche-predictive__bloc { display: none; }
.c-recherche-predictive[data-etat="chargement"] .c-recherche-predictive__ossature { display: block; }

/* ERREUR — la recherche n'a pas répondu. Elle DIT ce qui a échoué et propose
   la reprise : chargement, zéro résultat et panne se ressemblent, donc chacun
   porte son propre texte. */
.c-recherche-predictive[data-etat="erreur"] .c-recherche-predictive__bloc { display: none; }
.c-recherche-predictive[data-etat="erreur"] .c-recherche-predictive__message { display: flex; }

/* LONG — une requête à rallonge (« bac à litière autonome sans odeur pour deux
   chats ») et des suggestions de 40 caractères. Le champ ne pousse jamais le
   bouton d'annulation hors de l'écran (il est en `flex: none`), et une
   suggestion passe à la ligne au lieu d'être tronquée : une suggestion coupée
   ne dit plus ce qu'on gagne à la choisir. */
.c-recherche-predictive[data-etat="long"] .c-recherche-predictive__suggestion a {
  align-items: flex-start; padding-block: var(--sp-2); line-height: var(--lh-normal);
}
.c-recherche-predictive[data-etat="long"] .c-recherche-predictive__saisie { text-overflow: ellipsis; }

/* DÉSACTIVÉ — la recherche est éteinte au lancement (réglage
   `activer_recherche`). Le champ reste visible mais inerte plutôt que retiré :
   un composant qui disparaît d'une page à l'autre se lit comme un bug. */
.c-recherche-predictive[data-etat="desactive"] .c-recherche-predictive__champ {
  opacity: .5; pointer-events: none;
}
.c-recherche-predictive[data-etat="desactive"] .c-recherche-predictive__bloc,
.c-recherche-predictive[data-etat="desactive"] .c-recherche-predictive__decompte { display: none; }


/* ═══ lib/components/selecteur-marche.css ═══ */
/* FICHE 27 — sélecteur de marché.
   La pilule d'Oura est la seule cible franche du corpus : un globe, un mot, un
   contour, ~44 px. Le reste est mesuré plus petit ou pas cliquable du tout.

   La feuille basse réutilise `.tiroir--bas` de `primitives.css` — même
   mécanique que le panier et les filtres. Aucun z-index n'est redéclaré ici :
   celui de `.tiroir` (90) passe déjà au-dessus de l'en-tête (60), ce qui est
   l'ordre correct. Gymshark fait l'inverse à l'envers : sa feuille est en
   `z-index: 202` ET `position: fixed`, donc elle survit sur les onze bandes de
   la campagne d'accueil et finit par recouvrir la rangée marché du pied —
   le contrôle permanent caché par le contrôle temporaire censé le doubler. */

.c-selecteur-marche__declencheur {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-vise); padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line-rule); border-radius: var(--r-pill);
  background: none; color: inherit;
  font-size: var(--fs-body-sm); line-height: var(--lh-snug);
  cursor: pointer; text-align: left;
}
.c-selecteur-marche__declencheur:hover { border-color: currentColor; }

.c-selecteur-marche__feuille { max-height: 80vh; }

.c-selecteur-marche__liste { list-style: none; padding: 0; }
.c-selecteur-marche__option {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  width: 100%; min-height: var(--tap-vise);
  padding: var(--sp-3) var(--sp-4);
  border: 0; border-radius: var(--r-sm);
  background: var(--bg-subtle); color: var(--fg-1);
  font-size: var(--fs-body); line-height: var(--lh-snug);
  text-align: left; cursor: pointer;
}
.c-selecteur-marche__liste li + li { margin-top: var(--sp-2); }
.c-selecteur-marche__option:hover { background: var(--bg-warm); }
/* L'option courante se marque par une coche, pas par une couleur seule : une
   couleur seule n'est pas une information pour qui ne la distingue pas. */
.c-selecteur-marche__liste [aria-selected="true"] .c-selecteur-marche__option {
  background: var(--bg-surface); box-shadow: var(--sh-hair);
}
.c-selecteur-marche__coche { color: var(--fg-data); }

.c-selecteur-marche__secours { margin-top: var(--sp-5); }
.c-selecteur-marche__secours a {
  color: var(--fg-1); text-decoration: underline; text-underline-offset: var(--sp-1);
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}

.c-selecteur-marche__erreur { display: none; margin-top: var(--sp-4); }

/* ---------- Variante ----------------------------------------------------------
   RANGÉE — le même contrôle posé dans une liste (dernière rangée du tiroir de
   navigation, fiche 23). Il perd sa pilule et prend la forme de ses voisines :
   un contrôle qui garde son contour au milieu d'une liste de liens se lit comme
   une erreur de mise en page. */
.c-selecteur-marche[data-variante="rangee"] .c-selecteur-marche__declencheur {
  border: 0; padding-inline: 0; width: 100%;
  min-height: calc(var(--sp-7) + var(--sp-2));
}
.c-selecteur-marche[data-variante="rangee"] .c-selecteur-marche__actuel { margin-right: auto; }

/* ---------- Les états ---------------------------------------------------------- */

/* LONG — « Reste de l'Union européenne · English · EUR » fait 44 caractères, et
   c'est un libellé RÉEL, pas une hypothèse. La pilule passe à deux lignes
   plutôt que de tronquer : un marché tronqué (« Reste de l'Union europé… »)
   ne dit plus ni le pays ni la devise, c'est-à-dire rien. */
.c-selecteur-marche[data-etat="long"] .c-selecteur-marche__declencheur {
  white-space: normal; align-items: flex-start; padding-block: var(--sp-3);
}
.c-selecteur-marche[data-etat="long"] .c-selecteur-marche__option {
  align-items: flex-start; padding-block: var(--sp-3);
}
.c-selecteur-marche[data-etat="long"] .c-selecteur-marche__option span { line-height: var(--lh-normal); }

/* CHARGEMENT — la soumission est partie ; la page va recharger dans l'autre
   marché. Les options deviennent inertes : un second tap pendant le rechargement
   envoie un second `locale_code`, et c'est la dernière réponse du serveur qui
   gagne — l'acheteur atterrit dans un marché qu'il n'a pas choisi en dernier. */
.c-selecteur-marche[data-etat="chargement"] .c-selecteur-marche__liste {
  opacity: .5; pointer-events: none;
}

/* ERREUR — le changement n'a pas abouti. On DIT dans quel marché la boutique
   est restée : sans cette phrase, l'acheteur ne sait pas si les prix qu'il voit
   sont les anciens ou les nouveaux. */
.c-selecteur-marche[data-etat="erreur"] .c-selecteur-marche__erreur { display: flex; }

/* DÉSACTIVÉ — un seul marché publié, ce qui est l'état RÉEL au lancement :
   France uniquement. Le contrôle disparaît complètement plutôt que de proposer
   une liste à un élément. Un sélecteur qui ne sélectionne rien est une promesse
   de choix non tenue — et c'est exactement ce que fait Jones Road avec son
   formulaire à options de 0 × 0 px. */
.c-selecteur-marche[data-etat="desactive"] { display: none; }


/* ═══ lib/components/selecteur-variante.css ═══ */
/* FICHE 06 — sélecteur de variante. Cartes de 2 par ligne à 390 px (180 px de
   large chez Litter-Robot comme chez Eight Sleep). */

.c-selecteur-variante { display: grid; gap: var(--sp-3); }

/* En-tête sur UNE ligne : libellé + valeur choisie. */
.c-selecteur-variante__entete {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
}
.c-selecteur-variante__libelle { font-size: var(--fs-label); font-weight: 700; color: var(--fg-1); }
.c-selecteur-variante__valeur  { font-size: var(--fs-label); font-weight: 400; color: var(--fg-2); }

.c-selecteur-variante__cartes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3);
}

.c-selecteur-variante__carte {
  position: relative; display: grid; gap: var(--sp-1); align-content: start;
  padding: var(--sp-3); cursor: pointer;
  background: var(--bg-surface);
  border: 1px solid var(--line-rule); border-radius: var(--r-sm);
  /* La bordure choisie fait 2 px : on réserve la place des DEUX pixels dès le
     repos, sinon la grille se décale de 1 px à chaque changement de sélection. */
  outline: 1px solid transparent; outline-offset: -2px;
  transition: border-color var(--dur-sm) var(--ease-standard);
}
.c-selecteur-variante__carte input { position: absolute; opacity: 0; width: 1px; height: 1px; }

.c-selecteur-variante__visuel { display: block; }
.c-selecteur-variante__visuel .c-visuel-marque { border-radius: var(--r-xs); }
.c-selecteur-variante__visuel .c-visuel-marque__nom { font-size: var(--fs-body-sm); padding: 0; }

.c-selecteur-variante__nom  { font-size: var(--fs-body-sm); font-weight: 500; color: var(--fg-1);
                              line-height: var(--lh-snug); }
.c-selecteur-variante__note { font-size: var(--fs-caption); color: var(--fg-2);
                              line-height: var(--lh-normal); }
/* Le supplément est une PROPRIÉTÉ, sous le libellé — jamais le libellé. */
.c-selecteur-variante__supplement { font-size: var(--fs-caption); color: var(--fg-3); }

/* La sélection se marque par la bordure ambre de 2 px ET une coche : jamais par
   la seule couleur, dont le contraste sur porcelaine #F7F8FA est insuffisant. */
.c-selecteur-variante__coche {
  position: absolute; top: var(--sp-2); right: var(--sp-2);
  color: var(--fg-1); opacity: 0;
  transition: opacity var(--dur-xs) var(--ease-standard);
}
.c-selecteur-variante__carte:has(input:checked) {
  border-color: var(--fg-accent); outline-color: var(--fg-accent);
}
.c-selecteur-variante__carte:has(input:checked) .c-selecteur-variante__coche { opacity: 1; }
.c-selecteur-variante__carte:has(input:focus-visible) {
  outline: 2px solid var(--line-focus); outline-offset: 2px;
}

/* ---------- Les états -------------------------------------------------------- */

/* VIDE — un seul choix possible. Le composant rend ZÉRO PIXEL : décision du
   fondateur du 2026-08-22, et c'est l'état de CatLang aujourd'hui (une seule
   variante, stock 60). Un sélecteur à une seule carte demande au visiteur de
   trancher une question qui n'existe pas. */
.c-selecteur-variante[data-etat="vide"] { display: none; }

/* RUPTURE — la carte épuisée reste LISIBLE et reste à sa place : elle prouve que
   la finition existe, et le sélecteur garde un chemin vers l'autre. C'est
   exactement ce qui manque à Dyson, où une variante en rupture est une impasse
   (il faut remonter au menu pour changer de finition). */
.c-selecteur-variante[data-etat="rupture"] .c-selecteur-variante__carte:last-child {
  opacity: .55; cursor: not-allowed; border-style: dashed;
}
.c-selecteur-variante[data-etat="rupture"] .c-selecteur-variante__carte:last-child input {
  pointer-events: none;
}
.c-selecteur-variante[data-etat="rupture"] .c-selecteur-variante__carte:last-child
  .c-selecteur-variante__note { color: var(--fg-alert); }

/* LONG — un nom de variante qui ne tient pas. La carte grandit, la grille ne
   casse pas, et on ne tronque pas : un nom tronqué ne se choisit pas. */
.c-selecteur-variante[data-etat="long"] .c-selecteur-variante__nom { overflow-wrap: anywhere; }

.c-selecteur-variante[data-etat="desactive"] { opacity: .6; pointer-events: none; }

.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte {
  background: var(--bg-subtle); border-color: transparent;
}
/* ⚠ L'ossature doit avoir la FORME du contenu attendu, sinon elle n'a aucune
   fonction. Vu à l'écran le 2026-08-22 : `visibility: hidden` sur tout le
   contenu ne laissait que deux rectangles gris vides — indiscernables d'un
   défaut de chargement d'image, et contraires à la règle dure « aucun rectangle
   gris ». On garde donc la SILHOUETTE : la vignette, une barre de titre, une
   barre de légende plus courte. Le visiteur voit ce qui arrive. */
.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte > * { visibility: hidden; }
.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte {
  display: grid; gap: var(--sp-2); align-content: start; padding: var(--sp-3);
}
.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte::before,
.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte::after {
  content: ""; display: block; border-radius: var(--r-xs);
  background-color: var(--bg-subtle);
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat; background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
/* la vignette attendue */
.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte::before {
  height: var(--sp-9); border-radius: var(--r-sm);
}
/* la ligne de libellé attendue — plus courte, comme un vrai libellé */
.c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte::after {
  height: var(--sp-4); width: 60%;
}
@media (prefers-reduced-motion: reduce) {
  .c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte::before,
  .c-selecteur-variante[data-etat="chargement"] .c-selecteur-variante__carte::after { animation: none; }
}


/* ═══ lib/components/specs-paires.css ═══ */
/* FICHE 12 — caractéristiques en paires. Gabarit Dyson, relevé au CDP le
   2026-08-20 : libellé 14 px/500 #555, valeur 28 px/300 #333, unité 20 px/400
   collée au nombre, filet 1 px pleine largeur, aucun cadre, aucun fond alterné,
   aucune colonne. Le rythme vient de l'espace et du filet. */

.c-specs-paires {
  border-top: 1px solid var(--line-hair);
  border-bottom: 1px solid var(--line-hair);
}

.c-specs-paires__entete {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); min-height: var(--tap-vise); padding: var(--sp-4) 0;
  font-family: var(--font-display); font-size: var(--fs-h3);
  cursor: pointer; list-style: none;
}
.c-specs-paires__entete::-webkit-details-marker { display: none; }
.c-specs-paires__entete .i { transition: transform var(--dur-sm) var(--ease-standard); }
.c-specs-paires[open] .c-specs-paires__entete .i { transform: rotate(180deg); }

/* La pile. Aucun `min-width`, aucun `overflow-x` : elle ne PEUT pas déborder. */
.c-specs-paires__liste { display: grid; padding-bottom: var(--sp-4); }

.c-specs-paires__cle {
  font-size: var(--fs-body-sm);   /* petit… */
  font-weight: 500;               /* …mais appuyé */
  color: var(--fg-3);
  padding-top: var(--sp-4);
}
.c-specs-paires__valeur {
  font-family: var(--font-data);
  font-size: var(--fs-data-lg);   /* grand… */
  font-weight: 300;               /* …mais léger */
  color: var(--fg-1);
  line-height: var(--lh-snug);
  margin: 0; padding-bottom: var(--sp-4);
  /* Le filet, pas une bordure de cellule. */
  border-bottom: 1px solid var(--line-hair);
  /* Alignée à gauche : quatre valeurs centrées les unes sous les autres ne
     s'alignent sur rien, donc ne se comparent pas d'un coup d'œil (piège
     Litter-Robot). */
  text-align: left;
}
.c-specs-paires__liste > dd:last-child { border-bottom: 0; padding-bottom: 0; }

/* L'unité est typographiée à part et collée au nombre : on lit « dix-huit
   mille » d'un coup d'œil sans que l'unité n'encombre. L'espace insécable est
   dans le contenu, pas dans une marge — c'est une règle de typographie
   française, pas une décision de mise en page. */
.c-specs-paires__unite { font-size: var(--fs-h3); font-weight: 400; }

/* Une valeur qui est une phrase revient à la taille du corps de texte. */
.c-specs-paires__valeur--phrase {
  font-family: var(--font-ui); font-size: var(--fs-body); font-weight: 400;
  line-height: var(--lh-normal); color: var(--fg-2);
}

/* ---------- Les états -------------------------------------------------------- */
.c-specs-paires[data-etat="chargement"] .c-specs-paires__valeur,
.c-specs-paires[data-etat="chargement"] .c-specs-paires__cle {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
}
.c-specs-paires[data-etat="chargement"] .c-specs-paires__cle { max-width: 16ch; }
.c-specs-paires[data-etat="chargement"] .c-specs-paires__valeur { max-width: 8ch;
                                                                  border-bottom-color: transparent; }

/* Vide — aucune caractéristique publiée. On ne rend pas une pile de tirets : on
   dit pourquoi, et on nomme l'endroit où l'information arrivera. */
.c-specs-paires[data-etat="vide"] .c-specs-paires__liste { display: none; }
.c-specs-paires[data-etat="vide"] .c-specs-paires__entete { color: var(--fg-3); }

/* Long — une valeur-phrase de quatre lignes ne casse rien : la pile grandit. */
.c-specs-paires[data-etat="long"] .c-specs-paires__valeur { overflow-wrap: anywhere; }

.c-specs-paires[data-etat="desactive"] { opacity: .6; }


/* ═══ lib/components/tableau-comparatif.css ═══ */
/* FICHE 42 — tableau comparatif.
   Cotes relevées au navigateur sur whisker.com le 2026-08-20 :
   grille de 360 px, `113.328px 113.328px 113.344px`, `gap: 20px 10px`,
   gouttières de 15 px, barre collante de 51 px à `z-index: 1`, fond opaque.

   ⚠ AUCUN `overflow-x` DANS CE FICHIER. Ce n'est pas un oubli : c'est la
   solution de la fiche. La largeur ne se donne jamais à un conteneur défilant ;
   ici, il n'y a pas de conteneur défilant du tout. */

.c-tableau-comparatif { display: grid; gap: var(--sp-4); }
.c-tableau-comparatif__intro { display: grid; gap: var(--sp-2); }
.c-tableau-comparatif__titre { font-size: var(--fs-h1); }

/* ---------- La grille, une seule fois -------------------------------------
   La barre de sélection ET chaque rangée partagent EXACTEMENT ce gabarit :
   trois colonnes égales. La colonne de libellés fait donc un tiers, comme chez
   Whisker — ce n'est pas une colonne étroite, et c'est ce qui permet aux menus
   d'être alignés au pixel sur leur colonne produit.
   `minmax(0, 1fr)` et non `1fr` : sans le minimum à zéro, un mot long impose
   sa largeur à la colonne et la grille dépasse son conteneur. C'est la
   deuxième façon, plus discrète, de faire déborder une page. */
.c-tableau-comparatif__barre,
.c-tableau-comparatif__rangee {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
}

/* ---------- La barre collante ---------------------------------------------
   Fond OPAQUE obligatoire : Whisker met `rgb(253,250,243)`, la couleur exacte
   de sa page. Un fond transparent laisse lire les rangées au travers. */
.c-tableau-comparatif__barre {
  position: sticky; top: 0; z-index: 2;
  align-items: center;
  min-height: var(--sp-7);
  padding-block: var(--sp-2);
  background: var(--bg-page);
  border-bottom: 1px solid var(--line-hair);
}
.c-tableau-comparatif__barre-titre { align-self: center; }

/* `<select>` NATIF : sélecteur système, clavier, dictée vocale, zéro octet de
   JavaScript. Le nom accessible vient d'un `<label class="sr-only">`, jamais
   d'un `title=` — lu par certains lecteurs d'écran, ignoré par d'autres, et
   jamais visible.
   L'exclusion mutuelle (`<option disabled>` dans l'autre menu) est portée par
   le balisage : on ne compare pas un produit avec lui-même. C'est le défaut
   qu'Apple laisse passer, et le point à voler chez Whisker. */
.c-tableau-comparatif__select {
  width: 100%; min-height: var(--tap-vise);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-caption); font-weight: 500;
  color: var(--fg-1); background: var(--bg-surface);
  border: 1px solid var(--line-rule); border-radius: var(--r-sm);
}

/* ---------- Les rangées ----------------------------------------------------
   Hauteur JAMAIS fixe : elle varie avec le repli du libellé. Une hauteur fixe
   tronque le libellé français, systématiquement plus long que l'anglais. */
.c-tableau-comparatif__rangee {
  align-items: start;
  padding-block: var(--sp-3);
  border-bottom: 1px solid var(--line-hair);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal);
  /* Piège n°2 : la barre collante mange la première ligne de la rangée quand
     on y arrive par une ancre. Chez Whisker, `enthusiasts` est coupé net sous
     la barre ; chez Apple, « Écran Retina toujours activé » est amputé du haut.
     La marge de défilement vaut la hauteur RÉELLE de la barre. */
  scroll-margin-top: var(--sp-8);
}
.c-tableau-comparatif__libelle { font-weight: 500; color: var(--fg-1); }
.c-tableau-comparatif__cellule { min-width: 0; overflow-wrap: break-word; }

/* Les têtes de colonne : visuel, nom, prix, action. Le prix reste en police de
   labeur — le glyphe € rend comme un « C » en Cormorant. */
.c-tableau-comparatif__rangee--tetes { align-items: end; }
.c-tableau-comparatif__tete { display: grid; gap: var(--sp-2); }
.c-tableau-comparatif__tete .c-visuel-marque__nom {
  font-size: var(--fs-caption); padding: var(--sp-1);
}
.c-tableau-comparatif__nom {
  font-family: var(--font-display); font-size: var(--fs-body); line-height: var(--lh-snug);
}
.c-tableau-comparatif__prix { font-size: var(--fs-body-sm); }
.c-tableau-comparatif__hors { color: var(--fg-3); }

/* ---------- Les trois valeurs de cellule autorisées, et pas une quatrième ---
   1. la pastille ✓ — encre, JAMAIS verte ;
   2. une phrase de deux lignes au maximum ;
   3. l'absence — cadratin doublé d'un texte accessible.
   La cellule vide de Whisker ne dit rien à un lecteur d'écran et ressemble à un
   rendu raté ; le `-` de B&O est un trait d'union. On prend le cadratin
   d'Apple, doublé, vérifié dans son relevé de texte. Aucune croix, aucun
   rouge : une absence n'est pas une faute. */
.c-tableau-comparatif__oui {
  display: inline-grid; place-items: center;
  width: var(--sp-6); height: var(--sp-6);
  border: 1px solid var(--line-rule); border-radius: var(--r-pill);
  color: var(--fg-1);
}
.c-tableau-comparatif__non { color: var(--fg-3); }

/* Le terme propriétaire et son explication. Vrai `<button>`, plancher tactile
   sur toute la rangée de libellé — pas sur le glyphe. */
.c-tableau-comparatif__terme {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-vise); width: 100%;
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-weight: 500; color: inherit; text-align: left;
}

/* ---------- Les groupes en accordéon ---------------------------------------
   Un `<button aria-expanded>`, jamais une `<div>` : un élément non focalisable
   nativement n'est atteignable qu'à condition d'ajouter `tabindex` ET de gérer
   Entrée/Espace à la main. Whisker a mis le `tabindex` sur l'info-bulle et pas
   sur les en-têtes de groupe. Avec un `<button>`, la question ne se pose plus. */
.c-tableau-comparatif__groupe-titre { font-size: var(--fs-body); }
.c-tableau-comparatif__groupe-bouton {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  width: 100%; min-height: var(--sp-7);
  padding: var(--sp-3) 0; border: 0; border-bottom: 1px solid var(--line-rule);
  background: none; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--fs-body-sm); font-weight: 500;
  color: var(--fg-1); text-align: left;
  scroll-margin-top: var(--sp-8);
}
/* Chevron à ABSCISSE FIXE : il ne bouge pas d'un groupe à l'autre selon la
   longueur du titre. */
.c-tableau-comparatif__groupe-bouton .i {
  transition: transform var(--dur-sm) var(--ease-standard);
}
.c-tableau-comparatif__groupe[data-ouvert="oui"] .c-tableau-comparatif__groupe-bouton .i {
  transform: rotate(180deg);
}
/* Un seul groupe ouvert (« Résumé »), comme chez Whisker : `aria-expanded`
   `true` sur Summary, `false` sur les quatre autres. */
.c-tableau-comparatif__rangees { display: none; }
.c-tableau-comparatif__groupe[data-ouvert="oui"] .c-tableau-comparatif__rangees { display: block; }

/* ---------- La clôture ------------------------------------------------------ */
.c-tableau-comparatif__cloture {
  display: grid; gap: var(--sp-4); justify-items: center; text-align: center;
  padding: var(--sp-6) var(--sp-4);
  background: var(--bg-subtle); border-radius: var(--r-card);
}
.c-tableau-comparatif__reassurance {
  font-family: var(--font-display); font-size: var(--fs-h3); line-height: var(--lh-snug);
}
.c-tableau-comparatif__faq { display: grid; }
.c-tableau-comparatif__faq-titre { font-size: var(--fs-h3); margin-bottom: var(--sp-2); }
.c-tableau-comparatif__duel { border-bottom: 1px solid var(--line-hair); }
.c-tableau-comparatif__duel summary {
  display: flex; align-items: center;
  min-height: var(--tap-vise); padding: var(--sp-3) 0;
  font-size: var(--fs-body-sm); font-weight: 500; cursor: pointer;
}
.c-tableau-comparatif__duel p {
  padding-bottom: var(--sp-3); font-size: var(--fs-body-sm); color: var(--fg-2);
}

/* ---------- Les états -------------------------------------------------------- */
.c-tableau-comparatif__vide,
.c-tableau-comparatif__erreur { display: none; }

/* Vide — les deux menus pointent le même produit, ou aucun critère n'est
   renseigné. Une phrase qui dit quoi faire, jamais une zone blanche. */
.c-tableau-comparatif[data-etat="vide"] .c-tableau-comparatif__vide { display: grid; }
.c-tableau-comparatif[data-etat="vide"] .c-tableau-comparatif__groupe,
.c-tableau-comparatif[data-etat="vide"] .c-tableau-comparatif__rangee--tetes,
.c-tableau-comparatif[data-etat="erreur"] .c-tableau-comparatif__groupe,
.c-tableau-comparatif[data-etat="erreur"] .c-tableau-comparatif__rangee--tetes { display: none; }

.c-tableau-comparatif[data-etat="erreur"] .c-tableau-comparatif__erreur { display: flex; }

/* Chargement — une ossature qui a la FORME des rangées attendues : trois
   colonnes, la même hauteur. La page ne saute pas quand les données arrivent. */
.c-tableau-comparatif[data-etat="chargement"] .c-tableau-comparatif__cellule {
  min-height: var(--sp-6);
  background: var(--bg-subtle); border-radius: var(--r-xs); color: transparent;
}
.c-tableau-comparatif[data-etat="chargement"] .c-tableau-comparatif__cellule > * { visibility: hidden; }

/* Désactivé — le comparatif est éteint dans l'éditeur de thème. */
.c-tableau-comparatif[data-etat="desactive"] { display: none; }

/* ---------- Variante condensée (bloc de fiche produit) ---------------------
   Cinq à huit critères, aucun groupe, une sortie vers le comparatif complet.
   MÊME picker, MÊME grille : c'est le même composant, pas un second dialecte. */
.c-tableau-comparatif[data-variante="condense"] .c-tableau-comparatif__faq,
.c-tableau-comparatif[data-variante="condense"] .c-tableau-comparatif__groupe-titre { display: none; }
.c-tableau-comparatif[data-variante="condense"] .c-tableau-comparatif__rangees { display: block; }

/* ---------- Au-delà du téléphone -------------------------------------------
   La TROISIÈME colonne produit est un réglage de largeur, jamais une fatalité :
   au-delà de ~700 px on peut en afficher trois ; en dessous, JAMAIS. Elle n'est
   volontairement pas bâtie ici — 390 px fait foi, et un composant conçu pour
   quatre colonnes puis replié est exactement ce qui a produit le défaut B1. */
@media (min-width: 48rem) {
  .c-tableau-comparatif__rangee { font-size: var(--fs-body); }
}


/* ═══ lib/components/technologie-coupe.css ═══ */
/* FICHE 16 — technologie en coupe.
   ---------------------------------------------------------------------------
   Le SVG est le seul contenu qui a le droit d'occuper toute la largeur, mais il
   n'a PAS le droit de déborder : `width: 100%`, hauteur libre, aucun conteneur
   scrollable. Le seul débordement horizontal trouvé sur le panel est le tableau
   de valeurs B&O — 480 px de contenu dans 373 px de fenêtre, sans le moindre
   indice visuel. Un dessin technique tombe dans le même piège dès qu'on le
   pense en colonnes latérales : c'est pourquoi la viewBox est PORTRAIT.

   Aucune couleur n'est écrite dans le balisage : les classes du SVG sont
   stylées ici, en jetons, donc le dessin suit le thème au lieu de le contredire. */

.c-technologie-coupe { display: grid; gap: var(--sp-6); }
.c-technologie-coupe__entete { display: grid; gap: var(--sp-2); max-width: 34ch; }
.c-technologie-coupe__titre { font-size: var(--fs-h1); letter-spacing: var(--tr-tight); }

/* ---------- A · la coupe ------------------------------------------------------ */
.c-technologie-coupe__dessin { display: grid; gap: var(--sp-5); margin: var(--sp-0); }
.c-technologie-coupe__svg {
  width: 100%; height: auto; max-height: 70vh; margin-inline: auto;
  color: var(--fg-1);
  background: var(--bg-page); border-radius: var(--r-lg);
  padding: var(--sp-4);
}

/* Le trait. 1,5 px sur une viewBox de 320 rendue à ~358 px reste au-dessus du
   plancher de lisibilité une fois mis à l'échelle ; les organes de second plan
   descendent en OPACITÉ, jamais en finesse de trait. */
.c-technologie-coupe__coque { fill: var(--bg-subtle); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__paroi { fill: var(--bg-surface); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__hachure { stroke: var(--fg-1); stroke-width: 1; opacity: .35; }
.c-technologie-coupe__anneau { fill: none; stroke: var(--fg-1); stroke-width: 8; opacity: .28; }
.c-technologie-coupe__tambour { fill: var(--bg-page); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__litiere { fill: var(--bg-warm); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__grain { stroke: var(--fg-2); stroke-width: 1.5; stroke-linecap: round; }
.c-technologie-coupe__rotation { fill: none; stroke: var(--fg-data); stroke-width: 1.5;
                 stroke-linecap: round; stroke-linejoin: round; }
.c-technologie-coupe__goulotte { stroke: var(--fg-1); stroke-width: 1.5; stroke-dasharray: 6 5; opacity: .6; }
.c-technologie-coupe__temoin { fill: var(--bg-surface); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__temoin--actif { fill: var(--bg-intelligence); }
.c-technologie-coupe__socle { fill: var(--bg-subtle); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__tiroir { fill: var(--bg-surface); stroke: var(--fg-1); stroke-width: 1.5; }
.c-technologie-coupe__sac { fill: none; stroke: var(--fg-2); stroke-width: 1.5; stroke-dasharray: 5 4; }
.c-technologie-coupe__poignee { stroke: var(--fg-1); stroke-width: 3; stroke-linecap: round; }
.c-technologie-coupe__pied { stroke: var(--fg-1); stroke-width: 3; stroke-linecap: round; }

/* L'étiquette unique. Carré arrondi tracé, rayon 8, trait 1,5 — la géométrie de
   la puce H2 d'Apple, reprise telle quelle. Le texte reste du <text> : il est
   sélectionnable, traduisible, et il ne se déforme pas avec le dessin. */
.c-technologie-coupe__etiquette rect { fill: var(--bg-surface); stroke: var(--fg-1); stroke-width: 1.5; }
/* 12 unités de viewBox sur 320, rendues dans ~358 px de large : ~13,4 px CSS à
   390 px de fenêtre. La fiche pose un plancher à 12 px CSS pour tout texte SVG,
   et c'est la mise à l'ÉCHELLE qu'il faut calculer, pas la valeur écrite. */
.c-technologie-coupe__etiquette-texte {
  fill: var(--fg-1); font-family: var(--font-data); font-size: var(--fs-caption);
  letter-spacing: var(--tr-normal);
}

/* La légende. C'est elle qui nomme, et le GRAS y remplace la ligne de rappel. */
.c-technologie-coupe__legende {
  max-width: 44ch; margin-inline: auto; text-align: center;
  font-size: var(--fs-body-lg); line-height: var(--lh-normal); color: var(--fg-2);
}
.c-technologie-coupe__legende b { color: var(--fg-1); font-weight: 600; }

/* ---------- D · la bande de chiffres ----------------------------------------
   filet → libellé → NOMBRE → qualificatif + appel → filet. Empilée et alignée
   à gauche : c'est exactement ce que fait Apple, et c'est ce qui permet au
   nombre d'être énorme sans jamais déborder. */
.c-technologie-coupe__chiffres { display: grid; }
.c-technologie-coupe__chiffre {
  display: grid; gap: var(--sp-1);
  padding-block: var(--sp-5); border-top: 1px solid var(--fg-1);
}
.c-technologie-coupe__chiffre:last-child { border-bottom: 1px solid var(--fg-1); }
.c-technologie-coupe__libelle { font-size: var(--fs-body-sm); color: var(--fg-2); }
.c-technologie-coupe__nombre {
  font-family: var(--font-data); font-size: var(--fs-hero);
  line-height: var(--lh-tight); letter-spacing: var(--tr-tight);
  font-variant-numeric: tabular-nums;
}
.c-technologie-coupe__qualificatif {
  font-size: var(--fs-body-sm); color: var(--fg-2); max-width: 32ch;
  line-height: var(--lh-normal);
}
.c-technologie-coupe__qualificatif sup { font-size: var(--fs-caption); }

/* ---------- B · les pictogrammes -------------------------------------------- */
.c-technologie-coupe__pictos { display: grid; gap: var(--sp-6); }
.c-technologie-coupe__picto { display: grid; gap: var(--sp-3); justify-items: start; }
.c-technologie-coupe__picto .i { width: var(--sp-7); height: var(--sp-7); color: var(--fg-1); }
.c-technologie-coupe__picto p {
  font-size: var(--fs-body); line-height: var(--lh-normal); color: var(--fg-2); max-width: 36ch;
}
.c-technologie-coupe__picto b { color: var(--fg-1); font-weight: 600; }

.c-technologie-coupe__note { max-width: 44ch; }

/* ---------- Les variantes ---------------------------------------------------
   Un écran = une idée = une coupe. Les variantes ne SUPERPOSENT pas les trois
   dispositifs, elles en isolent un : c'est la règle 4 de la fiche. */
.c-technologie-coupe[data-variante="chiffres"] .c-technologie-coupe__dessin,
.c-technologie-coupe[data-variante="chiffres"] .c-technologie-coupe__pictos { display: none; }
.c-technologie-coupe[data-variante="pictos"] .c-technologie-coupe__dessin,
.c-technologie-coupe[data-variante="pictos"] .c-technologie-coupe__chiffres { display: none; }
.c-technologie-coupe[data-variante="coupe"] .c-technologie-coupe__chiffres,
.c-technologie-coupe[data-variante="coupe"] .c-technologie-coupe__pictos { display: none; }

/* La variante sombre : le fond noir du bloc de pictogrammes d'Apple. Le dessin
   n'a rien à redéclarer — il hérite de `currentColor` et des jetons. */
.c-technologie-coupe[data-variante="sombre"] {
  background: var(--bg-inverse); color: var(--fg-on-dark);
  border-radius: var(--r-lg); padding: var(--sp-6) var(--sp-4);
}
.c-technologie-coupe[data-variante="sombre"] .c-technologie-coupe__dessin,
.c-technologie-coupe[data-variante="sombre"] .c-technologie-coupe__chiffres { display: none; }
.c-technologie-coupe[data-variante="sombre"] .c-technologie-coupe__picto .i,
.c-technologie-coupe[data-variante="sombre"] .c-technologie-coupe__picto b { color: var(--fg-on-dark); }
.c-technologie-coupe[data-variante="sombre"] .c-technologie-coupe__picto p { color: var(--bg-subtle); }

/* ---------- Les états -------------------------------------------------------- */
.c-technologie-coupe__vide, .c-technologie-coupe__erreur { display: none; }

.c-technologie-coupe[data-etat="vide"] .c-technologie-coupe__dessin,
.c-technologie-coupe[data-etat="vide"] .c-technologie-coupe__chiffres,
.c-technologie-coupe[data-etat="vide"] .c-technologie-coupe__pictos { display: none; }
.c-technologie-coupe[data-etat="vide"] .c-technologie-coupe__vide {
  display: grid; border: 1px dashed var(--line-rule); border-radius: var(--r-lg);
}

.c-technologie-coupe[data-etat="chargement"] .c-technologie-coupe__svg,
.c-technologie-coupe[data-etat="chargement"] .c-technologie-coupe__legende {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-lg);
  position: relative; overflow: hidden;
}
.c-technologie-coupe[data-etat="chargement"] .c-technologie-coupe__svg > * { visibility: hidden; }
.c-technologie-coupe[data-etat="chargement"] .c-technologie-coupe__dessin { position: relative; }
.c-technologie-coupe[data-etat="chargement"] .c-technologie-coupe__dessin::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  transform: translateX(-100%);
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .c-technologie-coupe[data-etat="chargement"] .c-technologie-coupe__dessin::after { animation: none; }
}

.c-technologie-coupe[data-etat="erreur"] .c-technologie-coupe__erreur { display: flex; }
.c-technologie-coupe[data-etat="erreur"] .c-technologie-coupe__svg { display: none; }

/* long — une légende riche de 400 caractères et un qualificatif à rallonge. Le
   nombre descend d'un cran pour que le qualificatif garde sa place : c'est le
   couple nombre + note qui porte le sens, pas la taille du nombre. */
.c-technologie-coupe[data-etat="long"] .c-technologie-coupe__legende { max-width: 56ch; }
.c-technologie-coupe[data-etat="long"] .c-technologie-coupe__nombre { font-size: var(--fs-data-xl); }
.c-technologie-coupe[data-etat="long"] .c-technologie-coupe__qualificatif { max-width: 44ch; }

/* rupture — la technique reste vraie quand le produit n'est pas commandable. */
.c-technologie-coupe[data-etat="rupture"] .c-technologie-coupe__note { color: var(--fg-alert); }

/* succes — l'organe expliqué vient d'être mis en avant depuis la légende. */
.c-technologie-coupe[data-etat="succes"] .c-technologie-coupe__etiquette rect { fill: var(--bg-intelligence); }

/* desactive — une coupe dont un organe attend encore la confirmation du
   fournisseur : le dessin s'estompe et n'est pas exploitable comme preuve. */
.c-technologie-coupe[data-etat="desactive"] .c-technologie-coupe__svg { opacity: .45; }
.c-technologie-coupe[data-etat="desactive"] .c-technologie-coupe__legende { color: var(--fg-3); }

@media (min-width: 48rem) {
  .c-technologie-coupe__pictos { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .c-technologie-coupe__svg { max-width: 34rem; }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-technologie-coupe__erreur a { color: var(--fg-1); }


/* ═══ lib/components/temoignages.css ═══ */
/* FICHE 19 — témoignages.
   ---------------------------------------------------------------------------
   Ce composant a une particularité qu'aucun autre du lot n'a : SON ÉTAT NORMAL
   EST L'ÉTAT VIDE. CatLang n'a jamais été vendu, il n'existe donc aucun avis,
   aucune note, aucun nombre d'avis, et rien ici n'a le droit d'en fabriquer —
   même en maquette, même marqué. La porte 6 refuse les catégories
   `avis` / `note` / `notation` / `review` / `rating` sans dérogation possible.

   Conséquence sur la feuille : le style ne sert pas à faire briller un contenu
   qui n'existe pas, il sert à rendre DIGNE une absence. Trois choses seulement
   sont dessinées :
     1. l'entête et la POLITIQUE éditoriale — le seul élément du panel qui se
        publie avant le premier avis, et le seul qui transforme une note en
        promesse (« nous publierons tous les avis, les élogieux comme les
        critiques ») ;
     2. la citation de PRESSE, modèle B&O — nom du média en petites capitales
        espacées, la citation, beaucoup de vide, et zéro étoile ;
     3. l'OSSATURE de la future section d'avis — pour juger la mise en page
        avec des blocs de marque délibérés, jamais avec de faux avis. C'est la
        même règle dure que « aucun rectangle gris », appliquée au texte.

   Les variantes disent LEQUEL des trois est rendu :
     ""         (défaut) l'état vide digne. C'est l'état de Catharsis en 2026.
     "presse"   le modèle B&O.
     "ossature" la structure future, vide et explicite — atelier uniquement.

   ⚠ Ce que cette feuille ne fera JAMAIS, quelle que soit la demande : dessiner
   une étoile, remplir une barre d'histogramme, afficher un compteur. Une barre
   à 30 % est déjà une donnée inventée. */

.c-temoignages { display: grid; gap: var(--sp-5); }

/* Le rendu NUL. En production la section ne rend pas un cadre vide : elle ne
   rend rien du tout. La leçon du 13 août dit pourquoi il faut que ce soit
   explicite — un rendu vide ne ressemble pas à une erreur, il ressemble à un
   espacement, donc il ne se corrige jamais. */
.c-temoignages[data-rendu="zero"] { display: none; }

/* ═══ L'entête et la politique ═══════════════════════════════════════════════ */
.c-temoignages__entete { display: grid; gap: var(--sp-4); max-width: 44ch; }
.c-temoignages__titre { letter-spacing: var(--tr-tight); text-wrap: balance; }

/* La politique est un ENGAGEMENT, pas une mention légale : elle se lit, donc
   elle est en corps de texte et non en petites capitales grises. Le filet
   ambre à gauche est le seul ornement — c'est la couleur d'action de la marque
   employée là où il y a une promesse, pas un bouton. */
.c-temoignages__politique {
  display: grid; gap: var(--sp-3);
  padding-left: var(--sp-4);
  border-left: 2px solid var(--fg-accent);
  color: var(--fg-2); font-size: var(--fs-body-sm); line-height: var(--lh-normal);
}
.c-temoignages__politique p:first-child { color: var(--fg-1); font-size: var(--fs-body); }

/* ═══ L'agrégat — construit, et vide, explicitement ══════════════════════════
   Un agrégat sans histogramme cache la distribution : chez Tuft+Paw c'est
   `879/69/20/9/18` qui rend le 4,8 croyable, pas le 4,8. La forme est donc
   entière, et chaque case DIT qu'elle attend une valeur au lieu d'en porter
   une. Aucune piste n'a de remplissage. */
.c-temoignages__agregat {
  display: grid; gap: var(--sp-4); padding: var(--sp-4);
  background: var(--bg-surface); border: 1px solid var(--line-hair);
  border-radius: var(--r-card);
}
.c-temoignages__agregat-note {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
}
/* Reprise volontaire des quatre déclarations de `.etiquette` : la classe
   utilitaire n'est pas portée par ce `<span>` dans le balisage, et le balisage
   du composant n'est pas rouvert pour une question de style. */
.c-temoignages__place {
  font-family: var(--font-ui); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--tr-eyebrow); text-transform: uppercase; color: var(--fg-3);
}
/* L'absence, écrite. Géométrie de la fiche 03 : UNE ligne réservée, jamais
   deux, dans une typo plus petite et plus pâle que le corps de page. On
   réserve la ligne, on ne la remplit pas — et le jour où les avis arrivent,
   rien ne bouge de 24 px sous les yeux du visiteur. */
.c-temoignages__absence {
  display: flex; align-items: center; min-height: var(--sp-5);
  font-size: var(--fs-body-sm); color: var(--fg-3);
}

.c-temoignages__histogramme {
  display: grid; gap: var(--sp-2);
  margin: var(--sp-0); padding: var(--sp-0); list-style: none;
}
.c-temoignages__barre {
  display: grid; grid-template-columns: var(--sp-4) minmax(0, 1fr) var(--sp-6);
  align-items: center; gap: var(--sp-3);
  font-size: var(--fs-caption); color: var(--fg-3);
}
.c-temoignages__barre-rang { font-family: var(--font-data); }
/* La piste est VIDE, et elle le reste. Pas de `width` de remplissage, pas de
   dégradé qui suggère une proportion : la longueur d'une barre est une donnée. */
.c-temoignages__barre-piste {
  height: var(--sp-2); border-radius: var(--r-pill); background: var(--bg-subtle);
}
.c-temoignages__barre-compte {
  font-family: var(--font-data); text-align: right; font-variant-numeric: tabular-nums;
}

/* ═══ La carte d'avis — l'ossature, jamais le contenu ════════════════════════
   L'ordre vient de la carte mesurée chez Tuft+Paw : initiales → nom + statut
   vérifié → date relative → quel produit → emplacement de la note → titre →
   corps → utilité → réponse de la marque. */
.c-temoignages__carte {
  display: grid; gap: var(--sp-3); padding: var(--sp-4);
  background: var(--bg-surface); border: 1px solid var(--line-hair);
  border-radius: var(--r-card);
}
.c-temoignages__carte-tete {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start; gap: var(--sp-3);
}
/* La pastille d'initiales reste VIDE : deux initiales inventées sont déjà une
   identité inventée. Elle tient sa place et rien d'autre. */
.c-temoignages__pastille {
  width: var(--sp-7); height: var(--sp-7); border-radius: var(--r-pill);
  background: var(--bg-subtle);
}
.c-temoignages__identite { display: grid; gap: var(--sp-2); min-width: 0; }
/* Le statut vérifié est une COCHE FILAIRE, jamais une pastille pleine de
   couleur : chez Tuft+Paw la coche marine pleine se lit comme un label de
   confiance décerné par un tiers, et il n'y a ici aucun tiers. */
.c-temoignages__verifie {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-caption); color: var(--fg-2);
}
.c-temoignages__verifie .i { color: var(--fg-3); }
.c-temoignages__date { justify-self: end; width: var(--sp-8); height: var(--sp-3); }

.c-temoignages__produit {
  display: flex; align-items: center; gap: var(--sp-3);
  font-size: var(--fs-body-sm); color: var(--fg-2);
}
.c-temoignages__produit b { color: var(--fg-1); font-weight: 500; }
/* La vignette du produit évalué. Un panneau de marque chaud, pas un carré
   gris : là où une photo viendra, une surface délibérée. */
.c-temoignages__vignette {
  flex: none; width: var(--sp-7); height: var(--sp-7);
  border-radius: var(--r-sm); background: var(--bg-warm);
  box-shadow: var(--sh-inset-hair);
}

.c-temoignages__utilite {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-caption); color: var(--fg-3);
}
/* La réponse de la marque est une surface DISTINCTE de l'avis : sans le
   changement de fond, on lit une seconde voix comme la suite de la première. */
.c-temoignages__reponse {
  display: grid; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-subtle); border-radius: var(--r-sm);
}

/* Les ossatures. `.ossature` de `primitives.css` fait le fond animé — le
   balayage y est un `background-position`, jamais un pseudo-élément translaté
   (piège n°6). Ce qui est ajouté ici, c'est la GÉOMÉTRIE : une ossature n'a de
   sens que si elle a la forme du contenu attendu.

   ⚠ `font-size: 0` est load-bearing. Le texte des ossatures est une consigne de
   maquette (« Emplacement du titre de l'avis ») : il doit rester dans le DOM,
   mais il ne doit dessiner AUCUN pixel. Sans ça, il déborde la barre — et
   `.ossature` porte `overflow: hidden`, donc la sonde remonte un `clipped-x`
   GRAVE, « contenu définitivement inaccessible », sur de la décoration. */
.c-temoignages .ossature { font-size: 0; }
.c-temoignages__ossature-nom { display: block; width: 60%; height: var(--sp-4); }
.c-temoignages__ossature-titre { display: block; width: 70%; height: var(--sp-5); }
.c-temoignages__ossature-corps { display: block; height: var(--sp-8); }
.c-temoignages__ossature-reponse { display: block; height: var(--sp-6); }
/* ⚠ `width: 100%` est load-bearing, et ça s'est vu à l'écran avant de se voir
   dans le code : la figure de presse est en `justify-items: center`, donc ses
   enfants sont dimensionnés par leur CONTENU — et le contenu de cette ossature
   est un texte en `font-size: 0`. Résultat mesuré le 2026-08-22 :
   `h=48 w=0`. Une ossature de 48 px de haut et de zéro pixel de large, c'est-à-dire
   un trou de 48 px au milieu du bloc, exactement le défaut que ce composant
   existe pour ne pas commettre. Aucune sonde ne le voit : rien ne déborde,
   rien n'est rogné, la page est simplement vide à cet endroit. */
.c-temoignages__ossature-citation { display: block; width: 100%; height: var(--sp-7); }

/* ═══ La citation de presse — modèle B&O ═════════════════════════════════════
   Aucune note, aucune étoile, aucun visage : le nom du média en petites
   capitales espacées avec son signe de lien externe, la citation, et beaucoup
   de vide. C'est le modèle directement applicable à Catharsis, parce qu'il ne
   demande RIEN qui n'existe pas déjà — il demande un article, daté et
   vérifiable, pas un client. */
.c-temoignages__presse {
  display: grid; gap: var(--sp-5); justify-items: center; text-align: center;
  padding-block: var(--sp-6); margin: var(--sp-0);
}
.c-temoignages__media {
  font-family: var(--font-ui); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--tr-eyebrow); text-transform: uppercase; color: var(--fg-2);
}
.c-temoignages__media a {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap-vise); color: inherit;
  text-decoration: underline; text-underline-offset: 0.3em;
}
.c-temoignages__media .ossature { display: inline-block; width: var(--sp-9); height: var(--sp-3); }
/* Le signe de lien externe. Le jeu partagé n'a PAS d'icône de lien externe
   (37 icônes, aucune `i-lien-externe`) — signalé au lot qui tient
   `icones.html`. En attendant, la flèche droite pivotée d'un quart de tour
   donne exactement le glyphe « ↗ » que B&O emploie, sans ajouter de symbole au
   sprite partagé ni inventer un dessin. */
.c-temoignages__media .i { transform: rotate(-45deg); }

.c-temoignages__citation {
  font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 400;
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
  color: var(--fg-1); max-width: 26ch; text-wrap: balance;
}

/* Les points de pagination sont MASQUÉS tant qu'aucune citation réelle n'est
   fournie : cinq points annoncent cinq articles, et cinq articles qui
   n'existent pas sont un chiffre inventé comme un autre. Ils réapparaissent
   avec la première vraie citation. */
.c-temoignages__points { display: none; gap: var(--sp-2); justify-content: center; }
.c-temoignages__presse:has(.c-temoignages__citation) .c-temoignages__points { display: flex; }
.c-temoignages__points span {
  width: var(--sp-2); height: var(--sp-2); border-radius: var(--r-pill);
  background: var(--line-rule);
}
.c-temoignages__points span[data-actif="oui"] { background: var(--fg-1); }

.c-temoignages__note { max-width: 52ch; }

/* ═══ Les états ══════════════════════════════════════════════════════════════
   Tout est masqué au départ, et chaque variante rallume EXACTEMENT un bloc.
   C'est l'inverse de l'habitude — ici, montrer est l'exception. */
.c-temoignages__agregat,
.c-temoignages__carte,
.c-temoignages__presse,
.c-temoignages__vide,
.c-temoignages__erreur { display: none; }

/* Le défaut, et l'état d'aujourd'hui : l'absence, dite. Le constructeur écrit
   toujours l'attribut, même vide — d'où le garde `[data-variante=""]`, le même
   que celui du héros. */
.c-temoignages[data-variante=""] .c-temoignages__vide,
.c-temoignages[data-etat="vide"] .c-temoignages__vide { display: grid; }
.c-temoignages[data-etat="vide"] .c-temoignages__agregat,
.c-temoignages[data-etat="vide"] .c-temoignages__carte,
.c-temoignages[data-etat="vide"] .c-temoignages__presse { display: none; }
/* L'encadré pointillé dit « emplacement construit, source vide » sans jamais
   ressembler à une carte de contenu. */
.c-temoignages__vide {
  border: 1px dashed var(--line-rule); border-radius: var(--r-card);
}

.c-temoignages[data-variante="presse"] .c-temoignages__presse { display: grid; }
.c-temoignages[data-variante="presse"] .c-temoignages__vide { display: none; }

.c-temoignages[data-variante="ossature"] .c-temoignages__agregat,
.c-temoignages[data-variante="ossature"] .c-temoignages__carte { display: grid; }
.c-temoignages[data-variante="ossature"] .c-temoignages__vide { display: none; }

/* chargement — l'ossature a la forme du contenu attendu, donc la page ne saute
   pas quand les avis arrivent. C'est le seul état où la structure complète
   s'affiche sans qu'on ait demandé la variante « ossature » : on charge des
   avis, on montre la place des avis. */
.c-temoignages[data-etat="chargement"] .c-temoignages__agregat,
.c-temoignages[data-etat="chargement"] .c-temoignages__carte { display: grid; }
.c-temoignages[data-etat="chargement"] .c-temoignages__vide,
.c-temoignages[data-etat="chargement"] .c-temoignages__presse { display: none; }
.c-temoignages[data-etat="chargement"] .c-temoignages__politique { display: none; }

/* erreur — elle DIT ce qui a échoué, et surtout ce qui n'est pas affiché à la
   place. Un avis d'exemple servi pendant une panne est un avis fabriqué servi
   pendant une panne. */
.c-temoignages[data-etat="erreur"] .c-temoignages__erreur { display: flex; }
.c-temoignages[data-etat="erreur"] .c-temoignages__agregat,
.c-temoignages[data-etat="erreur"] .c-temoignages__carte,
.c-temoignages[data-etat="erreur"] .c-temoignages__presse,
.c-temoignages[data-etat="erreur"] .c-temoignages__vide { display: none; }

/* long — une politique éditoriale de 400 caractères, une citation de presse de
   deux phrases : la mesure s'élargit et la taille redescend d'un cran. On ne
   tronque pas un engagement, et on ne tronque pas une citation attribuée : une
   citation coupée est une citation déformée. */
.c-temoignages[data-etat="long"] .c-temoignages__entete { max-width: 62ch; }
.c-temoignages[data-etat="long"] .c-temoignages__citation {
  font-size: var(--fs-body-lg); font-family: var(--font-ui); max-width: 44ch;
  letter-spacing: var(--tr-normal);
}

/* desactive — la section existe dans l'éditeur mais n'est pas publiée. Elle
   reste visible en atelier, éteinte, avec sa sortie retirée : une issue qui ne
   mène nulle part vaut moins que pas d'issue. */
.c-temoignages[data-etat="desactive"] { opacity: .55; }
.c-temoignages[data-etat="desactive"] .c-temoignages__vide .b { display: none; }

/* Les états `rupture` et `succes` n'existent pas pour ce composant, et c'est
   une décision, pas un oubli : une section qui ne publie encore rien n'a ni
   stock ni confirmation à annoncer. Les inventer aurait produit deux écrans
   qu'aucun visiteur ne verra jamais. */

/* ═══ Au-delà du téléphone — on élargit, on ne redessine pas ═════════════════ */
@media (min-width: 48rem) {
  .c-temoignages__presse { padding-block: var(--sp-8); }
  .c-temoignages__citation { font-size: var(--fs-h2); max-width: 30ch; }
  .c-temoignages__carte { padding: var(--sp-5); }
}

/* Le lien de reprise de l'état d'erreur. Vu à l'écran le 2026-08-22 : il rendait
   en BLEU souligné, la couleur de lien par défaut du navigateur, au milieu
   d'une palette porcelaine et graphite. Aucune sonde ne le remonte — ce n'est
   ni un débordement ni une cible trop petite, c'est juste faux.
   ⚠ PRIMITIVE À PROMOUVOIR : la vraie place de cette règle est
   `.etat-erreur a { color: var(--fg-1); }` dans `primitives.css`. Le défaut est
   dans TOUS les états d'erreur de la bibliothèque, pas seulement ici ; en
   attendant que le fichier partagé soit rouvert, chaque composant du lot le
   corrige chez lui. */
.c-temoignages__erreur a { color: var(--fg-1); }


/* ═══ lib/components/tiroir-panier.css ═══ */
/* FICHE 33 — tiroir panier. Le composant central du lot E : cinq états, une
   seule mécanique de superposition (celle de `primitives.css`, §8), et deux
   cotes qui viennent de mesures, pas de confort.

   Ce que ce fichier N'ÉCRIT PAS, parce que c'est déjà écrit une fois pour les
   neuf composants qui en dépendent : le voile, sa transition, le panneau
   coulissant, le verrou de défilement `html[data-defilement="bloque"]`, la
   colonne flex en-tête / corps / pied. Réécrire l'un d'eux serait rouvrir la
   divergence que `primitives.css` existe pour fermer. */

.c-tiroir-panier {
  /* Le tiroir ne couvre PAS le bandeau d'annonce (règle 13 : chez Whisker le
     panneau commence à y = 40, exactement sous lui, donc la promesse de
     livraison reste lisible pendant tout le panier). En maquette il n'y a pas
     de bandeau, donc 0 ; le thème pose la hauteur réelle du bandeau ici, et
     rien d'autre ne change. Une valeur codée dans la primitive serait une
     constante à maintenir en deux endroits. */
  --panier-haut: 0px;
}

/* ---------- Le panneau -------------------------------------------------------
   `min(92vw, 440px)` → 358,8 px à 390. Whisker mesure 362 px, deux fois (au
   pixel sur la capture, puis au navigateur) : 3 px d'écart. La primitive
   `.tiroir` plafonne à `--container-narrow` (880 px) — la bonne valeur pour un
   tiroir de navigation, deux fois trop pour un panier, qui deviendrait un mur
   de 880 px sur une fenêtre large.

   Ce qui compte n'est pas la largeur, c'est la BANDE de page voilée qu'elle
   laisse : 31,2 px à 390 px. Elle doit rester ≥ 24 px (piège n°6), sinon le
   tiroir est indiscernable d'une page et le client perd le fil de sa
   navigation exactement comme sur une redirection /cart. Trois marques sur
   quatre ont renoncé à cette distinction. */
.c-tiroir-panier__panneau {
  top: var(--panier-haut);
  width: min(92vw, 440px);
}

/* ---------- L'en-tête --------------------------------------------------------
   Un titre en toutes lettres, pas une icône de panier centrée (Whisker) ni un
   interrupteur panier ⟷ liste de souhaits (Gymshark). Le titre est la cible de
   `aria-labelledby` ET la cible du focus à l'ouverture : il doit donc être
   visible et nommer son contenu. */
.c-tiroir-panier__titre {
  display: flex; align-items: center; gap: var(--sp-3);
  font-family: var(--font-display); font-size: var(--fs-h3);
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
}
/* Le compte reste sur une ligne à deux chiffres : « 12 » ne doit pas replier la
   pastille sous le titre. */
.c-tiroir-panier__compte {
  font-family: var(--font-data); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- Le corps ---------------------------------------------------------
   `overflow-y: auto` vit sur le CORPS, jamais sur le panneau entier (règle 10) :
   sinon l'en-tête et le pied défilent avec, et le total quitte l'écran au
   moment où on le lit. */
.c-tiroir-panier .tiroir__corps { display: grid; align-content: start; }
/* Un enfant de grille vaut `min-width: auto` : son contenu minimal peut alors
   ÉLARGIR la piste au lieu de se replier, et le corps — qui est le conteneur de
   défilement — se met à défiler horizontalement. C'est le débordement interne
   que la sonde remonte en GRAVE, et il ne se voit pas à l'œil sur un panier
   court. On le ferme à la source. */
.c-tiroir-panier .tiroir__corps > * { min-width: 0; }

/* ---------- La bande de succès, et son annulation ----------------------------
   Fiche 34, règle 11 : le « − » à quantité 1 supprime la ligne et déclenche
   cette bande. L'annulation est ce qui rend la suppression sans risque — sans
   elle, un client qui a touché le mauvais bouton doit refaire son panier. Fond
   menthe, jamais vert de validation : c'est une information, pas une médaille. */
.c-tiroir-panier__succes {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--bg-intelligence); color: var(--fg-data);
  font-size: var(--fs-body-sm); line-height: var(--lh-snug);
}
.c-tiroir-panier__succes > span { flex: 1; min-width: 0; }
.c-tiroir-panier__annuler {
  flex: none; min-height: var(--tap-vise); padding-inline: var(--sp-2);
  border: 0; background: none; color: inherit; cursor: pointer;
  font-size: var(--fs-caption); font-weight: 600;
  text-decoration: underline; text-underline-offset: 3px;
}

/* ---------- L'erreur ---------------------------------------------------------
   Elle DIT ce qui a échoué et donne l'action de reprise. Un liseré rouge sur la
   ligne fautive, seul, laisse deviner — et ce qu'on devine dans un panier, on
   le devine en défaveur du marchand. */
.c-tiroir-panier__erreur { margin-bottom: var(--sp-4); }
.c-tiroir-panier__erreur > div { display: grid; gap: var(--sp-1); }
.c-tiroir-panier__erreur > .i { color: var(--fg-alert); margin-top: var(--sp-1); }
.c-tiroir-panier__erreur-titre { font-size: var(--fs-body-sm); font-weight: 600; }
.c-tiroir-panier__erreur-texte {
  font-size: var(--fs-caption); line-height: var(--lh-normal); color: var(--fg-2);
}
.c-tiroir-panier__erreur-action {
  justify-self: start; display: inline-flex; align-items: center;
  min-height: var(--tap-vise);
  font-size: var(--fs-caption); font-weight: 500; color: var(--fg-1);
  text-decoration: underline; text-underline-offset: 3px;
}

/* ---------- L'état vide ------------------------------------------------------
   Un ÉCRAN, pas l'état plein à zéro : ni barre de livraison, ni récapitulatif à
   0 €, ni vente croisée — c'est le balisage du composant qui le garantit, pas
   une règle de style. Gymshark propose deux CTA parce qu'il a deux rayons ;
   Catharsis en a un, donc une seule.

   L'icône est en `--fg-3` et non en pleine encre : une grande illustration de
   panier vide en encre forte fait de l'absence un événement. */
.c-tiroir-panier__vide { padding-block: var(--sp-9) var(--sp-7); }
.c-tiroir-panier__vide-icone { color: var(--fg-3); }
.c-tiroir-panier__vide-texte {
  max-width: 32ch;
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
}

/* ---------- Les blocs du corps, dans l'ordre imposé (règle 4) ----------------
   barre de livraison → lignes article → vente croisée → [pied : récap + CTA].
   Les filets disent la séquence ; ils ne l'imposent pas — c'est le balisage
   qui l'impose. */
.c-tiroir-panier .c-barre-livraison {
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line-hair);
}
.c-tiroir-panier__lignes { display: grid; }
.c-tiroir-panier .c-vente-croisee {
  margin-top: var(--sp-5); padding-top: var(--sp-5);
  border-top: 1px solid var(--line-hair);
}
/* La dernière ligne ne se colle pas au pied : le filet du pied ne doit pas se
   lire comme le filet inter-lignes. */
.c-tiroir-panier__lignes:last-child { padding-bottom: var(--sp-3); }

/* ---------- Le pied ----------------------------------------------------------
   PIÈGE N°2 DE LA FICHE, et la seule raison pour laquelle il ne peut pas se
   reproduire ici : le pied est un BLOC À PART dans une colonne flex, pas une
   superposition collante. Chez Gymshark le bouton « Paiement sécurisé » occupe
   y = 811 → 862 pendant que deux lignes du récapitulatif sont à 805,3 et
   836,8 : le client doit défiler pour lire son propre total. Un
   `padding-bottom` calculé à la main serait une constante à maintenir ; une
   colonne flex n'a rien à maintenir. */
.c-tiroir-panier .tiroir__pied { box-shadow: var(--sh-hair); }

/* ═══ Les états ═══════════════════════════════════════════════════════════ */

/* Chargement. Le tiroir ne saute pas : les ossatures fines vivent dans
   `ligne-article` et `recap-panier`, chacune à largeur réservée. Ici il ne
   reste que le compte, qui devient un jeton de la bonne taille.
   ⚠ Le balayage est un FOND ANIMÉ (`background-position`), jamais un `::after`
   translaté — piège n°6 de CONVENTIONS.md : un pseudo-élément transformé entre
   dans la région de débordement défilable de Chrome et la sonde le remonte en
   `clipped-x` GRAVE, sur de la pure décoration. Même recette que `.ossature`. */
.c-tiroir-panier[data-etat="chargement"] .c-tiroir-panier__compte {
  color: transparent;
  background-color: var(--bg-subtle);
  background-image: linear-gradient(90deg, transparent, var(--bg-surface), transparent);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: ossature-balayage var(--dur-xl) var(--ease-soft) infinite;
}
.c-tiroir-panier[data-etat="chargement"] .c-tiroir-panier__lignes { opacity: .65; }

/* Erreur. Le tiroir ne se colore pas en entier — seul le bloc de message porte
   l'alerte, et la ligne fautive porte son propre `data-etat="erreur"`. Un
   panneau rouge entier transforme un incident de quantité en panne de
   boutique. */
.c-tiroir-panier[data-etat="erreur"] .c-tiroir-panier__erreur { margin-bottom: var(--sp-5); }

/* Long. Rien ne change de forme : c'est la colonne flex qui absorbe, et c'est
   exactement ce qu'on veut prouver. Un panier de cinq lignes, un nom de 69
   caractères et une quantité à deux chiffres doivent laisser le pied EXACTEMENT
   où il est. La seule concession est un peu d'air sous la dernière ligne, pour
   que le filet du pied ne se confonde pas avec un filet inter-lignes. */
.c-tiroir-panier[data-etat="long"] .c-tiroir-panier__lignes { padding-bottom: var(--sp-4); }

/* ═══ Vitrine d'atelier ═══════════════════════════════════════════════════
   MAQUETTE UNIQUEMENT — ne part pas en thème.

   Un tiroir fermé ne se voit pas sur une capture, et cinq tiroirs OUVERTS en
   `position: fixed` se superposent : un seul serait visible, les quatre autres
   invisibles ET comptés par la sonde. `data-vitrine` les remet dans le flux
   pour qu'ils se jugent côte à côte. La géométrie réelle reste celle des
   instances `data-demo` ci-dessous, qui sont, elles, de vrais tiroirs fixes. */
.c-tiroir-panier[data-vitrine="oui"] > .voile { display: none; }
.c-tiroir-panier[data-vitrine="oui"] .c-tiroir-panier__panneau {
  position: relative; inset: auto; z-index: auto;
  transform: none; transition: none;
  width: 100%; max-width: 440px; max-height: none;
  border: 1px solid var(--line-hair);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-2);
  overflow: hidden;
}
/* Hors superposition, le corps n'a plus rien à confiner : le laisser en
   `overflow-y: auto` créerait un conteneur de défilement sans contenu à faire
   défiler — donc un axe X en `auto` que la sonde surveille pour rien. */
.c-tiroir-panier[data-vitrine="oui"] .tiroir__corps { overflow: visible; }

/* ═══ Les cinq états, commutables depuis la barre de maquette ═════════════
   MAQUETTE UNIQUEMENT. `mockup/toolbar.js` pose `html[data-etat="panier-…"]`
   depuis l'URL (`?e=panier-1`), donc les cinq états se commutent SUR TÉLÉPHONE,
   sans recharger et sans console. Ce sont de vrais tiroirs `position: fixed` :
   ce qu'on voit ici est la géométrie réelle, voile compris.

   Hors état, ils sont retirés du rendu — pas seulement transparents : cinq
   panneaux fixes empilés sur une page d'atelier fausseraient toute mesure.

   ⚠ `:not([data-demo=""])` est LOAD-BEARING. Le gabarit rend `data-demo=""`
   sur toute instance qui ne reçoit pas la prop — la sémantique de Liquid pour
   une variable absente — et `[data-demo]` matche un attribut VIDE aussi bien
   qu'un attribut rempli. Écrit sans le `:not`, ce sélecteur a masqué les SEPT
   tiroirs de la page d'atelier, y compris ceux de la vitrine. Et le pire : la
   sonde est passée au vert, parce qu'un élément `display: none` n'a aucun
   défaut à signaler. Une porte verte sur une page où le composant central ne
   rend rien — exactement la panne que ce dépôt existe pour éliminer, et rien
   d'autre que la lecture des captures ne l'aurait attrapée. */
.c-tiroir-panier[data-demo]:not([data-demo=""]) { display: none; }
html[data-etat="panier-vide"]   .c-tiroir-panier[data-demo="vide"],
html[data-etat="panier-1"]      .c-tiroir-panier[data-demo="1"],
html[data-etat="panier-croise"] .c-tiroir-panier[data-demo="croise"],
html[data-etat="panier-seuil"]  .c-tiroir-panier[data-demo="seuil"],
html[data-etat="panier-erreur"] .c-tiroir-panier[data-demo="erreur"] { display: block; }

/* ---------- Au-delà du téléphone ---------------------------------------------
   Le point de rupture ÉLARGIT une mise en page qui marche déjà : le panneau
   garde ses 440 px et ne devient jamais une seconde page. */
@media (min-width: 48rem) {
  .c-tiroir-panier .tiroir__corps { padding: var(--sp-5); }
  .c-tiroir-panier .tiroir__pied { padding: var(--sp-5); }
}


/* ═══ lib/components/tri.css ═══ */
/* FICHE 30 — tri. Cotes transposées de gymshark.com (feuille de 295 px, quatre
   rangées de 47-49 px, poignée centrée, aucun titre), avec cutsclothing.com en
   contre-modèle assumé sur deux points : huit options au lieu de quatre, et
   36 px de pas au lieu de 48. */

/* ---------- Le déclencheur ---------------------------------------------------
   Un mot ET une icône. Et la VALEUR COURANTE — le seul écart volontaire au
   corpus : les quatre marques affichent leur déclencheur nu, donc leur visiteur
   ne sait pas dans quel ordre il regarde. Si la place manque, c'est la valeur
   qui se coupe, jamais le mot : `flex: none` sur le mot, ellipse sur la
   valeur. */
.c-tri__declencheur {
  width: 100%; justify-content: center; gap: var(--sp-2);
  color: var(--fg-1); min-width: 0;
}
.c-tri__mot { flex: none; }
.c-tri__valeur {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--fg-2);
}

/* ---------- La feuille -------------------------------------------------------
   Basse et courte : 65 % de l'écran reste du contexte. Aucun titre, aucune
   validation, aucune croix — la mécanique de `.tiroir--bas` fait le reste. */
.c-tri__feuille { max-height: 60vh; }
.c-tri__poignee {
  flex: none; width: var(--sp-7); height: var(--sp-1);
  margin: var(--sp-2) auto var(--sp-1);
  border-radius: var(--r-pill); background: var(--line-hair);
}
.c-tri__corps { padding: var(--sp-2) var(--sp-4) var(--sp-5); }
.c-tri__options { border: 0; padding: 0; margin: 0; display: grid; }

/* ---------- Une option -------------------------------------------------------
   48 px de haut minimum, toute la rangée cliquable par le `<label>`.
   L'état sélectionné se signale TROIS fois — pastille pleine + coche à
   l'intérieur + libellé souligné. Trois signaux pour un état : c'est robuste au
   daltonisme et au contraste faible, et ça ne coûte rien. C'est le seul détail
   à voler à Cuts. */
.c-tri__option { min-height: var(--sp-7); align-items: center; }
.c-tri__option .choix__marque { margin-top: 0; }
.c-tri__libelle { flex: 1; min-width: 0; font-size: var(--fs-body-sm); line-height: var(--lh-snug); }
.c-tri__option input:checked ~ .c-tri__libelle {
  font-weight: 500; text-decoration: underline; text-underline-offset: 0.25em;
}

/* ---------- Les états -------------------------------------------------------- */
.c-tri__erreur, .c-tri__rupture, .c-tri__vide { display: none; }
.c-tri[data-etat="erreur"] .c-tri__erreur { display: flex; margin-bottom: var(--sp-4); }
.c-tri[data-etat="erreur"] .c-tri__options { opacity: .45; }

/* Rupture : l'option mémorisée a disparu de la collection. Elle reste VISIBLE,
   barrée et inactive, avec l'ordre de repli nommé — sinon le visiteur croit
   avoir mal lu. */
.c-tri[data-etat="rupture"] .c-tri__rupture { display: flex; margin-bottom: var(--sp-4); }
.c-tri[data-etat="rupture"] .c-tri__option[data-etat="rupture"] { color: var(--fg-3); cursor: not-allowed; }
.c-tri[data-etat="rupture"] .c-tri__option[data-etat="rupture"] .c-tri__libelle { text-decoration: line-through; }

/* Vide : rien à trier. Le composant existe quand même — il dit pourquoi il ne
   sert à rien ici, et il donne l'issue. */
.c-tri[data-etat="vide"] .c-tri__vide { display: grid; }
.c-tri[data-etat="vide"] .c-tri__options { display: none; }

/* Chargement : la grille se retrie. La valeur du déclencheur passe en ossature
   — de la largeur d'un libellé, pas de la largeur du bouton. */
.c-tri[data-etat="chargement"] .c-tri__valeur {
  min-width: var(--sp-9); background: var(--bg-subtle); color: transparent;
  border-radius: var(--r-xs);
}

/* Long : un libellé d'option à rallonge. Le libellé du déclencheur se coupe,
   celui de la rangée passe à la ligne — une option ne se tronque jamais dans la
   liste, sinon on choisit à l'aveugle. */
.c-tri[data-etat="long"] .c-tri__libelle { white-space: normal; }

/* Désactivé : un seul produit, ou `enable_sort = false`. Le tri EXISTE pourtant
   même quand les filtres n'existent pas — Whisker et Litter-Robot n'ont aucun
   filtre et gardent un tri. C'est l'inverse qui est le défaut par défaut. */
.c-tri[data-etat="desactive"] .c-tri__declencheur { color: var(--fg-3); cursor: not-allowed; }


/* ═══ lib/components/vente-croisee.css ═══ */
/* FICHE 38 — vente croisée. Une PILE, pas un rail : il n'y a donc rien à faire
   déborder, et la recette `scrollWidth === clientWidth` est tenue par
   construction. Trois articles maximum, tous visibles sans geste horizontal. */

.c-vente-croisee { display: grid; gap: var(--sp-3); }
.c-vente-croisee__titre { color: var(--fg-3); }

.c-vente-croisee__liste { list-style: none; display: grid; gap: var(--sp-2); padding: 0; }

.c-vente-croisee__carte {
  --vignette: 64px;
  display: grid;
  grid-template-columns: var(--vignette) minmax(0, 1fr);
  grid-template-areas: "vignette texte" "vignette action";
  gap: var(--sp-1) var(--sp-3);
  align-content: start;
  padding: var(--sp-3);
  background: var(--bg-surface);
  border: 1px solid var(--line-hair);
  border-radius: var(--r-sm);
}

.c-vente-croisee__vignette {
  grid-area: vignette; align-self: start;
  width: var(--vignette); border-radius: var(--r-xs);
  background: var(--bg-subtle);
}
.c-vente-croisee__vignette .c-visuel-marque { border-radius: 0; }
.c-vente-croisee__vignette .c-visuel-marque__nom {
  font-size: var(--fs-caption); padding: 0 var(--sp-1);
}

.c-vente-croisee__texte { grid-area: texte; display: grid; gap: var(--sp-1); }
.c-vente-croisee__nom {
  font-size: var(--fs-body-sm); font-weight: 500; line-height: var(--lh-snug);
}
.c-vente-croisee__nom a { color: inherit; text-decoration: none; }
.c-vente-croisee__raison {
  font-size: var(--fs-caption); line-height: var(--lh-normal); color: var(--fg-2);
}
/* Le prix n'est PAS de la même graisse que le titre. Chez Gymshark les deux
   sont en 14 px / 400 : rien ne hiérarchise, et le prix est précisément
   l'information que l'œil cherche. */
.c-vente-croisee__prix {
  font-size: var(--fs-body-sm); font-weight: 600; color: var(--fg-1);
  white-space: nowrap;
}

.c-vente-croisee__ajout { grid-area: action; justify-self: end; }

/* Chargement — l'ossature garde la FORME de la carte attendue, donc la liste ne
   saute pas quand les suggestions arrivent. */
.c-vente-croisee[data-etat="chargement"] .c-vente-croisee__nom,
.c-vente-croisee[data-etat="chargement"] .c-vente-croisee__raison,
.c-vente-croisee[data-etat="chargement"] .c-vente-croisee__prix {
  background: var(--bg-subtle); color: transparent; border-radius: var(--r-xs);
}
.c-vente-croisee[data-etat="chargement"] .c-vente-croisee__prix { max-width: var(--sp-9); }
.c-vente-croisee[data-etat="chargement"] .c-vente-croisee__ajout { visibility: hidden; }

/* Au-delà du téléphone la pile RESTE une pile : le point de rupture élargit une
   mise en page qui marche déjà, il ne la redessine pas en carrousel. */
@media (min-width: 48rem) {
  .c-vente-croisee__carte { --vignette: 72px; }
}


/* ═══ lib/components/vente-incitative.css ═══ */
/* FICHE 36 — vente incitative. Trois variantes, trois moments d'une même offre.
   Les cotes de la feuille viennent de la mesure d'Oura : y = 190 → 844 sur un
   écran de 844, soit 654 px = 77 % de la hauteur. Elle ne prend pas tout
   l'écran, et c'est délibéré : voir le haut de la page rappelle qu'on peut en
   sortir. */

/* ═══ Feuille du bas — au moment de l'ajout ═══════════════════════════════ */
.c-vente-incitative__feuille {
  height: 77svh;
  max-height: 77svh;
  background: var(--bg-warm);   /* crème, comme la feuille mesurée : la couche
                                   d'offre se distingue de la page sans devoir
                                   crier. */
}
.c-vente-incitative__poignee {
  flex: none; width: var(--sp-7); height: 4px;
  margin: var(--sp-3) auto var(--sp-1);
  border-radius: var(--r-pill); background: var(--line-rule);
}
.c-vente-incitative__feuille .tiroir__corps { padding-top: var(--sp-4); }

.c-vente-incitative__titre {
  font-family: var(--font-display); font-size: var(--fs-h2);
  line-height: var(--lh-snug); letter-spacing: var(--tr-tight);
}
.c-vente-incitative__offre {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); margin-top: var(--sp-4);
  padding-top: var(--sp-3); border-top: 1px solid var(--line-rule);
  font-size: var(--fs-body-sm); font-weight: 500;
}
/* L'offre est nommée ET chiffrée, à côté d'elle. Jamais fondue dans le montant
   du bouton — c'est exactement ce que fait `Checkout+ | $62.25`. */
/* `white-space: nowrap` : sans lui, le prix est un item de flex rétrécissable
   et « + 20,00 € » se casse entre le nombre et le symbole — le € seul sur sa
   ligne. Mesuré le 2026-08-22 dans la feuille à 390 px. Un montant ne se coupe
   jamais, nulle part. */
.c-vente-incitative .prix { white-space: nowrap; }
.c-vente-incitative__offre .prix {
  flex: none;
  font-size: var(--fs-body-lg); font-weight: 600;
}

.c-vente-incitative__args {
  margin-top: var(--sp-5); padding-left: var(--sp-5);
  display: grid; gap: var(--sp-3);
  font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-2);
}
.c-vente-incitative__args li::marker { color: var(--fg-3); font-variant-numeric: tabular-nums; }

.c-vente-incitative__actions { display: grid; gap: var(--sp-2); }
.c-vente-incitative__feuille .tiroir__pied { background: var(--bg-warm); }
.c-vente-incitative__conditions { color: var(--fg-2); text-align: center; }

/* ═══ Sous-ligne — l'incitative acceptée, dans la ligne article ═══════════ */
.c-vente-incitative[data-variante="sous-ligne"] { display: block; }
.c-vente-incitative__sous {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--sp-1) var(--sp-3); align-items: start;
  margin-top: var(--sp-2); padding-top: var(--sp-3);
  border-top: 1px solid var(--line-hair);
}
.c-vente-incitative__icone { color: var(--fg-3); margin-top: var(--sp-1); }
.c-vente-incitative__sous-texte { display: grid; gap: var(--sp-1); }
.c-vente-incitative__sous-titre { font-size: var(--fs-body-sm); font-weight: 500; }
.c-vente-incitative__sous-detail {
  font-size: var(--fs-caption); line-height: var(--lh-normal); color: var(--fg-2);
}
.c-vente-incitative__sous-valeurs {
  display: grid; gap: var(--sp-1); justify-items: end; text-align: right;
}
.c-vente-incitative__sous-prix { font-size: var(--fs-body-sm); font-weight: 600; }
.c-vente-incitative__sous-valeurs .prix--barre { font-size: var(--fs-caption); }
.c-vente-incitative__modifier {
  font-size: var(--fs-caption); color: var(--fg-2);
  text-decoration: underline; text-underline-offset: 3px;
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}

/* ═══ Modale « Modifier » ═════════════════════════════════════════════════ */
.c-vente-incitative__entete {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4);
}
.c-vente-incitative__option {
  margin-top: var(--sp-5); padding: var(--sp-4);
  background: var(--bg-subtle); border-radius: var(--r-sm);
}
.c-vente-incitative__option-texte {
  display: grid; gap: var(--sp-1); font-size: var(--fs-body-sm);
}
.c-vente-incitative__option-texte .prix { font-weight: 600; font-size: var(--fs-body); }
.c-vente-incitative .modale__boite .c-vente-incitative__actions { margin-top: var(--sp-5); }
/* La case décochée ne montre pas sa coche : la primitive `.choix` colore le
   glyphe en `transparent` tant que l'entrée n'est pas cochée. Rien à ajouter —
   et surtout rien à présélectionner. */

/* ═══ Vitrine d'atelier ═══════════════════════════════════════════════════
   MAQUETTE UNIQUEMENT — ne part pas en thème. Une feuille et une modale sont
   `position: fixed` : posées telles quelles dans une page d'atelier, elles se
   superposent et une seule serait visible sur une capture. `data-vitrine`
   les remet dans le flux pour qu'on puisse les regarder côte à côte. */
.c-vente-incitative[data-vitrine="oui"] > .voile { display: none; }
.c-vente-incitative[data-vitrine="oui"] .c-vente-incitative__feuille {
  position: relative; inset: auto; transform: none;
  height: auto; max-height: none;
  width: min(100%, var(--container-narrow));
  border-radius: var(--r-xl);
  box-shadow: var(--sh-2);
}
.c-vente-incitative[data-vitrine="oui"] .modale {
  position: relative; inset: auto; padding: 0; pointer-events: auto;
}
.c-vente-incitative[data-vitrine="oui"] .modale__boite {
  opacity: 1; transform: none; max-height: none; box-shadow: var(--sh-2);
}


/* ═══ lib/components/visuel-marque.css ═══ */
.c-visuel-marque {
  display: grid; place-items: center; position: relative;
  background: var(--bg-subtle); color: var(--fg-1);
}
.c-visuel-marque[data-teinte="chaud"] { background: var(--bg-warm); }
.c-visuel-marque[data-teinte="sombre"] { background: var(--bg-inverse); color: var(--fg-on-dark); }
.c-visuel-marque[data-teinte="intelligence"] { background: var(--bg-intelligence); color: var(--fg-data); }
.c-visuel-marque__motif { position: absolute; inset: 0; width: 100%; height: 100%; }
.c-visuel-marque__nom {
  position: relative; padding: var(--sp-2) var(--sp-4);
  font-family: var(--font-display); font-size: var(--fs-h3);
  letter-spacing: var(--tr-tight); text-align: center;
}
