/* Feuille de style principale du thème Uptoo Digital.
   Ordre imposé : les tokens d'abord (tout le reste les consomme), puis reset,
   objets, éléments, composants, utilitaires. */

/* Tokens — design system 3 couches.
   tokens-primitives.css est le SEUL fichier qui lit les theme settings
   (CONVENTIONS §6) ; tout le reste est en var() pur. */

/* =============================================================================
   tokens-primitives.css — COUCHE 1 (primitives)
   -----------------------------------------------------------------------------
   SEUL fichier du thème qui consomme les champs de thème (règle CONVENTIONS §6 :
   « consommation des theme settings dans UN seul fichier ; tout le reste en var() pur »).
   Le HubL des fichiers CSS est évalué UNE FOIS à la publication du thème.

   Source des valeurs : charte Figma « Uptoo Digital » (fcV1mdU8PPHxyKkt8FnxZK),
   ⚠️ Cette ligne citait `bhHkBW4TJMDCCKSQwlCEYx` jusqu'au 2026-08-09 — clé que
   CONVENTIONS §8ter interdit nommément depuis l'arbitrage du 05/08. La piste
   écrite dans le code menait donc au mauvais fichier. Corrigé.
   page « Styleguide - Composants », frame charte-uptoo-digital — extraites des
   remplissages réels de l'export SVG et recoupées avec le panneau Properties de
   Figma. Voir docs/TOKENS-FIGMA-EXTRACTION.md (méthode, preuves, écarts relevés).

   ⚠️ Ne jamais écrire un hex dans un module : uniquement var(--token) sémantique.
   ============================================================================= */

:root {

  /* Purple */
  --c-purple-50: #EFF0FE;
  --c-purple-100: #E2E3FD;
  --c-purple-200: #CBCCFA;
  --c-purple-300: #ACABF6;
  --c-purple-400: #9289F0;
  --c-purple-500: #7A65E6;
  --c-purple-600: #7251DA;
  --c-purple-700: #6342C0;
  --c-purple-800: #50389B;
  --c-purple-900: #44347B;
  --c-purple-950: #281E48;

  /* Blue */
  --c-blue-50: #F1F6FD;
  --c-blue-100: #E0EAF9;
  --c-blue-200: #C9DAF4;
  --c-blue-300: #A3C3ED;
  --c-blue-400: #6C9BE0;
  --c-blue-500: #5883D9;
  --c-blue-600: #4368CD;
  --c-blue-700: #3A56BB;
  --c-blue-800: #344799;
  --c-blue-900: #2F3F79;
  --c-blue-950: #20284B;

  /* Green */
  --c-green-50: #F4FBF2;
  --c-green-100: #E4F8E0;
  --c-green-200: #C9F0C2;
  --c-green-300: #9DE293;
  --c-green-400: #83D477;
  --c-green-500: #45B136;
  --c-green-600: #359128;
  --c-green-700: #2C7322;
  --c-green-800: #275B20;
  --c-green-900: #214B1C;
  --c-green-950: #0C290A;

  /* Beige */
  --c-beige-50: #FCF8F0;
  --c-beige-100: #F8F0DC;
  --c-beige-200: #F0DEB8;
  --c-beige-300: #EACE9A;
  --c-beige-400: #DCA75B;
  --c-beige-500: #D48F3B;
  --c-beige-600: #C67830;
  --c-beige-700: #A45E2A;
  --c-beige-800: #844C28;
  --c-beige-900: #6B3F23;
  --c-beige-950: #391F11;

  /* Neutral */
  --c-neutral-50: #F8F9FB;
  --c-neutral-100: #F0F2F5;
  --c-neutral-200: #E2E6EC;
  --c-neutral-300: #CDD3DD;
  --c-neutral-400: #ADB7C6;
  --c-neutral-500: #8E9BAF;
  --c-neutral-600: #65758B;
  --c-neutral-700: #4F5C6E;
  --c-neutral-800: #3A4455;
  --c-neutral-900: #272F3D;
  --c-neutral-950: #161B26;

  /* --- Rouge — PROVISOIRE, en attente de la charte ---------------------------
     La charte ne comporte aucune gamme rouge, alors que les formulaires en ont
     besoin (trois hex en dur dans _forms.css avant le 2026-08-05, mesurés à
     2,72 / 3,05 / 3,41 contre un seuil de 4,5). Valeur relevée sur uptoo.fr, qui
     emploie #EF4444 comme couleur « destructive » sur les champs invalides.
     Deux nuances, parce qu'une seule ne peut pas tenir les deux seuils : #EF4444
     donne 3,57 sur --bg et 3,36 sur --surface, ce qui passe les 3:1 exigés pour
     la limite d'un composant (WCAG 1.4.11) mais PAS les 4,5 d'un texte. Le 700
     est le même rouge assombri à 80 % : 5,25 et 4,93, même teinte, seuil tenu.
     À confirmer par l'équipe design (ligne D-09) — c'est la seule couleur du
     thème qui ne vienne pas de la charte. */
  --c-red-200: #F69898;
  --c-red-500: #EF4444;
  --c-red-700: #BF3636;

  /* Base */
  --c-base-white: #FFFFFF;
  --c-base-black: #000000;
}

/* =============================================================================
   Primitives non-couleur — typographie, espacements, rayons, mouvement.
   ============================================================================= */

:root {
  /* --- Familles (charte : DM Sans + DM Mono, 2 Google Fonts) ---------------- */
  --font-sans: DM Sans, "Helvetica Neue", Arial, sans-serif;
  --font-mono: DM Mono, ui-monospace, "SF Mono", Menlo, monospace;
  --fw-regular: 400;
  --fw-medium: 500;

  /* --- Échelle typographique DESKTOP (charte « Échelle Typographique Desktop »)
     Chaque style est un couple taille + letter-spacing : le tracking négatif
     fait partie de l'identité, il ne se dissocie pas de la taille.

     ⚠ CORRECTION DU 2026-08-06 — les interlettrages étaient en PIXELS.
     La charte Figma les donne en POURCENTAGE de la taille de police ; le socle
     avait repris les nombres tels quels en px, sur les 13 styles. L'écart allait
     jusqu'à ×8 : l'eyebrow était espacé de 6px pour une police de 12px, là où la
     maquette mesure 0,72px.
     Trois valeurs vérifiées dans les relevés Figma (docs/contenu-maquettes/) :
       · eyebrow    12px → 0,72px  = 6 %    (relevé à ~10 endroits, constant)
       · display-xl 56px → -1,12px = -2 %
       · body-s     14px → -0,14px = -1 %
     Les 10 autres suivent la même règle de conversion (le nombre en px de la
     charte EST le pourcentage) — cohérent avec les 3 mesures, mais non vérifié
     nœud par nœud : à confirmer si un doute apparaît au rendu.

     Unité retenue : `em`, pas des px résolus. L'em suit la taille de police,
     donc l'échelle mobile ci-dessous n'a plus à redéclarer d'interlettrage —
     et un changement de taille ne désaccorde plus le tracking. ------------- */
  --fs-display-xl: 56px;  --ls-display-xl: -0.02em;
  --fs-display-l:  48px;  --ls-display-l:  -0.02em;
  --fs-display-m:  40px;  --ls-display-m:  -0.02em;
  --fs-heading-l:  32px;  --ls-heading-l:  -0.02em;
  --fs-heading-m:  24px;  --ls-heading-m:  -0.01em;
  --fs-heading-s:  20px;  --ls-heading-s:  -0.01em;
  --fs-body-l:     18px;  --ls-body-l:     -0.015em;
  --fs-body-m:     16px;  --ls-body-m:     -0.015em;
  --fs-body-s:     14px;  --ls-body-s:     -0.01em;
  --fs-label-m:    15px;  --ls-label-m:    -0.0125em;
  --fs-label-s:    13px;  --ls-label-s:    -0.01em;
  --fs-eyebrow:    12px;  --ls-eyebrow:    0.06em;  /* DM Mono, uppercase, tracking POSITIF */
  --fs-caption:    12px;  --ls-caption:    -0.01em;

  /* --- Interlignages : UN TOKEN PAR STYLE, comme --fs-* et --ls-* -----------
     Corrigé le 2026-08-08. Le socle posait quatre tokens (`--lh-display`,
     `--lh-heading`, `--lh-body`, `--lh-label`) pour douze styles, en les
     annonçant « non spécifiés par la charte Figma — valeurs de travail ».
     Les deux moitiés de cette phrase étaient fausses : la charte les spécifie
     (relevés `docs/contenu-maquettes/solutions-audit-crm.md`, section « Tokens
     Figma relevés », constants sur les 4 frames desktop), et les valeurs de
     travail étaient servies en préprod depuis le 06/08.

     Un token ne peut pas servir deux styles qui n'ont pas la même valeur :
     `--lh-body` couvrait Body/L (1,6), Body/M (1,5) et Body/S (1,5), donc
     deux des trois étaient faux quoi qu'on choisisse. Sept des huit styles
     mesurés étaient hors référence. Détail chiffré : DECISIONS-DESIGN §5.2,
     fiche N-01. Le grain est désormais celui de --fs-* : un style, un token. */
  --lh-display-xl: 1.1;
  --lh-display-l:  1.1;
  --lh-display-m:  1.12;
  --lh-heading-l:  1.15;
  --lh-heading-m:  1.25;
  --lh-heading-s:  1.3;
  --lh-body-l:     1.6;
  --lh-body-m:     1.5;
  --lh-body-s:     1.5;
  --lh-label-m:    1.35;
  --lh-label-s:    1.4;
  --lh-eyebrow:    1.4;
  /* ⚠ Caption n'a PAS de style Figma nommé : la charte ne relève pas de
     `Desktop/Caption`. 1,3 est donc la seule valeur de cette table qui reste
     une valeur de travail — reprise telle quelle de l'ancien `--lh-label`,
     et signalée comme non vérifiée plutôt que présentée comme relevée. */
  --lh-caption:    1.3;

  /* --- Espacements (base 8) ------------------------------------------------ */
  --sp-1: 4px;   --sp-2: 8px;   --sp-3: 12px;  --sp-4: 16px;
  --sp-5: 24px;  --sp-6: 32px;  --sp-7: 48px;  --sp-8: 64px;
  --sp-9: 80px;  --sp-10: 120px;

  /* --- Rythme vertical de section — TROIS PALIERS (arbitrage Antoine,
     2026-08-08) --------------------------------------------------------------
     Le socle posait `--sp-9` (80px) sur toutes les sections. Aucune section de
     la maquette n'est à 80. Le relevé du 07/08 (DECISIONS-DESIGN §4.4) donne
     trois familles — 89/96, 104/104, 120/120 — et une amplitude de 89 à 128px.
     Le seuil ERROR de V6 étant à 12px, AUCUNE valeur unique ne peut être juste :
     c'est ce qui a écarté l'option « une valeur, et on fait corriger la
     maquette ».

     Ces valeurs sont des relevés, pas des multiples de la base 8 : 89/96 n'est
     pas arrondi à 88/96, et c'est délibéré — l'écart de 1px resterait sous le
     seuil OK, mais rien n'oblige à s'éloigner de la maquette pour faire joli.

     La syntaxe à deux valeurs (`89px 96px`) est un raccourci `padding-block`
     haut/bas : la maquette est asymétrique sur ce palier, pas le socle. */
  --section-y-xs: 64px;        /* Preuves (listing HubSpot) — relevé le 2026-08-09 */
  --section-y-sm: 89px 96px;   /* Expertises, Méthode — et toute la famille 86→96, ci-dessous */
  --section-y-md: 104px;       /* Intégrations, Cas clients */
  --section-y-lg: 120px;       /* Hero, Problème, Offres, Nos différences, FAQ */

  /* `--section-y-sm` EST LE PALIER DE LA FAMILLE 86→96, pas seulement des deux
     sections qui lui donnent son nom. Arbitrage M-14 du 2026-08-11 : les quatre
     sections de `offre-audit-crm` qui manquaient d'un palier se câblent ICI,
     aucun 5e palier n'est ajouté.

     Le relevé ne donne pas une valeur mais une famille, dont les trois hauts
     mesurés tiennent dans 10 px — et dont le BAS ne varie pas :
       · 96 haut / 96 bas — `solutions-audit-crm.md:70` (Pour qui), `:91`
         (Le bon moment), `:173` (Votre livrable) ;
       · 89 haut / 96 bas — DECISIONS-DESIGN §4.4 (Expertises, Méthode) ;
       · 86 haut / 96 bas — `solutions-audit-crm.md:27` (hero de cette page),
         `a-propos.md:101`, et `mesures-manquantes-2026-08-10.md:174-177` qui le
         retrouve par soustraction (632 − 86 − 450 = 96).
     89 est donc à 7 px de la borne haute et à 3 px de la borne basse, bas exact
     dans les trois cas : sous le seuil ERROR de 12 px de la gate.

     ⚠️ Les deux sections relevées « 86 haut » de `offre-audit-crm`
     (`solutions-audit-crm.md:196` et `:232`) N'ONT PAS de padding bas mesuré —
     le relevé s'arrête au haut. Les 96 du palier leur sont servis par la famille
     ci-dessus, pas par une mesure de ces deux sections-là. Écart déclaré.

     Pourquoi PAS un 5e palier à 96/96, qui aurait pourtant été exact sur deux
     des quatre sections :
       ① il n'existerait qu'en desktop — sous 768 px toute la page est à 56/56
          (`mobile-solutions-audit-crm.md:41`, « gabarit constant de la page »),
          donc il se replierait exactement sur `sm` dans le bloc mobile ;
       ② il COUPERAIT la famille en deux SUR UNE MÊME PAGE : « Pour qui cet audit
          est-il fait ? » est relevée 96/96 elle aussi et rendue par
          `texte-highlight`, qui câble `section--rhythm-sm` en dur
          (`texte-highlight.module/module.html:53`). La section 2 resterait à 89
          pendant que la section 3, au relevé identique, passerait à 96. Deux
          relevés égaux servis différemment est un défaut pire que 7 px uniformes ;
       ③ six sites du dépôt mappent DÉJÀ 96/96 ou 86/96 sur `sm` en déclarant le
          résidu (`hero-page.module/module.css:173`,
          `texte-highlight.module/module.html:53`,
          `cards-hubs-stack.module/module.html:39`, `page-offre.html:203` et
          `:620`, `page-libre.html:426`). Un 5e palier les rendrait tous faux
          d'un coup, sans qu'aucun ait bougé. */

  /* --- Hauteurs de bloc FIGÉES PAR LA MAQUETTE (ajout du 2026-08-10) ---------
     Trois cotes que la maquette impose et que l'échelle `--sp-*` ne peut pas
     porter : elle décrit des ESPACEMENTS, s'arrête à 120, et aucune combinaison
     n'y vaut 208, 256 ni 444. Elles sont ici pour exactement la même raison que
     `--section-y-*` juste au-dessus — ce sont des relevés, pas des multiples de
     la base 8 — et parce qu'un module n'a pas le droit d'écrire une longueur en
     dur (CONTRAT-SOCLE §1 et §8).

     ⚠️ ELLES NE VALENT QU'EN DESKTOP, et c'est une donnée de maquette, pas une
     précaution : le mobile relève d'autres valeurs pour les mêmes blocs —
     panneau de badges 350 × **301** (`mobile-a-propos.md:176`) et cards de
     « Notre appartenance » en **hauteur libre** (`mobile-a-propos.md:204`). Les
     deux modules qui les consomment le font donc sous `@media (min-width: 64em)`
     et jamais en règle inconditionnelle. Aucune valeur mobile n'est servie à ce
     jour ; ces deux-là restent des écarts ouverts. */
  --panel-h-badges: 444px;  /* Panneau d'accréditations — a-propos.md:111, nœud 2201:10424 */

  /* Hauteurs de card, NOMMÉES PAR LEUR COTE (renommage du 2026-08-10, M-12).
     Elles s'appelaient `--card-h-sm` / `--card-h-md`, ce qui tenait tant qu'il n'y
     en avait que deux. Le relevé en donne CINQ, et elles ne s'ordonnent pas en
     tailles de tee-shirt : 240 s'intercale entre 208 et 256. Un nom de cote ne
     ment pas et n'a pas à être renuméroté au relevé suivant. */
  --card-h-208: 208px;  /* « Notre appartenance » — a-propos.md:137, nœuds 2198:14250 / 14256 */
  --card-h-240: 240px;  /* « Ce que nous faisons » — a-propos.md:86 */
  --card-h-256: 256px;  /* « Notre façon de travailler » — a-propos.md:155, nœuds 2198:9750 / 9756 / 9762 */
  --card-h-320: 320px;  /* « Une offre par secteur » — solutions-listing.md:105 · « Votre livrable » — solutions-audit-crm.md */
  --card-h-380: 380px;  /* « Nos différences » — accueil, 4 colonnes */

  /* En-tête latéral de `cards-grid` (M-15). Relevés : 462 px sur hub-solutions
     (`solutions-listing.md:99`, recoupé au nœud `1297:22848` — 462 × 198) et
     440 px sur page-libre (`a-propos.md:79`). Une seule valeur pour les deux, à
     22 px près : la gouttière de 120 et la largeur de grille qui suit sont, elles,
     exactes des deux côtés (1200 − 462 − 120 = 618 = 2 cards de 301 + 16 ;
     1200 − 440 − 120 = 640 = 2 cards de 312 + 16). L'écart de 22 px sur page-libre
     est déclaré, pas masqué. */
  --cards-head-aside-w: 462px;

  /* --- Panneau encarté du CTA final (ajout du 2026-08-10, punch list M-01/M-02)
     Le CTA final n'est PAS un bandeau sombre pleine largeur : la maquette pose une
     carte encartée dans une section claire, sur les 4 gabarits et aux 2 viewports.
     Relevés concordants : `faq-accordeon---cta-final.md:108` (« pleine largeur
     1200 px, radius 12px, dégradé 147.12deg #281e48 13,871 % → #50389b 96,042 %,
     padding 56px 56px 56px 80px, overflow hidden »), `solutions-listing.md:43`
     et `:168`, `solutions-audit-crm.md:291`, `a-propos.md:167`.
     Aucune de ces quatre cotes n'existe dans `--sp-*` : 56, 80 en asymétrie, 40 et
     l'angle. Elles sont ici pour la même raison que les trois hauteurs ci-dessus —
     ce sont des relevés, et un module n'a pas le droit d'écrire une longueur en dur
     (CONTRAT-SOCLE §1 et §8). Les mettre au socle plutôt qu'une media query dans le
     module préserve la promesse du fichier : `cta-final/module.css` reste sans
     aucun point de rupture, la bascule est portée par les tokens.
     ⚠️ Les deux relevés mobiles DIVERGENT sur le padding du panneau :
     `mobile-solutions-listing.md:57` donne 32/24, `mobile-a-propos.md:240` donne
     56 vertical / 20 latéral. La valeur retenue est celle du premier (la page qui
     porte deux instances) ; l'écart d'À propos reste ouvert, non masqué. */
  --panel-cta-pad-block:  56px;   /* haut et bas */
  --panel-cta-pad-inline: 56px;   /* droite */
  --panel-cta-pad-start:  80px;   /* gauche — l'asymétrie est au relevé */
  --panel-cta-angle:     147deg;  /* relevé 147,12° — l'arrondi est un écart assumé */
  --card-quiz-pad:        40px;   /* carte quiz blanche, `faq-accordeon---cta-final.md:110` */

  /* --- Grille & containers (relevés charte : frame 1440, padding latéral 120,
     donc largeur de contenu 1200) ------------------------------------------- */
  --container-max: 1200px;
  /* ⚠ CORRECTION DU 2026-08-06 — la gouttière était figée à 120px de 768px à
     l'infini, et ne retombait à 20px qu'en dessous de 768px. Deux conséquences
     mesurées en préprod : à 1024px le contenu tombait à 784px utiles (les 120px
     de la charte valent 8,3 % d'un cadre de 1440, pas 11,7 % d'un cadre de
     1024), et le passage de 768 à 767px élargissait le contenu de 199px d'un
     seul coup — une rupture visible au redimensionnement.
     La gouttière n'est pas une simple proportion : la charte pose 120px sur un
     cadre de 1440 (8,3 %) et 20px sur un cadre de 390 (5,1 %). Une seule valeur
     en vw ne peut pas satisfaire les deux. D'où une interpolation LINÉAIRE entre
     les deux points relevés, bornée aux extrémités : elle passe exactement par
     120px à 1440 et par 20px à 390, sans aucun palier — donc sans le saut de
     199px qu'on observait en franchissant 768px.
     Le pas : (120-20)/(1440-390) = 0,0952 px par px de viewport. */
  --container-pad: min(120px, max(16px, calc(20px + (100vw - 390px) * 0.0952)));
  /* Gouttière de `.grid--2|3|4`, c'est-à-dire celle des cards. Mesurée à 16px
     sur deux frames Figma indépendantes le 2026-08-05 ; les 32px d'origine
     n'avaient jamais été relevés sur la maquette. Les autres gouttières du site
     ne passent PAS par ce token : la maquette en emploie quatre (16 cards · 24
     rangées · 120 colonnes de hero · 0 section problème), aucune variable
     unique ne peut les porter, et chacune existe déjà dans l'échelle --sp-*. */
  --grid-gap: var(--sp-4);

  /* --- Rayons (charte : boutons et icon-buttons mesurés à 6px) ------------- */
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-pill: 999px;

  /* --- Mouvement — SPEC-ANIMATIONS-V1 §1 (tokens imposés, zéro valeur en dur
     dans les modules) -------------------------------------------------------- */
  --ease-brand: cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-out: ease-out;
  --dur-fast: 150ms;      /* hovers boutons / options / tabs */
  --dur-med: 300ms;       /* toggles, swaps de couleur, progress bars */
  --dur-slow: 500ms;      /* barres de graphe, transitions de visuel */
  --dur-enter: 600ms;     /* entrée hero (upfade) */
  --dur-menu: 200ms;      /* ouverture mega-menu (fourchette charte 180-220) */
  --stagger-hero: 150ms;  /* cascade des colonnes / cards du hero */
  --stagger-reveal: 70ms; /* cascade des scroll reveals (fourchette 60-80) */
  --dur-pulse: 2.4s;      /* dot « live » des eyebrows/badges */
  --dur-countup: 1200ms;  /* compteurs stats */

  /* Décalage vertical de l'upfade — la SEULE distance de mouvement de la charte.
     Elle sert à l'entrée des heros ET à la révélation au scroll : c'est pour
     qu'elle ne soit pas réécrite dans chaque module qu'elle est un token. */
  --shift-upfade: 14px;

  /* Durée d'un tour de piste de marquee. Ajoutée le 2026-08-05 : sans elle, les
     modules fabriquaient une vitesse de défilement en multipliant --dur-slow
     (un timing de hover) par 72 ou 80 — un détournement qui rendait les deux
     bandes solidaires d'un token qui n'a rien à voir. */
  --dur-marquee: 40s;
}

/* --- Échelle typographique MOBILE — styles Figma SÉPARÉS, pas un simple
   ratio : seuls Display XL/L/M, Heading L/M et Caption changent. ------------ */
@media (max-width: 767px) {
  :root {
    /* Seules les TAILLES changent. Les interlettrages ne sont plus redéclarés :
       depuis leur passage en `em` (2026-08-06), ils suivent la taille de police
       automatiquement — les redéclarer en px réintroduirait précisément le
       défaut qu'on vient de corriger.
       `--container-pad` n'est plus redéclaré non plus : l'interpolation posée
       en :root passe déjà par 20px à 390px, et le palier qui existait ici
       provoquait un saut de 199px au franchissement de 768px. */
    --fs-display-xl: 30px;
    --fs-display-l:  28px;
    --fs-display-m:  26px;
    --fs-heading-l:  24px;
    --fs-heading-m:  22px;
    --fs-caption:    11px;

    /* Rythme vertical mobile : UN SEUL palier, 56/56 — c'est la valeur relevée
       sur les deux sections mobiles mesurées le 07/08 (`1979:9740` Section/Run
       et `1979:9784` Section/Offres), et la maquette mobile n'en montre pas
       d'autre. Les trois paliers desktop s'y replient donc tous les trois : le
       rythme à trois niveaux est une composition d'écran large.
       Le socle servait 48/48 (`--sp-7`), soit 8px de moins sur chaque bord —
       point 20 du reste-à-faire, même arbitrage que le point 7. */
    --section-y-xs: 56px;
    --section-y-sm: 56px;
    --section-y-md: 56px;
    --section-y-lg: 56px;

    /* Panneau du CTA final en mobile — `mobile-solutions-listing.md:57` :
       « carte 350 × 882,5, padding 32px 24px, radius 12, dégradé 104,53° ».
       L'angle passe de 147° à ~105° : la carte est presque carrée en desktop et
       très haute en mobile, un même angle n'y donne pas la même diagonale.
       Le padding gauche perd son asymétrie : 24 des deux côtés. */
    --panel-cta-pad-block:  32px;
    --panel-cta-pad-inline: 24px;
    --panel-cta-pad-start:  24px;
    --panel-cta-angle:     105deg;  /* relevé 104,53° (listing) / 106,15° (À propos) */
    --card-quiz-pad:        24px;   /* `mobile-solutions-listing.md:75` */
  }
}

/* --- Accessibilité : le mouvement est une amélioration, jamais une condition
   d'affichage (SPEC-ANIMATIONS-V1 §4). Neutralise les durées à la source. --- */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 1ms; --dur-med: 1ms; --dur-slow: 1ms; --dur-enter: 1ms;
    --dur-menu: 1ms; --stagger-hero: 0ms; --stagger-reveal: 0ms;
    --dur-countup: 1ms;
  }
}
/* =============================================================================
   tokens-semantic.css — COUCHE 2 (rôles) + COUCHE 3 (contexte sombre)
   -----------------------------------------------------------------------------
   Aucune valeur littérale, aucun champ de thème : uniquement des références à la
   couche 1. Le mapping rôle → primitive est le contrat de design DÉCLARÉ PAR LA
   CHARTE elle-même (section « Couleurs sémantiques & Tokens », qui nomme la
   primitive cible sous chaque rôle : « bg/page → Neutral 50 », etc.).

   Les modules ne consomment QUE ces tokens (gate MODULE : zéro hex en dur).
   ============================================================================= */

/* Les rôles clairs sont déclarés pour la racine ET pour `.section--light` :
   UNE seule liste, deux sélecteurs, donc aucune valeur recopiée et aucune
   dérive possible entre les deux. `.section--light` est l'inverse manquant de
   `.section--dark` (ajouté le 2026-08-05) : il repose les rôles clairs sur son
   sous-arbre, à n'importe quelle profondeur d'une section sombre. C'est ce qui
   permet une carte blanche dans une section sombre — la maquette en pose au
   moins trois (carte du CTA final, 4 cards de Section/Couvre, pile hubs).

   `.section--white` (M-08, 2026-08-11) rejoint la même liste, et pour la même
   raison : posée dans un contexte sombre elle doit d'abord reposer les rôles
   clairs, avant de repointer `--bg` sur le blanc juste sous le bloc
   `.section--light` ci-dessous. Elle n'ajoute AUCUN rôle à cette liste. */
:root,
.section--light,
.section--white {
  /* Fonds — charte « Background (Arrière-plans) » */
  --bg-white: var(--c-base-white);
  --bg: var(--c-neutral-50);
  --surface: var(--c-neutral-100);
  --bg-purple-light: var(--c-purple-50);
  /* `bg/purple-dark` vaut #50389B dans la charte, c'est-à-dire purple-800 —
     pas purple-900 (#44347B), servi ici jusqu'au 2026-08-08.
     ⚠️ Corriger cette ligne ne change RIEN au rendu : `--bg-purple-dark` n'a
     aucun consommateur dans le thème. C'est `.section--dark { --surface }`, en
     bas de ce fichier, qui peint réellement le panneau CTA — et c'est là que
     l'écart se voyait. Les deux sont corrigés ; celui-ci l'est pour que le
     token ne mente pas le jour où quelqu'un s'en servira. */
  --bg-purple-dark: var(--c-purple-800);
  --bg-purple-darker: var(--c-purple-950);
  --bg-green-light: var(--c-green-50);
  --bg-blue-light: var(--c-blue-50);
  --bg-beige-light: var(--c-beige-50);

  /* Texte — charte « Text on Light (Texte sur fond clair) » */
  --text-title: var(--c-purple-950);
  --text: var(--c-neutral-900);
  --text-secondary: var(--c-neutral-700);
  --text-disabled: var(--c-neutral-500);
  --text-placeholder: var(--c-neutral-500);

  /* Rôles ajoutés le 2026-10-05 (blog, phase 4 — ECARTS-BLOG §3 et §5 B).
     Chacun traduit un token que la maquette nomme, et qu'aucun rôle ne portait :
     · `text/on-light/secondary` #65758b = neutral-600. N'est AA que sur fond
       BLANC (4,70:1) ; sur --bg #f8f9fb il tombe à 4,46 → ne le poser que sur
       --bg-white. --text-secondary (neutral-700) reste le défaut partout ailleurs
       (arbitrage 3.3 : deux rôles, pas de remappage global) ;
     · `purple/800` posé sur du TEXTE (lien « Retours aux articles ») : le seul
       rôle qui pointait purple-800 était un fond. */
  --text-secondary-on-white: var(--c-neutral-600);
  --accent-text-strong: var(--c-purple-800);

  /* Bordures — charte « Borders (Bordures & Séparateurs) » */
  --border: var(--c-neutral-200);
  --border-strong: var(--c-neutral-500);
  /* `border-1` #f1f2f5 (carte Sommaire, relevé blog-article.md §3b) : aucune
     primitive ne vaut #f1f2f5, neutral-100 #f0f2f5 est à 1 unité sur le canal
     rouge. Ajouté le 2026-10-05. */
  --border-subtle: var(--c-neutral-100);

  /* Halo de décor (hero d'article, ellipses « Jaune » `beige/200` du relevé
     blog-article.md §2) — rôle de décor, jamais de texte. Ajouté le 2026-10-05. */
  --decor-halo-beige: var(--c-beige-200);

  /* Accents — charte « Accents », variantes claires (posées SUR fond clair) */
  --accent-purple: var(--c-purple-600);
  --accent-blue: var(--c-blue-500);
  /* green_600 / beige_600, et non 500 : ce sont les valeurs que la maquette pose
     (`accent/on-light/green` #359128, `accent/on-light/beige` #c67830), et ce sont
     les seules qui passent AA-large sur le fond réel des sections. Mesuré le
     2026-08-09 sur `--bg-purple-light` #EFF0FE : green_500 sort à 2,44:1 (échec),
     green_600 à 3,55:1 (passe). Le violet était déjà juste (purple_600). */
  --accent-green: var(--c-green-600);
  --accent-beige: var(--c-beige-600);

  /* Accent courant d'une rubrique — réassigné par .section--accent-* */
  --accent: var(--accent-purple);

  /* --- Accent LISIBLE et accent DOUX (D6, mesuré le 2026-08-05) -------------
     Les accents ci-dessus sont des couleurs de MARQUE : elles sont calibrées
     pour un aplat, pas pour du texte. Mesuré sur --bg (#F8F9FB) : violet 5,11 ✓
     mais bleu 3,52 ✗, vert 2,62 ✗, beige 2,56 ✗ contre le seuil AA de 4,5.
     D'où deux rôles de plus, et une règle simple :
       · un TEXTE ou une icône porteuse de sens → --accent-text
       · un aplat, un trait, un pictogramme décoratif → --accent

     Nuances retenues, chacune vérifiée sur les TROIS fonds qu'elle rencontre
     (--bg / --surface / --accent-soft) :
       violet 600  5,11 · 4,80 · 4,75      bleu 600   4,86 · 4,57 · 4,72
       vert 700    5,56 · 5,23 · 5,56      beige 800  6,55 · 6,15 · 6,51
     Le beige est en 800 et non en 700 comme proposé initialement : 700 tient
     sur --bg (4,73) mais tombe à 4,44 sur --surface, donc sous le seuil dès
     qu'on le pose sur une card. */
  --accent-text-purple: var(--c-purple-600);
  --accent-text-blue: var(--c-blue-600);
  --accent-text-green: var(--c-green-700);
  --accent-text-beige: var(--c-beige-800);
  --accent-text: var(--accent-text-purple);

  /* Fond doux de la rubrique — pastilles, puces, aplats légers. Ce sont les
     rôles de fond clair qui existent déjà : aucune nouvelle couleur, juste leur
     version « suit la rubrique courante ». Remplace le color-mix() à 14 % que
     hero-home fabriquait, et qui plafonnait le contraste à 4,21 même avec un
     texte corrigé — un fond teinté à l'accent mange le contraste de l'accent. */
  --accent-soft-purple: var(--bg-purple-light);
  --accent-soft-blue: var(--bg-blue-light);
  --accent-soft-green: var(--bg-green-light);
  --accent-soft-beige: var(--bg-beige-light);
  --accent-soft: var(--accent-soft-purple);

  /* Boutons — charte « Bibliothèque de composants UI » (dégradés relevés dans
     Button.svg : primaire Purple 500 → 600, secondaire White → Neutral 100) */
  --btn-primary-from: var(--c-purple-500);
  --btn-primary-to: var(--c-purple-600);
  --btn-primary-border: var(--c-purple-500);
  --btn-primary-text: var(--c-base-white);
  --btn-secondary-from: var(--c-base-white);
  --btn-secondary-to: var(--c-neutral-100);
  --btn-secondary-border: var(--c-neutral-200);
  --btn-secondary-text: var(--c-purple-950);

  /* Élévation « flottante » — rôle ajouté le 2026-08-03. Existait déjà en dur
     dans components/_navbar.css:80 et réinventé par hero-home : un seul token
     désormais. La variante sombre est plus opaque, sinon l'ombre disparaît sur
     --bg (purple-950) et l'élévation ne se lit plus. */
  --shadow-card: 0 12px 32px rgb(0 0 0 / 8%);

  /* Style d'effet Figma `shadow/card` (relevé blog-listing.md §4, survol de la
     carte d'article) — rôle ajouté le 2026-10-05. Cinq couches dans la charte ;
     les deux dernières sont à 0 % d'opacité et ne peignent rien, elles sont
     omises. Distinct de --shadow-card, élévation « flottante » du socle. */
  --shadow-card-hover: 0 1px 3px rgb(0 0 0 / 2%), 0 5px 5px rgb(0 0 0 / 2%), 0 11px 7px rgb(0 0 0 / 1%);

  /* Limite d'un composant d'interface — rôle ajouté le 2026-08-03. WCAG 1.4.11
     exige 3:1 pour le trait qui DIT où commence et finit une zone cliquable.
     Mesuré sur --bg clair (#F8F9FB) : --border 1,19 ✗ · --border-strong 2,67 ✗ ·
     accent vert 2,62 ✗ · accent beige 2,56 ✗. Seul neutral-600 (4,46) tient.
     N'utiliser --border/--border-strong que pour des séparations décoratives. */
  --border-interactive: var(--c-neutral-600);

  /* Erreur — rôle ajouté le 2026-08-05, PROVISOIRE (cf. tokens-primitives).
     Deux rôles et pas un seul, parce que les deux seuils WCAG diffèrent :
       · --danger      bordure de champ invalide, anneau, pictogramme  (≥ 3:1)
       · --danger-text message d'erreur, astérisque de champ requis    (≥ 4,5:1)
     Ne jamais poser --danger sur du texte : 3,36 sur --surface. */
  --danger: var(--c-red-500);
  --danger-text: var(--c-red-700);

  /* Halo — le « dégradé » du panneau CTA de la maquette. Relevé à la source le
     2026-08-05 : ce n'est pas un dégradé linéaire mais une composition de
     4 formes floutées, et sa couleur est #7251DA, soit exactement --c-purple-600
     — la charte n'a donc AUCUNE couleur à fournir, elle en avait déjà une. */
  --halo: var(--c-purple-600);
  --halo-light: var(--c-base-white);
}

/* `.section--light` doit PEINDRE, exactement comme `.section--dark` le fait.
   Le bloc ci-dessus ne peut pas s'en charger : il porte aussi `:root`, et y
   mettre un `background-color` peindrait la racine du document.

   Sans cette règle, la classe était asymétrique et le cas d'usage pour lequel
   elle a été ajoutée ne marchait pas : posée seule dans une section sombre, elle
   réassignait bien les rôles clairs — donc du texte foncé — mais laissait le
   fond sombre du parent. Texte foncé sur fond sombre. Mesuré le 2026-08-05 :
   `.section--dark` seule peint rgb(40,30,72), `.section--light` seule ne peignait
   rien (transparent). Relevé par la session « décisions design ». */
.section--light {
  background-color: var(--bg);
  color: var(--text);
}

/* --- Fond BLANC PUR — M-08 (2026-08-11) --------------------------------------
   Il n'existait aucune classe de fond blanc. La maquette pose pourtant `bg/white`
   #FFFFFF sur des SECTIONS entières, pas seulement sur des cartes, et le thème
   servait #F8F9FB partout : `.section` peint `--bg` (objects/_layout.css:81) et
   `.section--light` peint `--bg` lui aussi (juste au-dessus), donc la valeur
   `background: "light"` des modules n'émettait rien de distinct.

   Sources, vérifiées à la ligne :
     · `contenu-maquettes/solutions-listing.md:43,136,168,186` — « section
       blanche », quatre fois, dont la section Méthode (:136) ;
     · `contenu-maquettes/a-propos.md:193` — FAQ, « Fond blanc (`bg/white`, seule
       section sur fond blanc pur) » ;
     · `contenu-maquettes/faq-accordeon---cta-final.md:99` — « Section 1440 px,
       fond blanc », et `:143` qui lie le token `bg/white` #ffffff ;
     · `contenu-maquettes/slider-temoignages.md:5,93,126` — Cas clients, « Fond
       `bg/white` (#ffffff) », le token servant à la fois la section et les cartes.

   Le rôle existait déjà (`--bg-white`, ligne 22) ; il ne manquait que la classe
   qui le peint. Aucune couleur littérale, aucune longueur, aucun rôle nouveau :
   cette règle repointe le seul `--bg`. Conséquence — le `background-color` de
   `.section` résout DÉJÀ le blanc, sans règle de plus, parce qu'une custom
   property se résout à sa déclaration gagnante et non à l'ordre des règles qui
   la lisent. Le `background-color` déclaré ici sert l'autre cas : un élément qui
   porte la classe sans porter `.section` — une carte blanche dans une section
   sombre, exactement l'usage de `.section--light` au-dessus.

   ACCESSIBILITÉ — le blanc est plus clair que `--bg` (#F8F9FB), donc aucun
   plancher ne baisse. Recalculé le 2026-08-11 (WCAG 2.1), #F8F9FB → #FFFFFF :
     --text 12,78 → 13,46   ·  --text-secondary 6,45 → 6,80
     --text-title 14,56 → 15,33  ·  --border-interactive 4,46 → 4,70
     accents-texte : violet 5,11 → 5,38 · bleu 4,86 → 5,12 ·
                     vert 5,56 → 5,86 · beige 6,55 → 6,90
   Tous montent. Cette classe ne peut donc pas dégrader un contraste existant.

   ⚠️ CE QU'ELLE NE RÈGLE PAS : `--surface` reste neutral-100 (#F0F2F5), soit
   1,12 sur blanc (contre 1,06 sur `--bg`). Une carte peinte en `--surface` dans
   une section blanche reste quasi invisible sans bordure — ce n'est pas une
   régression (c'était déjà vrai sur `--bg`), mais la maquette délimite ces
   cartes par `border/on-light/default`, pas par leur fond. Un module qui veut
   une carte lisible sur blanc doit porter son bord, pas compter sur son fond.

   ⚠️ ORDRE — déclarée ICI, avant `.section--dark`, et ce n'est pas indifférent :
   les deux ont la même spécificité (une classe), donc sur un élément qui
   porterait les deux, c'est le contexte sombre qui gagne. C'est le repli sûr.
   Le bloc `.section--accent-*` reste, lui, APRÈS `.section--dark` : rien n'a
   bougé de cet ordre-là, qui est ce qui résout D5 (avertissement plus bas). */
.section--white {
  --bg: var(--bg-white);

  background-color: var(--bg);
  color: var(--text);
}

/* -----------------------------------------------------------------------------
   COUCHE 3 — contexte sombre.
   Posée sur un conteneur de section (jamais globalement) : les rôles sont
   réassignés, les modules à l'intérieur n'ont rien à savoir du contexte.
   Charte : « Composants sur Fond Sombre (Inversés) », fond bg/purple-darker.
   -------------------------------------------------------------------------- */
.section--dark {
  /* Fonds */
  --bg: var(--c-purple-950);
  /* purple-800 (#50389B) = le rôle `bg/purple-dark` de la charte. Servait
     purple-900 (#44347B) jusqu'au 2026-08-08 — l'écart d'une nuance relevé en
     V8 sur le panneau CTA. C'EST CETTE LIGNE qui peignait le défaut, pas le
     `--bg-purple-dark` du haut de fichier que le rapport de gate désignait :
     ce dernier n'a aucun consommateur. La sonde du 07/08 l'avait attrapée en
     mobile — `inner[#44347B@350]` sur les 4 instances de cta-final et sur la
     grille sombre de hub-solutions (`preuves/…/sonde-mobile-390.txt`) — et pas
     en desktop, où la carte ne couvre pas les 60 % de largeur qui déclenchent
     la remontée des fonds peints par un enfant. */
  --surface: var(--c-purple-800);

  /* Texte — charte « Text on Dark (Texte sur fond sombre) » */
  --text-title: var(--c-purple-50);
  --text: var(--c-base-white);
  /* Rôles du 2026-10-05 : en contexte sombre, ils retombent sur les rôles du
     contexte (aucune teinte claire spécifique n'est dessinée). */
  --text-secondary-on-white: var(--text-secondary);
  --accent-text-strong: var(--accent-text);
  --border-subtle: var(--border);
  /* M-06 (2026-08-10) — le chapô sur fond sombre virait au mauve. La charte pose
     `text/on-dark/secondary` = #CDD3DD, c'est-à-dire NEUTRAL-300, pas purple-200
     (#CBCCFA) : deux couleurs proches à l'œil, l'une neutre et l'autre teintée.
     Relevé sur 9 chapôs (`solutions-listing.md:47`, `a-propos.md`, les deux
     mobiles). Le rôle existait déjà au socle, il n'était simplement pas pointé.
     Contraste recalculé avant application, méthode WCAG 2.1 :
       #CDD3DD sur --bg #281E48 → 10,19  ·  sur --surface #50389B → 5,86
     soit MIEUX que le purple-200 qu'il remplace (9,90 / 5,70). Le plancher du
     contexte sombre monte de 5,70 à 5,86 — cette correction ne coûte rien à
     l'accessibilité, elle lui rapporte. */
  --text-secondary: var(--c-neutral-300);
  --text-disabled: var(--c-purple-400);
  --text-placeholder: var(--c-purple-400);

  /* Bordures — CONSÉQUENCE OBLIGÉE du passage de --surface à purple-800.
     `--border` valait purple-800 lui aussi : laisser les deux sur la même
     nuance aurait fait disparaître purement et simplement le bord des cartes
     posées sur une surface sombre (contraste 1,00 — la carte du CTA final, les
     cards-grid sombres, la carte FAQ, les cartes du hero). Le correctif de
     couleur aurait alors introduit un défaut de rendu.
     purple-700 rétablit la séparation et l'améliore des deux côtés :
     bord/surface 1,18 → 1,29, bord/fond 1,74 → 2,24 (mesures du 2026-08-08).
     ⚠️ La charte ne nomme AUCUN rôle `border/on-dark` — les bordures sombres
     sont une invention du socle depuis l'origine, et cette valeur reste donc à
     faire valider par Quentin/Costel au même titre que la précédente. */
  --border: var(--c-purple-700);
  --border-strong: var(--c-purple-600);

  /* Accents — variantes sombres (posées SUR fond sombre)

     ⚠️ M-05 APPLIQUÉE LE 2026-08-10 — ET ELLE OUVRE UNE DETTE D'ACCESSIBILITÉ,
     ASSUMÉE, À ARBITRER PAR L'ÉQUIPE DESIGN.
     La charte pose `accent/on-dark/purple` = #ACABF6, soit PURPLE-300, sur ~19
     sur-titres de fond sombre ; le socle servait purple-200 (#CBCCFA).
     Contrastes calculés (WCAG 2.1) AVANT écriture, pour que la décision se prenne
     sur des chiffres et non sur une impression :

                        sur --bg #281E48   sur --surface #50389B
       purple-200 (avant)      9,90               5,70   ✓
       purple-300 (charte)     7,24               4,16   ✗ sous le seuil AA de 4,5

     La nuance de la charte est donc juste au pixel et insuffisante au contraste sur
     `--surface`. Arbitrage d'Antoine, 2026-08-10 : la phase en cours est la
     VALIDATION DES MAQUETTES — la conformité au pixel est la règle, l'écart
     d'accessibilité se note et part à l'équipe design. Il est consigné à
     `docs/ARBITRAGES-EQUIPE-DESIGN-2026-08-10.md`.

     Le cas dimensionnant, pour la décision à venir : depuis M-01 le panneau du CTA
     final est un dégradé qui FINIT sur `--surface` et porte un sur-titre accentué
     sur 4 gabarits — c'est là que se lisent les 4,16.
     Trois issues possibles, aucune du ressort d'un module : revenir à la 200 ;
     remonter `--surface`, ce qui touche tout le contexte sombre ; ou dédoubler le
     rôle (300 sur `--bg`, 200 sur `--surface`), ce qui suppose un rôle
     `accent-on-surface` que le socle n'a pas. */
  --accent-purple: var(--c-purple-300);
  --accent-blue: var(--c-blue-200);
  --accent-green: var(--c-green-200);
  --accent-beige: var(--c-beige-200);
  --accent: var(--accent-purple);

  /* En sombre, les accents de marque passent LARGEMENT le seuil de texte —
     mesuré le 2026-08-05 sur --bg (#281E48) : violet 9,90 · bleu 10,82 ·
     vert 12,21 · beige 11,57.
     Les valeurs sur --surface ont été RECALCULÉES le 2026-08-08, la surface
     étant passée de #44347B à #50389B : violet 5,70 (était 6,70) · bleu 6,23 ·
     vert 7,03 · beige 6,66. La surface est plus claire, donc les ratios
     baissent — ils restent tous au-dessus de 4,5, et le plancher de tout le
     contexte sombre est désormais ce 5,70. Aucune nuance de rattrapage n'est
     donc nécessaire : --accent-text pointe simplement l'accent lui-même. */
  --accent-text-purple: var(--accent-purple);
  --accent-text-blue: var(--accent-blue);
  --accent-text-green: var(--accent-green);
  --accent-text-beige: var(--accent-beige);
  --accent-text: var(--accent-text-purple);

  /* Le fond doux perd sa teinte de rubrique en contexte sombre : une pastille
     violet clair sur fond violet foncé ne se lit plus. --surface est le seul
     fond de composant réassigné en sombre, et les accents ci-dessus y tiennent
     tous (6,70 au pire). À revoir avec l'équipe design si la rubrique doit
     rester lisible sur la pastille en sombre. */
  --accent-soft-purple: var(--surface);
  --accent-soft-blue: var(--surface);
  --accent-soft-green: var(--surface);
  --accent-soft-beige: var(--surface);
  --accent-soft: var(--surface);

  /* Erreur sur fond sombre. Les nuances claires ne tiennent PAS ici, et je ne
     l'avais mesuré qu'en contexte clair en les posant : #BF3636 donne 2,77 sur
     --bg sombre et 1,88 sur --surface sombre, contre 4,5. Même #EF4444 tombe à
     2,76 sur --surface — sous le seuil de 3:1 d'une simple bordure.
     #F69898 (red 200) tient les deux : 7,20 et 4,88. Mesuré le 2026-08-05. */
  --danger: var(--c-red-200);
  --danger-text: var(--c-red-200);

  /* Le bouton secondaire reste clair sur fond sombre (charte) : seule la
     bordure change pour ne pas trancher avec le fond. */
  --btn-secondary-border: var(--c-purple-800);

  /* Élévation sur fond sombre : 8 % de noir sur purple-950 est imperceptible. */
  --shadow-card: 0 12px 32px rgb(0 0 0 / 40%);
  --shadow-card-hover: var(--shadow-card);

  /* Limite d'un composant d'interface sur fond sombre : neutral-500 = 5,45 sur
     --bg (#281E48). neutral-600 n'y ferait que 3,26 — trop juste. */
  --border-interactive: var(--c-neutral-500);

  /* Maquettes Claude Design 2026-08-18 : TOUTES les surfaces sombres sont des
     dégradés purple-950 → purple-800, jamais des aplats. Angles relevés dans
     les bundles : −38,1° (Problème), −33,7° (Expertises), −30,7° (Méthode),
     −34,6° (Secteurs), −40° (À propos), −45,8° (en 4 temps) — on fige −38°,
     la borne de fin à 118 % reproduit la douceur du relevé (le 800 n'est
     jamais atteint plein cadre). Le panneau du CTA final garde son angle
     propre (M-01). Ceci tranche de fait le B-07 « tokens de dégradé ». */
  --gradient-dark: linear-gradient(-38deg, var(--c-purple-950) 0%, var(--c-purple-800) 118%);

  background-color: var(--bg);
  background-image: var(--gradient-dark);
  color: var(--text);
}

/* -----------------------------------------------------------------------------
   Accent par rubrique — réassigne les trois rôles d'accent, consommés par les
   eyebrows, les liens d'accent, les pastilles et les icon-buttons. Fonctionne en
   clair comme en sombre puisqu'il pointe le rôle, jamais la primitive.

   ⚠️ CES RÈGLES SONT DÉCLARÉES APRÈS `.section--dark`, ET CE N'EST PAS UN HASARD.
   C'est ce qui résout D5 (vérifié par la mesure le 2026-08-05, cf. §2 du contrat
   socle) : sur un élément qui porte les DEUX classes, `.section--accent-blue`
   gagne à spécificité égale par l'ordre du fichier, et comme `.section--dark` a
   déjà réassigné `--accent-blue` à sa variante sombre juste au-dessus, `--accent`
   y résout la bonne nuance. Mesuré : `.section--dark.section--accent-blue` rend
   #C9DAF4, la variante bleue sombre exacte. Ne jamais remonter ce bloc.
   -------------------------------------------------------------------------- */
.section--accent-purple {
  --accent: var(--accent-purple);
  --accent-text: var(--accent-text-purple);
  --accent-soft: var(--accent-soft-purple);
}

.section--accent-blue {
  --accent: var(--accent-blue);
  --accent-text: var(--accent-text-blue);
  --accent-soft: var(--accent-soft-blue);
}

.section--accent-green {
  --accent: var(--accent-green);
  --accent-text: var(--accent-text-green);
  --accent-soft: var(--accent-soft-green);
}

.section--accent-beige {
  --accent: var(--accent-beige);
  --accent-text: var(--accent-text-beige);
  --accent-soft: var(--accent-soft-beige);
}

/* Generic
This is where reset, normalize & box-sizing styles go.
*/

*, *:before, *:after {
  box-sizing: border-box;
}
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/* Document
   ========================================================================== */

/**
 * 1. Correct the line height in all browsers.
 * 2. Prevent adjustments of font size after orientation changes in iOS.
 */

html {
  line-height: 1.15; /* 1 */
  -webkit-text-size-adjust: 100%; /* 2 */
}

/* Sections
   ========================================================================== */

/**
 * Remove the margin in all browsers.
 */

body {
  margin: 0;
}

/**
 * Correct the font size and margin on `h1` elements within `section` and
 * `article` contexts in Chrome, Firefox, and Safari.
 */

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

/* Grouping content
   ========================================================================== */

/**
 * Add the correct box sizing in Firefox.
 */

hr {
  box-sizing: content-box;
  height: 0;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

pre {
  font-family: monospace, monospace; /* 1 */
  font-size: 1em; /* 2 */
}

/* Text-level semantics
   ========================================================================== */

/**
 * 1. Remove the bottom border in Chrome 57-
 * 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.
 */

abbr[title] {
  border-bottom: none; /* 1 */
  text-decoration: underline; /* 2 */
  text-decoration: underline dotted; /* 2 */
}

/**
 * Add the correct font weight in Chrome, Edge, and Safari.
 */

b,
strong {
  font-weight: bolder;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

code,
kbd,
samp {
  font-family: monospace, monospace; /* 1 */
  font-size: 1em; /* 2 */
}

/**
 * Add the correct font size in all browsers.
 */

small {
  font-size: 80%;
}

/**
 * Prevent `sub` and `sup` elements from affecting the line height in
 * all browsers.
 */

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

/* Forms
   ========================================================================== */

/**
 * 1. Change the font styles in all browsers.
 * 2. Remove the margin in Firefox and Safari.
 */

button,
input,
optgroup,
select,
textarea {
  font-family: inherit; /* 1 */
  font-size: 100%; /* 1 */
  line-height: 1.15; /* 1 */
  margin: 0; /* 2 */
}

/**
 * Remove the inheritance of text transform in Edge and Firefox.
 * 1. Remove the inheritance of text transform in Firefox.
 */

button,
select { /* 1 */
  text-transform: none;
}

/**
 * Correct the inability to style clickable types in iOS and Safari.
 */

button,
[type="button"],
[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
}

/**
 * Remove the inner border and padding in Firefox.
 */

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

/**
 * Restore the focus styles unset by the previous rule.
 */

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

/**
 * Correct the padding in Firefox.
 */

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

/**
 * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers.
 */

legend {
  padding: 0;
}

/**
 * Add the correct vertical alignment in Chrome, Firefox, and Opera.
 */

progress {
  vertical-align: baseline;
}

/**
 * Correct the cursor style of increment and decrement buttons in Chrome.
 */

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

/**
 * 1. Correct the odd appearance in Chrome and Safari.
 * 2. Correct the outline style in Safari.
 */

[type="search"] {
  -webkit-appearance: textfield; /* 1 */
  outline-offset: -2px; /* 2 */
}

/**
 * Remove the inner padding in Chrome and Safari on macOS.
 */

[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

/**
 * 1. Correct the inability to style clickable types in iOS and Safari.
 * 2. Change font properties to `inherit` in Safari.
 */

::-webkit-file-upload-button {
  -webkit-appearance: button; /* 1 */
  font: inherit; /* 2 */
}

/* Interactive
   ========================================================================== */

/*
 * Add the correct display in Edge and Firefox.
 */

details {
  display: block;
}

/*
 * Add the correct display in all browsers.
 */

summary {
  display: list-item;
}

/* Objects
Non-cosmetic design patterns including grid and layout classes)
*/



/* CSS variables */

:root {
  --column-gap: 2.13%;
  --column-width-multiplier: 8.333;
}

/* Mobile layout */

.row-fluid {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
}


  .row-fluid .span1,
  .row-fluid .span2,
  .row-fluid .span3,
  .row-fluid .span4,
  .row-fluid .span5,
  .row-fluid .span6,
  .row-fluid .span7,
  .row-fluid .span8,
  .row-fluid .span9,
  .row-fluid .span10,
  .row-fluid .span11,
  .row-fluid .span12{
  min-height: 1px;
  width: 100%;
}

/* Desktop layout */

@media (min-width: 768px) {
  .row-fluid {
    flex-wrap: nowrap;
    justify-content: space-between;
  }

  
    .row-fluid .span1 {
      width: calc(var(--column-width-multiplier) * 1% * 1 - var(--column-gap) * (11 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span2 {
      width: calc(var(--column-width-multiplier) * 1% * 2 - var(--column-gap) * (10 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span3 {
      width: calc(var(--column-width-multiplier) * 1% * 3 - var(--column-gap) * (9 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span4 {
      width: calc(var(--column-width-multiplier) * 1% * 4 - var(--column-gap) * (8 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span5 {
      width: calc(var(--column-width-multiplier) * 1% * 5 - var(--column-gap) * (7 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span6 {
      width: calc(var(--column-width-multiplier) * 1% * 6 - var(--column-gap) * (6 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span7 {
      width: calc(var(--column-width-multiplier) * 1% * 7 - var(--column-gap) * (5 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span8 {
      width: calc(var(--column-width-multiplier) * 1% * 8 - var(--column-gap) * (4 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span9 {
      width: calc(var(--column-width-multiplier) * 1% * 9 - var(--column-gap) * (3 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span10 {
      width: calc(var(--column-width-multiplier) * 1% * 10 - var(--column-gap) * (2 * var(--column-width-multiplier) / 100));
    }
  
    .row-fluid .span11 {
      width: calc(var(--column-width-multiplier) * 1% * 11 - var(--column-gap) * (1 * var(--column-width-multiplier) / 100));
    }
  
}

/* =============================================================================
   Containers & sections — Uptoo Digital
   -----------------------------------------------------------------------------
   Relevés de la charte : frame 1440 avec 120px de gouttière latérale, donc une
   largeur de contenu de 1200px. Le container ne dépasse jamais cette valeur ;
   sur mobile la gouttière tombe à 20px (token --container-pad).
   ============================================================================= */

.container {
  width: 100%;
  max-width: calc(var(--container-max) + 2 * var(--container-pad));
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

/* Gabarit de hero — 598px. Ajouté le 2026-08-09 (REVUE-MODULES §M-A, arbitrage 2).
   598 est la largeur de TOUS les heros desktop du fichier Figma, mesurée sur les
   7 pages Solutions et les 6 pages Intégrations. `.container--narrow` (800px)
   n'était le gabarit d'AUCUN hero relevé — et il n'avait qu'un seul consommateur
   dans tout le dépôt : la variante A de `hero-page`, qui pointe désormais ici. */
.container--hero { max-width: calc(598px + 2 * var(--container-pad)); }

/* Conservé : plus aucun consommateur au 2026-08-09, mais la classe reste
   disponible pour un gabarit large intermédiaire. Ne pas la reprendre pour un
   hero — voir ci-dessus. */
.container--narrow { max-width: calc(800px + 2 * var(--container-pad)); }
.container--full { max-width: none; }

/* Le wrapper de contenu du boilerplate suit la même règle, pour que les zones
   dnd et nos partials s'alignent sur la même gouttière. */
.content-wrapper {
  width: 100%;
  max-width: calc(var(--container-max) + 2 * var(--container-pad));
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

/* --- Section : l'unité de rythme vertical. Porte aussi la couche 3 quand on
   lui ajoute .section--dark. ------------------------------------------------ */
.section {
  padding-block: var(--section-y-lg);
  background-color: var(--bg);
  color: var(--text);
}

/* Il n'y a plus de bascule mobile ICI : depuis le 2026-08-08, ce sont les
   TOKENS qui changent sous 768px (`tokens-primitives.css`, bloc mobile), pas la
   règle. Les trois paliers s'y replient sur 56/56, donc `.section` et ses
   paliers deviennent égaux en mobile sans qu'aucune règle ait à le redire.
   La règle `@media (max-width:767px) { .section { padding-block: var(--sp-7) } }`
   qui vivait ici est donc supprimée, pas déplacée. */

/* --- Paliers de rythme (arbitrage du 2026-08-08 ; palier `xs` ajouté le
   2026-08-09 par D14) ------------------------------------------------------
   `.section` sert le palier LARGE, qui couvre 5 des 9 sections de la maquette.
   Les trois autres paliers sont des modificateurs, posés par les modules qui
   rendent une section relevée à 64/64, à 104/104 ou dans la famille 86→96 — le
   mappage complet est dans DECISIONS-DESIGN §4.4.

   `--rhythm-sm` est le palier de TOUTE la famille 86→96, pas seulement des deux
   sections qui nomment le token : arbitrage M-14 du 2026-08-11, justifié au
   point de définition (`tokens-primitives.css`, bloc `--section-y-*`). Les
   quatre sections de `offre-audit-crm` relevées 96/96 et 86 en haut se câblent
   donc ici, et aucun 5e palier n'a été ajouté.

   ⚠️ ORDRE DE DÉCLARATION — ces quatre règles sont AVANT `--tight` et
   `--flush`, et l'inverse serait un défaut : les six modificateurs ont la même
   spécificité (une classe), donc c'est la dernière déclarée qui gagne. Un
   module qui porte `section section--rhythm-md section--flush` doit sortir à 0,
   pas à 104. C'est le même piège que celui corrigé le 2026-08-05 sur la bascule
   mobile, décrit juste en dessous. */
.section--rhythm-xs { padding-block: var(--section-y-xs); }
.section--rhythm-sm { padding-block: var(--section-y-sm); }
.section--rhythm-md { padding-block: var(--section-y-md); }
.section--rhythm-lg { padding-block: var(--section-y-lg); }

/* Les modificateurs d'espacement de l'éditeur sont déclarés EN DERNIER, et
   c'est le seul ordre qui marche : une media query n'ajoute AUCUNE
   spécificité, donc à spécificité égale c'est la dernière règle du fichier qui
   gagne. Déclarés avant (l'ordre d'origine, corrigé le 2026-08-05),
   `.section--flush` se faisait réappliquer les 48px de `.section` sous 768px —
   un espacement réglé sur « Aucun » par le marketeur restait donc visible sur
   mobile. */
.section--tight { padding-block: var(--sp-7); }

/* REPLI MOBILE DE `--tight` — M-09, posé le 2026-08-11.
   `--tight` était le SEUL palier de rythme sans repli : câblé en dur sur
   `--sp-7` (48), il sortait 48/48 à 390px pendant que les quatre `--section-y-*`
   se replient déjà à 56 dans le bloc mobile de `tokens-primitives.css`. Le
   relevé mobile ne connaît qu'un gabarit, `padding 56px 20px`, et il est
   explicitement donné pour constant sur toute la page
   (`mobile-solutions-audit-crm.md:41`, recoupé section par section dans
   `mobile-solutions-listing.md:27,57,82,110,131,147,175,190,226,255,287,317`).
   `--tight` n'a donc pas de valeur propre en mobile : il se confond avec
   `.section`, et sert le même token qu'elle.

   ⚠️ CETTE MEDIA QUERY EST UN PALLIATIF, ET LE FICHIER DIT POURQUOI douze lignes
   plus haut : depuis le 2026-08-08 la doctrine est que ce sont les TOKENS qui
   basculent sous 768px, pas les règles. Le correctif conforme serait un token
   `--section-y-tight` (48 en :root, 56 dans le bloc mobile) consommé sans media
   query, exactement comme les quatre autres paliers. Il n'est pas posé ici parce
   que `tokens-primitives.css` n'est pas le fichier de ce lot — le token est
   DEMANDÉ. Le jour où il arrive, cette media query se supprime et la règle
   au-dessus devient `padding-block: var(--section-y-tight)`.
   Tant que ce palliatif vit : si le rythme mobile cesse d'être unique, cette
   règle est à revisiter en même temps que le bloc mobile des tokens.

   ⚠️ PLACÉE AVANT `--flush`, et c'est la même raison qu'en dessous : une media
   query n'ajoute aucune spécificité. Déclarée APRÈS, elle rendrait 56px de
   padding à une section réglée sur « Aucun » par le marketeur — le défaut exact
   corrigé le 2026-08-05, réintroduit par l'autre bout.

   EFFET MESURÉ AVANT ÉCRITURE — la classe est émise par 15 modules et par
   `templates/blog-post.html:217`, mais elle n'est SERVIE que par `cta-final`
   sur les gabarits mesurés : 4 occurrences, les mêmes aux quatre viewports
   (`dom-390-v3.json` hub-solutions ×2, offre-audit-crm, page-libre ;
   `dom-1440-final.json` et `tablette-2026-08-11/dom-*.json` idem). Les
   14 autres modules peuvent porter `--tight` mais aucune instance en service ne
   le fait — le champ « espacement » est resté sur sa valeur par défaut.
   · à 390 : padding 48/48 → 56/56 sur ces 4 sections, +16px de hauteur chacune ;
   · à 768, 900, 1023, 1024, 1100 et 1440 : AUCUN effet, la règle ne s'applique
     pas. Le tablette 768→1023 garde donc 48/48 — non relevé, écart inchangé.
   · Non mesuré : les pages de blog (`blog-post.html` câble `--tight` en dur),
     absentes du jeu de sondes. Elles gagnent le même +8px haut et bas. */
@media (max-width: 767px) {
  .section--tight { padding-block: var(--section-y-lg); }
}

.section--flush { padding-block: 0; }

/* --- Grille utilitaire (cards, listes) ------------------------------------ */
.grid {
  display: grid;
  gap: var(--grid-gap);
}

.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 1023px) {
  .grid--3, .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 767px) {
  .grid--2, .grid--3, .grid--4 { grid-template-columns: minmax(0, 1fr); }
}

/* --- Lien d'évitement (skip-link) ----------------------------------------- */
.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-4);
  z-index: 1000;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-sm);
  background: var(--bg-white);
  color: var(--text-title);
  font-weight: var(--fw-medium);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}

.skip-link:focus { transform: translateY(0); }

/* Masquage visuel sans retirer du flux accessible. */
.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* La règle `.content-wrapper` du boilerplate HubSpot a été SUPPRIMÉE le
   2026-08-05. Elle redéclarait `padding: 0 1rem` puis `padding: 0` au-dessus de
   1380px, à spécificité égale avec `_layout.css` et APRÈS lui dans main.css :
   elle écrasait donc silencieusement la gouttière du socle, ce qui collait le
   logo de la navbar au bord de l'écran au-delà de 1380px là où la maquette pose
   80px. `_layout.css` définit déjà `.content-wrapper` avec les bons tokens. */

.dnd-section > .row-fluid {
  margin: 0 auto;
}

.dnd-section .dnd-column {
  padding: 0 1rem;
}

@media (max-width: 767px) {
  .dnd-section .dnd-column {
    padding: 0;
  }
}

/* Elements
Base HTML elements are styled in this section (<body>, <h1>, <a>, <p>, <button> etc.)
*/

/* =============================================================================
   Typographie — échelle du design system Uptoo Digital
   -----------------------------------------------------------------------------
   Deux échelles distinctes (styles Figma séparés Desktop / Mobile) : la bascule
   se fait par les tokens --fs-* / --ls-* dans tokens-primitives.css, pas ici.
   Une classe = un style de la charte. Les modules utilisent ces classes, jamais
   une taille en dur.
   ============================================================================= */

body {
  font-family: var(--font-sans);
  font-weight: var(--fw-regular);
  font-size: var(--fs-body-m);
  letter-spacing: var(--ls-body-m);
  line-height: var(--lh-body-m);
  color: var(--text);
  background-color: var(--bg);
  overflow-wrap: break-word;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Coupure de mots propre pour les langues CJK (conservé du boilerplate). */
html[lang^="ja"] body,
html[lang^="zh"] body,
html[lang^="ko"] body {
  line-break: strict;
  overflow-wrap: normal;
  word-break: break-all;
}

/* --- Titres : Medium + tracking négatif, couleur de rôle « titre » ---------- */
h1, h2, h3, h4, h5, h6,
.u-display-xl, .u-display-l, .u-display-m,
.u-heading-l, .u-heading-m, .u-heading-s {
  font-family: var(--font-sans);
  font-weight: var(--fw-medium);
  color: var(--text-title);
  margin: 0 0 var(--sp-4);
  text-wrap: balance;
}

/* Un interligne par style depuis le 2026-08-08 : chaque ligne appelle
   maintenant le `--lh-*` qui porte le MÊME suffixe que ses `--fs-*` et `--ls-*`.
   Avant, quatre tokens couvraient ces sept lignes, et deux styles qui n'ont pas
   le même interligne dans la maquette en recevaient un identique. */
h1, .u-display-xl { font-size: var(--fs-display-xl); letter-spacing: var(--ls-display-xl); line-height: var(--lh-display-xl); }
.u-display-l      { font-size: var(--fs-display-l);  letter-spacing: var(--ls-display-l);  line-height: var(--lh-display-l); }
h2, .u-display-m  { font-size: var(--fs-display-m);  letter-spacing: var(--ls-display-m);  line-height: var(--lh-display-m); }
h3, .u-heading-l  { font-size: var(--fs-heading-l);  letter-spacing: var(--ls-heading-l);  line-height: var(--lh-heading-l); }
h4, .u-heading-m  { font-size: var(--fs-heading-m);  letter-spacing: var(--ls-heading-m);  line-height: var(--lh-heading-m); }
h5, .u-heading-s  { font-size: var(--fs-heading-s);  letter-spacing: var(--ls-heading-s);  line-height: var(--lh-heading-s); }
h6                { font-size: var(--fs-label-m);    letter-spacing: var(--ls-label-m);    line-height: var(--lh-label-m); }

/* --- Corps de texte -------------------------------------------------------- */
p, ul, ol { margin: 0 0 var(--sp-4); }
p:last-child, ul:last-child, ol:last-child { margin-bottom: 0; }
ul ul, ol ul, ul ol, ol ol { margin: 0; }

ul.no-list { list-style: none; margin: 0; padding-left: 0; }

/* `--lh-body` couvrait ces trois lignes à lui seul, pour DEUX valeurs de
   maquette : Body/L est à 1,6 et Body/M comme Body/S à 1,5. Les deux dernières
   sortaient donc 1,6 px et 1,4 px trop haut. */
.u-body-l { font-size: var(--fs-body-l); letter-spacing: var(--ls-body-l); line-height: var(--lh-body-l); }
.u-body-m { font-size: var(--fs-body-m); letter-spacing: var(--ls-body-m); line-height: var(--lh-body-m); }
.u-body-s { font-size: var(--fs-body-s); letter-spacing: var(--ls-body-s); line-height: var(--lh-body-s); }

.u-label-m { font-size: var(--fs-label-m); letter-spacing: var(--ls-label-m); line-height: var(--lh-label-m); font-weight: var(--fw-medium); }
.u-label-s { font-size: var(--fs-label-s); letter-spacing: var(--ls-label-s); line-height: var(--lh-label-s); font-weight: var(--fw-medium); }
.u-caption { font-size: var(--fs-caption); letter-spacing: var(--ls-caption); line-height: var(--lh-caption); color: var(--text-secondary); }

.u-text-secondary { color: var(--text-secondary); }

/* Mot-clé accentué dans un titre — pattern récurrent des maquettes. */
.u-accent { color: var(--accent); }

/* --- Liens ----------------------------------------------------------------- */
a {
  cursor: pointer;
  color: var(--accent);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
a:hover { text-decoration: underline; }

/* Focus visible : jamais d'outline:none. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* --- Divers ---------------------------------------------------------------- */
pre { overflow: auto; }
code { vertical-align: bottom; font-family: var(--font-mono); }

blockquote {
  border-left: 2px solid var(--border-strong);
  margin: 0 0 var(--sp-4);
  padding-left: var(--sp-3);
}

hr { border: none; border-bottom: 1px solid var(--border); }

img { font-size: var(--fs-caption); word-break: normal; }

/* --- Sur-titre (Eyebrow) — DM Mono, uppercase, tracking POSITIF, préfixe ✦.
   Le losange est décoratif : porté par ::before, il ne fait pas partie du texte
   accessible et n'est pas sélectionnable comme du contenu. ------------------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-weight: var(--fw-medium);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  /* 1,4 relevé sur `Desktop/Eyebrow/M`, et non `1` comme ici jusqu'au
     2026-08-08. Ce `line-height: 1` n'était pas un héritage subi mais une
     valeur posée, et c'était le plus gros écart typographique du socle : 12px
     servis pour 16,8 attendus, soit −29 %, sur un élément présent dans presque
     toutes les sections des 4 pages pilotes. */
  line-height: var(--lh-eyebrow);
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 var(--sp-4);
}

/* Barre oblique de marque (maquettes Claude Design 2026-08-18, partout ; le
   design system la définit comme « le seul motif graphique du système »).
   Remplace le losange ✦ servi jusqu'au 2026-08-18. Un ::before vide n'est pas
   exposé aux lecteurs d'écran : le speak:never de l'ancien glyphe devient inutile. */
.eyebrow::before {
  content: "";
  width: 3px;
  height: 13px;
  background: currentColor;
  transform: skewX(-18deg);
  flex-shrink: 0;
}

/* Variante « live » : point qui pulse (SPEC-ANIMATIONS-V1 §1). */
.eyebrow--live::after {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: currentColor;
  animation: ud-pulse var(--dur-pulse) infinite;
}

@keyframes ud-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
/* =============================================================================
   Boutons — charte « Bibliothèque de composants UI »
   -----------------------------------------------------------------------------
   3 niveaux : primaire (dégradé violet), secondaire (dégradé clair + bordure),
   lien accentué. Plus l'icon-button en 2 styles. Toutes les couleurs passent par
   les tokens de rôle : les variantes fond sombre sont obtenues sans une seule
   règle supplémentaire, via la réassignation de .section--dark.
   ============================================================================= */

button,
.button,
.hs-button {
  cursor: pointer;
  display: inline-block;
  text-align: center;
  white-space: normal;
  font-family: var(--font-sans);
}

/* --- Base commune ----------------------------------------------------------
   Les deux sélecteurs de formulaire sont ajoutés ici, et non recopiés dans
   elements/_forms.css : c'est le même geste, il remonte au socle
   (CONVENTIONS.md §5). On ne peut pas poser la classe .btn sur le bouton
   d'envoi — c'est HubSpot qui génère ce markup — donc on étend le sélecteur.
   Les formulaires du CMS ignorent le style configuré dans les réglages globaux
   du portail : le CSS du thème est la seule source possible.
   Vérifié : `.hs-button:disabled` (0,2,0) reste plus spécifique que
   `form .hs-button` (0,1,1), l'état désactivé n'est donc pas écrasé. --------- */
.btn,
form input[type=submit],
form .hs-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  min-height: 45px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--fs-label-m);
  letter-spacing: var(--ls-label-m);
  font-weight: var(--fw-medium);
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    filter var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-brand);
}

.btn:hover {
  text-decoration: none;
}

.btn:active {
  transform: translateY(0);
}

/* La flèche ↗ accompagne le libellé et avance légèrement au survol. */
.btn__arrow {
  width: 1em;
  height: 1em;
  flex: none;
  transition: transform var(--dur-fast) var(--ease-brand);
}

.btn:hover .btn__arrow { transform: none; }

/* --- Primaire : dégradé Purple 500 → 600, bordure Purple 500 ---------------
   Le bouton d'envoi d'un formulaire est toujours l'action principale de sa
   carte : il prend la variante primaire, sans classe à poser côté marketeur. */
.btn--primary,
form input[type=submit],
form .hs-button {
  background-image: linear-gradient(180deg, var(--btn-primary-from), var(--btn-primary-to));
  border-color: var(--btn-primary-border);
  color: var(--btn-primary-text);
}

.btn--primary:hover,
form input[type=submit]:hover,
form .hs-button:hover {
  filter: none;
  background-image: linear-gradient(180deg, var(--c-purple-600), var(--c-purple-700));
  border-color: var(--c-purple-600);
}

/* --- Secondaire : dégradé White → Neutral 100, bordure Neutral 200 ---------
   Reste clair sur fond sombre (charte « Composants sur Fond Sombre ») : seule
   la bordure est réassignée par .section--dark. ---------------------------- */
.btn--secondary {
  background-image: linear-gradient(180deg, var(--btn-secondary-from), var(--btn-secondary-to));
  border-color: var(--btn-secondary-border);
  color: var(--btn-secondary-text);
}

/* Recette 01/10 (Quentin) : l'ancien survol (blanc → neutral 100) répétait le
   dégradé de repos — aucun changement visible. Survol = aplat neutral 200. */
.btn--secondary:hover {
  filter: none;
  background-image: none;
  background-color: var(--c-neutral-200);
  border-color: var(--c-neutral-300);
}

/* --- Lien accentué (« En savoir plus → ») ---------------------------------- */
.btn--link {
  padding: 0;
  min-height: 0;
  background: none;
  border: none;
  color: var(--accent);
}

.btn--link:hover { color: var(--c-purple-700); text-decoration: underline; }
.btn--link .btn__arrow { transition: none; }
.btn--link:hover .btn__arrow { transform: none; }

/* --- Icon button ----------------------------------------------------------- */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border: 1px solid var(--btn-secondary-border);
  border-radius: var(--radius-sm);
  background-image: linear-gradient(180deg, var(--btn-secondary-from), var(--btn-secondary-to));
  color: var(--btn-secondary-text);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    filter var(--dur-fast) var(--ease-out);
}

.icon-btn svg { width: 16px; height: 16px; }

.icon-btn--filled {
  background-image: linear-gradient(180deg, var(--btn-primary-from), var(--btn-primary-to));
  border-color: var(--btn-primary-border);
  color: var(--btn-primary-text);
}

.icon-btn--filled:hover {
  filter: none;
  background-image: linear-gradient(180deg, var(--c-purple-600), var(--c-purple-700));
  border-color: var(--c-purple-600);
}

/* --- États désactivés ------------------------------------------------------ */
.btn:disabled,
.btn[aria-disabled="true"],
.icon-btn:disabled,
button:disabled,
.button:disabled,
.hs-button:disabled {
  background-image: none;
  background-color: var(--surface);
  border-color: var(--border);
  color: var(--text-disabled);
  cursor: not-allowed;
  filter: none;
  transform: none;
}

/* --- Bouton neutralisé (utilitaire boilerplate conservé) ------------------- */
.no-button,
.no-button:hover,
.no-button:focus,
.no-button:active {
  background: none;
  border: none;
  border-radius: 0;
  color: initial;
  font-family: inherit;
  font-size: inherit;
  font-style: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  line-height: inherit;
  margin-bottom: 0;
  padding: 0;
  text-align: left;
  text-decoration: none;
  transition: none;
}
/* Compatibility layer for homepage modules ported from the local visual theme. */
:root {
  --ud-title: var(--text-title);
  --ud-body: var(--text-secondary);
  --ud-purple: var(--accent-purple);
  --ud-purple-500: var(--c-purple-500);
  --ud-purple-400: var(--c-purple-400);
  --ud-border: var(--border);
  --ud-surface: var(--bg);
  --ud-dark: var(--bg-purple-darker);
  --ud-green: var(--accent-green);
  --ud-orange: var(--accent-beige);
  --ud-radius: var(--radius-sm);
  --ud-radius-card: var(--radius-lg);
  --ud-gold: var(--c-beige-200);

  --font-headline: var(--font-sans);
  --font-body: var(--font-sans);
  --font-label: var(--font-mono);
  --container-padding: var(--container-pad);
  --primary-container: var(--bg-purple-darker);
  --secondary-container: var(--c-purple-500);
  --surface-container-lowest: var(--bg-white);
  --surface-container-low: var(--bg);
  --surface-container: var(--surface);
  --surface-container-high: var(--border);
  --on-primary-container: var(--text-secondary);
  --outline: var(--border-strong);
  --outline-variant: var(--border);
  --shadow-ambient: var(--shadow-card);
  --shadow-ambient-lg: var(--shadow-card);
  --transition-fast: var(--dur-fast) var(--ease-out);
}

.ud-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-4);
  color: var(--ud-purple);
  font-family: var(--font-label);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-medium);
  line-height: var(--lh-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
}

/* D6 : un texte coloré prend --accent-text-* (vert 600 = 2,6:1, beige 600 = 3,4:1 sur blanc). */
.ud-eyebrow--green { color: var(--accent-text-green); }
.ud-eyebrow--orange { color: var(--accent-text-beige); }
.ud-eyebrow--light { color: color-mix(in srgb, var(--bg-white) 85%, transparent); }

.ud-eyebrow img {
  display: block;
  width: 9px;
  height: 10px;
  flex-shrink: 0;
}

/* Recette 01/10 (Quentin, Nos différences) : la barre oblique est un SVG violet
   (`images/hero/slash.svg`) servi à toutes les variantes ; sur un eyebrow vert
   elle restait violette. Elle prend ici la couleur du texte de l'eyebrow : le
   contenu de l'image est poussé hors cadre (un élément remplacé est découpé à sa
   boîte) et la boîte, peinte en currentColor, est taillée à la forme exacte du
   tracé du SVG (M5 0 L9 0 L4 9.8125 H0, viewBox 9 × 9,8125). */
.ud-eyebrow--green img {
  object-position: -100px 0;
  background-color: currentColor;
  clip-path: polygon(55.56% 0, 100% 0, 44.44% 100%, 0 100%);
}

/* Eyebrow posé dans un flux de texte (modules Hub de génération 2, recette
   29/09 point 2.01) : il reste un bloc. En inline-flex, il prenait la hauteur
   de ligne du parent (+2 à +4 px par eyebrow, relevé sur le Sales Hub) et le
   bloc hérite de l'alignement du parent (centré ou non). La barre oblique suit
   le texte ; -1px la cale comme en inline-flex centré (3,5 px contre 3,4). */
.ud-eyebrow--flow { display: block; }

.ud-eyebrow--flow img {
  display: inline-block;
  margin-right: var(--sp-2);
  vertical-align: -1px;
}

.ud-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: auto;
  min-height: 45px;
  padding: 0 20px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: var(--fs-body-s);
  font-weight: var(--fw-medium);
  line-height: var(--lh-label-m);
  letter-spacing: var(--ls-label-m);
  text-align: center;
  text-decoration: none;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    background-image var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-brand);
  white-space: nowrap;
}

.ud-btn:hover {
  text-decoration: none;
  filter: none;
}

.ud-btn:active {
  transform: translateY(0);
}

.ud-btn--primary,
.ud-btn--primary:visited,
.ud-btn--primary:focus-visible {
  color: var(--btn-primary-text);
  border: 0.5px solid var(--btn-primary-border);
  background: linear-gradient(95.68deg, var(--btn-primary-from) -11.33%, var(--btn-primary-to) 103.16%);
  box-shadow:
    inset 0 -2px 2px rgb(116 87 222 / 24%),
    inset 0 1px 1px rgb(255 255 255 / 10%),
    inset 0 3px 4px rgb(223 238 255 / 10%);
}

.ud-btn--ghost,
.ud-btn--ghost:visited,
.ud-btn--ghost:focus-visible {
  color: var(--btn-secondary-text);
  border: 0.5px solid var(--btn-secondary-border);
  background: linear-gradient(128deg, var(--btn-secondary-from) 17%, var(--btn-secondary-to) 92%);
  box-shadow:
    inset 0 -2px 2px rgb(141 141 141 / 6%),
    inset 0 1px 1px rgb(255 255 255 / 35%);
}

.ud-btn--primary:hover {
  background: linear-gradient(95.68deg, var(--c-purple-600) -11.33%, var(--c-purple-700) 103.16%);
  border-color: var(--c-purple-600);
}

/* Recette 01/10 (Quentin) : survol visible = aplat neutral 200 (l'ancien
   dégradé blanc → neutral 100 était celui du repos). */
.ud-btn--ghost:hover {
  background: var(--c-neutral-200);
  border-color: var(--c-neutral-300);
}

.ud-btn__arrow {
  width: 18px;
  height: 18px;
  flex: none;
}

@media (max-width: 767px) {
  .ud-btn {
    width: 100%;
  }
}

/* Les scènes sticky (méthode) cassent si un wrapper DnD HubSpot clippe. */
.body-container--home,
.body-container--home .dnd-section,
.body-container--home .row-wrapper,
.body-container--home .row-fluid-wrapper,
.body-container--home .row-fluid,
.body-container--home .span12 {
  overflow: visible;
}
/* Visual canvas for page-offre (ported from the local theme, scoped to offre classes). */

:root {
  --ud-canvas-light:
    radial-gradient(ellipse 560px 560px at 852px 0px, rgba(114, 81, 218, 0.22) 0%, rgba(114, 81, 218, 0.08) 34%, rgba(114, 81, 218, 0) 72%),
    radial-gradient(ellipse 760px 640px at 1618px 622px, rgba(240, 222, 184, 0.38) 0%, rgba(240, 222, 184, 0.16) 36%, rgba(240, 222, 184, 0) 72%),
    linear-gradient(180deg, var(--ud-surface) 0%, var(--ud-surface) 100%);
  --ud-canvas-light-sm:
    radial-gradient(ellipse 520px 520px at 58% 0, rgba(114, 81, 218, 0.2) 0%, rgba(114, 81, 218, 0) 72%),
    radial-gradient(ellipse 560px 480px at 105% 34%, rgba(240, 222, 184, 0.32) 0%, rgba(240, 222, 184, 0) 72%),
    linear-gradient(180deg, var(--ud-surface) 0%, var(--ud-surface) 100%);
}

/* White strip around the purple CTA panel (not the panel itself). */
body .dnd-section:has(.cta-final),
body .dnd-section:has(.cta-final) .row-wrapper,
body .dnd-section:has(.cta-final) .row-fluid-wrapper,
body .dnd-section:has(.cta-final) .row-fluid,
body .dnd-section:has(.cta-final) .span12,
body .cta-final.section {
  background: #fff !important;
  background-color: #fff !important;
}

.body-container--offre,
.body-container--offre .dnd-section,
.body-container--offre .row-wrapper,
.body-container--offre .row-fluid-wrapper,
.body-container--offre .row-fluid,
.body-container--offre .span12 {
  overflow: visible;
}

.body-container--hub {
  background: var(--ud-canvas-light);
}

.body-container--hub .dnd-section,
.body-container--hub .row-wrapper,
.body-container--hub .row-fluid-wrapper,
.body-container--hub .row-fluid,
.body-container--hub .span12 {
  overflow: visible;
  background: transparent !important;
}

.body-container--hub .hero-carrousel,
.body-container--hub .liste-offres {
  background: transparent !important;
}

.texte-highlight,
.signal-cards,
.accordeon-visuel {
  position: relative;
}

body .texte-highlight {
  min-height: 478px;
  padding: 96px 0;
  overflow: visible;
  background: transparent !important;
}

body .texte-highlight::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 0;
  width: 1440px;
  height: 1722px;
  transform: translateX(-50%);
  pointer-events: none;
  background: var(--ud-canvas-light);
}

body .texte-highlight__halo {
  display: none;
}

body .texte-highlight .container {
  position: relative;
  z-index: 1;
}

body .signal-cards {
  z-index: 1;
  min-height: 638px;
  padding: 96px 0;
  background: transparent !important;
}

body .signal-cards .container {
  position: relative;
  z-index: 1;
}

body .signal-cards__grid--3 {
  gap: 16px;
}

body .signal-cards__card {
  min-height: 256px;
  background: #fff;
}

body .accordeon-visuel {
  z-index: 1;
  padding: 0;
  background: transparent !important;
}

.body-container--offre .pricing-pair,
.body-container--offre .signal-suite {
  overflow: hidden;
  background:
    radial-gradient(ellipse 560px 560px at 24% 0, rgba(114, 81, 218, 0.14) 0%, rgba(114, 81, 218, 0.05) 38%, rgba(114, 81, 218, 0) 74%),
    radial-gradient(ellipse 700px 560px at 100% 82%, rgba(240, 222, 184, 0.34) 0%, rgba(240, 222, 184, 0.13) 36%, rgba(240, 222, 184, 0) 72%),
    linear-gradient(180deg, var(--ud-surface) 0%, var(--ud-surface) 100%) !important;
}

.body-container--offre .pricing-pair__atmosphere {
  display: none;
}

@media (max-width: 1100px) {
  body .texte-highlight::before {
    width: 100vw;
    height: 1500px;
    background: var(--ud-canvas-light-sm);
  }

  .body-container--hub {
    background: var(--ud-canvas-light-sm);
  }
}

@media (max-width: 767px) {
  body .texte-highlight {
    min-height: 0;
    padding: 56px 0;
  }

  body .signal-cards {
    min-height: 0;
    padding: 56px 0;
  }

  body .accordeon-visuel {
    padding: 0;
  }
}

/* Résultats clients: opaque off-white canvas so pricing blobs never sit under the quote. */
.body-container--offre .slider-temoignages {
  position: relative;
  isolation: isolate;
  background-color: var(--bg-white);
}

.body-container--offre .slider-temoignages::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 820px 560px at 8% 78%, rgba(240, 222, 184, 0.12) 0%, rgba(240, 222, 184, 0) 68%),
    radial-gradient(ellipse 760px 520px at 94% 28%, rgba(114, 81, 218, 0.08) 0%, rgba(114, 81, 218, 0) 70%);
}

.body-container--offre .slider-temoignages > * {
  position: relative;
  z-index: 1;
}

.body-container--onboarding-formation .cards-grid--icon-inline.section--dark .cards-grid__list {
  gap: 16px;
}

/* Onboarding role programmes: keep the lateral heading on the dark Figma panel.
   The shared theme heading rules can otherwise flatten this instance into the
   light canvas, leaving the left column visually empty. */
.body-container--onboarding-formation .cards-grid--icon-inline.section--dark {
  color: var(--c-base-white);
  background-image: linear-gradient(-31deg, var(--bg-purple-darker), var(--bg-purple-dark));
}

.body-container--onboarding-formation .cards-grid--icon-inline.section--dark .cards-grid__title {
  color: var(--c-base-white);
}

.body-container--onboarding-formation .cards-grid--icon-inline.section--dark .cards-grid__intro {
  color: var(--c-neutral-300);
}

/* The copied garanties module owns its panel background. Keep the offer canvas
   from painting over it and keep the module content above the canvas layer. */
.body-container--onboarding-formation .garanties {
  position: relative;
  z-index: 2;
  display: block;
  visibility: visible;
  opacity: 1;
  background: linear-gradient(303.46deg, var(--bg-purple-darker) 3.81%, var(--bg-purple-dark) 115.06%) !important;
}

.body-container--onboarding-formation .garanties > .container,
.body-container--onboarding-formation .garanties__layout,
.body-container--onboarding-formation .garanties__intro,
.body-container--onboarding-formation .garanties__grid {
  position: relative;
  z-index: 1;
}

/* RevOps pilotage panel: restore the dark Figma background after the shared
   solution stylesheet makes the module transparent for light sections. */
.body-container--conseil-data-revops .garanties {
  position: relative;
  z-index: 2;
  background: linear-gradient(303.46deg, var(--bg-purple-darker) 3.81%, var(--bg-purple-dark) 115.06%) !important;
}

/* Audit CRM : pas de 2e CTA hero (déjà sur l’offre) ; pas de CTA d’en-tête
   cta-final tant que le form n’est pas branché. La carte « Nous contacter »
   (.cta-final__contact-cta) reste. */
.body-container--audit-crm .offre-hero__ctas .btn--secondary,
.body-container--audit-crm .offre-hero__btn.ud-btn--ghost {
  display: none;
}

.body-container--audit-crm .cta-final__cta {
  display: none;
}
/* Fields */

.hs-form-field {
  margin-bottom: 1.4rem;
}

/* Labels */

form label {
  display: block;
  font-size: var(--fs-label-s);
  letter-spacing: var(--ls-label-s);
  font-weight: var(--fw-medium);
  color: var(--text);
  margin-bottom: 0.35rem;
}

/* Form Title */
.form-title {
  margin-bottom: 0;
}

/* Help text */

form legend {
  font-size: 0.875rem;
}

/* Inputs */

form input[type=text],
form input[type=search],
form input[type=email],
form input[type=password],
form input[type=tel],
form input[type=number],
form input[type=file],
form select,
form textarea {
  display: inline-block;
  font-size: var(--fs-body-s);
  letter-spacing: var(--ls-body-s);
  font-family: var(--font-sans);
  padding: var(--sp-3) var(--sp-4);
  width: 100%;
  color: var(--text);
  background-color: var(--bg);
  border: 1px solid var(--border-interactive);
  border-radius: var(--radius-sm);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

/* `opacity: 1` : Firefox applique sinon une opacité par défaut au placeholder,
   ce qui casse le contraste de --text-placeholder. */
form ::placeholder {
  color: var(--text-placeholder);
  opacity: 1;
}

/* Au survol, l'anneau se pose CONTRE la bordure, qui garde --border-interactive :
   exactement le geste de .cta-final__option:hover, le contrôle voisin de la même
   carte. Ne PAS remplacer la couleur de bordure par --accent : c'est le défaut
   corrigé sur .cta-final__option le 2026-08-03. Mesuré sur --bg clair (#F8F9FB) :
   accent violet 5,11 ✓ · bleu 3,52 ✓ · vert 2,62 ✗ · beige 2,56 ✗ contre le seuil
   3:1 de WCAG 1.4.11 — la bordure d'un champ est le trait qui dit où commence la
   zone de saisie, elle ne peut pas passer sous le seuil sur deux rubriques. */
form input[type=text]:hover,
form input[type=search]:hover,
form input[type=email]:hover,
form input[type=password]:hover,
form input[type=tel]:hover,
form input[type=number]:hover,
form select:hover,
form textarea:hover {
  box-shadow: 0 0 0 1px var(--accent);
}

form textarea {
  resize: vertical;
}

form fieldset {
  max-width: 100% !important;
}

/* Inputs - checkbox/radio */

form .inputs-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

form .inputs-list > li {
  display: block;
  margin: 0.7rem 0;
}

form .inputs-list input,
form .inputs-list span {
  vertical-align: middle;
}

form input[type=checkbox],
form input[type=radio] {
  cursor: pointer;
  margin-right: 0.35rem;
}

/* Inputs - date picker */

.hs-dateinput {
  position: relative;
}

.hs-dateinput:before {
  content:'\01F4C5';
  position: absolute;
  right: 10%;
  top: 50%;
  transform: translateY(-50%);
}

.fn-date-picker .pika-table thead th {
  color: #FFF;
}

.fn-date-picker td.is-selected .pika-button {
  border-radius: 0;
  box-shadow: none;
}

.fn-date-picker td .pika-button:hover,
.fn-date-picker td .pika-button:focus {
  border-radius: 0 !important;
  color: #FFF;
}

/* Inputs - file picker */

form input[type=file] {
  background-color: transparent;
  border: initial;
  padding: initial;
}

/* Headings and text */

form .hs-richtext,
form .hs-richtext p {
  font-size: 0.875rem;
  margin: 0 0 1.4rem;
}

form .hs-richtext img {
  max-width: 100% !important;
}

/* GDPR */

.legal-consent-container {
  margin-bottom: var(--sp-4);
  color: var(--text-secondary);
}

.legal-consent-container .hs-richtext,
.legal-consent-container .hs-richtext p {
  font-size: var(--fs-body-s);
  letter-spacing: var(--ls-body-s);
}

/* --accent-text et non --accent : un lien est du texte porteur de sens, seuil
   4,5:1 (contrat du socle §1, D6 du 2026-08-05). */
.legal-consent-container a {
  color: var(--accent-text);
}

.legal-consent-container .hs-form-booleancheckbox-display > span,
.legal-consent-container .hs-form-booleancheckbox-display > span p {
  margin-left: 1rem !important;
}

/* Validation — deux rôles, parce que les deux seuils WCAG diffèrent : un texte
   d'erreur doit tenir 4,5:1, la bordure d'un champ invalide 3:1 (1.4.11). Le
   #EF6B51 en dur qui servait aux trois avant le 2026-08-05 ne tenait ni l'un ni
   l'autre : 2,89 sur --bg, 2,72 sur --surface. */

.hs-form-required {
  color: var(--danger-text);
}

/* --danger tient 3:1 en bordure, pas en texte : on l'épaissit d'un anneau plutôt
   que de le poser sur du texte. Mesuré : #EF4444 sur --bg clair = 3,57 ✓. */
.hs-input.invalid.error {
  border-color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger);
}

/* La liste d'erreurs est un <ul> : sans reset elle hérite des puces et du
   retrait, et le message se retrouve décalé sous le champ. */
.hs-error-msgs {
  margin: 0;
  padding: 0;
  list-style: none;
}

.hs-error-msg {
  color: var(--danger-text);
  font-size: var(--fs-body-s);
  margin-top: 0.35rem;
}

/* Submit button — l'apparence vient de elements/_buttons.css, dont les sélecteurs
   .btn / .btn--primary sont étendus à `form input[type=submit], form .hs-button`.
   Recopier ici la recette du bouton primaire donnerait deux sources de vérité
   pour un même geste (CONVENTIONS.md §5). Rien à déclarer dans ce fichier :
   cursor / text-align / white-space viennent déjà de _buttons.css:10-18, qui
   matche le submit HubSpot via sa classe .hs-button.
   `transition: all 0.15s linear` a été retirée : `all` fait transiter des
   propriétés de layout et sort des tokens --dur-* que CONVENTIONS.md §5 impose
   de centraliser. */

/* Captcha */

.grecaptcha-badge {
  margin: 0 auto;
}


  /* Search button input field and suggestions */
  .body-container-wrapper .hs-search-field__button {
    padding: 15px;
  }

  .body-container-wrapper .hs-search-field__bar--button-inline .hs-search-field__button {
    margin-left: 6px;
    margin-bottom: 0;
  }

  .body-container-wrapper .hs-search-field__button svg {
    height: 15px;
    fill: #fff;
  }

  .body-container-wrapper .hs-search-field__bar > form > .hs-search-field__input {
    padding: 10px;
  }

  .body-container-wrapper .hs-search-field__suggestions li a {
    color: #494A52;
    padding: 0.35rem 0.7rem;
    text-decoration: none;
    transition: background-color 0.3s;
  }

/* Table */

table {
  border-collapse: collapse;
  margin-bottom: 1.4rem;
  overflow-wrap: break-word;
}

/* Table cells */

td,
th {
  vertical-align: top;
}

/* Table header */

thead th {
  vertical-align: bottom;
}

/* Components
Specific pieces of UI that are stylized. Typically used for global partial styling
*/

/* =============================================================================
   Navbar — comportement calqué sur uptoo.fr (décision call du ~21/07)
   -----------------------------------------------------------------------------
   Desktop : barre sticky translucide + 4 mega-menus. Mobile : drawer latéral.
   Timings : SPEC-ANIMATIONS-V1 §3 (--dur-menu, 180-220 ms) et §2.8 (sticky
   backdrop-filter). Aucune valeur de couleur ni de durée en dur.
   ============================================================================= */

.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  border-bottom: 1px solid var(--border);
}

/* ⚠ Le fond translucide est porté par ce pseudo-élément, et NON par `.navbar`.
   Un `backdrop-filter` non nul fait de l'élément qui le porte le BLOC CONTENEUR
   de ses descendants en `position: fixed`. Posé sur `.navbar`, il ramenait le
   drawer (`inset: 0 0 0 auto`) et le voile (`inset: 0`) à la boîte de la barre
   au lieu du viewport : le menu mobile s'ouvrait en bandeau de 72 px de haut
   contenant 350 px à faire défiler, et le voile ne couvrait rien.
   Mesuré en préprod le 2026-08-06, à drawer ouvert : 343 × 72 px avec le filtre
   sur `.navbar`, 343 × 844 px sans — la seule variable changée étant celle-là.
   Un pseudo-élément n'est pas un ancêtre : il ne crée aucun bloc conteneur.
   Le rendu est identique (le backdrop est flouté, puis le blanc à 82 % est
   posé par-dessus), l'ordre de peinture ne change pas.
   Ne PAS remettre `backdrop-filter` sur `.navbar` — cf. `.navbar__nav` §mobile. */
.navbar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: color-mix(in srgb, var(--bg-white) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

/* ⚠ La barre est `position: sticky`, mais un sticky ne colle que dans la boîte
   de son PARENT. HubSpot enveloppe le module dans deux `div` qui épousent
   exactement sa hauteur — mesuré en préprod le 2026-08-07 : `.navbar` 73 px,
   `div.hs_cos_wrapper` 73 px, `div` sans classe 73 px, et seulement ensuite
   `.body-wrapper` à 8 089 px. Course disponible : ZÉRO. La barre défilait donc
   avec la page depuis toujours, alors que tout le CSS la décrit comme sticky.
   Vérifié que ce n'était pas un effet du `backdrop-filter` : en le remettant sur
   `.navbar`, elle ne collait pas davantage.
   `display: contents` efface ces deux boîtes sans toucher au balisage généré :
   le bloc conteneur de `.navbar` devient `.body-wrapper`, et le sticky retrouve
   sa course. Le `:has(> .navbar)` ne vise que les enveloppes de CE module — les
   autres modules de la page gardent les leurs. Si HubSpot change son balisage,
   le sélecteur cesse simplement de matcher : pas de régression silencieuse. */
.hs_cos_wrapper:has(> .navbar),
div:has(> .hs_cos_wrapper > .navbar) {
  display: contents;
}

/* HAUTEUR DE BARRE — relevée, pas arrondie.
   La maquette pose 68px en desktop et 64px en mobile :
   · desktop — `docs/contenu-maquettes/blog-listing.md:24` (nœud `1778:15706`,
     « largeur 1440, hauteur 68, padding 16px vertical / 80px horizontal ») et
     `blog-article.md:31` (nœud `2067:29364`, même composant `1101:2705`) ;
   · mobile — `docs/contenu-maquettes/mobile-menu.md:57` (nœud `2021:10003`,
     390 × 64, logo + burger).
   Le socle servait 72 + 1px de bordure = 73px à TOUTES les largeurs. Mesuré
   sur la préprod à +7,4 % au passage du 07/08 ET au rejeu du 08/08, sur les
   trois pages pilotes qui exposent la ligne « Navbar » séparément
   (`docs/GATE-VISUELLE-2026-08-07.md:157`, `:246`, `:335`, `:631`, `:651`) ;
   consigné aussi en écart de valeur socle ↔ maquette D-35
   (`docs/DECISIONS-DESIGN.md:355` : « 73 px / 45 px » servis contre « 68 / 64
   / 36 »).
   La bordure basse de 1px EST comprise dans la hauteur relevée — d'où 63 + 1
   et 67 + 1, et non 64 et 68. Le point de bascule est celui du composant
   (1024px, cf. la media query du drawer) et non un troisième seuil : c'est
   exactement là que le bouton d'appel quitte la barre pour le pied du drawer,
   donc là où la barre change de contenu. Entre 768 et 1023px la maquette ne
   relève rien ; la barre y sert la mise en page mobile (logo + burger), elle
   en prend donc la hauteur. */
.navbar__inner {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  min-height: 63px;
}

.navbar__logo { display: inline-flex; align-items: center; flex: none; }
.navbar__logo img {
  display: block;
  width: 125px;
  height: 20px;
  object-fit: contain;
  object-position: left center;
}

/* --- Navigation desktop ---------------------------------------------------- */
.navbar__nav { display: flex; align-items: center; gap: var(--sp-2); flex: 1 1 auto; }
.navbar__list { display: flex; align-items: center; gap: var(--sp-2); list-style: none; margin: 0; padding: 0; }
.navbar__item { position: relative; }

.navbar__link,
.navbar__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--fs-label-m);
  letter-spacing: var(--ls-label-m);
  font-weight: var(--fw-medium);
  color: var(--text-title);
  cursor: pointer;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}

.navbar__link:hover,
.navbar__trigger:hover,
.navbar__trigger[aria-expanded="true"] { color: var(--accent); text-decoration: none; }

/* Chevron pivotant à l'ouverture (SPEC §3). */
.navbar__chevron {
  width: 12px;
  height: 12px;
  flex: none;
  transition: transform var(--dur-menu) var(--ease-out);
}

.navbar__trigger[aria-expanded="true"] .navbar__chevron { transform: rotate(180deg); }

.navbar__actions {
  position: relative;
  z-index: 101;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: none;
  margin-left: auto;
}

/* BOUTON D'APPEL — 36px de haut, et c'est la cause mesurée de la barre à 73px.
   Relevé deux fois, de première main, sur deux frames indépendantes :
   `docs/contenu-maquettes/blog-article.md:39` (« radius 6, hauteur 36 ») et
   `blog-listing.md:28` (« 36px de haut, radius 6px »). Le socle impose
   `min-height: 45px` à tout `.btn` (`css/elements/_buttons.css:37`) — c'est
   le second chiffre de la fiche D-35 (`docs/DECISIONS-DESIGN.md:355`).
   ⚠️ La revue du 05/08 interdit explicitement de toucher au `min-height` du
   socle sans avoir mesuré les boutons des autres frames
   (`docs/REVUE-MODULES-2026-08-05.md:391`). L'écart est donc corrigé ICI,
   dans la seule portée de la navbar, et le socle n'est pas touché.
   `padding-block: 0` n'est pas cosmétique : `.btn` pose 12px en haut et en bas
   pour une ligne de 14px, soit 40px de contenu — un `min-height` de 36 ne
   serait jamais atteint sans lui. Le centrage vertical reste celui de `.btn`
   (`display: inline-flex` + `align-items: center`), la gouttière latérale
   aussi : elle n'est pas relevée, donc pas touchée. */
/* Recette 01/10 (Costel/Nicolas, « CTA sticky », présent sur toutes les pages
   puisque la barre colle au défilement) : typo alignée sur les autres CTA du
   site (`.ud-btn` : 14 px --fs-body-s, Medium, interligne et interlettrage
   label-m — c'est aussi le « texte 14 px » de la maquette, cas-client-wakam.md
   §1) au lieu des 15 px de `.btn`, et gouttière latérale ramenée de 24 à 16 px :
   le bouton passe de 171 à ~150 px de large, hauteur 36 px inchangée. */
.navbar__actions .btn {
  min-height: 36px;
  padding-block: 0;
  padding-inline: var(--sp-4);
  font-size: var(--fs-body-s);
  line-height: var(--lh-label-m);
  letter-spacing: var(--ls-label-m);
  position: relative;
  z-index: 1;
  text-decoration: none;
}

.navbar__actions .btn:hover,
.navbar__actions .btn:focus,
.navbar__actions .btn:focus-visible {
  text-decoration: none;
}

/* --- Mega-menu : fade + translateY(-4px → 0), 180-220 ms ------------------- */
/* ENVELOPPE DU PANNEAU — les trois cotes ci-dessous sont relevées, deux fois,
   sur deux sources indépendantes du même fichier Figma :
   · largeur des panneaux à UNE colonne **350 px** —
     `docs/contenu-maquettes/mega-menu-popovers.md` §0 (nœuds `2043:25280`,
     `2043:25307`, `2043:25339`, cotes lues au nœud) et `menu.md:38-40` (mesure
     1:1 sur l'export, calibrée sur `2043:26763`). Le `280px` servi jusqu'ici
     n'est adossé à aucun relevé ; il est la seule cause du **+50 px** mesuré
     sur « Ressources » (`docs/preuves/etats-2026-08-11/etats-1440.json`,
     `mega-menu[3]` : 280 × 343 contre 350 × 293). Un panneau plus étroit fait
     passer ses descriptions de 2 à 3 lignes : c'est la largeur qui pilote la
     hauteur, pas l'inverse.
   · padding **8 px** — `mega-menu-popovers.md` §7 (« padding 8 px », identique
     sur les 4) et `menu.md:78` (« 600 × 392, padding 8 px »). La géométrie
     boucle au pixel sur les 4 panneaux avec 8 (8 + 376 + 8 = 392 ; 8 + 404 + 8
     = 420 ; 8 + 457 + 8 = 473 ; 8 + 277 + 8 = 293) — `--sp-5` (24) ajoutait
     32 px à chaque panneau.
   · rayon **8 px** — `mega-menu-popovers.md` §7 et `menu.md:78`. Le 12 px
     relevé dans la maquette est celui de la GRILLE INTERNE (`2043:26764`), pas
     de l'enveloppe : `--radius-lg` posait le rayon du mauvais nœud.
   Ce qui n'est PAS aligné, et volontairement : la bordure et l'ombre. La
   maquette pose une bordure blanche à 24 % et AUCUNE ombre — un panneau blanc
   invisible sur ses bords, relevé comme défaut de source bloquant
   (`mega-menu-popovers.md`, « Fautes » n° 5). Le build garde `--border` +
   `--shadow-card` tant que la DA n'a pas tranché. */
.navbar__panel {
  position: absolute;
  top: calc(100% + var(--sp-3));
  left: 0;
  z-index: 90;
  min-width: 350px;
  padding: var(--sp-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background-color: var(--bg-white);
  box-shadow: var(--shadow-card);
  opacity: 0;
  transform: translateY(-4px);
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity var(--dur-menu) var(--ease-out),
    transform var(--dur-menu) var(--ease-out),
    visibility 0s linear var(--dur-menu);
}

/* Pont de survol : le décalage visuel (`top`) n'est pas dans la boîte de
   `.navbar__item`. Sans ce pseudo, le curseur quitte l'item en traversant
   l'air, `:hover` tombe, et `pointer-events: none` referme le panneau avant
   d'atteindre une entrée. Même hauteur que `top`. Desktop seulement. */
.navbar__panel::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: var(--sp-3);
}

.navbar__item:hover > .navbar__panel,
.navbar__item:focus-within > .navbar__panel,
.navbar__panel[data-ud-open="true"] {
  opacity: 1;
  transform: translateY(0);
  visibility: visible;
  pointer-events: auto;
  transition-delay: 0s;
}

/* Gouttière entre entrées : 8 px, pas 4. Relevée sur les 4 panneaux
   (`mega-menu-popovers.md` §2-§5 « gap 8 px », et la grille interne du nœud
   `2043:26764` en `menu.md:79`). C'est le chiffre qui fait boucler les 4
   hauteurs au pixel : 4×88 + 3×8 = 376 (Solutions), 3×87 + 2×8 = 277
   (Ressources), etc. `--sp-1` retirait 4 px par gouttière. */
.navbar__panel-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }

/* Le panneau de « Nos solutions » est sur DEUX colonnes dans la maquette
   (`2043:25871`, 600 px de large, 4 + 3), les trois autres sur une seule
   (350 px). Plutôt qu'un champ à régler, on bascule au-delà de 6 entrées :
   c'est la règle que la maquette applique de fait, et elle se maintient toute
   seule si une offre est ajoutée.

   ⚠️ CE SEUIL EST DÉSORMAIS SOUS TENSION — il n'a pas changé, c'est le contenu
   servi qui a débordé. Mesuré le 11/08 (`etats-1440.json`) : le panneau
   « HubSpot » sert **7** entrées (la 7e, `IA Breeze`, n'existe dans AUCUN des
   deux relevés — `mega-menu-popovers.md` §4 tranche explicitement pour
   `Revenue Hub` en 6e et dernière) et « Intégrations » en sert **8** (dont
   `Architecture & Tech` et `Connecteurs ERP`, sans icône ni description :
   `nEntrees: 8` pour `nIcones: 6`, back-chevron compris). Les deux franchissent
   donc le seuil et basculent à 600 px / 2 colonnes alors que la maquette les
   pose à 350 px / 1 colonne. C'est l'intégralité du **−152 px** de « HubSpot »
   (600 × 321 servis contre 350 × 473 relevés) : à 600 px de large, une
   description qui tenait sur 2 lignes en tient une, et 6 rangées deviennent 4.
   La cause est le CONTENU, pas cette règle — ne pas « corriger » le seuil pour
   compenser. Ramené à 6 / 5 / 3 entrées, l'arithmétique retombe sur les cotes
   relevées (473 / 420 / 293) sans une ligne de CSS de plus.

   ORDRE DE LECTURE — corrigé ici. `grid-template-columns` seul remplit en
   RANGÉES : la colonne A recevait les entrées 1·3·5·7 et la colonne B 2·4·6.
   La maquette empile 1-4 puis 5-7 et se lit « colonne par colonne, pas en
   serpentin » (`mega-menu-popovers.md` §2 ; colonnes `2043:25872` et
   `2043:25920`). `grid-auto-flow: column` + 4 rangées explicites rendent ce
   4 + 3.

   PORTÉE DESKTOP — la bascule 2 colonnes était hors media query et
   s'appliquait donc AUSSI au niveau 2 du drawer mobile, où la maquette empile
   les 7 entrées sur UNE colonne (`menu.md:172` et §6-C). À 390 px, les deux
   colonnes tombaient à ~145 px chacune. Le `@media` referme cette portée. */
@media (min-width: 1024px) {
  .navbar__panel-list:has(> li:nth-child(7)) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-flow: column;
    grid-template-rows: repeat(4, auto);
  }

  .navbar__panel:has(.navbar__panel-list > li:nth-child(7)) { min-width: 600px; }

  /* Gabarit d'entrée du panneau à 2 colonnes — celui de « Nos solutions », et
     le seul des quatre qui diffère : hauteur **fixe 88 px** (donnée de nœud,
     certaine : `menu.md:81` « hauteur fixe 88 px », `mega-menu-popovers.md` §2
     « item, 288 × 88 px fixes »). Les trois autres panneaux ont une hauteur
     d'entrée variable, 66 ou 87 px selon que la description tient sur 1 ou 2
     lignes — c'est le comportement par défaut, il n'a besoin d'aucune règle.
     Sans ce plancher, la rangée `Conseil Data & RevOps` (description sur 1
     ligne) retombe à 66 px et le panneau perd 22 px sur les 392 relevés.
     Le `li` passe en grille pour que le lien occupe toute sa cellule : sinon
     son fond de survol s'arrête avant le bas de la rangée. */
  .navbar__panel-list:has(> li:nth-child(7)) > li { display: grid; }
  .navbar__panel-list:has(> li:nth-child(7)) .navbar__panel-link { min-height: 88px; }
}

/* Icône 32 × 32 dans « Nos solutions » — et 24 × 24 dans les trois autres.
   Ce n'est pas une nuance : le panneau Solutions porte des illustrations
   `illu-offre-*` en 32, les trois autres des pictos Remix en 24
   (`mega-menu-popovers.md` §2 contre §3 « icône 24 × 24 (et non 32) », lu au
   nœud). L'arithmétique le confirme des deux côtés, à 12 px de padding et
   16 px de gouttière : 288 − 24 − 32 − 16 = **216** = le bloc texte relevé de
   Solutions ; 334 − 24 − 24 − 16 = **270** = celui des trois autres ; et
   350 − 24 − 32 − 16 = **278** = celui du niveau 2 du drawer mobile
   (`menu.md:175`). C'est pourquoi cette règle-ci n'est PAS enfermée dans le
   media query desktop : le drawer sert le même gabarit à 32.
   Elle roule sur le même seuil de 7 entrées que la bascule 2 colonnes — donc
   elle vise aujourd'hui aussi « HubSpot » et « Intégrations », à tort, tant
   que leur contenu déborde. */
.navbar__panel-list:has(> li:nth-child(7)) .navbar__panel-icon { width: 32px; height: 32px; }

/* Icône + (titre / description). Une entrée sans icône ni description reste un
   simple lien : c'est le cas du repli sur le menu HubSpot, qui ne sait porter
   ni l'une ni l'autre.

   ⚠️ CE BLOC EST DÉSORMAIS LA SEULE RÈGLE DU FICHIER À VISER
   `.navbar__panel-link`, et ce n'était pas le cas — c'est le défaut corrigé
   ici. Le `display: flex` avait été ajouté par le lot mega-menu (`aef71bb`)
   AU-DESSUS d'un bloc du socle P1 (`2d2afe1`) qui répétait le même sélecteur,
   à spécificité égale (0,1,0), avec `display: block`. À spécificité égale
   c'est la dernière règle qui gagne : `block` l'emportait, `align-items` et
   `gap` étaient inertes, et l'icône se posait AU-DESSUS du libellé au lieu
   d'être à sa gauche — l'inverse du gabarit relevé
   (`docs/contenu-maquettes/mega-menu-popovers.md §2` : item 288 × 88,
   `align-items: start`, icône puis bloc texte de 216px).
   Les 21 icônes étaient bien servies et bien colorées (vérifié le 07/08,
   `docs/VERIF-ICONES-MEGA-MENU.md §5`) : c'est leur MISE EN PAGE qui ne
   l'était pas — un contrôle de couleur ne pouvait pas l'attraper.
   Les deux blocs sont fusionnés en un seul : plus de sélecteur en double,
   donc plus de piège à réamorcer. Aucune valeur de couleur n'est modifiée. */
/* Padding 12 px sur les QUATRE côtés (et non 8/12), gouttière icône ↔ texte
   16 px (et non 12) : gabarit relevé à l'identique sur les 4 panneaux et sur
   le niveau 2 du drawer (`mega-menu-popovers.md` §2-§5 « padding 12, gap 16 »,
   `menu.md:81` et `:175`). Ces deux cotes portent la hauteur d'entrée :
   12 + 21 + 21 + 12 = **66** pour une description sur 1 ligne, 12 + 21 + 42 +
   12 = **87** sur 2 — exactement les 66 / 87 relevés au nœud. Avec 8 px de
   padding vertical, la même entrée mesurait 60 et 81. */
.navbar__panel-link {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  padding: var(--sp-3);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-body-s);
  letter-spacing: var(--ls-body-s);
  line-height: var(--lh-body-s);
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

.navbar__panel-icon {
  width: 24px;
  height: 24px;
  flex: none;
  margin-top: 2px;
  /* La maquette dessine ces icônes en #8E9BAF, soit la primitive neutral-500 —
     et non `--text-secondary`, qui vaut neutral-700 (#4F5C6E) et les rendait
     nettement plus sombres que le relevé. Aucun rôle sémantique ne porte
     aujourd'hui « pictogramme discret » : le jour où il existera, c'est ici
     qu'il se substitue. Le pictogramme est décoratif et doublé d'un libellé
     visible, donc hors seuil de contraste AA. */
  color: var(--c-neutral-500);
  transition: color var(--dur-fast) var(--ease-out);
}

/* Les 4 icônes de tête de panneau n'existaient QUE dans la couleur active dans
   Figma ; le sprite les a normalisées en `currentColor`, donc l'état se pilote
   ici et nulle part ailleurs. */
.navbar__panel-link:hover .navbar__panel-icon,
.navbar__panel-link:focus-visible .navbar__panel-icon { color: var(--accent); }

/* Aucune gouttière entre le titre et la description : les deux lignes sont
   collées, la maquette n'insère rien entre elles. C'est démontrable, pas
   supposé — 12 + 21 + 21 + 12 = 66 est la hauteur d'entrée relevée au nœud
   (`mega-menu-popovers.md` §3), et elle ne boucle qu'avec 0. Les 2 px servis
   ajoutaient 2 px par entrée, soit 6 px sur « Ressources » et 14 sur
   « HubSpot ». */
.navbar__panel-texte { display: flex; flex-direction: column; gap: 0; min-width: 0; }

/* Le libellé d'entrée est en DM Sans **Medium** dans la maquette — style
   `[Desktop]/text-small-medium`, relevé sur les 21 entrées
   (`mega-menu-popovers.md` §7, `menu.md:71` « les titres d'entrée sont des
   libellés de lien, DM Sans Medium 14 »). Il héritait ici du 400 du corps de
   page : titre et description étaient dans la même graisse, donc rien ne
   distinguait l'un de l'autre. `line-height` est posé sur `.navbar__panel-link`
   ci-dessus (1,5 → 21 px), il n'a pas à être répété. */
.navbar__panel-titre { color: var(--text-title); font-weight: var(--fw-medium); }

.navbar__panel-desc {
  color: var(--text-secondary);
  font-size: var(--fs-body-s);
  letter-spacing: var(--ls-body-s);
  line-height: var(--lh-body-s);
}

.navbar__panel-link:hover { background-color: var(--surface); color: var(--text-title); text-decoration: none; }

/* --- Bascule mobile -------------------------------------------------------- */
.navbar__toggle { display: none; }
.navbar__drawer-close { display: none; }

/* Retour au niveau 1 de la pile mobile. Absent en desktop, où le survol
   n'empile rien. Gabarit repris de la maquette `2043:27137` : rangée pleine
   largeur en tête de liste, filet bas, chevron pointant à gauche. */
.navbar__back {
  display: none;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-3);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  color: var(--text-title);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Le chevron du module pointe vers le BAS ; 90° le fait pointer à gauche. */
.navbar__back svg { width: 12px; height: 12px; flex: none; transform: rotate(90deg); }

@media (max-width: 1023px) {
  .navbar__toggle { display: inline-flex; margin-left: auto; }

  /* Drawer : hors écran, glissé à l'ouverture. Reste dans le flux accessible
     uniquement quand il est ouvert (inert posé par le JS). */
  .navbar__nav {
    position: fixed;
    inset: 0 0 0 auto;
    /* Au-dessus du voile (`.navbar__scrim`, z-index 99). Sans cette valeur, le
       drawer est un positionné en `z-index: auto` et le voile, lui, est à 99 :
       le noir à 40 % passerait PAR-DESSUS le menu. Le défaut était masqué tant
       que le voile restait bridé à la boîte de la navbar (cf. `.navbar::before`). */
    z-index: 100;
    width: min(88vw, 380px);
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-4);
    padding: var(--sp-6) var(--sp-5);
    background-color: var(--bg-white);
    border-left: 1px solid var(--border);
    overflow-y: auto;
    /* ⚠️ `overflow-y: auto` passe AUSSI l'axe horizontal en `auto` (une valeur
       `visible` est recalculée dès que l'autre axe défile). Les panneaux de
       niveau 2 en attente, à `translateX(100%)`, rendaient alors le drawer
       défilable latéralement : mesuré en prod le 2026-10-02 à 390 px,
       `scrollWidth` 612 pour `clientWidth` 342. Sur iPhone, le contenu glissait
       à gauche à l'ouverture d'un sous-menu et laissait une bande blanche à
       droite. La cause est close sur `.navbar__list` (`overflow-x: clip`) ;
       ce `hidden` est le filet des navigateurs qui ignorent `clip`. */
    overflow-x: hidden;
    transform: translateX(100%);
    transition: transform var(--dur-med) var(--ease-brand);
  }

  .navbar[data-ud-drawer="open"] .navbar__nav { transform: translateX(0); }

  .navbar__drawer-close { display: inline-flex; align-self: flex-end; }
  /* La liste est le CONTENEUR DE POSITIONNEMENT du panneau de niveau 2 — pas
     `.navbar__item`. C'est ce qui permet au panneau de couvrir toute la zone de
     liste SANS masquer le bouton d'appel, qui reste en pied de drawer aux deux
     niveaux (maquette `2043:26962`, identique en tous points à `2043:26933`).
     `flex: 1` reproduit le `Frame 427320756` de la maquette (350 × 648,
     `flex: 1 0 0`), qui pousse le CTA en bas.
     `overflow-x: clip` retient les panneaux en attente dans la largeur de la
     liste : ils ne comptent plus dans le `scrollWidth` du drawer, qui n'a plus
     rien à faire défiler latéralement (cf. `.navbar__nav`). `clip` et non
     `hidden` : un conteneur en `hidden` reste défilable par le focus, et c'est
     ce qui déclenchait le décalage ; `clip` ne crée aucun conteneur de
     défilement, garde `overflow-y: visible` et donc la hauteur minimale de la
     liste en flex. */
  .navbar__list {
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-1);
    position: relative;
    flex: 1 1 auto;
    overflow-x: clip;
  }

  .navbar__item { position: static; }
  .navbar__link, .navbar__trigger { width: 100%; justify-content: space-between; padding: var(--sp-3); }
  .navbar__actions { margin-left: 0; flex-direction: column; align-items: stretch; }

  /* En pied de drawer, la maquette ne pose PAS les 36px de la barre desktop :
     elle relève 40px et la pleine largeur (350 sur un cadre de 390) —
     `docs/contenu-maquettes/mobile-menu.md:267`, qui compare explicitement les
     deux : « dans la navbar, 36 px de haut, largeur auto » contre « en bas du
     drawer, 40 px, pleine largeur 350 px ». Ce bloc est déclaré APRÈS la règle
     desktop `.navbar__actions .btn` et a la même spécificité (0,2,0) : c'est
     l'ordre du fichier qui tranche, comme partout ailleurs ici. */
  .navbar__actions .btn { width: 100%; min-height: 40px; }

  /* PILE À DEUX NIVEAUX (arbitrage Antoine, 2026-08-07) — et non un accordéon.
     Le panneau RECOUVRE la liste des rubriques et la remplace ; on en revient
     par un « Retour » explicite. Fondement : dans la maquette `2043:26938`, le
     niveau 2 ne laisse AUCUNE trace des rubriques de niveau 1, pas même
     masquée — ce n'est donc pas un dépliement. Le build servait un accordéon,
     qui gardait les 4 rubriques visibles et ajoutait les 7 sous-items dessous.
     ⚠️ `SPEC-COMPORTEMENTS-INTERACTIFS.md §3` désigne uptoo.fr comme source du
     comportement des mega-menus : cet arbitrage vaut pour le DRAWER MOBILE
     seulement, le survol desktop est inchangé. */
  .navbar__panel::before { content: none; }

  .navbar__panel {
    position: absolute;
    inset: 0;
    z-index: 1;
    min-width: 0;
    padding: 0;
    border: none;
    border-radius: 0;
    background-color: var(--bg-white);
    box-shadow: none;
    display: block;
    opacity: 1;
    overflow-y: auto;
    visibility: hidden;
    pointer-events: none;
    transform: translateX(100%);
    transition: transform var(--dur-med) var(--ease-brand),
                visibility 0s linear var(--dur-med);
  }

  /* Le survol n'ouvre rien dans le drawer : seul l'appui, via `data-ud-open`.
     Ces deux règles neutralisent l'état visible posé hors media query — et pas
     seulement sa `transform`. La règle desktop pose AUSSI `visibility: visible`
     et `pointer-events: auto` en (0,3,0), donc au-dessus de la base mobile en
     (0,1,0) : sans ce rappel, un panneau refermé redevenait « visible » dès que
     le focus revenait sur sa rubrique (`:focus-within`), et laissait dépasser
     une languette cliquable au bord droit du drawer. Constaté au rendu. */
  .navbar__item:hover > .navbar__panel,
  .navbar__item:focus-within > .navbar__panel {
    transform: translateX(100%);
    visibility: hidden;
    pointer-events: none;
  }

  /* Niveau 2 ouvert : les rubriques de niveau 1 sont RECOUVERTES par le panneau
     opaque, mais elles resteraient dans le parcours de tabulation et dans
     l'arbre d'accessibilité — on tabulerait sur quatre boutons invisibles.
     `visibility: hidden` les en retire. La règle vise les rubriques et NON
     `.navbar__item`, dont le panneau ouvert hériterait. */
  .navbar__list[data-ud-level="2"] > .navbar__item > .navbar__trigger,
  .navbar__list[data-ud-level="2"] > .navbar__item > .navbar__link {
    visibility: hidden;
  }

  /* Spécificité (0,3,0), égale à celle des deux règles ci-dessus, et placée
     APRÈS : elle l'emporte. Sans ça, `:focus-within` — qui est vrai dès que le
     focus entre dans le panneau ouvert — le renverrait hors écran. */
  .navbar__item > .navbar__panel[data-ud-open="true"] {
    transform: translateX(0);
    visibility: visible;
    pointer-events: auto;
    transition-delay: 0s;
  }

  .navbar__back { display: flex; }
}

/* --- Barre desktop — complément EXACT de la media query du drawer ----------
   `min-width: 1024px` est le complément de `max-width: 1023px` ci-dessus :
   au-dessus de ce seuil, ni le drawer ni le voile n'existent, donc aucun des
   deux `position: fixed` du module n'est dans le champ de ce bloc. C'est ce
   qui le rend sûr, et c'est la raison de son emplacement. */
@media (min-width: 1024px) {
  /* Hauteur relevée en desktop : 67 + 1px de bordure = 68 (voir le bloc de
     `.navbar__inner` en tête de fichier pour les sources et la mesure). */
  .navbar__inner { min-height: 67px; }

  /* L'item épouse la hauteur de la barre : `top: 100%` du panneau est alors
     le bas de la navbar, pas le bas du libellé centré. Le `--sp-3` au-dessus
     devient un vrai filet d'air sous la barre. */
  .navbar__nav {
    align-self: stretch;
    align-items: stretch;
  }
  .navbar__list { align-items: stretch; }
  .navbar__item {
    display: flex;
    align-items: center;
  }

  /* ⚠️ V13 — DÉBORDEMENT HORIZONTAL DE 17 px À 1024 px, sur 100 % des pages.
     Mesuré le 2026-08-07 sur la préprod (`docs/VERIF-SOCLE-5a88ecd.md §4`) :
     un panneau de mega-menu FERMÉ est `position: absolute` + `min-width` fixe
     + `visibility: hidden`. Fermé, il reste compté dans le `scrollWidth` du
     document — seul `display: none` l'en sortirait, et `display: none` tuerait
     la transition d'ouverture. Le bord droit du dernier panneau tombait à
     1041 px pour une fenêtre de 1024 : une barre de défilement horizontale sur
     toutes les pages. Le rapport le renvoyait au « chantier mega-menu », et il
     y est resté ouvert (`docs/VERIF-ICONES-MEGA-MENU.md §6`, « relevé, non
     corrigé »). La gate visuelle ne pouvait pas l'attraper : elle ne mesure V13
     qu'à 1440 et à 390.
     `overflow-x: clip` clôt l'axe horizontal SANS créer de conteneur de
     défilement ni de bloc conteneur — contrairement à `hidden`, qui forcerait
     l'autre axe en `auto` et enfermerait le panneau ouvert dans la barre.
     `overflow-y: visible` reste donc honoré, et le panneau continue de pendre
     sous la barre comme aujourd'hui.
     Ce que ça change à l'écran : rien tant que le panneau tient dans la
     fenêtre. Là où il ne tenait pas, un bord de panneau coupé remplace une
     barre de défilement sur la page entière. */
  .navbar { overflow-x: clip; overflow-y: visible; }
}

/* Voile d'arrière-plan du drawer. */
.navbar__scrim {
  position: fixed;
  inset: 0;
  z-index: 99;
  background-color: rgb(0 0 0 / 40%);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-med) var(--ease-out), visibility 0s linear var(--dur-med);
}

.navbar[data-ud-drawer="open"] .navbar__scrim {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}
/* =============================================================================
   Footer — maquette « Footer » du styleguide (fond sombre)
   -----------------------------------------------------------------------------
   Le footer porte .section--dark : toutes ses couleurs viennent de la couche 3,
   il n'a aucune couleur propre. Structure : bandeau marque + badge ELITE,
   5 colonnes de liens, bloc « sites du groupe », barre légale.
   ============================================================================= */

/* Padding vertical SYMÉTRIQUE — 64/64 en desktop, 56/56 sous 768 (M-41).
   Desktop, cinq relevés concordants : `blog-listing.md:190` (« padding vertical
   64px »), `cas-client-wakam.md:181`, `a-propos.md:223`, `hubspot-listing.md:326`,
   `blog-article.md:167` (« padding-y 64 »). Mobile : `mobile-accueil.md:456`
   (footer `1979:7374`, « padding 20 / 56 »), recoupé par la règle de gabarit de
   `mobile-a-propos.md:39` — « 20 px latéral / 56 px vertical sur TOUTES les
   sections, sans exception ».
   Le bas servait `--sp-6` (32) : −24 px, le double du seuil ERROR.
   Le token retenu n'est pas `--sp-8` : 56 n'existe dans AUCUNE échelle
   d'espacement (`--sp-7` 48 puis `--sp-8` 64), et `--sp-8` aurait donc imposé
   une media query et un `56px` en dur ici. `--section-y-xs` porte exactement les
   deux relevés — 64px en :root (`tokens-primitives.css:203`), 56px sous 768px
   (`:390`) — et la bascule reste dans les tokens.
   ⚠️ Couplage assumé : ce token est aussi le rythme des sections `--rhythm-xs`
   (`objects/_layout.css:111`). Le jour où l'un des deux relevés bouge sans
   l'autre, c'est ici qu'il faut revenir. */
.footer {
  padding-block: var(--section-y-xs);
  /* Maquette 08-18 : le footer est le SEUL contexte sombre en aplat purple-950 —
     toutes les autres surfaces sombres prennent le dégradé `--gradient-dark`
     posé par `.section--dark` (tokens-semantic). Opt-out explicite ici. */
  background-image: none;
}

.footer a { color: var(--text-secondary); }
.footer a:hover { color: var(--text); }

/* --- Bandeau haut : logo + badge partenaire + actions --------------------- */
.footer__brand {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  flex-wrap: wrap;
  padding-bottom: var(--sp-6);
}

.footer__logo img {
  display: block;
  width: 125px;
  height: 20px;
  object-fit: contain;
  object-position: left center;
}
/* 72 : cote du badge hexagonal de la maquette 08-18 (F-03) — était plafonné 64. */
.footer__badge img { display: block; height: 72px; width: auto; }

.footer__actions { display: flex; align-items: center; gap: var(--sp-3); margin-left: auto; }

/* Bouton LinkedIn — 45 × 45, glyphe 18 (M-43). Le socle sert un icon-button de
   32 × 32 (`elements/_buttons.css:105-120`, `width`/`height` en `:109-110`,
   glyphe 16 en `:122`) : c'est le gabarit générique du site, il ne bouge pas.
   La cote du footer est donc portée ici, en surcharge locale — `_footer.css` est
   inclus après `_buttons.css` (`main.css:31` puis `:40`), et les deux sélecteurs
   passent devant par spécificité : (0,2,0) contre (0,1,0) pour la boîte, (0,2,1)
   contre (0,1,1) pour le glyphe (`.icon-btn svg` vaut une classe ET un type,
   une classe seule ne suffirait pas). Même geste, même raison qu'à
   `modules/cards-hubs-stack.module/module.css:129-132`, qui remonte déjà un
   glyphe d'`icon-btn` de 16 à 18.
   Quatre relevés concordants : `mobile-solutions-audit-crm.md:352` (« bouton
   LinkedIn 45 × 45 rayon 6 … linkedin-fill 18 px »), `blog-listing.md:194`,
   `mobile-hubspot-sales-hub.md:263`, `a-propos.md:225`.
   Le rayon relevé (6) est DÉJÀ servi : `.icon-btn` pose `--radius-sm`
   (`_buttons.css:113`), qui vaut 6px (`tokens-primitives.css:333`) — rien à
   redéclarer, ce n'était pas un écart.
   45 et 18 n'existent dans aucune échelle (`--sp-*` va 16 · 24 · 32 · 48) : ce
   sont des cotes de boîte, écrites en dur comme les quatre autres de ce fichier
   (logo 150, badge 72, rond de retour 48, son glyphe 18) et NON dans un
   `footer.module/module.css`, où CONTRAT-SOCLE §1 interdit les longueurs en dur.
   45 est par ailleurs la hauteur du bouton primaire voisin (`_buttons.css:37`,
   `min-height: 45px`) : les deux boutons de la rangée s'alignent, comme au
   relevé. Le `viewBox="0 0 16 16"` du SVG suit l'échelle, aucun HTML à toucher. */
.footer__actions .icon-btn { width: 45px; height: 45px; }
.footer__actions .icon-btn svg { width: 18px; height: 18px; }

.footer__divider {
  border: none;
  border-top: 1px solid rgb(255 255 255 / 12%);
  margin: 0;
}

/* --- Colonnes de liens ---------------------------------------------------- */
/* F-07, tranché le 2026-08-18 : l'écart « gouttière 80, contact en bout de
   rangée » n'était porté que par UNE source (`cas-client-wakam.md:185`) et
   restait déclaré ouvert. Le bundle de la maquette Claude Design le RECOUPE
   (gap 80 entre les 4 colonnes de menus, CONTACT poussée au bord droit par
   `space-between`) : deux sources concordantes, on applique. Flex — et non
   grid — parce que les colonnes prennent leur largeur de contenu et que seule
   la dernière est repoussée. Les paliers ≤1023 reviennent en grille (bas de
   fichier). */
.footer__columns {
  display: flex;
  gap: var(--sp-9);
  padding-block: var(--sp-6);
}

.footer__columns .footer__col:last-child { margin-left: auto; }

.footer__col-title {
  font-family: var(--font-mono);
  font-weight: var(--fw-medium);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text);
  margin: 0 0 var(--sp-4);
}

.footer__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); }

.footer__list a {
  font-size: var(--fs-body-s);
  letter-spacing: var(--ls-body-s);
  text-decoration: none;
}

/* --- Sites du groupe + barre légale --------------------------------------- */
.footer__group { padding-block: var(--sp-6); display: grid; gap: var(--sp-5); }

.footer__group-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); }
/* F-06 (maquette 08-18) : liens des sites du groupe en médium, teinte purple-200
   — le rôle `text/on-dark/secondary` de la CHARTE (le socle sombre sert
   neutral-300 sur `--text-secondary` depuis M-06, ces liens-ci restent teintés). */
.footer__group-list a {
  font-size: var(--fs-body-m);
  letter-spacing: var(--ls-body-m);
  font-weight: var(--fw-medium);
  color: var(--c-purple-200);
}

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
  font-size: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  color: var(--text-secondary);
  /* F-05 : le légal vit dans la rangée du bas, poussé côté droit (le bouton
     de retour en haut reste après lui, à l'extrême droite). */
  margin: 0 0 0 auto;
}

.footer__legal a { font-size: inherit; letter-spacing: inherit; }

.footer__bottom {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-5);
}

/* Retour en haut : cercle, contrairement aux icon-buttons carrés du corps. */
.footer__top {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  flex: none;
  border: 1px solid rgb(255 255 255 / 12%);
  border-radius: var(--radius-pill);
  background: none;
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}

.footer__top:hover { background-color: var(--surface); border-color: var(--text-secondary); }
.footer__top svg { width: 18px; height: 18px; }

@media (max-width: 1023px) {
  /* La rangée flex desktop (F-07) redevient une grille : 2 colonnes égales,
     le `margin-left: auto` de CONTACT n'a plus de sens en grille. */
  .footer__columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-6); }
  .footer__columns .footer__col:last-child { margin-left: 0; }
}

@media (max-width: 767px) {
  /* Colonnes EMPILÉES : la gouttière devient l'écart vertical entre blocs, et la
     maquette mobile le pose à 48 — `mobile-accueil.md:460` (nav-row `1979:7297`,
     « gap 48 »), recoupé par `mobile-hubspot-sales-hub.md:265` (« 6 blocs
     empilés (gap 48) »). Sans cette ligne le palier héritait les 32 du palier
     1023 ci-dessus, qui n'a jamais été redéclaré ici : c'était M-42. */
  .footer__columns { grid-template-columns: minmax(0, 1fr); gap: var(--sp-7); }
  .footer__actions { margin-left: 0; width: 100%; }
  .footer__bottom { flex-direction: column; align-items: stretch; }
  .footer__legal { margin-left: 0; }
}
/* =============================================================================
   Corps d'article — feuille de style de CONTENU (`.article`)
   -----------------------------------------------------------------------------
   Ce fichier n'habille pas une mise en page : il habille du HTML libre produit
   par l'éditeur (`content.post_body`) et par 135 articles importés. Chaque règle
   doit tenir seule, dans n'importe quel ordre, sans rien supposer de la
   structure. C'est le livrable B8 du plan de construction.

   ── Ce qu'il porte, mesuré à la source le 2026-08-10 sur les 135 articles du
   portail 1555866 (GET /cms/v3/blogs/posts, lecture seule) :

     balise       articles / occurrences        ce qui l'exige
     p            135 / 5683                    tout
     strong       134 / 2676                    tout
     h2           134 /  958                    tout
     a            133 / 1223                    tout
     li           117 / 1605                    listes
     ul           115 /  457
     details      100 /  500                    FAQ dépliables
     summary      100 /  500
     section      100 /  100                    conteneur de FAQ
     h3            83 /  471
     table         79 /   97                    ⚠ 79 articles ont un tableau NU,
     td            79 / 1461                      hors `.article-table`
     figure        58 /   64                    images légendées
     figcaption    57 /   62
     aside         48 /   48                    maillage interne
     img           45 /  121
     blockquote    16 /   30
     hr            13 /   18

   ── Les 13 classes `.article-*` : portées par 7 articles seulement (les clones
   du gabarit `blog/modele-structure-article`, id 429380151536). Le plan §B8
   annonçait « ~95 enrichis au contrat .article-* » — c'est faux, la mesure dit
   7. Les ~93 réellement enrichis portent `dw-faq` / `dw-uptoo-signature` /
   `dw-cluster-mesh`, qui sont un autre contrat (voir l'avertissement ci-dessous).

   ── Portage : les 29 règles `.article-*` du site live ont été relevées à la
   source (https://www.digitaweb.com, feuille `template_assets/…`, 2026-08-10).
   Elles sont écrites sur des tokens Material qui n'existent pas ici. Table de
   correspondance appliquée, rôle par rôle :

     live (résout en)                     → rôle Uptoo
     --surface-container-low (dw-linen)   → --surface
     --surface-container-lowest (warm)    → --bg-white
     --secondary (dw-terracotta)          → --accent (aplat, filet)
                                            --accent-text (texte, contrat §1 D6)
     --primary (dw-deep)                  → --bg-purple-darker
     --outline-variant (dw-line)          → --border
     --error #ba1a1a                      → ⚠ AUCUN rôle d'alerte au socle.
                                            `--accent-beige` posé par défaut,
                                            à arbitrer (voir §callout).
     --space-xs 8 / sm 16 / md 24         → --sp-2 / --sp-4 / --sp-5
     --space-lg 40                        → --sp-7 (48) — 40 n'est pas dans
                                            l'échelle base-8 du socle
     --space-xl 64                        → --sp-8

   La police `Newsreader, Georgia, serif` de `.article-cta__title` n'est PAS
   portée : c'est le serif de l'ancienne DA DigitaWeb, le socle Uptoo est en
   DM Sans (une seule famille de titre).

   ── ⚠️ AVERTISSEMENT, mesuré et non résolu par ce fichier.
   Les trois blocs les plus répandus du corpus portent 100 % de leur mise en
   forme en `style=` inline, avec la palette DigitaWeb :

     dw-faq              93 occurrences — 93 avec style inline
     dw-uptoo-signature  91 occurrences — 91 avec style inline
     dw-cluster-mesh     48 occurrences — 48 avec style inline

   Exemple relevé : `background:#faf6f0; border-left:4px solid #c14b2a`
   (terracotta DigitaWeb). Au total 134 articles sur 135 portent du style
   inline — 9084 attributs, dominés par `color` (3194), `font-weight` (2004),
   `padding` (1437), `font-size` (820).

   Aucune feuille de style ne peut reprendre la main dessus sans `!important`.
   Ce fichier n'en pose AUCUN : neutraliser 9084 déclarations inline à coups de
   `!important` produirait une cascade impossible à maintenir, et le vrai
   correctif est une réécriture des `post_body` (opération d'écriture sur le
   contenu live — hors périmètre, à arbitrer). Conséquence assumée et à
   connaître : au go-live, ~93 articles afficheront encore des encarts
   terracotta DigitaWeb au milieu d'un thème violet Uptoo.
   ============================================================================= */

/* --- Colonne de texte -------------------------------------------------------
   ⚠️ Corrigé le 2026-08-11 : ce commentaire annonçait une seule source de
   largeur, il y en a DEUX, et une seule est `.container--narrow`.
     · page libre → module `rich-text-article`, `.container--narrow` = 800 px
       (`objects/_layout.css:65`) ;
     · article de blog → la GRILLE de `css/templates/blog.css`, colonne
       `minmax(0, 1fr)` face à un sommaire de 320 et une gouttière de 80, ce qui
       donne 800 px dans le `.container` de 1200. Aucun `.container--narrow`
       n'intervient (`templates/blog-post.html:57-64`).
   Les deux convergent sur 800 px, contre 814 relevés (`blog-article.md:81`) :
   −1,7 %, arbitrage assumé et justifié au point de définition dans `blog.css`.
   La coïncidence est utile mais elle n'est PAS structurelle — si le container
   du socle bouge, seule la page libre suivra. Ici : rythme vertical, couleur,
   et le contrat de lecture.

   `--ls-body-m` (−0,015em) donne −0,24 px à 16 px : la valeur exacte du corps
   relevé (`blog-article.md:91`, « 16 px / 1,5, tracking −0,24 px »). */

.article {
  /* B-23, tranché par Antoine le 2026-08-18 (« voir maquette ») : le corps de
     lecture est en COULEUR DE TITRE (purple-950) — systématique sur les deux
     instances d'article du bundle. Le chapô et les extraits, eux, restent en
     secondary : l'intention de la maquette est délibérée. */
  color: var(--text-title);
  font-size: var(--fs-body-m);
  line-height: var(--lh-body-m);
  letter-spacing: var(--ls-body-m);
}

/* B-24, même arbitrage : paragraphe LEAD 20/400/1,4 — la maquette en intercale
   après les figures et citations. Aucune classe n'existe dans le HTML des 135
   articles migrés : `.article-lead` est la convention offerte à l'éditeur (et
   aux gabarits d'article), elle ne s'applique jamais toute seule. */
.article .article-lead {
  font-size: var(--fs-heading-s);
  font-weight: var(--fw-regular);
  line-height: var(--lh-tight);
}

/* Le premier et le dernier enfant ne poussent jamais la boîte : c'est la
   section qui commande le rythme, pas la marge d'un <p> importé. */
.article > :first-child { margin-top: 0; }
.article > :last-child { margin-bottom: 0; }

/* --- Titres -----------------------------------------------------------------
   Le H1 appartient au hero de l'article, jamais au corps : aucune règle ici.
   `scroll-margin-top` est ce qui empêche la navbar collante de recouvrir un
   titre quand le sommaire y saute (piège SPEC §6).

   ── ÉCART TYPOGRAPHIQUE ASSUMÉ SUR LE H2, documenté le 2026-08-11 parce qu'il
   ne l'était pas. Le H2 du corps est un style LOCAL de la maquette, non
   tokenisé, et le relevé le dit deux fois (`blog-article.md:87` et `:231` :
   « la typo du rich-text n'est pas tokenisée ») :
                      relevé desktop   relevé mobile   ce que posent les tokens
     taille           32               24              32 / 24 → EXACT
     interligne       1,3              1,3             --lh-heading-l = 1,15
     tracking         −0,96 px (−3 %)  −0,72 px (−3 %) --ls-heading-l = −2 %
   Seule la taille tombe juste, aux deux paliers (`tokens-primitives.css:140` et
   `:379`). L'interligne et le tracking sont ceux de `Desktop/Heading/L`, qui est
   le style du titre « Articles similaires », PAS celui du H2 du corps.
   Choix : les tokens. Ouvrir deux rôles (`--lh-article-h2`, `--ls-article-h2`)
   pour un style que la maquette elle-même déclare non tokenisé coûterait plus
   cher que l'écart — un H2 de deux lignes sera ~5 px plus serré que la maquette.
   À mesurer au rendu ; à rouvrir seulement si l'écart se voit.

   Le `padding-bottom 24` relevé sous le H2 (`blog-article.md:87`) est bien
   servi : `--sp-5` = 24 en marge basse. */

.article h2,
.article h3,
.article h4 {
  color: var(--text-title);
  scroll-margin-top: var(--sp-8);
}

.article h2 {
  font-size: var(--fs-heading-l);
  line-height: var(--lh-heading-l);
  letter-spacing: var(--ls-heading-l);
  margin: var(--sp-7) 0 var(--sp-5);
}

.article h3 {
  font-size: var(--fs-heading-s);
  line-height: var(--lh-heading-s);
  letter-spacing: var(--ls-heading-s);
  margin: var(--sp-5) 0 var(--sp-4);
}

.article h4 {
  font-size: var(--fs-body-l);
  line-height: var(--lh-body-l);
  margin: var(--sp-5) 0 var(--sp-2);
}

/* --- Flux courant ----------------------------------------------------------- */

.article p { margin: 0 0 var(--sp-4); }

.article strong { font-weight: var(--fw-medium); }

.article a {
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: var(--sp-1);
}

/* ⚠️ 2px SANS SOURCE, et c'est le seul de ce fichier. Aucun relevé du corpus ne
   dessine l'état survolé d'un lien de corps d'article : `blog-article.md:227` et
   `mobile-blog.md:445` déclarent tous deux que les états d'interaction ne sont
   pas nommés dans Figma (une seule exception dans tout le lot blog : la carte 2
   du listing, annotée `Hover`). C'est donc une décision de build — épaissir le
   soulignement plutôt que changer la couleur, pour ne pas faire clignoter
   `--accent-text` au survol — et non une cote. À faire trancher par la DA. */
.article a:hover { text-decoration-thickness: 2px; }

.article ul,
.article ol {
  margin: 0 0 var(--sp-4);
  padding-left: var(--sp-5);
}

.article li { margin-bottom: var(--sp-2); }

.article li:last-child { margin-bottom: 0; }

.article hr {
  background: var(--border);
  border: 0;
  height: 1px;
  margin: var(--sp-7) 0;
}

/* --- Images et figures ------------------------------------------------------
   Maquette `2067:29397` : image pleine colonne 814×450 à ANGLES DROITS (radius 0,
   contrairement à l'image du hero qui est en radius 12) + légende à filet
   vertical violet 2px. Le radius 0 est donc un relevé, pas un oubli.

   Cotes recoupées ligne à ligne avec `blog-article.md:97-101` :
     figure `padding 48 / 0`  → `margin: var(--sp-7) 0`   = 48   EXACT
     légende `gap 8`          → `padding-left: var(--sp-2)` = 8  EXACT
     légende 14 px            → `--fs-body-s` = 14                EXACT
     filet 2 px               → 2px, seule longueur en dur, relevée

   ⚠️ LE VIOLET DU FILET EST UN CHOIX, PAS UNE RECOPIE. Le desktop relève
   `accent/on-light/purple` #7251da (`blog-article.md:99`), mais le mobile relève
   `theme/primary` #0a6afa — BLEU (`mobile-blog.md:189`). Le relevé tranche
   lui-même : c'est un défaut de la maquette mobile, une variable de thème non
   redéfinie qui retombe sur un bleu par défaut, à corriger EN #7251da
   (`mobile-blog.md:430`, défaut n° 8 ; écart confirmé `:387`). `--accent` est
   donc le bon rôle aux deux paliers, et la maquette mobile est en tort — pas
   cette feuille. Ne pas « re-corriger » vers le bleu en relisant le mobile.

   ⚠️ NON RENDU, faute de prise : la maquette pose la hauteur d'image à 450 px
   fixes, ce qui la rend PORTRAIT sur mobile (350 × 450, `mobile-blog.md:431`,
   défaut n° 9). Aucune règle ici ne peut appliquer un ratio à une `<img>` de
   HTML importé sans connaître ses dimensions : `height: auto` laisse l'image à
   son ratio natif, ce qui est le comportement souhaitable et diverge donc de la
   maquette mobile — qui est elle-même signalée fautive sur ce point. */

.article img {
  height: auto;
  max-width: 100%;
}

.article figure {
  margin: var(--sp-7) 0;
}

.article figure img { display: block; width: 100%; }

.article figcaption {
  border-left: 2px solid var(--accent);
  color: var(--text-secondary);
  font-size: var(--fs-body-s);
  line-height: var(--lh-body-s);
  margin-top: var(--sp-4);
  padding-left: var(--sp-2);
}

/* --- Citation ---------------------------------------------------------------
   Même filet violet 2px que la légende (maquette : `Figure` du bloc ⑤,
   `blog-article.md:107`) — et là le mobile est d'accord avec le desktop
   (`mobile-blog.md:191`), c'est la légende deux nœuds plus haut qui décroche.

   Recoupement de `blog-article.md:107` (« `Figure`, `pt 20 pb 32`, gap 24,
   texte 20 px / 28 px, sans tracking ») :
     gap 24        → `padding-left: var(--sp-5)`  = 24        EXACT
     pb 32         → marge basse `--sp-6`         = 32        EXACT
     pt 20         → marge haute `--sp-5`         = 24        +4, VOIR CI-DESSOUS
     20 px         → `--fs-heading-s`             = 20        EXACT
     28 px (1,4)   → `--lh-heading-s`             = 1,3 → 26  −2 px
     sans tracking → `--ls-heading-s`             = −1 %      écart

   Les trois écarts relèvent de la même règle, déjà appliquée à `--space-lg 40`
   dans l'en-tête de portage : 20 n'est pas un palier de l'échelle base-8 du
   socle (16 ou 24, `tokens-primitives.css:184-186`), et le socle n'a aucun rôle
   typographique « 20 px / 1,4 / sans tracking » — le relevé dit lui-même que
   cette citation n'est PAS `Desktop/Heading/S` (`blog-article.md:231`). Rien
   n'est inventé : on retombe sur le palier voisin. À mesurer au rendu. */

.article blockquote {
  border-left: 2px solid var(--accent);
  color: var(--text-title);
  font-size: var(--fs-heading-s);
  line-height: var(--lh-heading-s);
  margin: var(--sp-5) 0 var(--sp-6);
  padding-left: var(--sp-5);
}

.article blockquote p:last-child { margin-bottom: 0; }

/* --- Tableaux ---------------------------------------------------------------
   79 articles portent un tableau NU, hors `.article-table`. Sans wrapper
   scrollable — et on ne peut pas en injecter un dans du HTML importé sans JS —
   un tableau large déborde la page et provoque une barre horizontale, le défaut
   exact que la gate visuelle traque. `display: block` + `overflow-x: auto` sur
   le tableau lui-même est la seule parade qui ne demande aucun balisage.

   ⚠️ CETTE PARADE N'EST PAS AUTONOME. Elle ne tient que si le PARENT accepte de
   descendre sous la largeur de son contenu. Dans un article de blog, ce parent
   est un item de grille, dont le `min-width` vaut `auto` par défaut : sans le
   `min-width: 0` posé sur `.blog-post__body` (`templates/blog.css`), la grille
   s'élargirait avant que l'`overflow` ne s'active, et le tableau déborderait
   quand même. Les deux règles sont solidaires — ne pas en retirer une en
   croyant l'autre suffisante.

   La coupure des URLs longues, elle, est déjà acquise par héritage :
   `body { overflow-wrap: break-word }` (`elements/_typography.css:18`) descend
   dans `.article`. Rien à ajouter ici, et surtout rien à redéclarer.

   ⚠️ NON COUVERT, et non mesuré : `<pre>` et `<code>` ne figurent PAS dans
   l'inventaire des 135 articles ci-dessus, donc aucune règle n'existe pour eux.
   Si un article en porte un, `white-space: pre` neutralise l'`overflow-wrap`
   hérité et une ligne longue déborderait. Ajouter une garde reviendrait à
   styler un cas que la mesure ne montre pas : à RE-MESURER sur le portail
   (lecture seule) avant d'écrire quoi que ce soit. */

.article table {
  border-collapse: collapse;
  display: block;
  margin: var(--sp-7) 0;
  max-width: 100%;
  overflow-x: auto;
  width: 100%;
  -webkit-overflow-scrolling: touch;
}

.article th,
.article td {
  border-bottom: 1px solid var(--border);
  padding: var(--sp-4);
  text-align: left;
  vertical-align: top;
}

.article thead th {
  background: var(--surface);
  color: var(--text-title);
  font-weight: var(--fw-medium);
}

/* --- FAQ dépliable native ---------------------------------------------------
   100 articles utilisent <details>/<summary>. Le socle a déjà `_disclosure.css`
   pour les modules ; ici on ne cible QUE le corps d'article, pour ne pas
   marcher sur les accordéons de module. */

.article details {
  border-bottom: 1px solid var(--border);
}

.article summary {
  align-items: center;
  color: var(--text-title);
  cursor: pointer;
  display: flex;
  font-size: var(--fs-body-l);
  font-weight: var(--fw-medium);
  gap: var(--sp-5);
  justify-content: space-between;
  list-style: none;
  padding: var(--sp-5) 0;
}

.article summary::-webkit-details-marker { display: none; }

.article summary::after {
  color: var(--accent-text);
  content: "+";
  flex-shrink: 0;
}

.article details[open] > summary { color: var(--accent-text); }

.article details[open] > summary::after { content: "\2013"; }

/* =============================================================================
   Les 13 classes `.article-*` — portage des 29 règles du site live.
   Ordre conservé à l'identique pour que la relecture soit possible ligne à ligne.

   ⚠️ LIRE CE BLOC AVEC LA BONNE SOURCE EN TÊTE. Tout ce qui suit vient de la
   feuille LIVE de digitaweb.com (relevée le 2026-08-10, table de correspondance
   en en-tête), PAS des maquettes Figma : aucune des 13 classes n'apparaît dans
   `blog-article.md` ni dans `mobile-blog.md`, qui ne dessinent qu'un flux nu.
   Conséquence pratique, à ne pas prendre pour une incohérence :
     · les filets de 3 px (`.article-tldr`) et de 4 px (`.article-callout`) sont
       des valeurs LIVE. Ils ne contredisent pas le filet de 2 px de la légende
       et de la citation, qui est une cote de MAQUETTE. Deux sources, deux
       grammaires — ne pas les aligner sans arbitrage de la DA ;
     · les longueurs de ce bloc ne sont donc pas recoupables contre le corpus de
       relevés. Elles sont traçables (feuille live) mais non mesurées ici.
   ============================================================================= */

/* --- En bref (TL;DR) --- */

.article .article-tldr {
  background: var(--surface);
  border-left: 3px solid var(--accent);
  margin: var(--sp-7) 0;
  padding: var(--sp-5) var(--sp-7);
}

.article .article-tldr__label {
  color: var(--accent-text);
  display: block;
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-eyebrow);
  margin-bottom: var(--sp-2);
  text-transform: uppercase;
}

.article .article-tldr p:last-child,
.article .article-tldr li:last-child { margin-bottom: 0; }

/* --- Encart ----------------------------------------------------------------
   ⚠️ `--callout-warning` : le socle n'a AUCUN rôle d'alerte (contrat §1, 29
   rôles, aucun rouge). Le live posait `--error #ba1a1a`. Faute d'arbitrage, la
   variante `--warning` retombe sur l'accent beige, qui est la seule couleur
   d'attention de la palette. À faire trancher par la DA : ouvrir un rôle
   `--accent-danger`, ou assumer le beige. */

.article .article-callout {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  margin: var(--sp-7) 0;
  padding: var(--sp-5) var(--sp-7);
}

.article .article-callout--tip { border-left-color: var(--accent); }

.article .article-callout--warning { border-left-color: var(--accent-beige); }

.article .article-callout__title {
  color: var(--text-title);
  font-weight: var(--fw-medium);
  margin: 0 0 var(--sp-2);
}

.article .article-callout p:last-child { margin-bottom: 0; }

/* --- CTA d'article ----------------------------------------------------------
   ⚠️ DEUX VALEURS NON RECOUPÉES, signalées le 2026-08-11 : `opacity: 0.85` sur
   le paragraphe et `filter: brightness(0.97)` au survol du bouton blanc. Elles
   sont présumées venir du portage de la feuille live, mais elles ne figurent
   dans aucun relevé du corpus et le portage n'a pas été re-vérifié ligne à
   ligne. Les seules opacités relevées dans tout le lot blog sont 75 % sur la
   date du hero (`blog-article.md:60`) et 60 % sur l'auteur des cards mobiles
   (`mobile-blog.md:225`) — ni l'une ni l'autre n'est 0,85. À recouper contre la
   feuille live avant le go-live, ou à remplacer par un rôle de texte sur fond
   sombre (`--text-on-dark-secondary` si le socle en ouvre un) plutôt que par une
   opacité, qui délave aussi le contraste. */

.article .article-cta {
  background: var(--bg-purple-darker);
  border-radius: var(--radius-lg);
  margin: var(--sp-8) 0;
  padding: var(--sp-7);
  text-align: center;
}

.article .article-cta__title {
  color: var(--bg-white);
  font-size: var(--fs-heading-m);
  line-height: var(--lh-heading-m);
  letter-spacing: var(--ls-heading-m);
  margin: 0 0 var(--sp-2);
}

.article .article-cta p {
  color: var(--bg-white);
  margin: 0 0 var(--sp-5);
  opacity: 0.85;
}

.article .article-cta a { color: inherit; }

/* `btn btn--white` : dépendance hors-namespace du corpus (7 articles). Le socle
   n'a que `.btn--primary` et `.btn--secondary` — la variante blanche n'existe
   que pour ce fond violet foncé, elle vit donc ici et pas dans `_buttons.css`. */
.article .article-cta .btn--white {
  background: var(--bg-white);
  border-color: var(--bg-white);
  color: var(--text-title);
  text-decoration: none;
}

.article .article-cta .btn--white:hover { filter: brightness(0.97); }

/* --- Étapes numérotées ------------------------------------------------------ */

.article .article-steps {
  counter-reset: step;
  list-style: none;
  margin: var(--sp-7) 0;
  padding: 0;
}

.article .article-steps > li {
  counter-increment: step;
  margin-bottom: var(--sp-5);
  padding-left: var(--sp-8);
  position: relative;
}

.article .article-steps > li::before {
  background: var(--accent);
  border-radius: var(--radius-pill);
  color: var(--bg-white);
  content: counter(step);
  display: grid;
  font-size: var(--fs-body-s);
  font-weight: var(--fw-medium);
  height: var(--sp-6);
  left: 0;
  place-items: center;
  position: absolute;
  top: 0;
  width: var(--sp-6);
}

/* --- Tableau encadré (variante explicite du corpus) ------------------------- */

.article .article-table { margin: var(--sp-7) 0; }

.article .article-table table { margin: 0; }

.article .article-table tbody tr:nth-child(2n) { background: var(--surface); }

/* --- FAQ balisée ------------------------------------------------------------ */

.article .article-faq {
  border-top: 1px solid var(--border);
  margin: var(--sp-8) 0 0;
}

.article .article-faq__title {
  font-size: var(--fs-heading-m);
  line-height: var(--lh-heading-m);
  margin: var(--sp-7) 0 var(--sp-5);
}

.article .article-faq__answer { padding: 0 0 var(--sp-5); }

.article .article-faq__answer p:last-child { margin-bottom: 0; }

/* --- Résumé ----------------------------------------------------------------
   `article-summary` est relevé dans le corpus mais n'a AUCUNE règle dans le CSS
   live : la classe existe sans style. Traité comme un encart neutre plutôt que
   laissé nu — signalé pour arbitrage. */

.article .article-summary {
  background: var(--surface);
  border-radius: var(--radius-md);
  margin: var(--sp-7) 0;
  padding: var(--sp-5);
}

.article .article-summary p:last-child { margin-bottom: 0; }

/* --- Mobile -----------------------------------------------------------------
   Les tailles de police basculent seules (tokens sous 768px). Ne restent que
   les respirations horizontales, qui ne sont pas tokenisées par viewport.

   ── C'EST LA SEULE MEDIA QUERY DE CE FICHIER, et elle est en `max-width` :
   aucune règle de cette feuille n'est enfermée derrière un `min-width`, donc
   aucune largeur ne peut retomber sur du non-stylé. Le 767 est celui de la
   bascule des tokens (`tokens-primitives.css:367`) : la typo et les
   respirations horizontales changent au même palier, jamais l'une sans l'autre.

   ── LA BANDE 768–1023, relue le 2026-08-11 : rien ne s'y applique ici, et
   c'est correct. Sur cette bande la grille de `templates/blog.css` est déjà
   repliée en une colonne, la colonne de texte y vaut 656 → 862 px, soit plus
   large que partout ailleurs — les encarts y gardent donc leur padding 24/48
   sans se resserrer, ce qui est le comportement voulu.

   ⚠️ LE POINT LE PLUS SERRÉ N'EST PAS EN MOBILE. C'est à 1024 px, juste
   au-dessus de la bascule de la grille : la colonne de texte y retombe à 463 px
   (deux colonnes) tandis que cette media query est éteinte, donc un
   `.article-callout` y rend 463 − 2 × 48 = 367 px de contenu utile. Vérifié
   comme non débordant, mais c'est le pire cas de toute la plage 391–1439 et
   c'est là qu'il faut regarder au premier rendu. Aucun relevé tablette
   n'existe : rien n'est changé sans mesure. */

@media (max-width: 767px) {
  .article .article-tldr,
  .article .article-callout { padding: var(--sp-4) var(--sp-5); }

  .article .article-cta { padding: var(--sp-5); }

  .article .article-steps > li { padding-left: var(--sp-7); }
}
/* =============================================================================
   Disclosure — la primitive « bouton qui déplie un panneau »
   -----------------------------------------------------------------------------
   Remontée au socle le 2026-08-05. Elle était recopiée mot pour mot dans
   `faq-accordeon` et `accordeon-visuel` : même trigger, même chevron, même
   panneau en `grid-template-rows: 0fr → 1fr`, même bloc `@media (scripting:
   none)`. Deux copies d'une mécanique d'accessibilité, c'est deux endroits où
   la corriger — et un seul qu'on pense à corriger.

   Contrat de markup (le HubL des modules le respecte, le JS n'invente rien) :

     <li class="ud-disclosure__item" data-ud-open="false">
       <h3 class="ud-disclosure__heading u-heading-s">
         <button class="ud-disclosure__trigger" aria-expanded="false"
                 aria-controls="…">
           …intitulé…
           <svg class="ud-disclosure__chevron" aria-hidden="true">…</svg>
         </button>
       </h3>
       <div class="ud-disclosure__panel" id="…">
         <div class="ud-disclosure__panel-inner">…contenu…</div>
       </div>
     </li>

   L'état ouvert est porté par `data-ud-open="true"` sur l'ITEM, écrit par le
   `module.js` du module concerné — c'est le seul hook, il était déjà commun aux
   deux modules avant la remontée.

   Deux règles de fond, héritées des deux modules et conservées telles quelles :
   · l'ouverture anime `grid-template-rows` de 0fr à 1fr — jamais `display:
     none`, qui ne s'anime pas et retire le contenu du flux. Le panneau replié
     reste dans le HTML (indexable) mais sort de l'arbre d'accessibilité via
     `visibility` ;
   · aucune couleur, aucune taille, aucune durée littérale : uniquement des
     rôles et des tokens du socle. La primitive fonctionne donc telle quelle
     dans une `.section--dark`.

   Cette feuille vit dans main.css, donc chargée dans le `<head>` et jamais
   différée : c'est elle qui replie les panneaux. Différée, la page afficherait
   d'abord tous les panneaux ouverts puis les replierait.
   ============================================================================= */

/* --- Item ------------------------------------------------------------------
   Les séparateurs dessinent la liste : une bordure haute sur le premier item,
   une bordure basse sur chacun — d'où un trait unique entre deux items. ----- */

.ud-disclosure__item { border-bottom: 1px solid var(--border); }
.ud-disclosure__item:first-child { border-top: 1px solid var(--border); }

/* Le titre porte le style typographique (classe utilitaire du socle) ; sa marge
   basse n'a pas de sens ici, le rythme vient du padding du bouton. À
   spécificité égale avec `.u-heading-s`, c'est l'ordre d'inclusion dans
   main.css qui tranche — et les composants y passent après les éléments. */
.ud-disclosure__heading { margin: 0; }

/* --- Trigger ---------------------------------------------------------------
   Un vrai <button> : c'est lui qui porte `aria-expanded` et l'anneau de focus
   du socle. Il hérite de la typographie du titre qui l'enveloppe, donc aucune
   valeur typographique n'est redéclarée ici. -------------------------------- */

.ud-disclosure__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  width: 100%;
  padding: var(--sp-5) 0;
  border: none;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}

.ud-disclosure__trigger:hover { color: var(--accent); }

/* --- Chevron ---------------------------------------------------------------
   Purement décoratif (aria-hidden dans le markup) : l'état lisible est porté
   par `aria-expanded` sur le bouton. Dimensionné en `em`, donc à l'échelle du
   texte du titre quel que soit le point de rupture. ------------------------- */

.ud-disclosure__chevron {
  width: 1em;
  height: 1em;
  flex: none;
  color: var(--accent);
  transition: transform var(--dur-med) var(--ease-brand);
}

.ud-disclosure__item[data-ud-open="true"] .ud-disclosure__chevron {
  transform: rotate(180deg);
}

/* --- Panneau ---------------------------------------------------------------
   `grid-template-rows: 0fr → 1fr` est la seule façon d'animer une hauteur
   inconnue sans la mesurer en JavaScript. L'enfant direct porte l'`overflow`,
   sans quoi le contenu déborderait pendant la transition. ------------------- */

.ud-disclosure__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-med) var(--ease-brand);
}

.ud-disclosure__panel-inner {
  overflow: hidden;
  /* Replié, le panneau quitte l'arbre d'accessibilité et l'ordre de tabulation.
     Le basculement est retardé jusqu'à la fin de la fermeture, sinon le contenu
     disparaîtrait avant d'avoir fini de se replier. */
  visibility: hidden;
  transition: visibility 0s linear var(--dur-med);
}

.ud-disclosure__item[data-ud-open="true"] .ud-disclosure__panel {
  grid-template-rows: 1fr;
}

.ud-disclosure__item[data-ud-open="true"] .ud-disclosure__panel-inner {
  visibility: visible;
  transition-delay: 0s;
}

/* --- Sans JavaScript -------------------------------------------------------
   Évaluée à l'analyse du document, donc sans décalage de mise en page : les
   panneaux restent lisibles et le chevron, qui ne signifierait plus rien,
   disparaît. --------------------------------------------------------------- */

@media (scripting: none) {
  .ud-disclosure__panel { grid-template-rows: 1fr; }
  .ud-disclosure__panel-inner { visibility: visible; }
  .ud-disclosure__chevron { display: none; }
}
/* =============================================================================
   Hscroll — la primitive « piste qui défile horizontalement »
   -----------------------------------------------------------------------------
   Remontée au socle le 2026-08-08 (point 18 du reste-à-faire de la GATE
   TEMPLATE VISUELLE, arbitrage Antoine). Même raison que `disclosure` le
   2026-08-05 : la mécanique était écrite EN ENTIER dans `slider-temoignages`,
   et la maquette mobile la redemande à trois autres endroits — le Problème et
   les Expertises de l'accueil, le déroulé d'`offre-audit-crm`. Quatre copies,
   c'est quatre endroits où réapprendre les trois pièges ci-dessous.

   ⚠️ Cette feuille n'invente rien : elle DÉPLACE des règles qui tournaient déjà
   en préprod, avec leurs commentaires d'origine. Les trois notes marquées
   « banc d'essai » viennent du module et sont conservées mot pour mot — ce sont
   des mesures, pas des opinions.

   Contrat de markup (le HubL des modules le respecte, aucune classe n'est
   ajoutée par le JS) :

     <div class="ud-hscroll"            ← le conteneur qui défile
          tabindex="0" role="group" aria-label="…">
       <ul class="ud-hscroll__track">   ← la piste, plus large que le conteneur
         <li class="ud-hscroll__item">…</li>
       </ul>
     </div>

   ⚠️ `tabindex="0"` + `role="group"` + `aria-label` ne sont PAS décoratifs.
   Une zone défilante qui n'est pas focusable n'est pas atteignable au clavier
   (WCAG 2.1.1) — et la maquette ne dessine AUCUN contrôle (ni flèche, ni
   pastille) pour les trois nouveaux consommateurs, donc le clavier est le seul
   recours. Un module qui pose `.ud-hscroll` sans ces trois attributs est en
   défaut, même si le rendu paraît juste.

   ------------------------------------------------------------------- VARIANTES
   · `.ud-hscroll` seule       → défile à TOUTES les largeurs (`slider-temoignages`,
                                 dont la maquette desktop pose déjà un débordement
                                 volontaire de 1816 px dans un cadre de 1440).
   · `+ .ud-hscroll--sous-md`  → ne défile QUE sous 768 px. Au-dessus, la piste
                                 disparaît de la mise en page (`display: contents`)
                                 et les items redeviennent enfants directs du
                                 conteneur, qui reprend SON layout à lui —
                                 typiquement la grille du module. C'est ce qui
                                 rend le desktop rigoureusement inchangé : même
                                 élément, même règle, mêmes enfants.

   ⚠️ `display: contents` sur la piste : à ne poser que sur un conteneur dont la
   sémantique ne porte rien. Sur un `<ul>` dont le rôle `list` compte, il le
   retire de l'arbre d'accessibilité sur plusieurs moteurs. Les consommateurs
   `--sous-md` posent donc leur piste sur un `<div>`, jamais sur une liste.

   -------------------------------------------------------------------- RÉGLAGES
   `--ud-hscroll-gap`   : gouttière entre items. Défaut `--sp-6` (32 px, la
                          valeur relevée pour les cas clients). Le Problème de
                          l'accueil la pose à `--sp-5` (24 px).
   `--ud-hscroll-bleed` : de combien la piste déborde de son parent pour aller
                          jusqu'au bord. Défaut `--container-pad`, la gouttière
                          du `.container`.

   ⚠️ `--ud-hscroll-bleed` n'est PAS un raffinement : il existe parce que la
   primitive ne sait pas dans quoi elle est posée. `slider-temoignages` la met
   directement dans un `.container`, donc le débord vaut la gouttière du
   container. `section-probleme` la met DANS une carte sombre qui a son propre
   padding (`--ud-probleme-pad`) et un `overflow: hidden` — y appliquer la
   gouttière du container décalerait la piste d'une valeur qui n'a aucun rapport
   avec son parent réel. Un consommateur qui pose la primitive ailleurs que dans
   un `.container` DOIT donc régler ce token sur le padding de son parent.

   La largeur des items, elle, n'est pas un réglage de la primitive : elle
   appartient au module, parce qu'elle vient de sa maquette à lui.
   ============================================================================= */

/* --- Conteneur défilant ----------------------------------------------------
   Il déborde de la gouttière du `.container` (marge négative) pour que les
   items sortent jusqu'au bord de l'écran, pendant que le padding interne de la
   piste réaligne le premier item sur le contenu. La largeur reste bornée par
   celle du `.container` : aucun débordement de page (V13). ------------------ */
.ud-hscroll {
  overflow-x: auto;
  margin-inline: calc(-1 * var(--ud-hscroll-bleed, var(--container-pad)));
  /* `proximity` et non `mandatory` : avec `mandatory`, toute géométrie dont la
     course de défilement ne tombe pas pile sur un point d'accroche rend les
     derniers items INATTEIGNABLES (vérifié au banc d'essai). Le nombre d'items
     étant réglé par le marketeur, on refuse ce mode de défaillance :
     `proximity` accroche de la même façon à l'usage, sans jamais piéger de
     contenu. */
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--ud-hscroll-bleed, var(--container-pad));
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.ud-hscroll::-webkit-scrollbar { display: none; }

/* Pendant le glisser au pointeur : le snap se tait (il se rebranche au
   relâchement et ramène l'item le plus proche), et la sélection de texte est
   suspendue. `data-ud-dragging` est écrit par le module.js du module qui offre
   le glisser — les consommateurs sans JS n'ont jamais cet attribut, et cette
   règle ne les concerne pas. */
[data-ud-dragging="true"] .ud-hscroll {
  scroll-snap-type: none;
  cursor: grabbing;
  user-select: none;
}

/* --- Piste ----------------------------------------------------------------
   Le padding latéral fixe la boîte de contenu à la largeur utile du
   `.container` : c'est lui qui rend exactes les formules de largeur d'item
   écrites dans les modules. ------------------------------------------------ */
.ud-hscroll__track {
  display: flex;
  gap: var(--ud-hscroll-gap, var(--sp-6));
  margin: 0;
  padding-block: 0;
  padding-inline: var(--ud-hscroll-bleed, var(--container-pad));
  list-style: none;
}

/* Cale de fin de piste. Sans elle, le dernier item ne peut pas dégager la
   gouttière droite : Chrome ignore le padding de fin d'un conteneur flex dans
   le calcul du débordement défilable (mesuré au banc d'essai : course amputée
   d'une gouttière, dernier item inatteignable). La cale est un vrai élément
   flex, donc comptée ; on lui retire la gouttière que le `gap` ajoute déjà
   devant elle. */
.ud-hscroll__track::after {
  content: "";
  flex: 0 0 calc(var(--ud-hscroll-bleed, var(--container-pad)) - var(--ud-hscroll-gap, var(--sp-6)));
}

.ud-hscroll__item {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* --- Variante « sous 768 px seulement » ------------------------------------
   Au-dessus du palier, la primitive se retire complètement : plus de
   débordement de gouttière, plus de snap, et la piste s'efface de la mise en
   page. Le module retrouve exactement le rendu qu'il avait avant d'adopter
   la primitive — c'est la propriété qui rend cette migration sûre. --------- */
@media (min-width: 768px) {
  .ud-hscroll--sous-md {
    overflow-x: visible;
    margin-inline: 0;
    scroll-snap-type: none;
    scroll-padding-inline: 0;
  }

  .ud-hscroll--sous-md > .ud-hscroll__track {
    display: contents;
  }

  /* La cale n'a plus de piste flex pour la porter : `display: contents` la
     ferait remonter comme enfant du conteneur, où elle occuperait une cellule
     de grille vide. */
  .ud-hscroll--sous-md > .ud-hscroll__track::after { content: none; }

  .ud-hscroll--sous-md .ud-hscroll__item {
    flex: initial;
    scroll-snap-align: none;
  }
}
/* =============================================================================
   Upfade & révélation au scroll — le mouvement partagé du socle
   -----------------------------------------------------------------------------
   Remonté au socle le 2026-08-05, pour deux raisons distinctes.

   ① UNE SEULE KEYFRAME D'UPFADE. Elle était déclarée à l'identique dans
     `hero-home`, `hero-page` et `liste-offres`, et une quatrième fois en dur
     (14px) sous forme de transition dans `cards-grid`. Quatre endroits pour un
     seul geste de la charte. La distance est désormais le token
     `--shift-upfade` : la corriger se fait à un seul endroit, et `cards-grid`
     consomme la même valeur que les heros.

   ② UN SEUL CONTRAT DE RÉVÉLATION. Il en existait DEUX, incompatibles :
     `liste-offres` armait `[data-ud-reveal]` sur le CONTENEUR et révélait
     toutes ses rangées d'un coup, `cards-grid` armait `[data-ud-anim]` et
     révélait ITEM PAR ITEM. Deux JS quasi identiques, deux jeux d'attributs,
     et aucun moyen pour un troisième module de savoir auquel se brancher.
     C'est le contrat par item qui survit : il fait tout ce que faisait
     l'autre, plus la révélation progressive d'une longue liste.

   -----------------------------------------------------------------------------
   CONTRAT — trois attributs, rien d'autre :

     conteneur   data-ud-reveal="on"        posé par le HubL du module
                 data-ud-reveal="ready"     posé par le module.js, jamais par HubL
     item        data-ud-reveal-item        posé par le HubL du module
                 data-ud-revealed="true"    posé par le module.js à l'entrée
     cascade     --ud-reveal-index          posé par le module.css (0 par défaut)

   Le CONTRAT est ici, au socle ; l'IMPLÉMENTATION est dans le `module.js` de
   chaque module qui l'utilise (décision Antoine du 2026-08-05 : `js/main.js`
   est chargé sur toutes les pages, y compris celles sans module animé, et la
   home est le point de LCP le plus surveillé du thème). Les deux `module.js`
   sont donc volontairement jumeaux — mais ils ne peuvent plus DIVERGER, puisque
   les attributs et le CSS qu'ils manipulent sont définis une seule fois, ici.
   Chaque script ne cible que ses propres conteneurs : deux modules animés sur
   la même page ne s'arment jamais l'un l'autre.

   L'état masqué n'existe QUE sous `[data-ud-reveal="ready"]`, et « ready » n'est
   écrit que par un script. Conséquence tenue : sans JS, sans
   IntersectionObserver, en `prefers-reduced-motion` ou si la section est déjà à
   l'écran au chargement, RIEN n'est masqué. Une animation n'est jamais une
   condition d'affichage (SPEC-ANIMATIONS-V1 §4.2).

   L'apparition est une `animation` et non une `transition` : la propriété
   `transition` est une abréviation, et un module qui l'utilise déjà pour son
   survol (c'est le cas de `liste-offres`) la verrait écrasée par une règle du
   socle plus spécifique. Une animation ne prend pas cette place.
   ============================================================================= */

/* La seule keyframe d'upfade du thème. Consommée aussi par l'entrée des heros,
   qui ne passent pas par la révélation au scroll (ils sont sur le chemin du
   LCP : ils entrent au chargement, pas à l'intersection). */
@keyframes ud-upfade {
  from { opacity: 0; transform: translateY(var(--shift-upfade)); }
  to   { opacity: 1; transform: none; }
}

[data-ud-reveal="ready"] [data-ud-reveal-item] {
  opacity: 0;
}

[data-ud-reveal="ready"] [data-ud-reveal-item][data-ud-revealed="true"] {
  animation: ud-upfade var(--dur-med) var(--ease-brand) both;
  /* La cascade est décidée par le module : lui seul sait si ses items forment
     une colonne, une grille de 3 ou une liste à plafonner. Sans index, tous
     les items entrent ensemble — le défaut le plus sobre. */
  animation-delay: calc(var(--stagger-reveal) * var(--ud-reveal-index, 0));
}

/* Filets de sécurité. Les `module.js` n'arment déjà pas la révélation quand le
   visiteur demande moins d'animations, mais un état posé AVANT que la
   préférence ne change ne doit rien laisser masqué ; et une page imprimée en
   cours de lecture doit sortir tous ses items. */
@media (prefers-reduced-motion: reduce) {
  [data-ud-reveal="ready"] [data-ud-reveal-item] {
    opacity: 1;
    animation: none;
  }
}

@media print {
  [data-ud-reveal="ready"] [data-ud-reveal-item] {
    opacity: 1;
    animation: none;
  }
}
/* Menu and simple menu */

.hs-menu-wrapper ul {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: 0;
  padding-left: 0;
}

/* Horizontal menu */

.hs-menu-wrapper.hs-menu-flow-horizontal .hs-menu-children-wrapper {
  flex-direction: column;
}

@media (max-width: 767px) {
  .hs-menu-wrapper.hs-menu-flow-horizontal ul {
    flex-direction: column;
  }
}

/* Vertical menu */

.hs-menu-wrapper.hs-menu-flow-vertical ul {
  flex-direction: column;
}

/* Flyouts */

.hs-menu-wrapper.hs-menu-flow-vertical.flyouts ul {
  display: inline-flex;
}

@media (max-width: 767px) {
  .hs-menu-wrapper.hs-menu-flow-vertical ul {
    display: flex;
  }
}

.hs-menu-wrapper.flyouts .hs-item-has-children {
  position: relative;
}

.hs-menu-wrapper.flyouts .hs-menu-children-wrapper {
  left: -9999px;
  opacity: 0;
  position: absolute;
}

.hs-menu-wrapper.flyouts .hs-menu-children-wrapper a {
  display: block;
  white-space: nowrap;
}

.hs-menu-wrapper.hs-menu-flow-horizontal.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper {
  left: 0;
  opacity: 1;
  top: 100%;
}

.hs-menu-wrapper.hs-menu-flow-vertical.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper {
  left: 100%;
  opacity: 1;
  top: 0;
}

@media (max-width: 767px) {
  .hs-menu-wrapper.flyouts .hs-menu-children-wrapper,
  .hs-menu-wrapper.hs-menu-flow-horizontal.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper,
  .hs-menu-wrapper.hs-menu-flow-vertical.flyouts .hs-item-has-children:hover > .hs-menu-children-wrapper {
    left: 0;
    opacity: 1;
    position: relative;
    top: auto;
  }
}

/* CTA, logo, and rich text images */

.hs_cos_wrapper_type_cta img,
.hs_cos_wrapper_type_logo img,
.hs_cos_wrapper_type_rich_text img {
  height: auto;
  max-width: 100%;
}

/* Utilities
Helper classes with ability to override anything that comes before it
*/

/* For content that needs to be visually hidden but stay visible for screenreaders */

.show-for-sr {
  border: 0 !important;
  clip: rect(0, 0, 0, 0) !important;
  height: 1px !important;
  overflow: hidden !important;
  padding: 0 !important;
  position: absolute !important;
  white-space: nowrap !important;
  width: 1px !important;
}

@media (max-width: 767px) {
  .show-for-sr--mobile {
    border: 0 !important;
    clip: rect(0, 0, 0, 0) !important;
    height: 1px !important;
    overflow: hidden !important;
    padding: 0 !important;
    position: absolute !important;
    white-space: nowrap !important;
    width: 1px !important;
  }
}
/* --- En-tête de section centré ----------------------------------------------
   Ajouté le 2026-08-05 (arbitrage A2). La maquette centre sur-titre, titre et
   chapô sur plusieurs sections ; trois modules les rendaient en drapeau gauche
   et deux autres avaient recopié le correctif localement.

   Le `margin-inline: auto` n'est pas décoratif : les chapôs sont plafonnés en
   largeur (62ch, 68ch), et `text-align` seul centrerait leur TEXTE en laissant
   le bloc collé à gauche. C'est le détail qui se perd quand on recopie. ------ */
.u-head-center {
  text-align: center;
}

.u-head-center > * {
  margin-inline: auto;
}

/* --- Halo de fond -----------------------------------------------------------
   Le « dégradé » du panneau CTA, transposé en CSS le 2026-08-05 après relevé de
   l'asset Figma (nœud 1332:26716). Ce que la maquette contient réellement :
   4 formes floutées dans une boîte de 1812 × 1744, pas un dégradé.

     forme                cx      cy      rayon   flou σ
     cercle  #7251DA     48,8 %  40,8 %  16,9 %   202,8
     ellipse blanche     37,6 %  68,2 %  13,9 %   206,0   (rx), 8,2 % (ry)
     cercle  #7251DA     32,8 %  36,1 %  10,4 %   202,8
     cercle  #7251DA     58,5 %  65,9 %   8,6 %   188,0

   Rendu en `radial-gradient` plutôt qu'en image, pour trois raisons : la couleur
   reste un token (donc elle suit le thème), il n'y a aucun asset à servir, et le
   halo se redimensionne avec son conteneur. Les arrêts traduisent le flou
   gaussien : plein jusqu'à environ r − σ, éteint vers r + 2σ (l'étendue visible
   d'une forme floutée), ce qui donne les pourcentages ci-dessous.

   ⚠️ La géométrie est mesurée, la FIDÉLITÉ VISUELLE ne l'est pas : un flou
   gaussien et un dégradé radial ne tombent pas exactement l'un sur l'autre. À
   comparer à l'œil avec la maquette une fois posé sur une vraie page.

   Usage : sur un conteneur en `position: relative`, qui rogne (`overflow`). --- */
.u-halo {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.u-halo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    radial-gradient(circle at 49% 41%, var(--halo) 0%, transparent 39%),
    radial-gradient(ellipse 37% 22% at 38% 68%, var(--halo-light) 0%, transparent 60%),
    radial-gradient(circle at 33% 36%, var(--halo) 0%, transparent 33%),
    radial-gradient(circle at 59% 66%, var(--halo) 0%, transparent 29%);
  opacity: 0.85;
}

/* Le halo est une décoration : sous prefers-reduced-transparency, on le retire
   plutôt que d'imposer une superposition contrastée non maîtrisée. */
@media (prefers-reduced-transparency: reduce) {
  .u-halo::before { display: none; }
}